de.rpgframework.jfx.pages.CharactersOverviewPage Maven / Gradle / Ivy
package de.rpgframework.jfx.pages;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import org.prelle.javafx.AlertManager;
import org.prelle.javafx.AppBarButton;
import org.prelle.javafx.CloseType;
import org.prelle.javafx.CommandBar;
import org.prelle.javafx.JavaFXConstants;
import org.prelle.javafx.ManagedDialog;
import org.prelle.javafx.OptionalNodePane;
import org.prelle.javafx.Page;
import org.prelle.javafx.ResponsiveControl;
import org.prelle.javafx.SymbolIcon;
import org.prelle.javafx.WindowMode;
import de.rpgframework.ResourceI18N;
import de.rpgframework.character.Attachment;
import de.rpgframework.character.Attachment.Format;
import de.rpgframework.character.Attachment.Type;
import de.rpgframework.character.CharacterHandle;
import de.rpgframework.character.CharacterIOException;
import de.rpgframework.character.CharacterProvider;
import de.rpgframework.character.CharacterProviderListener;
import de.rpgframework.character.CharacterProviderLoader;
import de.rpgframework.character.RuleSpecificCharacterObject;
import de.rpgframework.core.BabylonEvent;
import de.rpgframework.core.BabylonEventBus;
import de.rpgframework.core.BabylonEventListener;
import de.rpgframework.core.BabylonEventType;
import de.rpgframework.genericrpg.chargen.CharacterController;
import de.rpgframework.genericrpg.chargen.CharacterGenerator;
import de.rpgframework.jfx.CharacterHandleBox;
import de.rpgframework.jfx.StupidSimpleSingleSelectionModel;
import de.rpgframework.reality.EdenConnectionState;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
/**
* @author stefan
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class CharactersOverviewPage extends Page implements ResponsiveControl, CharacterProviderListener, BabylonEventListener {
protected final static ResourceBundle RES = ResourceBundle.getBundle(CharactersOverviewPage.class.getName());
private static final PseudoClass PSEUDO_CLASS_SELECTED =
PseudoClass.getPseudoClass("selected");
private final static Logger logger = System.getLogger(CharactersOverviewPage.class.getPackageName());
protected CommandBar cbCharacters;
protected AppBarButton btnCreate;
protected AppBarButton btnSynchronize;
// protected SelectingGridView tpCharacters;
protected TilePane tpCharacters;
protected HBox bxLayout;
protected StupidSimpleSingleSelectionModel selectionModel;
protected ObservableList handles = FXCollections.observableArrayList();
protected Map boxByHandle = new HashMap<>();
protected OptionalNodePane withDesc;
//-------------------------------------------------------------------
public CharactersOverviewPage() {
super(ResourceI18N.get(RES, "page.title"));
selectionModel = new StupidSimpleSingleSelectionModel<>(handles);
initComponents();
initLayout();
initInteractivity();
setOnEnterAction(ev -> logger.log(Level.WARNING, "onEnter: "+ev));
setOnLeaveAction(ev -> logger.log(Level.WARNING, "onLeave: "+ev));
refresh();
}
//-------------------------------------------------------------------
private void initComponents() {
// Left/Main side
btnCreate = new AppBarButton(ResourceI18N.get(RES, "button.create"), new SymbolIcon("add"));
btnSynchronize = new AppBarButton(ResourceI18N.get(RES, "button.sync"), new SymbolIcon("sync"));
cbCharacters = new CommandBar();
cbCharacters.getPrimaryCommands().addAll(btnCreate, btnSynchronize);
tpCharacters = new TilePane();
tpCharacters.setPrefTileHeight(96);
tpCharacters.setPrefTileWidth(330);
tpCharacters.setHgap(10);
tpCharacters.setVgap(10);
}
//-------------------------------------------------------------------
private void initLayout() {
VBox bxMain = new VBox(10, cbCharacters, tpCharacters);
VBox.setVgrow(tpCharacters, Priority.ALWAYS);
bxMain.setMaxWidth(Double.MAX_VALUE);
bxLayout = new HBox(20, bxMain);
HBox.setHgrow(bxMain, Priority.ALWAYS);
withDesc = new OptionalNodePane(bxLayout, new Label(""));
bxLayout.setMaxHeight(Double.MAX_VALUE);
withDesc.setMaxHeight(Double.MAX_VALUE);
VBox.setVgrow(withDesc, Priority.ALWAYS);
setContent(withDesc);
}
//-------------------------------------------------------------------
private void initInteractivity() {
btnCreate.setOnAction(ev -> createClicked());
btnSynchronize.setOnAction(ev -> synchronizeClicked());
CharacterProviderLoader.getCharacterProvider().setListener(new CharacterProviderListener() {
public void characterRemoved(CharacterHandle handle) {
logger.log(Level.WARNING, "REMOVED");
CharacterHandleBox box = boxByHandle.get(handle);
handles.remove(handle);
boxByHandle.remove(handle);
if (box!=null) {
tpCharacters.getChildren().remove(box);
}
}
public void characterModified(CharacterHandle handle) {
logger.log(Level.WARNING, "MODIFIED");
refresh();
}
public void characterAdded(CharacterHandle handle) {
logger.log(Level.WARNING, "ADDED");
refresh();
}
});
selectionModel.selectedItemProperty().addListener( (ov,o,n) -> {
logger.log(Level.WARNING, "To Do: show PDF");
// if (n==null) {
// withDesc.setOptional(null);
// } else {
// Label mess = new Label("When this is finsihed, this panel should show the first page of the PDF of the character "+n.getName()+"- or some kind of brief summary");
// mess.setWrapText(true);
// withDesc.setOptional(mess);
// }
});
BabylonEventBus.add(this);
// Add drag&drop listener
this.setOnDragEntered(ev -> onDragged(ev));
this.setOnDragOver(ev -> onDragOver(ev));
this.setOnDragDropped(ev -> onDragDropped(ev));
}
//-------------------------------------------------------------------
protected void onDragged(DragEvent event) {
logger.log(Level.ERROR, "Drag entered: "+event.getDragboard().getFiles());
if (event.getGestureSource() == withDesc) return;
if (event.getGestureSource() != withDesc && event.getDragboard().hasString()) {
//withDesc.setFill(Color.GREEN);
}
event.consume();
}
//-------------------------------------------------------------------
protected void onDragOver(DragEvent event) {
boolean foundXML = false;
for (File file : event.getDragboard().getFiles()) {
if (file.exists() && file.canRead() && file.getName().toLowerCase().endsWith(".xml")) {
foundXML = true;
}
}
if (event.getGestureSource() != this && event.getDragboard().hasFiles() && foundXML) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
event.consume();
}
//-------------------------------------------------------------------
public void onDragDropped(DragEvent event) {
boolean success = false;
for (File file : event.getDragboard().getFiles()) {
if (file.exists() && file.canRead() && file.getName().toLowerCase().endsWith(".xml")) {
logger.log(Level.WARNING, "TODO: Import {0}", file);
try {
byte[] raw = Files.readAllBytes(file.toPath());
String fname = file.getName().substring(0, file.getName().length()-4);
importCharacter(raw, fname);
success = true;
} catch (Exception e) {
logger.log(Level.ERROR, "Failed to import character from "+file,e);
}
}
}
event.setDropCompleted(success);
event.consume();
}
//-------------------------------------------------------------------
/**
* Overload in RPG specific implementations
*/
protected void styleCharacterHandleBox(CharacterHandle charac, CharacterHandleBox card) {
}
//-------------------------------------------------------------------
private void initPerCharacterInteractivity(CharacterHandle charac, CharacterHandleBox card) {
if (charac==null)
throw new NullPointerException("CharacterHandle may not be null");
card.setOnMouseClicked(new EventHandler() {
public void handle(MouseEvent event) {
logger.log(Level.WARNING, "Event "+event.getEventType()+" from "+event.getSource());
// TODO Auto-generated method stub
if (event.getClickCount() == 1) {
//do something when it's clicked
logger.log(Level.DEBUG, "Clicked "+charac);
boolean oldState = selectionModel.isSelected(handles.indexOf(charac));
logger.log(Level.INFO, "Old state: "+oldState);
card.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, !oldState);
if (oldState) {
selectionModel.clearSelection(handles.indexOf(charac));
} else {
selectionModel.select(handles.indexOf(charac));
}
// Eventually load character
if (!oldState && charac.getCharacter()==null) {
// Character has been selected, but not loaded yet
logger.log(Level.INFO, "Trying to load character "+charac);
Attachment attach = null;
try {
attach = CharacterProviderLoader.getCharacterProvider().getFirstAttachment(charac, Type.CHARACTER, Format.RULESPECIFIC);
} catch (Exception e) {
logger.log(Level.ERROR, "Error loading character attachment",e);
StringWriter buf = new StringWriter();
e.printStackTrace(new PrintWriter(buf));
AlertManager.showAlertAndCall(AlertType.ERROR,
ResourceI18N.get(RES, "error.loading.char.title"),
ResourceI18N.format(RES,"error.loading.char.message", buf.toString())
);
}
if (attach==null) {
logger.log(Level.WARNING, "No rule-specific character attachment for handle "+charac);
return;
}
try {
RuleSpecificCharacterObject,?,?,?> parsed = loadRuleSpecific(attach.getData());
logger.log(Level.INFO, "Parsed character: "+parsed);
charac.setCharacter(parsed);
charac.setShortDescription(parsed.getShortDescription());
card.setHandle(charac);
} catch (CharacterIOException e) {
e.setPath(attach.getLocalFile()+"");
logger.log(Level.ERROR, "Error parsing character attachment "+attach.getLocalFile(),e);
showException(e);
} catch (Exception e) {
logger.log(Level.ERROR, "Error parsing character attachment",e);
StringWriter buf = new StringWriter();
e.printStackTrace(new PrintWriter(buf));
AlertManager.showAlertAndCall(AlertType.ERROR,
ResourceI18N.get(RES, "error.loading.char.title"),
ResourceI18N.format(RES,"error.loading.char.message", buf.toString())
);
}
}
}
if (event.getClickCount() >= 2) {
//do something when it's clicked
logger.log(Level.DEBUG, "Double Clicked "+charac);
openClicked(charac);
}
}
});
card.setOnDelete(ev -> {
logger.log(Level.INFO, "Delete");
deleteClicked(charac);
});
card.setOnOpen(ev -> {
logger.log(Level.INFO, "Open "+charac.getName());
openClicked(charac);
});
card.setOnExport(ev -> {
logger.log(Level.WARNING, "Export");
exportClicked(charac);
});
card.setOnOpenDir(ev -> {
logger.log(Level.INFO, "Open Directory");
openDirectoryClicked(charac);
});
}
//-------------------------------------------------------------------
/**
* @see org.prelle.javafx.ResponsiveControl#setResponsiveMode(org.prelle.javafx.WindowMode)
*/
@Override
public void setResponsiveMode(WindowMode value) {
// TODO Auto-generated method stub
logger.log(Level.WARNING, "***ToDo**********Set mode to "+value);
}
//-------------------------------------------------------------------
protected abstract CharacterGenerator, ?> createCharacterGenerator();
//-------------------------------------------------------------------
/**
* Depending on the model, create either a Generator or a Leveller
*/
protected abstract CharacterController, ?> createCharacterController(RuleSpecificCharacterObject,?,?,?> model, CharacterHandle handle);
//-------------------------------------------------------------------
protected abstract CharacterViewLayout,?,?> createCharacterAppLayout(CharacterController, ?> control);
//-------------------------------------------------------------------
/**
* Load the rule specific character object into the handle.
* @return NULL, if everything worked - an error message otherwise
*/
protected abstract RuleSpecificCharacterObject,?,?,?> loadRuleSpecific(byte[] raw) throws Exception;
//-------------------------------------------------------------------
private void createClicked() {
logger.log(Level.INFO, "ENTER createClicked()---------------------------------------------------------------------");
try {
CharacterGenerator,?> charGen = createCharacterGenerator();
CharacterViewLayout,?,?> layout = createCharacterAppLayout(charGen);
logger.log(Level.DEBUG, "open character view layout "+layout);
logger.log(Level.DEBUG, "open character view layout2 "+getAppLayout());
getAppLayout().getApplication().openScreen(layout);
charGen.runProcessors();
logger.log(Level.INFO, "calling startCreation() on "+layout.getClass());
layout.startCreation(charGen);
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.ERROR, "Error on createClicked()",e);
} finally {
logger.log(Level.INFO, "LEAVE createClicked()---------------------------------------------------------------------");
}
}
//-------------------------------------------------------------------
protected void synchronizeClicked() {
CharacterProviderLoader.getCharacterProvider().initiateCharacterSynchronization();
}
//-------------------------------------------------------------------
protected abstract List loadCharacters();
//-------------------------------------------------------------------
public void refresh() {
logger.log(Level.TRACE, "ENTER refresh()");
try {
tpCharacters.getChildren().clear();
List list = loadCharacters();
handles.setAll(list);
boxByHandle.clear();
// tpCharacters.getItems().setAll(list);
// logger.log(Level.DEBUG, "Display "+list.size()+" characters");
for (CharacterHandle charac : list) {
CharacterHandleBox card = new CharacterHandleBox();
card.getStyleClass().addAll("grid-cell", "character-handle-box");
logger.log(Level.DEBUG, "handle {0} / {1} has char {2}", charac.getUUID(), charac.getName(), charac.getCharacter());
if (charac.getCharacter() != null) {
if (charac.getCharacter().isInCareerMode()) {
card.getStyleClass().add("career-mode");
} else {
card.getStyleClass().add("chargen-mode");
}
}
card.setHandle(charac);
boxByHandle.put(charac, card);
card.setUserData(charac);
styleCharacterHandleBox(charac, card);
initPerCharacterInteractivity(charac, card);
tpCharacters.getChildren().add(card);
}
boolean syncSupported = CharacterProviderLoader.getCharacterProvider().isSynchronizeSupported();
if (syncSupported && !cbCharacters.getPrimaryCommands().contains(btnSynchronize)) {
cbCharacters.getPrimaryCommands().add(btnSynchronize);
} else if (!syncSupported && cbCharacters.getPrimaryCommands().contains(btnSynchronize)) {
cbCharacters.getPrimaryCommands().remove(btnSynchronize);
}
selectionModel.getSelectedItems().addListener(new ListChangeListener() {
@Override
public void onChanged(Change extends CharacterHandle> c) {
while (c.next()) {
if (c.wasRemoved()) {
for (CharacterHandle index : c.getRemoved()) {
CharacterHandleBox box = boxByHandle.get(index);
if (box == null) {
logger.log(Level.WARNING, "No CharacterHandleBox for index {0}", index);
continue;
}
if (c.getRemoved().contains(index)) {
box.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, false);
}
}
}
if (c.wasAdded()) {
for (CharacterHandle index : c.getRemoved()) {
CharacterHandleBox box = boxByHandle.get(index);
if (c.getAddedSubList().contains(index)) {
box.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
}
}
}
}
}
});
} finally {
logger.log(Level.DEBUG, "LEAVE refresh()");
}
}
//-------------------------------------------------------------------
@Override
public void characterAdded(CharacterHandle handle) {
refresh();
}
//-------------------------------------------------------------------
/**
* @see de.rpgframework.character.CharacterProviderListener#characterModified(de.rpgframework.character.CharacterHandle)
*/
@Override
public void characterModified(CharacterHandle handle) {
refresh();
}
//-------------------------------------------------------------------
/**
* @see de.rpgframework.character.CharacterProviderListener#characterRemoved(de.rpgframework.character.CharacterHandle)
*/
@Override
public void characterRemoved(CharacterHandle handle) {
refresh();
}
//-------------------------------------------------------------------
private void deleteClicked(CharacterHandle selected) {
logger.log(Level.DEBUG, "Delete "+selected);
CloseType response = AlertManager.showAlertAndCall(
AlertType.CONFIRMATION,
ResourceI18N.get(RES,"confirm.delete.char"),
ResourceI18N.format(RES,"confirm.delete.char.long", selected.getRuleIdentifier().getName(Locale.getDefault()), selected.getName())
);
logger.log(Level.DEBUG, "Response was "+response);
if (response==CloseType.OK || response==CloseType.YES) {
logger.log(Level.INFO, "delete character "+selected);
try {
CharacterProvider charProv = CharacterProviderLoader.getCharacterProvider();
charProv.deleteCharacter(selected);
handles.remove(selected);
} catch (Exception e) {
StringWriter buf = new StringWriter();
e.printStackTrace(new PrintWriter(buf));
AlertManager.showAlertAndCall(AlertType.ERROR,
ResourceI18N.get(RES, "error.deleting.char.title"),
ResourceI18N.format(RES,"error.deleting.char.message", buf.toString())
);
}
}
};
//-------------------------------------------------------------------
private void openClicked(CharacterHandle charac) {
//do something when it's clicked
logger.log(Level.DEBUG, "open "+charac);
RuleSpecificCharacterObject,?,?,?> model = charac.getCharacter();
if (model==null) {
logger.log(Level.ERROR, "No character attachment in character "+charac.getName());
return;
}
logger.log(Level.DEBUG, "Create character controller");
CharacterController, ?> controller = createCharacterController(model, charac);
logger.log(Level.DEBUG, "Created character controller: {0}", controller);
logger.log(Level.DEBUG, "Create CharacterViewLayout");
CharacterViewLayout view = createCharacterAppLayout(controller);
logger.log(Level.DEBUG, "Created CharacterViewLayout "+view);
view.setController(controller);
if (!model.isInCareerMode()) {
logger.log(Level.INFO, "Continue creation for "+model+" using "+view.getClass());
getAppLayout().getApplication().openScreen(view);
logger.log(Level.INFO, "Call continueCreation");
view.continueCreation(model, charac);
} else {
logger.log(Level.INFO, "Edit in career mode for "+model+" using "+view.getClass());
getAppLayout().getApplication().openScreen(view);
view.edit(model, charac);
}
}
//-------------------------------------------------------------------
public boolean needsCreationModeWarning(CharacterHandle charac) {
return !charac.getCharacter().isInCareerMode();
}
//-------------------------------------------------------------------
public String getCreationModeWarning(CharacterHandle charac) {
return ResourceI18N.get(CharacterExportPluginConfigPane.RES, "dialog.exportcreationmodewarning.message");
}
//-------------------------------------------------------------------
protected void exportClicked(CharacterHandle charac) {
logger.log(Level.WARNING, "export "+charac);
// Check if warning message is necessary
if (needsCreationModeWarning(charac)) {
getAppLayout().getApplication().showAlertAndCall(org.prelle.javafx.AlertType.NOTIFICATION, "", getCreationModeWarning(charac));
}
CharacterExportPluginSelectorPane selector = new CharacterExportPluginSelectorPane(charac.getCharacter());
// ApplicationScreen screen = new ApplicationScreen(selector);
// getAppLayout().getApplication().openScreen(screen);
ManagedDialog dialog = new ManagedDialog(
ResourceI18N.get(CharacterExportPluginConfigPane.RES, "dialog.exportpluginselector.title")+" "+charac.getName(),
selector, CloseType.BACK);
selector.setToClose(dialog);
getAppLayout().getApplication().showAlertAndCall(dialog, null);
}
//-------------------------------------------------------------------
private void openDirectoryClicked(CharacterHandle charac) {
//do something when it's clicked
logger.log(Level.DEBUG, "open directory off "+charac);
logger.log(Level.WARNING,charac.getPath());
BabylonEventBus.fireEvent(BabylonEventType.OPEN_FILE, charac.getPath());
}
//-------------------------------------------------------------------
private void showException(CharacterIOException e) {
StringWriter buf = new StringWriter();
if (e.getCause()!=null)
e.getCause().printStackTrace(new PrintWriter(buf));
else
e.printStackTrace(new PrintWriter(buf));
Label trace = new Label(buf.toString());
trace.setWrapText(true);
VBox content = new VBox(5);
Label errorName = new Label();
Label errorExpl = new Label();
errorName.getStyleClass().add(JavaFXConstants.STYLE_HEADING4);
String i18nKeyName = "error.chario."+e.getCode().name().toLowerCase()+".title";
String i18nKeyExplain = "error.chario."+e.getCode().name().toLowerCase()+".explain";
errorName.setText(ResourceI18N.get(RES, i18nKeyName));
errorExpl.setText(ResourceI18N.get(RES, i18nKeyExplain));
content.getChildren().addAll(errorExpl, trace);
if (e.getPath()!=null) {
Label path = new Label(e.getPath());
path.getStyleClass().add(JavaFXConstants.STYLE_HEADING5);
content.getChildren().add(1, path);
}
AlertManager.showAlertAndCall(AlertType.ERROR,
ResourceI18N.get(RES, i18nKeyName),
content
);
}
//-------------------------------------------------------------------
protected void showAnyException(Exception e, RuleSpecificCharacterObject,?,?,?> model, String title, String message) {
StringWriter out = new StringWriter();
if (e!=null)
e.printStackTrace(new PrintWriter(out));
Label mess = new Label((message!=null)?message:e.getMessage());
Label trace = new Label(out.toString());
mess.setWrapText(true);
trace.setWrapText(true);
AlertManager.showAlertAndCall(AlertType.ERROR, title, new VBox(10, mess, trace));
}
//-------------------------------------------------------------------
/**
* @see de.rpgframework.core.BabylonEventListener#handleAppEvent(de.rpgframework.core.BabylonEvent)
*/
@SuppressWarnings("incomplete-switch")
@Override
public void handleAppEvent(BabylonEvent event) {
logger.log(Level.DEBUG, "RCV "+event);
switch (event.getType()) {
case CHAR_ADDED:
case CHAR_REMOVED:
case CHAR_MODIFIED:
case CHAR_RENAMED:
refresh();
break;
case EDEN_STATE_CHANGED:
try {
EdenConnectionState state = (EdenConnectionState) event.getData()[0];
btnSynchronize.setDisable(state==EdenConnectionState.NOT_CONNECTED);
} catch (Exception e) {
logger.log(Level.ERROR, "Unexpected paramter in "+event,e);
}
break;
}
}
//-------------------------------------------------------------------
protected abstract void importCharacter(byte[] raw, String filename) throws Exception;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy