Textsheet

A few years ago, I discovered a Mac program called Soulver. This quickly became one of my favorite tools for doing math and quick calculations. The idea is simply genius: it combined a calculator, spreadsheet, and text editor into a single program.

In early 2016, I researched to see whether this program, or a similar one, was available for Windows. Unfortunately, I found that it was not, which presented me with a problem, since I ran into a situation where it would be very useful on one of my windows machines. I mulled over the problem for a few weeks, and decided to take a shot at writing a similar program on my own. This would allow cross-platform users to benefit from a similar concept, while also offering new features in a program that was growing rather out of date. Enter Textsheet, my latest open source project.

Screenshot

Engineering Design

In Textsheet, and similar programs, the user can enter code similar to a spreadsheet, which is then evaluated to give them the result in the right corner. It's similar to programming, but much, much simpler.

I began my design by researching existing methods for accomplishing this type of goal. I found that one of the best ways to evaluate an expression in Java (my programming language of choice for this project) was to use a scripting language engine. I settled on the Groovy scripting language, which is well known for offering useful features for the development of Domain Specific Languages (DSLs). Essentially, my goal in this project was to develop a new DSL, but a very simple one.

My engineering design for this project included an evaluator, which was a class able to take the code in the text field and return a list of "results:" one for each line. The evaluator included a Pre-processor, which would convert my custom syntax into Groovy, before passing it to the GroovyShell, which is the class provided within the Groovy evaluation library for Java.

Screenshot

After designing the basic structure of the application, I proceeded to the implementation phase.

Software Implementation

The current implementation of the CodePreprocessor uses a series of regular expressions to transform the entered code into valid Groovy syntax. This has a number of pros and cons: it is much simpler to implement and maintain than a full compliler, and the performance is quite fast, although a compiler would doubtlessly run faster. I may in future implement a compiler, although the current implementation serves as an excellent proof of concept for the project.

package com.creationshare.codecalc.evaluation;

/**
 * The code preprocessor accepts one line entered by the user
 * at a time, and converts it into groovy code. This is used by
 * the CodeEvaluator to prepare code for evaulation in Groovy.
 *
 * Created by matt on 4/20/16.
 */
public class CodePreprocessor {
	public static String process(String line) {

		// Support for colons-comments
		if (line.contains(":")) {
			line = line.substring(line.indexOf(':')+1);
		}

		// Basic mathematical operations
		line = line.replaceAll("([0-9.]+)( |)\\^( |)([0-9.]+)", "$1 ** $4");

		// Binary operations
		line = line.replaceAll("(^|\\s)([0-9.]+)d", "$1dec2dec($2)");
		line = line.replaceAll("(^|\\s)([0-1.]+)b", "$1bin2dec($2)");
		line = line.replaceAll("(^|\\s)([0-9a-fA-F.]+)h", "$1hex2dec(\"$2\")");

		// Percentage operations
		line = line.replaceAll("([0-9.]+)%", "percent($1)");

		// Currency operations
		line = line.replaceAll("\\$([0-9.]+)", "dollars($1)");
		line = line.replaceAll("€([0-9.]+)", "euros($1)");
		line = line.replaceAll("([0-9.]+) (dollar|euro)(s|)", "$2s($1)");

		// Time operations
		line = line.replaceAll("([0-9.]+) (hour|minute|second|day|year|month|week)(s|)", "$2s($1)");
		line = line.replaceAll("([0-9]?[0-9])/([0-9]?[0-9])/([0-9]?[0-9]?[0-9][0-9])", "date(\"$1\", \"$2\", \"$3\")");

		// Length operations
		line = line.replaceAll("([0-9.]+) (meter|centimeter|kilometer|feet|foot|yard|mile)(s|)", "$2s($1)");

		// Binary operations
		line = line.replaceAll("([0-9a-zA-Z\\.\\(\\)]+)\\s+mod\\s+([0-9a-zA-Z\\.\\(\\)]+)", "$1 % $2");
		line = line.replaceAll("([0-9a-zA-Z\\.\\(\\)]+)\\s+and\\s+([0-9a-zA-Z\\.\\(\\)]+)", "$1 & $2");
		line = line.replaceAll("([0-9a-zA-Z\\.\\(\\)]+)\\s+xor\\s+([0-9a-zA-Z\\.\\(\\)]+)", "$1 ^ $2");
		line = line.replaceAll("([0-9a-zA-Z\\.\\(\\)]+)\\s+or\\s+([0-9a-zA-Z\\.\\(\\)]+)", "$1 | $2");

		// Conversions
		line = line.replaceAll("(.*?) (in|to|as) (binary|hex|decimal|bin|dec)(s|)", "dec_to_$3($1)");
		line = line.replaceAll("(.*?) (in|to|as) (hour|minute|second|day|year|month|week)(s|)", "seconds_to_$3s($1)");
		line = line.replaceAll("(.*?) (in|to|as) (dollar|euro)(s|)", "dollars_to_$3s($1)");
		line = line.replaceAll("(.*?) (in|to|as) (meter|centimeter|kilometer|feet|foot|yard|mile)(s|)", "meters_to_$3s($1)");
		line = line.replaceAll("(.*?) (in|to|as) (date)", "date($1)");

		// Support for unknown units
		line = line.replaceAll("([0-9.]+) ([a-zA-Z0-9]+)", "$1");

		return line;

	}
}

The CalculatorAPI is another very important class within the design. It essentially implements all of the functions that the user can call from their code, both explicitly through the form functionName(arg1, arg2) and implicitly through the form 10 meters to feet.

package com.creationshare.codecalc.evaluation.api;

import com.creationshare.codecalc.evaluation.CodeEvaluator;
import groovy.lang.Script;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * This class implements the API functions that can be
 * used within the user's code. Each function can be called
 * by the user in the form functionName(arg1, arg2). Some
 * are also called using implicit forms defined by the
 * preprocessor.
 *
 * Created by matt on 4/20/16.
 */
public abstract class CalculatorAPI extends Script {
	public double sin(double num) {
		return Math.sin(num);
	}

	public double cos(double num) {
		return Math.cos(num);
	}

	// ...

	/**
	 * Converts the specified month, day, and year into the
	 * timestamp for midnight of that day.
	 */
	public double date(String month, String day, String year) {

		// Allow for two digit dates
		if (year.length() == 2) {
			int yearInt = Integer.parseInt(year);
			if (yearInt > 30) {
				year = "19"+year;
			} else {
				year = "20"+year;
			}
		}

		// Format the date into a string
		String date = month+"/"+day+"/"+year;

		// Create a Java date formatter using the Java API
		DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");

		try {
			// Use the formatter to create a timestamp
			return (((Date) formatter.parse(date)).getTime()) / 1000.;
		} catch (Exception ex) {
			return 0;
		}
	}

	// ...

	public double seconds_to_minutes(double num) {
		return num / 60.;
	}

	// ...

}

You can view the complete code for the Textsheet project by visiting the Github repository.

Screenshot

User Interface Design

After created the fundamental class functions for converting and executing the code, I proceeded to design and build the user interface. As with my other projects, my interface design process began with first examining similar, competing apps to get a general idea for how others have solved the problem, and then writing down ways I could improve on the design.

For Soulver in particular, I feel that the design could be improved in a number of ways. First of all, the design does not include tabs, which means that you are stuck with one document per window. Second, the design in general feels rather outdated: the top icons are too large, particularly for the purpose they fulfill. The user could also benefit from buttons similar to a calculator, as that would provide a source of familiarity to them. The Soulver mobile app does this well, but not the desktop app.

With a general idea of the layout formed, I opened up Illustrator to create my first mockup.

Screenshot

GUI Implementation

I have never been font of visual GUI builders, such as the one built into Visual Studios or NetBeans. The benefit of hand-writing the GUI code is that it is much more responsive: you have control over how it reacts to the user resizing the window, and for a sufficiently skilled user, the resulting code is much easier to create, maintain, and read.

I decided to create an abstract class called TabbedWindow which would provide the basic tab functionality. The tab code could be very useful in future projects; it is best to design abstract code rather than concrete code, particularly when you are likely to re-use it in the future. It prevents copy and pasting, which can lead to messy and difficult to maintain code.

Here is an example of the GUI code from TabbedWindow.

package com.creationshare.docframe;

import com.creationshare.docframe.tabs.BasicTab;
import com.creationshare.docframe.utils.IconLabel;
import com.creationshare.docframe.top.TopPanel;
import com.creationshare.docframe.utils.OSOptimizer;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;

/**
 * This class creates an abstract window with tabs. It will need
 * to be implemented in a sub-class which creates needed menus
 * and tabs.
 *
 * Created by matt on 4/21/16.
 */
public abstract class TabbedWindow extends JFrame {

	// Store a list of the tabs being managed by this window
	protected ArrayList tabs = new ArrayList<>();

	// These are the window's GUI components
	private TopPanel top;
	private TabCenter tabCenter;

	// This is a class with basic info about the window
	public final ProductInformation info;

	/**
	 * This constructor will build the GUI for this window.
	 */
	public TabbedWindow() {

		// Get the product info from the sub-class
		info = getProductInformation();

		// Optimize the window for the current OS
		OSOptimizer.optimize(this);

		// Build the graphical user interface
		add(top = new TopPanel(this), BorderLayout.NORTH);
		add(tabCenter = new TabCenter(), BorderLayout.CENTER);

		// Set a few GUI properties
		setTitle(info.name);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setMinimumSize(new Dimension(300,200));
		setSize(600, 400);
		setLocationRelativeTo(null);
	}

	/**
	 * This abstract method lets the sub-class specify the name
	 * of the window and other parameters.
	 */
	public abstract ProductInformation getProductInformation();

	/**
	 * This adds an icon to the top right menu.
	 *
	 * @param action The ID of the action
	 * @param icon The hex-encoded ID of an icon from the Icomoon set
	 * @param name The tooltip for this menu item.
	 * @param listener The callback for when this item is clicked.
	 */
	public IconLabel addMenuAction(String action, String icon, String name, ActionListener listener) {
		return top.addMenuAction(action, icon, name, listener);
	}

	/**
	 * This adds a tab to the window.
	 */
	public void registerTab(BasicTab tab) {
		boolean empty = tabCenter.getComponentCount() == 0;
		tabCenter.add(tab.component, tab.uuid);
		tabs.add(tab);
		showTab(tab);
	}

	/**
	 * This highlights the selected tab, so that its
	 * contents are visible in the window.
	 */
	public void showTab(BasicTab tab) {
		for (BasicTab t: tabs) t.selected = false;
		tab.selected = true;
		tabCenter.card.show(tabCenter, tab.uuid);
		top.updateTabs(tabs);
	}

	/**
	 * This removes the tab from the window
	 */
	public void removeTab(BasicTab tab) {
		// Show the confirm dialog if needed
		if (tab.changed) {
			int value = JOptionPane.showConfirmDialog(this, "You have unsaved changes. Close this file?");
			if (value != JOptionPane.YES_OPTION) {
				return;
			}
		}

		boolean empty = tabCenter.getComponentCount() == 1;

		// Change the selected tab
		int nextIndex = tabs.indexOf(tab)+1;
		int previousIndex = tabs.indexOf(tab)-1;
		if (nextIndex < tabs.size()) showTab(tabs.get(nextIndex));
		else if (previousIndex >= 0) showTab(tabs.get(previousIndex));

		// De-register the tab
		tabs.remove(tab);
		tabCenter.remove(tab.component);

		top.updateTabs(tabs);

		if (empty) {
			setVisible(false);
			System.exit(0);
		} else {

		}
	}

	/**
	 * This returns the currently selected tab.
	 */
	public BasicTab getSelectedTab() {
		for (BasicTab t: tabs) {
			if (t.selected) return t;
		}
		return null;
	}

	/**
	 * This function can optionally be overridden by the subclass.
	 * It is called when the user clicks the add tab button.
	 */
	public void addTab() {

	}

	/**
	 * Returns a list of all tabs in the window.
	 */
	public ArrayList getTabs() {
		return tabs;
	}

	/**
	 * This inner class implements a CardLayout to show the current tab.
	 */
	class TabCenter extends JPanel {
		private CardLayout card;

		public TabCenter() {
			setLayout(card = new CardLayout());
		}
	}
}

The GUI contains much more code, although this class provides a good, basic example of how my Java Swing GUI code is typically formatted.