/** * An interface to help organizes a multi-file gcode workflow. */ /* Copywrite 2016 Will Winder This file is part of Universal Gcode Sender (UGS). UGS 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. UGS 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 UGS. If not, see <http://www.gnu.org/licenses/>. */ package com.willwinder.ugs.nbm.workflow; import com.willwinder.ugs.nbp.lib.services.LocalizingService; import com.willwinder.ugs.nbp.lib.lookup.CentralLookup; import com.willwinder.universalgcodesender.i18n.Localization; import com.willwinder.universalgcodesender.listeners.UGSEventListener; import com.willwinder.universalgcodesender.model.BackendAPI; import com.willwinder.universalgcodesender.model.UGSEvent; import com.willwinder.universalgcodesender.model.UGSEvent.ControlState; import com.willwinder.universalgcodesender.uielements.components.GcodeFileTypeFilter; import com.willwinder.universalgcodesender.utils.Settings; import java.io.File; import java.util.Arrays; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import org.netbeans.api.settings.ConvertAsProperties; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.util.Exceptions; import org.openide.windows.TopComponent; import org.openide.util.NbBundle.Messages; @ConvertAsProperties( dtd = "-//com.willwinder.ugs.nbm.workflow//WorkflowWindow//EN", autostore = false ) @TopComponent.Description( preferredID = "WorkflowWindowTopComponent", //iconBase="SET/PATH/TO/ICON/HERE", persistenceType = TopComponent.PERSISTENCE_ALWAYS ) @TopComponent.Registration(mode = "editor", openAtStartup = false) @Messages({ }) @ActionID(category = LocalizingService.WorkflowWindowCategory, id = LocalizingService.WorkflowWindowActionId) @ActionReference(path = LocalizingService.WorkflowWindowWindowPath) @TopComponent.OpenActionRegistration( displayName = "<Not localized:WorkflowWindow>", preferredID = "WorkflowWindowTopComponent" ) /** * UGSEventListener - this is how a plugin can listen to UGS lifecycle events. * ListSelectionListener - listen for table selections. */ public final class WorkflowWindowTopComponent extends TopComponent implements UGSEventListener, ListSelectionListener { // These are the UGS backend objects for interacting with the backend. private final Settings settings; private final BackendAPI backend; // This is used to identify when a stream has completed. private boolean wasSending; // This is used in most functions, so cache it here. DefaultTableModel model; /** * Initialize the WorkflowWindow, register with the UGS Backend and set some * of the required JTable settings. */ public WorkflowWindowTopComponent() { setName(LocalizingService.WorkflowWindowTitle); setToolTipText(LocalizingService.WorkflowWindowTooltip); initComponents(); // This is how to access the UGS backend and register the listener. // CentralLookup is used to get singleton instances of the UGS Settings // and BackendAPI objects. settings = CentralLookup.getDefault().lookup(Settings.class); backend = CentralLookup.getDefault().lookup(BackendAPI.class); backend.addUGSEventListener(this); // Only allow contiguous ranges of selections and register as a listener. this.fileTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); ListSelectionModel cellSelectionModel = this.fileTable.getSelectionModel(); cellSelectionModel.addListSelectionListener(this); } /** * Events from backend. Take specific actions based on the control state. * FILE_CHANGED: Add the file to the workflow, always do this if the workflow page is loaded. * * STATE_CHANGED: Attempt to detect the file stream completion. * * @param cse */ @Override public void UGSEvent(UGSEvent cse) { if (cse.isStateChangeEvent()) { if (wasSending && cse.getControlState() == ControlState.COMM_IDLE) this.completeFile(backend.getGcodeFile()); wasSending = backend.isSendingFile(); } if (cse.isFileChangeEvent()) { this.addFileToWorkflow(backend.getGcodeFile()); } } /** * Call when a file's work has been completed to progress to the next step * of the work flow. * @param gcodeFile the file which is completing. */ public void completeFile(File gcodeFile) { if (gcodeFile == null) return; // Make sure the file is loaded in the table. int fileIndex = findFileIndex(gcodeFile); if (fileIndex < 0) return; // Mark that it has been completed. model.setValueAt(true, fileIndex, 2); fileIndex++; String message; // Make sure there is another command left. if (fileIndex < fileTable.getRowCount()) { String nextTool = (String) model.getValueAt(fileIndex, 1); String messageTemplate = "Finished sending '%s'.\n" + "The next file uses tool '%s'\n" + "Load tool and move machine to its zero location\n" + "and click send to continue this workflow."; message = String.format(messageTemplate, gcodeFile.getName(), nextTool); // Select the next row, this will trigger a selection event. fileTable.setRowSelectionInterval(fileIndex, fileIndex); // Use a different message if we're finished. } else { message = "Finished sending the last file!"; } // Display a notification. java.awt.EventQueue.invokeLater(() -> { JOptionPane.showMessageDialog(new JFrame(), message, "Workflow Event", JOptionPane.PLAIN_MESSAGE); }); } /** * ListSelectionListener - load files when they are selected. * @param e */ @Override public void valueChanged(ListSelectionEvent e) { int[] selectedRow = fileTable.getSelectedRows(); // Only load files when there is a single selection. if (selectedRow.length == 1) { // Pull the file out of the table and set it in the backend. String file = (String) model.getValueAt(selectedRow[0], 0); try { backend.setGcodeFile(new File(file)); } catch (Exception ex) { Exceptions.printStackTrace(ex); } } } /** * Add a file to the table. * @param gcodeFile */ public void addFileToWorkflow(File gcodeFile) { if (gcodeFile == null) { return; } int fileIndex = findFileIndex(gcodeFile); // Don't re-add a file. if (fileIndex >= 0) { return; } model.addRow(new Object[]{ gcodeFile.getAbsolutePath(), "default", false }); // Fire off the selection event to load the file. int lastRow = fileTable.getRowCount() - 1; fileTable.setRowSelectionInterval(lastRow, lastRow); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { addButton = new javax.swing.JButton(); removeButton = new javax.swing.JButton(); tablePanel = new javax.swing.JPanel(); upButton = new javax.swing.JButton(); tableScrollPane = new javax.swing.JScrollPane(); fileTable = new javax.swing.JTable(); downButton = new javax.swing.JButton(); org.openide.awt.Mnemonics.setLocalizedText(addButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.addButton.text")); // NOI18N addButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { addButtonActionPerformed(evt); } }); org.openide.awt.Mnemonics.setLocalizedText(removeButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.removeButton.text")); // NOI18N removeButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { removeButtonActionPerformed(evt); } }); upButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/resources/Arrowhead-Up-01-32.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(upButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.upButton.text")); // NOI18N upButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { upButtonActionPerformed(evt); } }); tableScrollPane.setMinimumSize(new java.awt.Dimension(100, 60)); fileTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { }, new String [] { "Filename", "Toolname", "Finished" } ) { Class[] types = new Class [] { java.lang.String.class, java.lang.String.class, java.lang.Boolean.class }; boolean[] canEdit = new boolean [] { false, true, false }; public Class getColumnClass(int columnIndex) { return types [columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit [columnIndex]; } }); fileTable.setMinimumSize(new java.awt.Dimension(100, 60)); tableScrollPane.setViewportView(fileTable); if (fileTable.getColumnModel().getColumnCount() > 0) { fileTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title0")); // NOI18N fileTable.getColumnModel().getColumn(1).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title1")); // NOI18N fileTable.getColumnModel().getColumn(2).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title3")); // NOI18N } downButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/resources/Arrowhead-Down-01-32.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(downButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.downButton.text")); // NOI18N downButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { downButtonActionPerformed(evt); } }); javax.swing.GroupLayout tablePanelLayout = new javax.swing.GroupLayout(tablePanel); tablePanel.setLayout(tablePanelLayout); tablePanelLayout.setHorizontalGroup( tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(tablePanelLayout.createSequentialGroup() .addContainerGap() .addGroup(tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(upButton) .addComponent(downButton, javax.swing.GroupLayout.Alignment.TRAILING)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(tableScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) .addContainerGap()) ); tablePanelLayout.setVerticalGroup( tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(tablePanelLayout.createSequentialGroup() .addContainerGap() .addComponent(tableScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) .addContainerGap()) .addGroup(tablePanelLayout.createSequentialGroup() .addComponent(upButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 29, Short.MAX_VALUE) .addComponent(downButton)) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(addButton, javax.swing.GroupLayout.DEFAULT_SIZE, 202, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(removeButton, javax.swing.GroupLayout.DEFAULT_SIZE, 211, Short.MAX_VALUE)) .addComponent(tablePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(addButton) .addComponent(removeButton)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(tablePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addContainerGap()) ); }// </editor-fold>//GEN-END:initComponents private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed JFileChooser fileChooser = GcodeFileTypeFilter.getGcodeFileChooser(settings.getLastOpenedFilename()); int returnVal = fileChooser.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { File gcodeFile = fileChooser.getSelectedFile(); settings.setLastOpenedFilename(gcodeFile.getParent()); addFileToWorkflow(gcodeFile); } }//GEN-LAST:event_addButtonActionPerformed private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed int[] selectedRows = fileTable.getSelectedRows(); if (selectedRows.length == 0) return; Arrays.sort(selectedRows); for (int i = selectedRows.length - 1; i >= 0; i--) { int row = selectedRows[i]; this.model.removeRow(row); this.model.fireTableRowsDeleted(row, row); } }//GEN-LAST:event_removeButtonActionPerformed private void upButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_upButtonActionPerformed int[] selectedRows = fileTable.getSelectedRows(); // Exit early if nothing is selected. if (selectedRows.length == 0) return; Arrays.sort(selectedRows); // Exit early if the selected range can't move. if (selectedRows[0] == 0) return; for (int i = 0; i < selectedRows.length; i++) { selectedRows[i] = this.moveRow(selectedRows[i], -1); } int first = selectedRows[0]; int last = selectedRows[selectedRows.length-1]; fileTable.setRowSelectionInterval(first, last); }//GEN-LAST:event_upButtonActionPerformed private void downButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_downButtonActionPerformed int[] selectedRows = fileTable.getSelectedRows(); // Exit early if nothing is selected. if (selectedRows.length == 0) return; Arrays.sort(selectedRows); // Exit early if the selected range can't move. if (selectedRows[selectedRows.length-1] == fileTable.getRowCount()) return; for (int i = selectedRows.length - 1; i >= 0; i--) { selectedRows[i] = this.moveRow(selectedRows[i], 1); } fileTable.setRowSelectionInterval(selectedRows[0], selectedRows[selectedRows.length-1]); }//GEN-LAST:event_downButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton addButton; private javax.swing.JButton downButton; private javax.swing.JTable fileTable; private javax.swing.JButton removeButton; private javax.swing.JPanel tablePanel; private javax.swing.JScrollPane tableScrollPane; private javax.swing.JButton upButton; // End of variables declaration//GEN-END:variables /** * NetBeans module overrides. */ @Override public void componentOpened() { this.wasSending = backend.isSendingFile(); model = (DefaultTableModel)this.fileTable.getModel(); } @Override public void componentClosed() { // TODO add custom code on component closing } /** * Helper functions. */ /** * Look for the provided file in the file table. * @param gcodeFile * @return Row index of the provided file or -1 if not found. */ private int findFileIndex(File gcodeFile) { if (gcodeFile == null) return -1; for (int i = 0; i < model.getRowCount(); i++) { String file = (String) model.getValueAt(i, 0); if (file != null && gcodeFile.getAbsolutePath().equals(file)) { return i; } } return -1; } /** * Move a given row by some offset. If the offset would move the row outside * of the current table size, the row is not moved. * @param row row to move. * @param offset how far to move row. * @return location of row after move. */ private int moveRow(int row, int offset) { int dest = row + offset; if (dest < 0 || dest >= model.getRowCount()) { return row; } model.moveRow(row, row, dest); return dest; } void writeProperties(java.util.Properties p) { // better to version settings since initial version as advocated at // http://wiki.apidesign.org/wiki/PropertyFiles p.setProperty("version", "1.0"); // TODO store your settings } void readProperties(java.util.Properties p) { String version = p.getProperty("version"); // TODO read your settings according to their version } }