/* The MIT License (MIT) Copyright (c) 2017 Pierre Lindenbaum 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. */ package com.github.lindenb.jvarkit.tools.jfx; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import com.github.lindenb.jvarkit.jfx.components.FileChooserPane; import com.github.lindenb.jvarkit.jfx.components.FilesChooserPane; import javafx.application.Application; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Rectangle2D; import javafx.scene.Parent; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.ScrollPane; import javafx.scene.control.Spinner; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.stage.Screen; import javafx.stage.Stage; public abstract class AbstractJfxApplication extends Application { protected static final PrintStream realStderr=System.err; protected static final PrintStream realStdout=System.out; @FXML private Button runCommandButton; @FXML private Button cancelCommandButton; @FXML protected TextArea console; protected PrintStream printToConsole; protected Thread commandThread=null; protected AbstractJfxApplication() { } @FXML protected void doMenuAbout(final ActionEvent event) { final Alert alert = new Alert(AlertType.INFORMATION); alert.setHeaderText("About..."); alert.setContentText("Pierre Lindenbaum PhD. Institut du Thorax. Nantes. France."); alert.showAndWait(); } @FXML protected void doMenuQuit(final ActionEvent event) { // http://stackoverflow.com/questions/12153622 Platform.exit(); } @Override public void start(Stage stage) throws Exception { Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds(); stage.setX(50); stage.setY(50); stage.setWidth(primaryScreenBounds.getWidth()-100); stage.setHeight(primaryScreenBounds.getHeight()-100); stage.show(); this.runCommandButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { doCommandStart(event); } }); this.cancelCommandButton.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { doCommandEnd(event); } }); this.cancelCommandButton.setDisable(true); this.printToConsole= new PrintStream(new Console(console),true); } protected abstract Runnable createRunnable() throws JFXException; protected void displayAlert(final Throwable err) { final Alert alert = new Alert(AlertType.ERROR); alert.setHeaderText("Cannot create Command."); alert.setContentText(String.valueOf(err.getMessage())); // Create expandable Exception. StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); err.printStackTrace(pw); TextArea textArea = new TextArea(sw.toString()); textArea.setEditable(false); textArea.setWrapText(true); BorderPane pane=new BorderPane(new ScrollPane(textArea)); alert.getDialogPane().setExpandableContent(pane); alert.showAndWait(); } private void doCommandStart(final ActionEvent event) { doCommandEnd(event); final Runnable target; try { target = createRunnable(); if( target ==null ) return; } catch(final Throwable err) { err.printStackTrace(realStderr); displayAlert(err); return; } synchronized(AbstractJfxApplication.class) { try { this.commandThread=null; this.runCommandButton.setDisable(true); this.cancelCommandButton.setDisable(false); this.commandThread = new Thread(new RunnerDelegate(target)); this.commandThread.start(); } catch(final Throwable err) { err.printStackTrace(realStderr); System.setErr(realStderr); System.setOut(realStdout); } } } private void doCommandEnd(final ActionEvent event) { synchronized(AbstractJfxApplication.class) { this.runCommandButton.setDisable(false); this.cancelCommandButton.setDisable(true); if(this.commandThread==null) return; try { this.commandThread.interrupt(); } catch(Throwable err) { } this.commandThread=null; System.setErr(realStderr); System.setOut(realStdout); } } protected Parent fxmlLoad(final String resource) throws Exception { try { java.net.URL url= getClass().getResource(resource); if(url==null) throw new java.io.IOException("cannot get resource \""+resource+"\" for Class:"+this.getClass()); final FXMLLoader loader = new FXMLLoader(url); loader.setController(this); return loader.load(); } catch(final Exception err) { err.printStackTrace(); throw err; } } private class RunnerDelegate implements Runnable { final Runnable delegate; RunnerDelegate(final Runnable delegate) { this.delegate = delegate; } @Override public void run() { try { System.setErr(AbstractJfxApplication.this.printToConsole); //not stdout, things like snpeff write to stdout this.delegate.run(); } catch(final Throwable err) { err.printStackTrace(realStderr); } Platform.runLater(new Runnable() { @Override public void run() { doCommandEnd(null); } }); } } protected class OptionBuilder { protected final Parent component; protected final String option; protected int _minCardinality=0; protected int _maxCardinality=-1; protected Pattern splitPattern=null; protected Class<?> itemClass=null; public OptionBuilder(final Parent component,final String option) throws JFXException { this.component=component; this.option=option; if(this.component==null) throw new JFXException("component is null in ctor ("+option+")"); if(this.option==null) throw new JFXException("opt is null in ctor"); } protected void fill(final List<String> args,String s) { if(this.option.startsWith("-")) { args.add(this.option); args.add(s); } else if(this.option.endsWith("="))//picard like option { args.add(this.option+s); } } protected String validateType(String s) throws JFXException { if(itemClass!=null) { try { this.itemClass.getConstructor(String.class).newInstance(s); } catch (Exception e) { throw new JFXException("Cannot cast "+s +" to "+itemClass,e); } } return s; } public OptionBuilder minCardinality(int v) { this._minCardinality=v; return this;} public OptionBuilder maxCardinality(int v) { this._maxCardinality=v; return this;} public OptionBuilder split(final Pattern pat) { this.splitPattern=pat; return this;} public OptionBuilder split() {return this.split(Pattern.compile("[\\s]+"));} public OptionBuilder itemClass(Class<?> C) { this.itemClass=C;return this;} protected void validateCardinality(List<?> list) throws JFXException { if(list.size()<this._minCardinality) throw new JFXException("Expected at least "+this._minCardinality+" items for "+this.option); if(this._maxCardinality!=-1 && list.size()>this._maxCardinality) throw new JFXException("Expected less than "+this._maxCardinality+" items for "+this.option); } public void fill(final List<String> args) throws JFXException { if( component==null) { throw new JFXException("component is null ("+this.option+")"); } else if(component instanceof FileChooserPane){ final FileChooserPane comp = FileChooserPane.class.cast(component); final File f= comp.getSelectedFile(); if(comp.isRequired() && f==null) throw new JFXException("missing file for "+this.option); if(f==null) return; fill(args,f.getPath()); } else if(component instanceof FilesChooserPane){ final FilesChooserPane comp = FilesChooserPane.class.cast(component); final List<File> list= comp.getSelectedFiles(); if(list.size()<comp.getMinCardinality()) throw new JFXException("Expected at least "+comp.getMinCardinality()+" items for "+this.option); if(comp.getMaxCardinality()!=-1 && list.size()>comp.getMaxCardinality()) throw new JFXException("Expected less than "+comp.getMaxCardinality()+" items for "+this.option); for(final File f: list) { fill(args,f.getPath()); } } else if(component instanceof CheckBox) { final CheckBox comp = CheckBox.class.cast(component); if(this.option.endsWith("=")) //picard { fill(args,Boolean.toString(comp.isSelected())); } else if(comp.isSelected()) { args.add(this.option); } } else if(component instanceof Spinner) { Spinner<?> comp = Spinner.class.cast(component); Object v=comp.getValue(); fill(args,String.valueOf(v)); } else if(component instanceof ComboBox) { ComboBox<?> comp = ComboBox.class.cast(component); Object o=comp.getValue(); if(o==null) return; String s=o.toString(); if(s.isEmpty()) return; fill(args,validateType(s)); } else if(component instanceof TextArea) { TextArea comp = TextArea.class.cast(component); final List<String> list=new ArrayList<>(); for(final String s: comp.getText().split("[\n]")) { if(s.trim().isEmpty() || s.startsWith("#")) continue; list.add(s); } validateCardinality(list); for(final String s: list) { fill(args,validateType(s)); } } else if(component instanceof TextField) { final TextField comp = TextField.class.cast(component); if(this.splitPattern==null) { String s= comp.getText().trim(); if(s.isEmpty()) return; fill(args,validateType(s)); } else { List<String> list=new ArrayList<>(); for(final String s: this.splitPattern.split(comp.getText())) { if(s.trim().isEmpty()) continue; list.add(s); } validateCardinality(list); for(final String s: list) { fill(args,validateType(s)); } } } else { throw new JFXException("undefined Class of component ("+this.option+") "+this.component.getClass()); } } } private class Console extends OutputStream { private final TextArea output; public Console(TextArea ta) { this.output = ta; } @Override public void write(byte[] b, int off, int len) throws IOException { final String str=new String(b, off, len); if(str.isEmpty()) return; Platform.runLater(new Runnable() { @Override public void run() { output.appendText(str); if(output.getLength()>50000) { output.deleteText(0, output.getLength()-1000); } } }); } @Override public void write(byte[] b) throws IOException { super.write(b,0,b.length); } @Override public void write( int c) throws IOException { if(c==-1) return; byte a[]=new byte[]{(byte)c}; a[0]=(byte)c; write(a); } } public static class JFXException extends Exception { private static final long serialVersionUID = 1L; public JFXException() { super();} public JFXException(final String msg) { super(msg);} public JFXException(final Exception err) { super(err);} public JFXException(final String msg,final Exception err) { super(msg,err);} } }