com.rvantwisk.cnctools.controllers.CNCToolsController.java Source code

Java tutorial

Introduction

Here is the source code for com.rvantwisk.cnctools.controllers.CNCToolsController.java

Source

/*
 * Copyright (c) 2013, R. van Twisk
 * All rights reserved.
 * Licensed under the The BSD 3-Clause License;
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 * http://opensource.org/licenses/BSD-3-Clause
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of the aic-util nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.rvantwisk.cnctools.controllers;

import com.rvantwisk.cnctools.ScreensConfiguration;
import com.rvantwisk.cnctools.data.*;
import com.rvantwisk.cnctools.data.tools.BallMill;
import com.rvantwisk.cnctools.data.tools.EndMill;
import com.rvantwisk.cnctools.misc.*;
import com.rvantwisk.cnctools.operations.createRoundStock.RoundStockModel;
import com.rvantwisk.cnctools.operations.customgcode.CustomGCodeController;
import com.rvantwisk.cnctools.operations.customgcode.GCodeTaskModel;
import com.rvantwisk.gcodegenerator.GCodeCollection;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;
import javafx.stage.WindowEvent;
import javafx.util.Callback;
import jfxtras.labs.dialogs.MonologFX;
import jfxtras.labs.dialogs.MonologFXButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;

/**
 * Created with IntelliJ IDEA.
 * User: rvt
 * Date: 10/5/13
 * Time: 6:11 PM
 * To change this template use File | Settings | File Templates.
 */
@SuppressWarnings("SpringJavaAutowiringInspection")
public class CNCToolsController extends AbstractController {
    private static final String SEPERATOR = System.getProperty("line.separator");
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @FXML
    TextArea descriptionValue;
    @FXML
    TableView tbl_millTasks;
    @FXML
    ListView<Project> v_projectList;
    @FXML
    TableColumn<TaskRunnable, Boolean> milltaskEnabled;
    @FXML
    Button addMillTask;
    @FXML
    Button removeMilltask;
    @FXML
    Button editMilltask;
    @FXML
    Button deleteProject;
    @FXML
    Button generateGCode;
    @FXML
    Button btnView;
    @FXML
    Button btnPostProcessor;
    @Autowired
    private ProjectModel projectModel;
    @Autowired
    private ToolDBManager toolDBManager;
    @FXML
    private ResourceBundle resources;
    @FXML
    private URL location;
    @FXML
    private AnchorPane details;
    @FXML
    private Label displayedIssueLabel;
    @FXML
    private Button btnMoveTaskDown;
    @FXML
    private Button btnMoveTaskUp;
    private ScreensConfiguration screens;

    public CNCToolsController(ScreensConfiguration screens) {
        this.screens = screens;
    }

    /**
     * Show's a dialog's with gcode from file
     *
     * @param project
     * @param fName
     */
    public static void showGCodeFromFile(final Project project, final String fName) {
        FXMLDialog dialog = ScreensConfiguration.getInstance().taskEditDialog();

        GCodeTaskModel model = new GCodeTaskModel();
        model.setgCodeFile(fName);
        model.setReferencedFile(true);

        TaskRunnable taskRunnable = new TaskRunnable("G-Code", "", CustomGCodeController.class.getName(),
                "CustomGCode.fxml");
        taskRunnable.setMilltaskModel(model);

        ScreensConfiguration.getInstance().registerBean(taskRunnable.getClassName());
        TaskEditController controller = dialog.getController();
        controller.setViewAs(TaskEditController.ViewAs.CLOSE);
        controller.setTask(project, taskRunnable.copy());
        dialog.showAndWait();
    }

    @FXML
    public void addProject(ActionEvent event) throws Exception {
        try {
            screens.projectDialog().showAndWait();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void showAbout(ActionEvent event) throws Exception {
        try {
            screens.aboutDialog().showAndWait();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void onPostProcessorConfig(ActionEvent actionEvent) {
        try {
            screens.postProcessorsDialog().showAndWait();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void generateGCode(ActionEvent event) throws Exception {
        if (v_projectList.getSelectionModel().selectedItemProperty().get() != null) {
            final Project p = v_projectList.getSelectionModel().selectedItemProperty().get();

            if (p.postProcessorProperty().get() == null) {
                MonologFX dialog = new MonologFX(MonologFX.Type.QUESTION);
                dialog.setTitleText("No postprocessor");
                dialog.setMessage("No post processor configured, please select a post processor first!");
                dialog.show();
            } else {

                final GCodeCollection gCode = p.getGCode(toolDBManager);
                gCode.merge();

                FileChooser fileChooser = new FileChooser();
                fileChooser.getExtensionFilters()
                        .addAll(new FileChooser.ExtensionFilter("NC File", "*.tap", "*.ngc"));
                fileChooser.setTitle("Save GCode");
                File choosenName = fileChooser.showSaveDialog(null);
                if (choosenName != null) {
                    choosenName.delete();

                    if (!p.getPostProcessor().isHasToolChanger()) {
                        final GCodeCollection.GeneratedGCode preAmble = gCode.get(0);
                        final GCodeCollection.GeneratedGCode postAmble = gCode.get(gCode.size() - 1);
                        for (int i = 1; i <= (gCode.size() - 2); i++) {
                            final String[] path = choosenName.getPath().split("\\.(?=[^\\.]+$)");
                            File thisSet = new File(path[0] + "-" + i + "." + path[1]);
                            thisSet.delete();
                            // Concate all g-code
                            String file = preAmble.getGCode().toString() + SEPERATOR
                                    + gCode.get(i).getGCode().toString() + SEPERATOR
                                    + postAmble.getGCode().toString() + SEPERATOR;
                            saveGCode(file, thisSet);
                        }
                    } else {
                        saveGCode(gCode.concate().toString(), choosenName);
                    }
                }

            }

        }
    }

    private void saveGCode(String gCode, final File filename) {
        if (gCode == null) {
            throw new IllegalArgumentException("gCode most not be null");
        }
        if (filename == null) {
            throw new IllegalArgumentException("filename most not be null");
        }
        try (BufferedWriter br = Files.newBufferedWriter(filename.toPath(), Charset.forName("UTF-8"),
                new OpenOption[] { StandardOpenOption.CREATE_NEW })) {

            // Cleanup empty lines
            // We do this currently in memory because we don't expect large files anyways
            while (gCode.lastIndexOf(SEPERATOR + SEPERATOR) != -1) {
                gCode = gCode.replaceAll(SEPERATOR + SEPERATOR, SEPERATOR);
            }

            br.write(gCode.trim());
            br.flush();
            br.close();
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }
    }

    @FXML
    public void deleteProject(ActionEvent event) throws Exception {
        try {
            if (v_projectList.getSelectionModel().selectedItemProperty().get() != null) {
                MonologFX dialog = new MonologFX(MonologFX.Type.QUESTION);
                dialog.setTitleText("Deleting a project");
                dialog.setMessage("Are you sure you want to delete this project?");
                if (dialog.show() == MonologFXButton.Type.YES) {
                    Project p = v_projectList.getSelectionModel().getSelectedItem();
                    descriptionValue.setText("");
                    projectModel.projectsProperty().remove(v_projectList.getSelectionModel().getSelectedItem());
                }
            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void machinesConfiguration(ActionEvent event) throws Exception {
    }

    @FXML
    public void toolsConfiguration(ActionEvent event) throws Exception {
        try {
            screens.toolConfigurationsDialog().showAndWait();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void addMillTask(ActionEvent event) throws Exception {
        try {
            FXMLDialog mt = screens.millTaskDialog();
            AddMillTaskController mtc = mt.getController();
            mtc.setCurrentProject(v_projectList.getSelectionModel().getSelectedItem());
            mt.showAndWait();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void removeMillTask(ActionEvent event) throws Exception {
        try {
            if (tbl_millTasks.getSelectionModel().getSelectedCells().size() > 0) {
                MonologFX dialog = new MonologFX(MonologFX.Type.QUESTION);
                dialog.setTitleText("Deleting a milltask");
                dialog.setMessage("Are you sure you want to delete this task?");
                if (dialog.show() == MonologFXButton.Type.YES) {
                    Project project = projectModel.projectsProperty()
                            .get(v_projectList.getSelectionModel().getSelectedIndex());
                    project.millTasksProperty().remove(tbl_millTasks.getSelectionModel().getSelectedIndex());
                }
            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void editMillTask(ActionEvent event) throws Exception {
        try {
            final Project project = v_projectList.getSelectionModel().selectedItemProperty().get();
            TaskRunnable taskRunnable = (TaskRunnable) tbl_millTasks.getSelectionModel().selectedItemProperty()
                    .get();
            screens.registerBean(taskRunnable.getClassName());
            FXMLDialog dialog = screens.taskEditDialog();
            TaskEditController controller = dialog.getController();
            controller.setTask(project, taskRunnable.copy());
            dialog.showAndWait();
            if (controller.getReturned() == Result.SAVE) {
                int index = tbl_millTasks.getSelectionModel().getSelectedIndex();
                TaskRunnable modified = controller.getTask();
                project.millTasksProperty().add(index, modified);
                project.millTasksProperty().remove(tbl_millTasks.getItems().indexOf(taskRunnable));
                tbl_millTasks.getSelectionModel().select(modified);
            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    private void addProjectDefaults() {
        Project p = new Project("Round stock 30mm", "Creates a round stock of 100mx30mm. Feedrate 2400");
        projectModel.addProject(p);
        TaskRunnable mt = new TaskRunnable("Make Round", "Make square stock round (invalid milltask)",
                "com.rvantwisk.cnctools.operations.createRoundStock.CreateRoundStockController",
                "CreateRoundStock.fxml");

        ToolParameter nt = Factory.newTool();
        nt.setName("6MM end Mill");
        mt.setMilltaskModel(new RoundStockModel(new SimpleStringProperty(""), DimensionProperty.DimMM(30.0),
                DimensionProperty.DimMM(20.0), DimensionProperty.DimMM(100.0)));
        p.millTasksProperty().add(mt);

        mt = new TaskRunnable("Make Square", "Make round stock square",
                "com.rvantwisk.cnctools.operations.createRoundStock.CreateRoundStockController",
                "CreateRoundStock.fxml");
        nt = Factory.newTool();
        nt.setName("8MM end Mill");
        nt.radialDepthProperty().set(DimensionProperty.DimMM(4.0));
        nt.axialDepthProperty().set(DimensionProperty.DimMM(4.0));
        nt.setToolType(new EndMill(new DimensionProperty(8.0, Dimensions.Dim.MM)));
        mt.setMilltaskModel(new RoundStockModel(new SimpleStringProperty(""), DimensionProperty.DimMM(30.0),
                DimensionProperty.DimMM(20.0), DimensionProperty.DimMM(100.0)));
        p.millTasksProperty().add(mt);

        p = new Project("Facing 30mmx30mm", "Facing of wood. Feedrate 2400");
        mt = new TaskRunnable("Make Round Form Square", "Make feed edge",
                "com.rvantwisk.cnctools.operations.createRoundStock.CreateRoundStockController",
                "CreateRoundStock.fxml");
        nt = Factory.newTool();
        nt.setName("10mm end Mill");
        nt.radialDepthProperty().set(DimensionProperty.DimMM(5.0));
        nt.axialDepthProperty().set(DimensionProperty.DimMM(5.0));
        nt.setToolType(new EndMill(new DimensionProperty(10.0, Dimensions.Dim.MM)));
        mt.setMilltaskModel(new RoundStockModel(new SimpleStringProperty(""), DimensionProperty.DimMM(30.0),
                DimensionProperty.DimMM(20.0), DimensionProperty.DimMM(100.0)));
        p.millTasksProperty().add(mt);
        projectModel.addProject(p);

    }

    private void addDefaultToolSet() {
        StockToolParameter stp = Factory.newStockTool();
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(4.0));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(5.0));
        stp.setToolType(new EndMill(new DimensionProperty(10.0, Dimensions.Dim.MM)));
        stp.setName("End Mill 10mm (Spindle)");
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(3.0));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(4.0));
        stp.setToolType(new EndMill(new DimensionProperty(8.0, Dimensions.Dim.MM)));
        stp.setName("End Mill 8mm");
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(2.5));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(3.0));
        stp.setToolType(new EndMill(new DimensionProperty(6.0, Dimensions.Dim.MM)));
        stp.setName("End Mill 6mm");
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(4.0));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(5.0));
        stp.setToolType(new BallMill(new DimensionProperty(10.0, Dimensions.Dim.MM)));
        stp.setName("Ball Mill 10mm");
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(3.0));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(4.0));
        stp.setToolType(new BallMill(new DimensionProperty(8.0, Dimensions.Dim.MM)));
        stp.setName("Ball Mill 8mm");
        projectModel.toolDBProperty().add(stp);

        stp = Factory.newStockTool();
        stp.radialDepthProperty().set(DimensionProperty.DimMM(2.5));
        stp.axialDepthProperty().set(DimensionProperty.DimMM(5.0));
        stp.setToolType(new BallMill(new DimensionProperty(6.0, Dimensions.Dim.MM)));
        stp.setName("Ball Mill 6mm");
        projectModel.toolDBProperty().add(stp);

    }

    private void addDefaultPostprocessorSet() {
        // Defaul't MM post processor
        CNCToolsPostProcessConfig ppc = Factory.newPostProcessor();
        ppc.setName("LinuxCNC (mm)");
        ppc.preabmleProperty().set("%\n" + "G17 G21 G40 G49\n" + "G64 P0.01");
        projectModel.postProcessorsProperty().add(ppc);

        ppc = Factory.newPostProcessor();
        ppc.setName("LinuxCNC (inch)");
        ppc.axisDecimalsProperty().put("A", 4);
        ppc.axisDecimalsProperty().put("B", 4);
        ppc.axisDecimalsProperty().put("C", 4);
        ppc.axisDecimalsProperty().put("X", 4);
        ppc.axisDecimalsProperty().put("Y", 4);
        ppc.axisDecimalsProperty().put("Z", 4);
        ppc.axisDecimalsProperty().put("U", 4);
        ppc.axisDecimalsProperty().put("V", 4);
        ppc.axisDecimalsProperty().put("W", 4);
        ppc.preabmleProperty().set("%\n" + "G17 G20 G40 G49\n" + "G64 P0.001");
        projectModel.postProcessorsProperty().add(ppc);
    }

    @FXML
    // This method is called by the FXMLLoader when initialization is complete
    void initialize() {
        assert addMillTask != null : "fx:id=\"addMillTask\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert btnMoveTaskDown != null : "fx:id=\"btnMoveTaskDown\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert btnMoveTaskUp != null : "fx:id=\"btnMoveTaskUp\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert btnPostProcessor != null : "fx:id=\"btnPostProcessor\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert btnView != null : "fx:id=\"btnView\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert deleteProject != null : "fx:id=\"deleteProject\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert descriptionValue != null : "fx:id=\"descriptionValue\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert details != null : "fx:id=\"details\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert displayedIssueLabel != null : "fx:id=\"displayedIssueLabel\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert editMilltask != null : "fx:id=\"editMilltask\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert generateGCode != null : "fx:id=\"generateGCode\" was not injected: check your FXML file 'CNCTools.fxml'.";
        //assert millTaskDescription != null : "fx:id=\"millTaskDescription\" was not injected: check your FXML file 'CNCTools.fxml'.";
        //assert millTaskName != null : "fx:id=\"millTaskName\" was not injected: check your FXML file 'CNCTools.fxml'.";
        //assert millTaskOperation != null : "fx:id=\"millTaskOperation\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert milltaskEnabled != null : "fx:id=\"milltaskEnabled\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert removeMilltask != null : "fx:id=\"removeMilltask\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert tbl_millTasks != null : "fx:id=\"tbl_millTasks\" was not injected: check your FXML file 'CNCTools.fxml'.";
        assert v_projectList != null : "fx:id=\"v_projectList\" was not injected: check your FXML file 'CNCTools.fxml'.";

        v_projectList.setItems(projectModel.projectsProperty());

        projectModel.loadPostProcessors();
        if (projectModel.postProcessorsProperty().size() == 0) {
            addDefaultPostprocessorSet();
            projectModel.savePostProcessors();
            MonologFX dialog = new MonologFX(MonologFX.Type.INFO);
            dialog.setTitleText("Postprocessor defaults loaded");
            dialog.setMessage("No Post Processors where found, a new a new post processor set has been created.");
            dialog.show();
        }

        projectModel.loadToolsFromDB();
        if (projectModel.toolDBProperty().size() == 0) {
            addDefaultToolSet();
            projectModel.saveToolDB();
            MonologFX dialog = new MonologFX(MonologFX.Type.INFO);
            dialog.setTitleText("Tools defaults loaded");
            dialog.setMessage("No tools where found, a new a new toolset has been created.");
            dialog.show();
        }

        projectModel.loadProjectsFromDB();
        if (projectModel.projectsProperty().size() == 0) {
            addProjectDefaults();
            projectModel.saveProjects();
            MonologFX dialog = new MonologFX(MonologFX.Type.INFO);
            dialog.setTitleText("Project defaults loaded");
            dialog.setMessage(
                    "No project was found, a new template project was created. Feel free to delete or modify");
            dialog.show();
        }

        deleteProject.disableProperty().bind(v_projectList.getSelectionModel().selectedItemProperty().isNull());
        btnPostProcessor.disableProperty().bind(v_projectList.getSelectionModel().selectedItemProperty().isNull());
        btnView.disableProperty().bind(v_projectList.getSelectionModel().selectedItemProperty().isNull());
        removeMilltask.disableProperty().bind(tbl_millTasks.getSelectionModel().selectedItemProperty().isNull());
        addMillTask.disableProperty().bind(v_projectList.getSelectionModel().selectedItemProperty().isNull());
        editMilltask.disableProperty().bind(tbl_millTasks.getSelectionModel().selectedItemProperty().isNull());
        generateGCode.disableProperty().bind(v_projectList.getSelectionModel().selectedItemProperty().isNull());

        // Update description when project changes by means of databinding
        v_projectList.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Project>() {
            @Override
            public void changed(ObservableValue<? extends Project> observable, Project oldValue, Project newValue) {
                if (oldValue != null) {
                    descriptionValue.textProperty().unbindBidirectional(oldValue.descriptionProperty());
                }
                if (projectModel.projectsProperty().size() == 0) {
                    tbl_millTasks.getItems().clear();
                } else {
                    tbl_millTasks.setItems(newValue.millTasksProperty());
                    descriptionValue.textProperty().bindBidirectional(newValue.descriptionProperty());
                }
            }
        });

        // Enable/Disable up/Down arrows
        tbl_millTasks.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<TaskRunnable>() {
            @Override
            public void changed(ObservableValue<? extends TaskRunnable> observable, TaskRunnable o,
                    TaskRunnable o2) {
                if (o2 != null) {
                    final int index = tbl_millTasks.getSelectionModel().getSelectedIndex();
                    btnMoveTaskUp.setDisable(index == 0 ? true : false);
                    btnMoveTaskDown.setDisable((index + 1) == tbl_millTasks.getItems().size() ? true : false);
                } else {
                    btnMoveTaskUp.setDisable(true);
                    btnMoveTaskDown.setDisable(true);
                }
            }
        });
        btnMoveTaskUp.setDisable(true);
        btnMoveTaskDown.setDisable(true);

        // Set text in ListView
        v_projectList.setCellFactory(new Callback<ListView<Project>, ListCell<Project>>() {
            @Override
            public ListCell<Project> call(ListView<Project> p) {
                ListCell<Project> cell = new ListCell<Project>() {
                    @Override
                    protected void updateItem(Project t, boolean bln) {
                        super.updateItem(t, bln);
                        if (t != null) {
                            setText(t.nameProperty().getValue());
                        } else {
                            setText(null);
                        }
                    }
                };
                return cell;
            }
        });

        // checkbox renderer for milltasks
        milltaskEnabled.setCellFactory(
                new Callback<TableColumn<TaskRunnable, Boolean>, TableCell<TaskRunnable, Boolean>>() {
                    @Override
                    public TableCell<TaskRunnable, Boolean> call(TableColumn<TaskRunnable, Boolean> p) {
                        CheckBoxTableCell<TaskRunnable, Boolean> cell = new CheckBoxTableCell<>();
                        cell.setEditable(true);
                        cell.setAlignment(Pos.CENTER);
                        return cell;
                    }
                });
        tbl_millTasks.setEditable(true);

        // Save DB on exit
        getDialog().setOnHidden(new EventHandler<WindowEvent>() {
            public void handle(WindowEvent we) {
                save(null);
            }
        });
    }

    public void save(ActionEvent actionEvent) {
        try {
            projectModel.saveProjects();
        } catch (Exception e) {
            handleException(e);
        }
        try {
            projectModel.saveToolDB();
        } catch (Exception e) {
            handleException(e);
        }
        try {
            projectModel.savePostProcessors();
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void onSelectPostprocessor(ActionEvent actionEvent) {
        try {
            if (v_projectList.getSelectionModel().selectedItemProperty().get() != null) {
                Project project = v_projectList.getSelectionModel().getSelectedItem();
                final FXMLDialog dialog = screens.postProcessorsDialog();
                PostProcessorsController controller = dialog.getController();
                controller.setMode(PostProcessorsController.Mode.SELECT);
                controller.setPostProcessConfig(project.getPostProcessor());
                dialog.showAndWait();
                if (controller.getReturned() == Result.USE) {
                    project.setPostProcessor(
                            ProjectModel.<CNCToolsPostProcessConfig>deepCopy(controller.getPostProcessConfig()));
                }
            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    @FXML
    public void onViewGCode(ActionEvent actionEvent) {
        try {
            if (v_projectList.getSelectionModel().selectedItemProperty().get() != null) {
                final Project p = v_projectList.getSelectionModel().selectedItemProperty().get();

                if (p.postProcessorProperty().get() == null) {
                    MonologFX dialog = new MonologFX(MonologFX.Type.QUESTION);
                    dialog.setTitleText("No postprocessor");
                    dialog.setMessage("No post processor configured, please select a post processor first!");
                    dialog.show();
                } else {

                    File file = File.createTempFile("cnctools", "gcode");
                    final GCodeCollection gCode = p.getGCode(toolDBManager);
                    file.createNewFile();
                    file.deleteOnExit();
                    if (file != null) {
                        try (BufferedWriter br = Files.newBufferedWriter(file.toPath(), Charset.forName("UTF-8"),
                                new OpenOption[] { StandardOpenOption.WRITE })) {
                            br.write(gCode.concate().toString());
                            br.write("\n");
                            br.flush();
                            br.close();

                            Project project = projectModel.projectsProperty()
                                    .get(v_projectList.getSelectionModel().getSelectedIndex());
                            showGCodeFromFile(project, file.getAbsolutePath());

                        } catch (IOException ex) {
                            handleException(ex);
                        }
                    }
                }

            }
        } catch (Exception e) {
            handleException(e);
        }
    }

    /**
     * Handle exception and show a strack trace, at least to inform the user that something was wrong
     * This is also a last resort, if you can handle the exception in the dialog, please do so and instruct the user!
     *
     * @param exception
     */
    public void handleException(Exception exception) {
        logger.error("generateGCode: General Exception", exception);
        final FXMLDialog dialog = screens.errorDialog();
        ErrorController controller = dialog.getController();
        StringBuilder sb = new StringBuilder();

        sb.append(exception.toString()).append("\n");
        for (StackTraceElement trace : exception.getStackTrace()) {
            if (trace.getClassName().startsWith("com.rvantwisk")) {
                sb.append(trace.getClassName()).append(":").append(trace.getMethodName()).append(":")
                        .append(trace.getLineNumber()).append("\n");
            }
        }
        controller.setMessage(sb.toString());
        dialog.showAndWait();
    }

    @FXML
    void onMoveTaskUp(ActionEvent event) {
        final Project p = v_projectList.getSelectionModel().selectedItemProperty().get();
        final int index = tbl_millTasks.getSelectionModel().getSelectedIndex() - 1;

        final TaskRunnable t1 = p.millTasksProperty().get(index);
        final TaskRunnable t2 = p.millTasksProperty().get(index + 1);

        p.millTasksProperty().remove(index);
        p.millTasksProperty().remove(index);

        p.millTasksProperty().add(index, t2);
        p.millTasksProperty().add(index + 1, t1);
    }

    @FXML
    void onMovetaskDown(ActionEvent event) {
        final Project p = v_projectList.getSelectionModel().selectedItemProperty().get();
        final int index = tbl_millTasks.getSelectionModel().getSelectedIndex();

        final TaskRunnable t1 = p.millTasksProperty().get(index);
        final TaskRunnable t2 = p.millTasksProperty().get(index + 1);

        p.millTasksProperty().remove(index);
        p.millTasksProperty().remove(index);

        p.millTasksProperty().add(index, t2);
        p.millTasksProperty().add(index + 1, t1);
    }
}