All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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