Do not store file contents in SketchCode

Now that each file in the sketch has its own text area in the GUI, it is
no longer needed to store the (possibly modified) contents of each file
inside SketchCode. Keeping the contents in the text area is sufficient.
Doing so allows removing the code that dealt with copying contents from
the text area into the SketchCode instance at the right time, which was
fragile and messy.

However, when compiling a sketch, the current (modified) file contents
still should be used. To allow this, the TextStorage interface is
introduced. This is a simple interface implemented by EditorTab, that
allows the SketchCode class to query the GUI for the current contents.
By using an interface, there is no direct dependency on the GUI code. If
no TextStorage instance is attached to a SketchCode, it will just assume
that the contents are always unmodified and the contents from the file
will be used during compilation.

When not using the GUI (e.g. just compiling something from the
commandline), there is no need to load the file contents from disk at
all, the filenames just have to be passed to arduino-builder and the
compiler. So, the SketchCode constructor no longer calls its `load()`
function, leaving this to the GUI code to call when appropriate. This
also modifies the `SketchCode.load()` function to return the loaded
text, instead of storing it internally.

To still support adding new files to a sketch (whose file does not
exist on disk yet), the EditorTab constructor now allows an initial
contents to be passed in, to be used instead of loading from disk. Only
the empty string is passed for new files now, but this could also be
used for the bare minimum contents of a new sketch later (which is now
down by creating a .ino file in a temporary directory).

Another side effect of this change is that all changes to the contents
now happen through the text area, which keeps track of modifications
already. This allows removing all manual calls to `Sketch.setModified()`
(even more, the entire function is removed, making `Sketch.isModified()`
always check the modification status of the contained files).
This commit is contained in:
Matthijs Kooijman 2015-12-08 10:46:12 +01:00 committed by Martino Facchin
parent 422c111d81
commit ca573351bb
7 changed files with 142 additions and 120 deletions

View File

@ -94,7 +94,6 @@ public class AStyle implements Tool {
textArea.getUndoManager().beginInternalAtomicEdit();
editor.removeAllLineHighlights();
editor.getCurrentTab().setText(formattedText);
editor.getSketch().setModified(true);
textArea.getUndoManager().endInternalAtomicEdit();
if (line != -1 && lineOffset != -1) {

View File

@ -392,7 +392,6 @@ public class FindReplace extends javax.swing.JFrame {
if (find(false, false, searchAllFilesBox.isSelected(), -1)) {
foundAtLeastOne = true;
editor.getCurrentTab().setSelectedText(replaceField.getText());
editor.getSketch().setModified(true); // TODO is this necessary?
}
if (!foundAtLeastOne) {
@ -430,7 +429,6 @@ public class FindReplace extends javax.swing.JFrame {
if (find(false, false, searchAllFilesBox.isSelected(), -1)) {
foundAtLeastOne = true;
editor.getCurrentTab().setSelectedText(replaceField.getText());
editor.getSketch().setModified(true); // TODO is this necessary?
} else {
break;
}

View File

@ -1317,7 +1317,6 @@ public class Editor extends JFrame implements RunnerListener {
pasteItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
getCurrentTab().handlePaste();
sketch.setModified(true);
}
});
menu.add(pasteItem);
@ -1482,7 +1481,6 @@ public class Editor extends JFrame implements RunnerListener {
public void actionPerformed(ActionEvent e) {
try {
getCurrentTab().handleUndo();
sketch.setModified(true);
} catch (CannotUndoException ex) {
//System.out.println("Unable to undo: " + ex);
//ex.printStackTrace();
@ -1516,7 +1514,6 @@ public class Editor extends JFrame implements RunnerListener {
public void actionPerformed(ActionEvent e) {
try {
getCurrentTab().handleRedo();
sketch.setModified(true);
} catch (CannotRedoException ex) {
//System.out.println("Unable to redo: " + ex);
//ex.printStackTrace();
@ -1622,7 +1619,7 @@ public class Editor extends JFrame implements RunnerListener {
tabs.ensureCapacity(sketch.getCodeCount());
for (SketchCode code : sketch.getCodes()) {
try {
addTab(code);
addTab(code, null);
} catch(IOException e) {
// TODO: Improve / move error handling
System.err.println(e);
@ -1638,8 +1635,18 @@ public class Editor extends JFrame implements RunnerListener {
selectTab(findTabIndex(codeDoc.getCode()));
}
protected void addTab(SketchCode code) throws IOException {
EditorTab tab = new EditorTab(this, code);
/**
* Add a new tab.
*
* @param code
* The file to show in the tab.
* @param contents
* The contents to show in the tab, or null to load the
* contents from the given file.
* @throws IOException
*/
protected void addTab(SketchCode code, String contents) throws IOException {
EditorTab tab = new EditorTab(this, code, contents);
tabs.add(tab);
}

View File

@ -51,6 +51,7 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
import org.fife.ui.rtextarea.Gutter;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.fife.ui.rtextarea.RUndoManager;
import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
import processing.app.helpers.DocumentTextChangeListener;
@ -63,37 +64,64 @@ import processing.app.tools.DiscourseFormat;
/**
* Single tab, editing a single file, in the main window.
*/
public class EditorTab extends JPanel {
public class EditorTab extends JPanel implements SketchCode.TextStorage {
protected Editor editor;
protected SketchTextArea textarea;
protected RTextScrollPane scrollPane;
protected SketchCode code;
protected boolean modified;
public EditorTab(Editor editor, SketchCode code) throws IOException {
/**
* Create a new EditorTab
*
* @param editor
* The Editor this tab runs in
* @param code
* The file to display in this tab
* @param contents
* Initial contents to display in this tab. Can be used when
* editing a file that doesn't exist yet. If null is passed,
* code.load() is called and displayed instead.
* @throws IOException
*/
public EditorTab(Editor editor, SketchCode code, String contents)
throws IOException {
super(new BorderLayout());
// Load initial contents contents from file if nothing was specified.
if (contents == null) {
contents = code.load();
modified = false;
} else {
modified = true;
}
this.editor = editor;
this.code = code;
this.textarea = createTextArea();
RSyntaxDocument document = createDocument(contents);
this.textarea = createTextArea(document);
this.scrollPane = createScrollPane(this.textarea);
applyPreferences();
add(this.scrollPane, BorderLayout.CENTER);
UndoManager undo = new LastUndoableEditAwareUndoManager(this.textarea, this.editor);
((RSyntaxDocument)textarea.getDocument()).addUndoableEditListener(undo);
RUndoManager undo = new LastUndoableEditAwareUndoManager(this.textarea, this.editor);
document.addUndoableEditListener(undo);
textarea.setUndoManager(undo);
code.setStorage(this);
}
private RSyntaxDocument createDocument() {
private RSyntaxDocument createDocument(String contents) {
RSyntaxDocument document = new RSyntaxDocument(new ArduinoTokenMakerFactory(editor.base.getPdeKeywords()), RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size"));
// insert the program text into the document object
try {
document.insertString(0, code.getProgram(), null);
document.insertString(0, contents, null);
} catch (BadLocationException bl) {
bl.printStackTrace();
}
document.addDocumentListener(new DocumentTextChangeListener(
() -> code.setModified(true)));
() -> setModified(true)));
return document;
}
@ -112,8 +140,8 @@ public class EditorTab extends JPanel {
return scrollPane;
}
private SketchTextArea createTextArea() throws IOException {
RSyntaxDocument document = createDocument();
private SketchTextArea createTextArea(RSyntaxDocument document)
throws IOException {
final SketchTextArea textArea = new SketchTextArea(document, editor.base.getPdeKeywords());
textArea.setName("editor");
textArea.setFocusTraversalKeysEnabled(false);
@ -318,6 +346,29 @@ public class EditorTab extends JPanel {
textarea.setText(what);
}
/**
* Is the text modified since the last save / load?
*/
public boolean isModified() {
return modified;
}
/**
* Clear modified status. Should only be called by SketchCode through
* the TextStorage interface.
*/
public void clearModified() {
setModified(false);
}
private void setModified(boolean value) {
if (value != modified) {
modified = value;
// TODO: Improve decoupling
editor.getSketch().calcModified();
}
}
public String getSelectedText() {
return textarea.getSelectedText();
}

View File

@ -57,9 +57,6 @@ import static processing.app.I18n.tr;
public class Sketch {
private final Editor editor;
/** true if any of the files have been modified. */
private boolean modified;
private SketchCodeDocument current;
private int currentIndex;
@ -319,7 +316,6 @@ public class Sketch {
// first get the contents of the editor text area
if (current.getCode().isModified()) {
current.getCode().setProgram(editor.getCurrentTab().getText());
try {
// save this new SketchCode
current.getCode().save();
@ -402,7 +398,7 @@ public class Sketch {
ensureExistence();
SketchCode code = (new SketchCodeDocument(this, newFile)).getCode();
try {
editor.addTab(code);
editor.addTab(code, "");
} catch (IOException e) {
Base.showWarning(tr("Error"),
I18n.format(
@ -509,31 +505,16 @@ public class Sketch {
setCurrentCode((currentIndex + 1) % data.getCodeCount());
}
/**
* Sets the modified value for the code in the frontmost tab.
* Called whenever the modification status of one of the tabs changes. TODO:
* Move this code into Editor and improve decoupling from EditorTab
*/
public void setModified(boolean state) {
//System.out.println("setting modified to " + state);
//new Exception().printStackTrace();
current.getCode().setModified(state);
calcModified();
}
private void calcModified() {
modified = false;
for (SketchCode code : data.getCodes()) {
if (code.isModified()) {
modified = true;
break;
}
}
public void calcModified() {
editor.header.repaint();
if (OSUtils.isMacOS()) {
// http://developer.apple.com/qa/qa2001/qa1146.html
Object modifiedParam = modified ? Boolean.TRUE : Boolean.FALSE;
Object modifiedParam = isModified() ? Boolean.TRUE : Boolean.FALSE;
editor.getRootPane().putClientProperty("windowModified", modifiedParam);
editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam);
}
@ -541,7 +522,11 @@ public class Sketch {
public boolean isModified() {
return modified;
for (SketchCode code : data.getCodes()) {
if (code.isModified())
return true;
}
return false;
}
@ -552,14 +537,6 @@ public class Sketch {
// make sure the user didn't hide the sketch folder
ensureExistence();
// first get the contents of the editor text area
if (current.getCode().isModified()) {
current.getCode().setProgram(editor.getCurrentTab().getText());
}
// don't do anything if not actually modified
//if (!modified) return false;
if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
Base.showMessage(tr("Sketch is read-only"),
tr("Some files are marked \"read-only\", so you'll\n" +
@ -605,7 +582,6 @@ public class Sketch {
}
data.save();
calcModified();
return true;
}
@ -707,12 +683,6 @@ public class Sketch {
// now make a fresh copy of the folder
newFolder.mkdirs();
// grab the contents of the current tab before saving
// first get the contents of the editor text area
if (current.getCode().isModified()) {
current.getCode().setProgram(editor.getCurrentTab().getText());
}
// save the other tabs to their new location
for (SketchCode code : data.getCodes()) {
if (data.indexOfCode(code) == 0) continue;
@ -912,18 +882,6 @@ public class Sketch {
data.sortCode();
}
setCurrentCode(filename);
editor.header.repaint();
if (editor.untitled) { // TODO probably not necessary? problematic?
// Mark the new code as modified so that the sketch is saved
current.getCode().setModified(true);
}
} else {
if (editor.untitled) { // TODO probably not necessary? problematic?
// If a file has been added, mark the main code as modified so
// that the sketch is properly saved.
data.getCode(0).setModified(true);
}
}
return true;
}
@ -965,7 +923,6 @@ public class Sketch {
buffer.append(editor.getCurrentTab().getText());
editor.getCurrentTab().setText(buffer.toString());
editor.getCurrentTab().setSelection(0, 0); // scroll to start
setModified(true);
}
@ -987,10 +944,6 @@ public class Sketch {
return;
}
// get the text currently being edited
if (current != null)
current.getCode().setProgram(editor.getCurrentTab().getText());
current = (SketchCodeDocument) data.getCode(which).getMetadata();
currentIndex = which;
editor.setCode(current);
@ -1049,8 +1002,6 @@ public class Sketch {
// make sure the user didn't hide the sketch folder
ensureExistence();
current.getCode().setProgram(editor.getCurrentTab().getText());
// TODO record history here
//current.history.record(program, SketchHistory.RUN);
@ -1223,7 +1174,6 @@ public class Sketch {
"but anything besides the code will be lost."), null);
try {
data.getFolder().mkdirs();
modified = true;
for (SketchCode code : data.getCodes()) {
code.save(); // this will force a save

View File

@ -67,9 +67,7 @@ public class FixEncoding implements Tool {
try {
for (int i = 0; i < sketch.getCodeCount(); i++) {
SketchCode code = sketch.getCode(i);
code.setProgram(loadWithLocalEncoding(code.getFile()));
code.setModified(true); // yes, because we want them to save this
editor.findTab(code).setText(code.getProgram());
editor.findTab(code).setText(loadWithLocalEncoding(code.getFile()));
}
} catch (IOException e) {
String msg =

View File

@ -45,15 +45,34 @@ public class SketchCode {
*/
private File file;
/**
* Text of the program text for this tab
*/
private String program;
private boolean modified;
private Object metadata;
/**
* Interface for an in-memory storage of text file contents. This is
* intended to allow a GUI to keep modified text in memory, and allow
* SketchCode to check for changes when needed.
*/
public static interface TextStorage {
/** Get the current text */
public String getText();
/**
* Is the text modified externally, after the last call to
* clearModified() or setText()?
*/
public boolean isModified();
/** Clear the isModified() result value */
public void clearModified();
};
/**
* A storage for this file's text. This can be set by a GUI, so we can
* have access to any modified version of the file. This can be null,
* in which case the file is never modified, and saving is a no-op.
*/
private TextStorage storage;
public SketchCode(File file) {
init(file, null);
}
@ -65,13 +84,14 @@ public class SketchCode {
private void init(File file, Object metadata) {
this.file = file;
this.metadata = metadata;
}
try {
load();
} catch (IOException e) {
System.err.println(
I18n.format(tr("Error while loading code {0}"), file.getName()));
}
/**
* Set an in-memory storage for this file's text, that will be queried
* on compile, save, and whenever the text is needed.
*/
public void setStorage(TextStorage text) {
this.storage = text;
}
@ -159,36 +179,33 @@ public class SketchCode {
public String getProgram() {
return program;
}
if (storage != null)
return storage.getText();
public void setProgram(String replacement) {
program = replacement;
}
public void setModified(boolean modified) {
this.modified = modified;
return null;
}
public boolean isModified() {
return modified;
if (storage != null)
return storage.isModified();
return false;
}
/**
* Load this piece of code from a file.
* Load this piece of code from a file and return the contents. This
* completely ignores any changes in the linked storage, if any, and
* just directly reads the file.
*/
private void load() throws IOException {
program = BaseNoGui.loadFile(file);
public String load() throws IOException {
String text = BaseNoGui.loadFile(file);
if (program == null) {
if (text == null) {
throw new IOException();
}
if (program.indexOf('\uFFFD') != -1) {
if (text.indexOf('\uFFFD') != -1) {
System.err.println(
I18n.format(
tr("\"{0}\" contains unrecognized characters. " +
@ -201,8 +218,7 @@ public class SketchCode {
);
System.err.println();
}
setModified(false);
return text;
}
@ -211,11 +227,11 @@ public class SketchCode {
* flag is set or not.
*/
public void save() throws IOException {
// TODO re-enable history
//history.record(s, SketchHistory.SAVE);
if (storage == null)
return; /* Nothing to do */
BaseNoGui.saveFile(program, file);
setModified(false);
BaseNoGui.saveFile(storage.getText(), file);
storage.clearModified();
}
@ -223,7 +239,10 @@ public class SketchCode {
* Save this file to another location, used by Sketch.saveAs()
*/
public void saveAs(File newFile) throws IOException {
BaseNoGui.saveFile(program, newFile);
if (storage == null)
return; /* Nothing to do */
BaseNoGui.saveFile(storage.getText(), newFile);
}