X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.scl.ui%2Fsrc%2Forg%2Fsimantics%2Fscl%2Fui%2Fconsole%2FAbstractCommandConsole.java;h=a8931ecffa22dbf57be5054b60022b2cbb624a55;hb=refs%2Fchanges%2F31%2F3731%2F1;hp=388c87806ee7a5e8b1cd6b2d47794f51db2cb1c1;hpb=06ee0c4c71cd9e372969da1570e7fcac2c4397a5;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java index 388c87806..a8931ecff 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java @@ -2,29 +2,36 @@ package org.simantics.scl.ui.console; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Deque; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.preference.IPersistentPreferenceStore; import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.FontDescriptor; +import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.window.DefaultToolTip; import org.eclipse.jface.window.ToolTip; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; -import org.eclipse.swt.custom.VerifyKeyListener; -import org.eclipse.swt.events.ModifyEvent; -import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.custom.StyledTextContent; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; @@ -33,11 +40,15 @@ import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.preferences.ScopedPreferenceStore; +import org.simantics.scl.runtime.tuple.Tuple2; +import org.slf4j.Logger; /** * A console with input and output area that can be embedded @@ -46,16 +57,24 @@ import org.eclipse.ui.preferences.ScopedPreferenceStore; */ public abstract class AbstractCommandConsole extends Composite { - public static final String PLUGIN_ID = "org.simantics.scl.ui"; + /** + * Use this option mask to hide and disable the console input field. + */ + public static final int HIDE_INPUT = 1 << 0; + + public static final String PLUGIN_ID = "org.simantics.scl.ui"; //$NON-NLS-1$ public static final int COMMAND_HISTORY_SIZE = 50; public static final int SASH_HEIGHT = 3; LocalResourceManager resourceManager; - - StyledText output; + + protected final int options; + + StyledText output; Sash sash; + StyledText deco; protected StyledText input; int userInputHeight=0; @@ -63,39 +82,84 @@ public abstract class AbstractCommandConsole extends Composite { protected Color greenColor; protected Color redColor; - + + FontRegistry fontRegistry; + FontDescriptor textFontDescriptor; + Font textFont; + ArrayList commandHistory = new ArrayList(); int commandHistoryPos = 0; boolean outputModiLock = false; - + + boolean limitConsoleOutput; + + /** + * The amount of buffered characters to adjust {@link #output} to when trimming + * the console buffer after its length has exceeded {@link #highWatermark}. + */ + int lowWatermark; + /** + * The maximum amount of buffered characters allowed in {@link #output} before + * the buffer is pruned to under {@link #lowWatermark} characters. + */ + int highWatermark; + + /** + * The console preference scope listened to. + */ + IEclipsePreferences preferences; + + /** + * The console preference listener. + */ + IPreferenceChangeListener preferenceListener = e -> { + String k = e.getKey(); + if (Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT.equals(k)) { + limitConsoleOutput = preferences.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT, Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT_DEFAULT); + } else if (Preferences.CONSOLE_LOW_WATER_MARK.equals(k)) { + lowWatermark = preferences.getInt(Preferences.CONSOLE_LOW_WATER_MARK, Preferences.CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE); + } else if (Preferences.CONSOLE_HIGH_WATER_MARK.equals(k)) { + highWatermark = preferences.getInt(Preferences.CONSOLE_HIGH_WATER_MARK, Preferences.CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE); + } + }; + /* Shell tip = null; Label label = null; */ - - public AbstractCommandConsole(Composite parent, int style) { - super(parent, style); + + public AbstractCommandConsole(Composite parent, int style, int options) { + super(parent, style); + this.options = options; createControl(); } - + @Override public boolean setFocus() { - return input.setFocus(); + return input != null ? input.setFocus() : output.setFocus(); } - + protected boolean canExecuteCommand() { return true; } + protected boolean hasOption(int mask) { + return (options & mask) != 0; + } + private void createControl() { resourceManager = new LocalResourceManager(JFaceResources.getResources(), this); greenColor = resourceManager.createColor(new RGB(0, 128, 0)); redColor = resourceManager.createColor(new RGB(172, 0, 0)); - + + // Initialize current text font + fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry(); + fontRegistry.addListener(fontRegistryListener); + FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") ); //$NON-NLS-1$ + setTextFont(font); + setLayout(new FormLayout()); - - Font textFont = new Font(getDisplay(),"Courier New",12,SWT.NONE); // Sash sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL); @@ -115,121 +179,114 @@ public abstract class AbstractCommandConsole extends Composite { // Upper output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL); output.setFont(textFont); - { - FormData formData = new FormData(); - formData.top = new FormAttachment(0); - formData.bottom = new FormAttachment(sash); - formData.left = new FormAttachment(0); - formData.right = new FormAttachment(100); - output.setLayoutData(formData); - } + output.setLayoutData( formData(0, sash, 0, 100) ); output.addVerifyListener(new VerifyListener() { @Override public void verifyText(VerifyEvent e) { if(outputModiLock) return; - input.append(e.text); - input.setFocus(); - input.setCaretOffset(input.getText().length()); + if (input != null) { + input.append(e.text); + input.setFocus(); + input.setCaretOffset(input.getText().length()); + } e.doit = false; } }); - // Deco - StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY); + if (hasOption(HIDE_INPUT)) { + sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) ); + layout(true); + } else { + createInputArea(); + } + + readPreferences(); + + addListener(SWT.Dispose, event -> { + if (fontRegistry != null) + fontRegistry.removeListener(fontRegistryListener); + if (preferences != null) + preferences.removePreferenceChangeListener(preferenceListener); + try { + writePreferences(); + } catch (IOException e) { + getLogger().error("Failed to store command history in preferences", e); + } + }); + } + + protected void createInputArea() { + deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY); deco.setFont(textFont); deco.setEnabled(false); GC gc = new GC(deco); int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2; - deco.setText(">"); - { - FormData formData = new FormData(); - formData.top = new FormAttachment(sash); - formData.bottom = new FormAttachment(100); - formData.left = new FormAttachment(0); - formData.right = new FormAttachment(0, inputLeftPos); - deco.setLayoutData(formData); - } - - // Input area - input = new StyledText(this, SWT.MULTI); + gc.dispose(); + deco.setText(">"); //$NON-NLS-1$ + deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) ); + + // Input area + input = new StyledText(this, SWT.MULTI); input.setFont(textFont); - { - FormData formData = new FormData(); - formData.top = new FormAttachment(sash); - formData.bottom = new FormAttachment(100); - formData.left = new FormAttachment(0, inputLeftPos); - formData.right = new FormAttachment(100); - input.setLayoutData(formData); - } - adjustInputSize(""); - input.addVerifyKeyListener(new VerifyKeyListener() { - - @Override - public void verifyKey(VerifyEvent event) { - switch(event.keyCode) { - case SWT.KEYPAD_CR: - case SWT.CR: - if((event.stateMask & SWT.CTRL) == 0) { - if(canExecuteCommand()) - execute(); - event.doit = false; + input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) ); + adjustInputSize(""); //$NON-NLS-1$ + input.addVerifyKeyListener(event -> { + switch(event.keyCode) { + case SWT.KEYPAD_CR: + case SWT.CR: + if((event.stateMask & SWT.CTRL) == 0) { + if(canExecuteCommand()) + execute(); + event.doit = false; + } + break; + case SWT.ARROW_UP: + case SWT.ARROW_DOWN: + if((event.stateMask & SWT.CTRL) != 0) { + int targetHistoryPos = commandHistoryPos; + if(event.keyCode == SWT.ARROW_UP) { + if(commandHistoryPos <= 0) + return; + --targetHistoryPos; } - break; - case SWT.ARROW_UP: - case SWT.ARROW_DOWN: - if((event.stateMask & SWT.CTRL) != 0) { - int targetHistoryPos = commandHistoryPos; - if(event.keyCode == SWT.ARROW_UP) { - if(commandHistoryPos <= 0) - return; - --targetHistoryPos; - } - else { - if(commandHistoryPos >= commandHistory.size()-1) - return; - ++targetHistoryPos; - } - setInputText(commandHistory.get(targetHistoryPos)); - commandHistoryPos = targetHistoryPos; - event.doit = false; + else { + if(commandHistoryPos >= commandHistory.size()-1) + return; + ++targetHistoryPos; } - break; -// case SWT.ESC: -// setInputText(""); -// commandHistoryPos = commandHistory.size(); -// break; - } - } - - }); - input.addVerifyListener(new VerifyListener() { - @Override - public void verifyText(VerifyEvent e) { - if(e.text.contains("\n")) { - int lineId = input.getLineAtOffset(e.start); - int lineOffset = input.getOffsetAtLine(lineId); - int indentAmount; - for(indentAmount=0; - lineOffset+indentAmount < input.getCharCount() && - input.getTextRange(lineOffset+indentAmount, 1).equals(" "); - ++indentAmount); - StringBuilder indent = new StringBuilder(); - indent.append('\n'); - for(int i=0;i { + if(e.text.contains("\n")) { //$NON-NLS-1$ + int lineId = input.getLineAtOffset(e.start); + int lineOffset = input.getOffsetAtLine(lineId); + int indentAmount; + for(indentAmount=0; + lineOffset+indentAmount < input.getCharCount() && + input.getTextRange(lineOffset+indentAmount, 1).equals(" "); //$NON-NLS-1$ + ++indentAmount); + StringBuilder indent = new StringBuilder(); + indent.append('\n'); + for(int i=0;i { + adjustInputSize(input.getText()); + commandHistoryPos = commandHistory.size(); + //asyncValidate(); + }); Listener hoverListener = new Listener() { DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true); @@ -285,30 +342,42 @@ public abstract class AbstractCommandConsole extends Composite { toolTipVisible = false; } return; - } - } + } + } }; input.addListener(SWT.MouseHover, hoverListener); input.addListener(SWT.MouseMove, hoverListener); input.addListener(SWT.MouseExit, hoverListener); - - readPreferences(); - - addListener(SWT.Dispose, new Listener() { - - @Override - public void handleEvent(Event event) { - try { - writePreferences(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - }); - } - + + private FormData formData(Object top, Object bottom, Object left, Object right) { + return formData(top, bottom, left, right, null); + } + + private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) { + FormData d = new FormData(); + d.top = formAttachment(top); + d.bottom = formAttachment(bottom); + d.left = formAttachment(left); + d.right = formAttachment(right); + d.height = height != null ? (Integer) height : SWT.DEFAULT; + return d; + } + + private FormAttachment formAttachment(Object o) { + if (o == null) + return null; + if (o instanceof Control) + return new FormAttachment((Control) o); + if (o instanceof Integer) + return new FormAttachment((Integer) o); + if (o instanceof Tuple2) { + Tuple2 t = (Tuple2) o; + return new FormAttachment((Integer) t.c0, (Integer) t.c1); + } + throw new IllegalArgumentException("argument not supported: " + o); //$NON-NLS-1$ + } + private int getOffsetInInput(int x, int y) { int offset; try { @@ -327,6 +396,8 @@ public abstract class AbstractCommandConsole extends Composite { } public void setInputText(String text) { + if (input == null) + return; input.setText(text); input.setCaretOffset(text.length()); adjustInputSize(text); @@ -334,7 +405,7 @@ public abstract class AbstractCommandConsole extends Composite { String validatedText; - Job validationJob = new Job("SCL input validation") { + Job validationJob = new Job("SCL input validation") { //$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { @@ -345,20 +416,17 @@ public abstract class AbstractCommandConsole extends Composite { }; - Job preValidationJob = new Job("SCL input validation") { + Job preValidationJob = new Job("SCL input validation") { //$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { if(!input.isDisposed()) { - input.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - if(!input.isDisposed()) { - validatedText = input.getText(); - validationJob.setPriority(Job.BUILD); - validationJob.schedule(); - } - } - }); + input.getDisplay().asyncExec(() -> { + if(!input.isDisposed()) { + validatedText = input.getText(); + validationJob.setPriority(Job.BUILD); + validationJob.schedule(); + } + }); } return Status.OK_STATUS; @@ -391,36 +459,103 @@ public abstract class AbstractCommandConsole extends Composite { } private void setInputHeight(int inputHeight) { - FormData formData = new FormData(); - formData.top = new FormAttachment(100, -inputHeight); - formData.left = new FormAttachment(0); - formData.right = new FormAttachment(100); - formData.height = SASH_HEIGHT; - sash.setLayoutData(formData); + sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) ); AbstractCommandConsole.this.layout(true); } + private StringBuilder outputBuffer = new StringBuilder(); + private ArrayList styleRanges = new ArrayList(); + private AtomicBoolean outputScheduled = new AtomicBoolean(false); + public void appendOutput(final String text, final Color foreground, final Color background) { - final Display display = Display.getDefault(); - if(display.isDisposed()) return; - display.asyncExec(new Runnable() { - @Override - public void run() { + boolean scheduleOutput = false; + synchronized (outputBuffer) { + styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background)); + outputBuffer.append(text); + scheduleOutput = outputScheduled.compareAndSet(false, true); + } + if(scheduleOutput) { + final Display display = Display.getDefault(); + if(display.isDisposed()) return; + display.asyncExec(() -> { if(output.isDisposed()) return; - int pos = output.getCharCount(); - outputModiLock = true; - output.replaceTextRange(pos, 0, text); - outputModiLock = false; - output.setStyleRange(new StyleRange(pos, text.length(), - foreground, background)); + String outputText; + StyleRange[] styleRangeArray; + synchronized(outputBuffer) { + outputScheduled.set(false); + + outputText = outputBuffer.toString(); + outputBuffer = new StringBuilder(); + + styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]); + styleRanges.clear(); + } + + int addedLength = outputText.length(); + int currentLength = output.getCharCount(); + int insertPos = currentLength; + int newLength = insertPos + addedLength; + + if (limitConsoleOutput && newLength > highWatermark) { + // Test for corner case: buffer overflows and more text is incoming than fits low watermark + if (addedLength > lowWatermark) { + // Prune the new input text first if it is too large to fit in the buffer even on its own to be < lowWatermark + int removedCharacters = addedLength - lowWatermark; + + outputText = outputText.substring(removedCharacters); + addedLength = outputText.length(); + newLength = insertPos + addedLength; + + // Prune new incoming style ranges also + int firstStyleRangeToCopy = 0; + for (int i = 0; i < styleRangeArray.length; ++i, ++firstStyleRangeToCopy) { + StyleRange sr = styleRangeArray[i]; + if ((sr.start + sr.length) > removedCharacters) { + if (sr.start < removedCharacters) + sr.start = removedCharacters; + break; + } + } + styleRangeArray = Arrays.copyOfRange(styleRangeArray, firstStyleRangeToCopy, styleRangeArray.length); + for (StyleRange sr : styleRangeArray) + sr.start -= removedCharacters; + } + + int minimallyRemoveFromBegin = Math.min(currentLength, newLength - lowWatermark); + + // Find the next line change to prune the text until then + StyledTextContent content = output.getContent(); + int lineCount = content.getLineCount(); + int lastRemovedLine = content.getLineAtOffset(minimallyRemoveFromBegin); + int removeUntilOffset = lastRemovedLine >= (lineCount-1) + ? currentLength + : content.getOffsetAtLine(lastRemovedLine + 1); + + insertPos -= removeUntilOffset; + + outputModiLock = true; + output.replaceTextRange(0, removeUntilOffset, ""); + output.replaceTextRange(insertPos, 0, outputText); + outputModiLock = false; + } else { + // Buffer does not need to be pruned, just append at end + outputModiLock = true; + output.replaceTextRange(insertPos, 0, outputText); + outputModiLock = false; + } + + for (StyleRange styleRange : styleRangeArray) { + styleRange.start += insertPos; + output.setStyleRange(styleRange); + } + output.setCaretOffset(output.getCharCount()); output.showSelection(); - } - - }); + }); + } } - - private void execute() { + + private void execute() { String command = input.getText().trim(); if(command.isEmpty()) return; @@ -436,7 +571,7 @@ public abstract class AbstractCommandConsole extends Composite { // Print it into output area //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null); - input.setText(""); + input.setText(""); //$NON-NLS-1$ // Execute execute(command); @@ -473,8 +608,7 @@ public abstract class AbstractCommandConsole extends Composite { range.start = 0; range.length = 1; input.setStyleRange(range); - System.err.println("The following error message didn't have a proper location:"); - System.err.println(annotation.description); + getLogger().error("The following error message didn't have a proper location: {}", annotation.description, e); //$NON-NLS-1$ } } } @@ -482,18 +616,15 @@ public abstract class AbstractCommandConsole extends Composite { private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) { if(input.isDisposed()) return; - input.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - if(input.isDisposed()) - return; - if(!input.getText().equals(forCommand)) - return; - syncSetErrorAnnotations(forCommand, annotations); - } + input.getDisplay().asyncExec(() -> { + if(input.isDisposed()) + return; + if(!input.getText().equals(forCommand)) + return; + syncSetErrorAnnotations(forCommand, annotations); }); } - + private boolean readPreferences() { IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID); @@ -504,6 +635,13 @@ public abstract class AbstractCommandConsole extends Composite { commandHistory = new ArrayList(recentImportPaths); commandHistoryPos = commandHistory.size(); + limitConsoleOutput = store.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT); + lowWatermark = store.getInt(Preferences.CONSOLE_LOW_WATER_MARK); + highWatermark = store.getInt(Preferences.CONSOLE_HIGH_WATER_MARK); + + preferences = InstanceScope.INSTANCE.getNode(PLUGIN_ID); + preferences.addPreferenceChangeListener(preferenceListener); + return true; } @@ -523,7 +661,7 @@ public abstract class AbstractCommandConsole extends Composite { public void clear() { outputModiLock = true; - output.setText(""); + output.setText(""); //$NON-NLS-1$ outputModiLock = false; } @@ -531,4 +669,34 @@ public abstract class AbstractCommandConsole extends Composite { return output; } + IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) ); + } + }; + + private void setTextFont(FontDescriptor font) { + FontDescriptor oldFontDesc = textFontDescriptor; + textFont = resourceManager.createFont(font); + textFontDescriptor = font; + applyTextFont(textFont); + + // Only destroy old font after the new font has been set! + if (oldFontDesc != null) + resourceManager.destroyFont(oldFontDesc); + } + + private void applyTextFont(Font font) { + if (output != null) + output.setFont(font); + if (deco != null) + deco.setFont(font); + if (input != null) { + input.setFont(font); + adjustInputSize(input.getText()); + } + } + + public abstract Logger getLogger(); }