diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6143e53
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8296b21
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# Trailing space trimmer
+
+This JAVA application allows you to trim trailing spaces and empty tabs over a whole project.
+It's able to get rid of all trailing spaces for all code files in a directory and its subdirectories, so you could work on a whole repository at once.
+
+All programming languages are supported, you can input a comma-separated list of code-file extensions into the app's GUI, so that all matching files are included in the operation.
+
+Definition of GUI option 1 ("Remove trailing spaces/tabs"):
+- empty whitespaces at the end of a line consisting of characters (only the very end of it, no loss of indent)
+- actual tabs/spaces content in empty lines (it won't delete the empty lines itself, only bring it back to 0 bytes)
+
+[Example] See below image for the effects of this option:
+
+[img] https://i.imgur.com/i35spWx.png
+
+Definition of GUI option 2 ("Remove empty lines"):
+- delete empty lines by completely removing them. This will most often be separator lines between blocks of code, added by the developer's preference over the lifespan of a project.
+
+[Example] See below image for the effects of this option:
+
+[img] https://i.imgur.com/KGGnhz8.png
+
+
+The stable, multi-threaded approach and progress bar, should allow you to work on an high amount of files simultaneously and large repositories.
+
+This is freeware and free to use, distribute, compilate and modify on private and corporate level. No further updates will be commited by myself, although you're free to submit Pull requests.
+
+GUI interface:
+
+https://i.imgur.com/N5dEWo3.png
+
+Processing the 1100 files in above example GUI image, costed just under 3 seconds.
\ No newline at end of file
diff --git a/SpaceTrimmer.iml b/SpaceTrimmer.iml
new file mode 100644
index 0000000..c90834f
--- /dev/null
+++ b/SpaceTrimmer.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..a039fd1
--- /dev/null
+++ b/src/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: nl.dutchman.spacetrimmer.launcher.Launcher
+
diff --git a/src/nl/dutchman/spacetrimmer/launcher/Launcher.java b/src/nl/dutchman/spacetrimmer/launcher/Launcher.java
new file mode 100644
index 0000000..d78abcc
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/launcher/Launcher.java
@@ -0,0 +1,19 @@
+package nl.dutchman.spacetrimmer.launcher;
+
+import nl.dutchman.spacetrimmer.ui.WindowController;
+
+public class Launcher
+{
+ public static void main(String[] args)
+ {
+ try
+ {
+ WindowController windowController = new WindowController();
+ windowController.display();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/nl/dutchman/spacetrimmer/model/ProcessableFile.java b/src/nl/dutchman/spacetrimmer/model/ProcessableFile.java
new file mode 100644
index 0000000..3e99cdc
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/model/ProcessableFile.java
@@ -0,0 +1,18 @@
+package nl.dutchman.spacetrimmer.model;
+
+import java.io.File;
+
+public class ProcessableFile
+{
+ private File file;
+ private long fileSize;
+
+ public ProcessableFile(File file, long fileSize)
+ {
+ this.file = file;
+ this.fileSize = fileSize;
+ }
+
+ public File getFile() { return file; }
+ public long getFileSize() { return fileSize; }
+}
diff --git a/src/nl/dutchman/spacetrimmer/resources/icon.png b/src/nl/dutchman/spacetrimmer/resources/icon.png
new file mode 100644
index 0000000..d41d71e
Binary files /dev/null and b/src/nl/dutchman/spacetrimmer/resources/icon.png differ
diff --git a/src/nl/dutchman/spacetrimmer/resources/processing.gif b/src/nl/dutchman/spacetrimmer/resources/processing.gif
new file mode 100644
index 0000000..5331edb
Binary files /dev/null and b/src/nl/dutchman/spacetrimmer/resources/processing.gif differ
diff --git a/src/nl/dutchman/spacetrimmer/resources/ready.png b/src/nl/dutchman/spacetrimmer/resources/ready.png
new file mode 100644
index 0000000..0d2bbec
Binary files /dev/null and b/src/nl/dutchman/spacetrimmer/resources/ready.png differ
diff --git a/src/nl/dutchman/spacetrimmer/ui/MainWindow.form b/src/nl/dutchman/spacetrimmer/ui/MainWindow.form
new file mode 100644
index 0000000..eecae88
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/ui/MainWindow.form
@@ -0,0 +1,229 @@
+
+
diff --git a/src/nl/dutchman/spacetrimmer/ui/MainWindow.java b/src/nl/dutchman/spacetrimmer/ui/MainWindow.java
new file mode 100644
index 0000000..6caaaea
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/ui/MainWindow.java
@@ -0,0 +1,64 @@
+package nl.dutchman.spacetrimmer.ui;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class MainWindow extends JFrame
+{
+ public static final int WIDTH = 800, HEIGHT = 600;
+
+ private JPanel contentPane;
+ private JPanel consoleContainer;
+ private JScrollPane consoleScrollContainer;
+ private JTextPane consoleArea;
+ private JPanel interfaceContainer;
+ private JPanel directoryContainer;
+ private JPanel optionsContainer;
+ private JPanel utilityContainer;
+ private JButton directoryButton;
+ private JTextField directoryField;
+ private JPanel processContainer;
+ private JButton processButton;
+ private JPanel progressContainer;
+ private JPanel statusContainer;
+ private JLabel statusLabel;
+ private JLabel statusIcon;
+ private JLabel threadsLabel;
+ private JLabel currentLabel;
+ private JProgressBar progressBar;
+ private JCheckBox endTrimCheckbox;
+ private JCheckBox emptyLineCheckBox;
+ private JPanel fileFormatContainer;
+ private JLabel fileFormatLabel;
+ private JTextField fileFormatField;
+
+ public JTextPane getConsoleArea() { return consoleArea; }
+ public JButton getDirectoryButton() { return directoryButton; }
+ public JTextField getDirectoryField() { return directoryField; }
+ public JButton getProcessButton() { return processButton; }
+ public JLabel getStatusLabel() { return statusLabel; }
+ public JLabel getStatusIcon() { return statusIcon; }
+ public JLabel getThreadsLabel() { return threadsLabel; }
+ public JLabel getCurrentLabel() { return currentLabel; }
+ public JProgressBar getProgressBar() { return progressBar; }
+ public JCheckBox getEndTrimCheckbox() { return endTrimCheckbox; }
+ public JCheckBox getEmptyLineCheckBox() { return emptyLineCheckBox; }
+ public JTextField getFileFormatField() { return fileFormatField; }
+
+ public MainWindow()
+ {
+ this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ this.setSize(WIDTH, HEIGHT);
+ this.setLocationRelativeTo(null);
+ this.setContentPane(contentPane);
+ this.setIconImage(Toolkit.getDefaultToolkit().getImage(MainWindow.class.getResource("/nl/dutchman/spacetrimmer/resources/icon.png")));
+ this.setTitle("Space Trimmer - Remove trailing spaces/empty lines");
+ this.setResizable(false);
+
+ this.consoleArea.setFont(new Font("Consolas", Font.PLAIN, 12));
+ this.consoleArea.setEditable(false);
+
+ this.endTrimCheckbox.setSelected(true);
+ this.emptyLineCheckBox.setSelected(true);
+ }
+}
diff --git a/src/nl/dutchman/spacetrimmer/ui/WindowController.java b/src/nl/dutchman/spacetrimmer/ui/WindowController.java
new file mode 100644
index 0000000..c9eeb75
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/ui/WindowController.java
@@ -0,0 +1,312 @@
+package nl.dutchman.spacetrimmer.ui;
+
+import nl.dutchman.spacetrimmer.model.ProcessableFile;
+import nl.dutchman.spacetrimmer.utils.DialogManager;
+import nl.dutchman.spacetrimmer.utils.MessageConsole;
+import nl.dutchman.spacetrimmer.utils.ProcessInformation;
+
+import javax.swing.*;
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DocumentFilter;
+import java.awt.*;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.DecimalFormat;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class WindowController
+{
+ private MainWindow mainWindow;
+ private DialogManager dialogManager;
+ private int threadsToUse;
+ private ProcessInformation processInformation;
+ private ProcessInformation[] stepInformation = new ProcessInformation[2];
+ private int filesToProcess, filesProcessed;
+ private long lengthToProcess, lengthProcessed;
+
+ public WindowController()
+ {
+ threadsToUse = Runtime.getRuntime().availableProcessors();
+ dialogManager = new DialogManager();
+ setLookAndFeel();
+ initializeComponents();
+ initializeListeners();
+ }
+
+ private void setStatus(int step)
+ {
+ if (step == 0)
+ {
+ filesProcessed = 0;
+ filesToProcess = 0;
+ lengthProcessed = 0;
+ lengthToProcess = 0;
+ this.mainWindow.getProgressBar().setValue(0);
+ this.mainWindow.getStatusIcon().setIcon(new ImageIcon(WindowController.class.getResource("/nl/dutchman/spacetrimmer/resources/processing.gif")));
+ this.mainWindow.getStatusLabel().setText("Status: PROCESSING");
+ this.mainWindow.getThreadsLabel().setText("Analyzing directory");
+ this.mainWindow.getCurrentLabel().setText("Processed 0,0 kB of 0,0 kB. Files processed: 0 out of 0");
+ this.mainWindow.getProcessButton().setEnabled(false);
+ }
+ else if (step == 1)
+ this.mainWindow.getThreadsLabel().setText("Processing in parallel mode. Threads being used: " + threadsToUse);
+ else if (step == -1)
+ {
+ this.mainWindow.getStatusIcon().setIcon(new ImageIcon(WindowController.class.getResource("/nl/dutchman/spacetrimmer/resources/ready.png")));
+ this.mainWindow.getStatusLabel().setText("Status: READY");
+ this.mainWindow.getThreadsLabel().setText("Currently idle. Awaiting for input");
+ this.mainWindow.getProcessButton().setEnabled(true);
+ }
+ }
+
+ private void initializeProcess(String directory, String extensionStr, boolean trimSpaces, boolean trimLines)
+ {
+ setStatus(0);
+
+ processInformation = new ProcessInformation();
+ processInformation.start();
+ System.out.println("Process starting at " + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now()));
+
+ boolean noExtensions = extensionStr == null || extensionStr.trim().isEmpty();
+ List extensions;
+ if (noExtensions)
+ extensions = new ArrayList(); // empty list, just in case, to avoid nullptr
+ else
+ {
+ extensions = Arrays.asList(extensionStr.split(","));
+ }
+
+ ExecutorService processService = Executors.newSingleThreadExecutor();
+ processService.submit(() -> {
+ try
+ {
+
+ BiFunction, Boolean> endsWithAny = (dirName, exts) -> {
+ for (String ext : exts)
+ {
+ if (dirName.endsWith(ext))
+ return true;
+ }
+ return false;
+ };
+ System.out.println("Step 0 --> Gathering files to modify");
+ stepInformation[0] = new ProcessInformation();
+ stepInformation[0].start();
+ int c = 0;
+ List processableFiles = Files.walk(Paths.get(directory)).
+ map(p -> new ProcessableFile(p.toFile(), p.toFile().length())).
+ filter(p -> !p.getFile().isDirectory()).
+ filter(noExtensions ? p -> true : p -> endsWithAny.apply(p.getFile().getAbsolutePath(), extensions)).
+ collect(Collectors.toList());
+ stepInformation[0].end();
+
+ filesToProcess = processableFiles.size();
+ lengthToProcess = processableFiles.stream().map(p -> p.getFileSize()).mapToInt(i -> i.intValue()).sum();
+
+ System.out.println("Step 0 --> Files gathered in " + stepInformation[0].getTime() + "ms");
+
+ ExecutorService executorService = Executors.newFixedThreadPool(threadsToUse);
+
+ setStatus(1);
+
+ System.out.println("Step 1 --> Processing files. This may take a while...");
+ System.out.println("Settings >- trimSpaces: " + trimSpaces + " | trimLines: " + trimLines);
+ this.mainWindow.getCurrentLabel().setText("Processed 0.0 kB of " + readableFileSize(lengthToProcess) + ". Files processed: 0 out of " + filesToProcess);
+ stepInformation[1] = new ProcessInformation();
+ stepInformation[1].start();
+
+ for (ProcessableFile file : processableFiles)
+ {
+ executorService.submit(() -> {
+ try
+ {
+ List lineFiles = new ArrayList<>();
+ for (String line : Files.readAllLines(file.getFile().toPath(), StandardCharsets.ISO_8859_1))
+ {
+ if (trimSpaces)
+ line = line.replaceAll("\\s+$", "");
+ if (!trimLines || (trimLines && line != null && !line.trim().isEmpty()))
+ lineFiles.add(line);
+ }
+ Files.write(file.getFile().toPath(), lineFiles);
+ }
+ catch (IOException e)
+ {
+ System.err.println("Could not process " + file.getFile().getAbsolutePath());
+ e.printStackTrace();
+ }
+
+ synchronized (this)
+ {
+ filesProcessed++;
+ lengthProcessed += file.getFileSize();
+
+ this.mainWindow.getProgressBar().setValue((int) (lengthProcessed / (lengthToProcess / 100)));
+ this.mainWindow.getCurrentLabel().setText("Processed " + readableFileSize(lengthProcessed) + " of "
+ + readableFileSize(lengthToProcess) + ". Files processed: " + filesProcessed + " out of " + filesToProcess);
+ }
+ });
+ }
+
+ executorService.shutdown();
+
+ try
+ {
+ executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ dialogManager.displayError("ERR5 - Awaiting process", "The process has been interrupted");
+ e.printStackTrace();
+ }
+ finally
+ {
+ stepInformation[1].end();
+ System.out.println("Step 1 --> Files processed in " + stepInformation[1].getTime() + "ms");
+ processInformation.end();
+ System.out.println("Process finished succesfully in " + processInformation.getTime() + "ms");
+ }
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ dialogManager.displayError("ERR4 - Process Error", "Could not finish processing.\n" +
+ "Please check console for further information");
+ return;
+ }
+ finally
+ {
+ setStatus(-1);
+ dialogManager.displayInfo("INF1 - Finished process", "Finish processing " + filesProcessed + " files");
+ }
+ });
+ }
+
+ private String readableFileSize(long size)
+ {
+ if(size <= 0) return "0";
+ final String[] units = new String[] { "B", "kB", "MB", "GB", "TB" };
+ int digitGroups = (int) (Math.log10(size)/Math.log10(1024));
+ return new DecimalFormat("#,##0.#").format(size/Math.pow(1024, digitGroups)) + " " + units[digitGroups];
+ }
+
+ private void setLookAndFeel()
+ {
+ try
+ {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ }
+ catch (IllegalAccessException | InstantiationException | UnsupportedLookAndFeelException | ClassNotFoundException e)
+ {
+ dialogManager.displayError("ERR1 - Look and Feel", "Could not set application's look and feel");
+ e.printStackTrace();
+ }
+ }
+
+ public void display()
+ {
+ this.mainWindow.setVisible(true);
+ }
+
+ private void initializeComponents()
+ {
+ this.mainWindow = new MainWindow();
+ MessageConsole console = new MessageConsole(this.mainWindow.getConsoleArea());
+ console.redirectOut(null, System.out);
+ console.redirectErr(Color.RED, null);
+ console.setMessageLines(1000);
+ }
+
+ private void initializeListeners()
+ {
+ setExtensionsDocumentFilter();
+ addFileChooserListener();
+ addProcessListener();
+ }
+
+ private void setExtensionsDocumentFilter()
+ {
+ Pattern extensionPattern = Pattern.compile("[a-zA-Z0-9,]*");
+ AbstractDocument document = (AbstractDocument) this.mainWindow.getFileFormatField().getDocument();
+ document.setDocumentFilter(new DocumentFilter(){
+ @Override
+ public void replace(DocumentFilter.FilterBypass byPass, int offset, int length, String text, AttributeSet attributes) throws BadLocationException
+ {
+ Matcher matcher = extensionPattern.matcher(text);
+ if (matcher.matches())
+ super.replace(byPass, offset, length, text, attributes);
+ }
+ });
+ }
+
+ private void addFileChooserListener()
+ {
+ JButton fileChooserButton = mainWindow.getDirectoryButton();
+ fileChooserButton.addActionListener(actionListener -> {
+ JTextField fileChooserField = mainWindow.getDirectoryField();
+ String startPath = (!fileChooserField.getText().isEmpty() &&
+ Files.isDirectory(Paths.get(fileChooserField.getText()))) ?
+ fileChooserField.getText() : System.getProperty("user.home");
+ JFileChooser fileChooser = new JFileChooser(startPath);
+ fileChooser.setDialogTitle("Select a directory");
+ fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ if (fileChooser.showDialog(mainWindow, "Select directory") == JFileChooser.APPROVE_OPTION)
+ {
+ fileChooserField.setText(fileChooser.getSelectedFile().getAbsolutePath());
+ }
+ });
+ }
+
+ private void addProcessListener()
+ {
+ JButton processButton = mainWindow.getProcessButton();
+ processButton.addActionListener(actionListener -> {
+ JTextField fileChooserField = mainWindow.getDirectoryField();
+ JTextField fileExtensionField = mainWindow.getFileFormatField();
+ String selectedDirectory = fileChooserField.getText();
+ String selectedExtensions = fileExtensionField.getText();
+ boolean trimSpaces = mainWindow.getEndTrimCheckbox().isSelected();
+ boolean trimLines = mainWindow.getEmptyLineCheckBox().isSelected();
+
+ // Cleaning up empty extensions here.
+ while (selectedExtensions.contains(",,"))
+ selectedExtensions = selectedExtensions.replace(",,", ",");
+ if (selectedExtensions.startsWith(","))
+ selectedExtensions = selectedExtensions.substring(1);
+ if (selectedExtensions.endsWith(","))
+ selectedExtensions = selectedExtensions.substring(0, selectedExtensions.length() - 1);
+
+ if (selectedDirectory == null || selectedDirectory.trim().isEmpty())
+ {
+ dialogManager.displayError("ERR2 - No directory", "No directory has been selected");
+ return;
+ }
+ if (!Files.isDirectory(Paths.get(selectedDirectory)))
+ {
+ dialogManager.displayError("ERR3 - Invalid directory", "Selected directory is invalid");
+ return;
+ }
+ if (selectedExtensions == null || selectedExtensions.trim().isEmpty())
+ {
+ dialogManager.displayError("ERR6 - No extensions selected", "No extensions have been selected.");
+ return;
+ }
+ initializeProcess(selectedDirectory, selectedExtensions, trimSpaces, trimLines);
+ });
+ }
+}
diff --git a/src/nl/dutchman/spacetrimmer/utils/DialogManager.java b/src/nl/dutchman/spacetrimmer/utils/DialogManager.java
new file mode 100644
index 0000000..8dea7c2
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/utils/DialogManager.java
@@ -0,0 +1,28 @@
+package nl.dutchman.spacetrimmer.utils;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class DialogManager
+{
+ public void displayInfo(String title, String message)
+ {
+ displayFrame(title, message, JOptionPane.INFORMATION_MESSAGE);
+ }
+
+ public void displayWarning(String title, String message)
+ {
+ displayFrame(title, message, JOptionPane.WARNING_MESSAGE);
+ }
+
+ public void displayError(String title, String message)
+ {
+ displayFrame(title, message, JOptionPane.ERROR_MESSAGE);
+ }
+
+ private void displayFrame(String title, String message, int msgType)
+ {
+ Toolkit.getDefaultToolkit().beep();
+ JOptionPane.showMessageDialog(null, message, title, msgType);
+ }
+}
diff --git a/src/nl/dutchman/spacetrimmer/utils/LimitLinesDocumentListener.java b/src/nl/dutchman/spacetrimmer/utils/LimitLinesDocumentListener.java
new file mode 100644
index 0000000..bfbd93d
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/utils/LimitLinesDocumentListener.java
@@ -0,0 +1,108 @@
+package nl.dutchman.spacetrimmer.utils;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+
+/**
+ * From http://tips4java.wordpress.com/2008/10/15/limit-lines-in-document/
+ *
+ * @author Rob Camick
+ */
+public class LimitLinesDocumentListener implements DocumentListener {
+ private int maximumLines;
+ private boolean isRemoveFromStart;
+
+ /**
+ * Specify the number of lines to be stored in the Document. Extra lines
+ * will be removed from the start or end of the Document, depending on
+ * the boolean value specified.
+ *
+ * @param maximumLines number of lines
+ * @param isRemoveFromStart true to remove from the start
+ */
+ public LimitLinesDocumentListener(int maximumLines,
+ boolean isRemoveFromStart) {
+ setLimitLines(maximumLines);
+ this.isRemoveFromStart = isRemoveFromStart;
+ }
+
+ /**
+ * Set the maximum number of lines to be stored in the Document
+ *
+ * @param maximumLines number of lines
+ */
+ public void setLimitLines(int maximumLines) {
+ if (maximumLines < 1) {
+ throw new IllegalArgumentException("Maximum lines must be greater than 0");
+ }
+
+ this.maximumLines = maximumLines;
+ }
+
+ @Override
+ public void insertUpdate(final DocumentEvent e) {
+ // Changes to the Document can not be done within the listener
+ // so we need to add the processing to the end of the EDT
+
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ removeLines(e);
+ }
+ });
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ }
+
+ private void removeLines(DocumentEvent e) {
+ // The root Element of the Document will tell us the total number
+ // of line in the Document.
+
+ Document document = e.getDocument();
+ Element root = document.getDefaultRootElement();
+
+ while (root.getElementCount() > maximumLines) {
+ if (isRemoveFromStart) {
+ removeFromStart(document, root);
+ } else {
+ removeFromEnd(document, root);
+ }
+ }
+ }
+
+ private void removeFromStart(Document document, Element root) {
+ Element line = root.getElement(0);
+ int end = line.getEndOffset();
+
+ try {
+ document.remove(0, end);
+ } catch (BadLocationException ble) {
+ System.out.println(ble);
+ }
+ }
+
+ private void removeFromEnd(Document document, Element root) {
+ // We use start minus 1 to make sure we remove the newline
+ // character of the previous line
+
+ Element line = root.getElement(root.getElementCount() - 1);
+ int start = line.getStartOffset();
+ int end = line.getEndOffset();
+
+ try {
+ document.remove(start - 1, end - start);
+ } catch (BadLocationException ble) {
+ System.out.println(ble);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/nl/dutchman/spacetrimmer/utils/MessageConsole.java b/src/nl/dutchman/spacetrimmer/utils/MessageConsole.java
new file mode 100644
index 0000000..d965c84
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/utils/MessageConsole.java
@@ -0,0 +1,243 @@
+package nl.dutchman.spacetrimmer.utils;
+
+import java.io.*;
+import java.awt.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+
+/*
+ * Create a simple console to display text messages.
+ *
+ * Messages can be directed here from different sources. Each source can
+ * have its messages displayed in a different color.
+ *
+ * Messages can either be appended to the console or inserted as the first
+ * line of the console
+ *
+ * You can limit the number of lines to hold in the Document.
+ */
+public class MessageConsole
+{
+ private JTextComponent textComponent;
+ private Document document;
+ private boolean isAppend;
+ private DocumentListener limitLinesListener;
+
+ public MessageConsole(JTextComponent textComponent)
+ {
+ this(textComponent, true);
+ }
+
+ /*
+ * Use the text component specified as a simply console to display
+ * text messages.
+ *
+ * The messages can either be appended to the end of the console or
+ * inserted as the first line of the console.
+ */
+ public MessageConsole(JTextComponent textComponent, boolean isAppend)
+ {
+ this.textComponent = textComponent;
+ this.document = textComponent.getDocument();
+ this.isAppend = isAppend;
+ textComponent.setEditable( false );
+ }
+
+ /*
+ * Redirect the output from the standard output to the console
+ * using the default text color and null PrintStream
+ */
+ public void redirectOut()
+ {
+ redirectOut(null, null);
+ }
+
+ /*
+ * Redirect the output from the standard output to the console
+ * using the specified color and PrintStream. When a PrintStream
+ * is specified the message will be added to the Document before
+ * it is also written to the PrintStream.
+ */
+ public void redirectOut(Color textColor, PrintStream printStream)
+ {
+ ConsoleOutputStream cos = new ConsoleOutputStream(textColor, printStream);
+ System.setOut( new PrintStream(cos, true) );
+ }
+
+ /*
+ * Redirect the output from the standard error to the console
+ * using the default text color and null PrintStream
+ */
+ public void redirectErr()
+ {
+ redirectErr(null, null);
+ }
+
+ /*
+ * Redirect the output from the standard error to the console
+ * using the specified color and PrintStream. When a PrintStream
+ * is specified the message will be added to the Document before
+ * it is also written to the PrintStream.
+ */
+ public void redirectErr(Color textColor, PrintStream printStream)
+ {
+ ConsoleOutputStream cos = new ConsoleOutputStream(textColor, printStream);
+ System.setErr( new PrintStream(cos, true) );
+ }
+
+ /*
+ * To prevent memory from being used up you can control the number of
+ * lines to display in the console
+ *
+ * This number can be dynamically changed, but the console will only
+ * be updated the next time the Document is updated.
+ */
+ public void setMessageLines(int lines)
+ {
+ if (limitLinesListener != null)
+ document.removeDocumentListener( limitLinesListener );
+
+ limitLinesListener = new LimitLinesDocumentListener(lines, isAppend);
+ document.addDocumentListener( limitLinesListener );
+ }
+
+ /*
+ * Class to intercept output from a PrintStream and add it to a Document.
+ * The output can optionally be redirected to a different PrintStream.
+ * The text displayed in the Document can be color coded to indicate
+ * the output source.
+ */
+ class ConsoleOutputStream extends ByteArrayOutputStream
+ {
+ private final String EOL = System.getProperty("line.separator");
+ private SimpleAttributeSet attributes;
+ private PrintStream printStream;
+ private StringBuffer buffer = new StringBuffer(1000);
+ private boolean isFirstLine;
+
+ /*
+ * Specify the option text color and PrintStream
+ */
+ public ConsoleOutputStream(Color textColor, PrintStream printStream)
+ {
+ if (textColor != null)
+ {
+ attributes = new SimpleAttributeSet();
+ StyleConstants.setForeground(attributes, textColor);
+ }
+
+ this.printStream = printStream;
+
+ if (isAppend)
+ isFirstLine = true;
+ }
+
+ /*
+ * Override this method to intercept the output text. Each line of text
+ * output will actually involve invoking this method twice:
+ *
+ * a) for the actual text message
+ * b) for the newLine string
+ *
+ * The message will be treated differently depending on whether the line
+ * will be appended or inserted into the Document
+ */
+ public void flush()
+ {
+ String message = toString();
+
+ if (message.length() == 0) return;
+
+ if (isAppend)
+ handleAppend(message);
+ else
+ handleInsert(message);
+
+ reset();
+ }
+
+ /*
+ * We don't want to have blank lines in the Document. The first line
+ * added will simply be the message. For additional lines it will be:
+ *
+ * newLine + message
+ */
+ private void handleAppend(String message)
+ {
+ // This check is needed in case the text in the Document has been
+ // cleared. The buffer may contain the EOL string from the previous
+ // message.
+
+ if (document.getLength() == 0)
+ buffer.setLength(0);
+
+ if (EOL.equals(message))
+ {
+ buffer.append(message);
+ }
+ else
+ {
+ buffer.append(message);
+ clearBuffer();
+ }
+
+ }
+ /*
+ * We don't want to merge the new message with the existing message
+ * so the line will be inserted as:
+ *
+ * message + newLine
+ */
+ private void handleInsert(String message)
+ {
+ buffer.append(message);
+
+ if (EOL.equals(message))
+ {
+ clearBuffer();
+ }
+ }
+
+ /*
+ * The message and the newLine have been added to the buffer in the
+ * appropriate order so we can now update the Document and send the
+ * text to the optional PrintStream.
+ */
+ private void clearBuffer()
+ {
+ // In case both the standard out and standard err are being redirected
+ // we need to insert a newline character for the first line only
+
+ if (isFirstLine && document.getLength() != 0)
+ {
+ buffer.insert(0, "\n");
+ }
+
+ isFirstLine = false;
+ String line = buffer.toString();
+
+ try
+ {
+ if (isAppend)
+ {
+ int offset = document.getLength();
+ document.insertString(offset, line, attributes);
+ textComponent.setCaretPosition( document.getLength() );
+ }
+ else
+ {
+ document.insertString(0, line, attributes);
+ textComponent.setCaretPosition( 0 );
+ }
+ }
+ catch (BadLocationException ble) {}
+
+ if (printStream != null)
+ {
+ printStream.print(line);
+ }
+
+ buffer.setLength(0);
+ }
+ }
+}
diff --git a/src/nl/dutchman/spacetrimmer/utils/ProcessInformation.java b/src/nl/dutchman/spacetrimmer/utils/ProcessInformation.java
new file mode 100644
index 0000000..4642aa8
--- /dev/null
+++ b/src/nl/dutchman/spacetrimmer/utils/ProcessInformation.java
@@ -0,0 +1,21 @@
+package nl.dutchman.spacetrimmer.utils;
+
+public class ProcessInformation
+{
+ private long startTime, endTime;
+
+ public void start()
+ {
+ this.startTime = System.currentTimeMillis();
+ }
+
+ public void end()
+ {
+ this.endTime = System.currentTimeMillis();
+ }
+
+ public long getTime()
+ {
+ return this.endTime - this.startTime;
+ }
+}