commit 54c68d737525b81da68cf12f4289575505c4b9f0 Author: Tomasz Półgrabia Date: Sun Jan 3 20:18:57 2021 +0100 Initial version of the project for java studies from 2013. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b12e5aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.class + +# Package Files # +*.jar +*.war +*.ear + +.idea +.gradle +out +build diff --git a/BFInterpreter/build.gradle b/BFInterpreter/build.gradle new file mode 100644 index 0000000..52edf42 --- /dev/null +++ b/BFInterpreter/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' +apply plugin: 'application' + +repositories { + mavenCentral() +} + +dependencies { + +} + +mainClassName = 'tpsa.BFInterpreter' diff --git a/BFInterpreter/src/main/java/tpsa/BFInterpreter.java b/BFInterpreter/src/main/java/tpsa/BFInterpreter.java new file mode 100644 index 0000000..7349dd5 --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/BFInterpreter.java @@ -0,0 +1,70 @@ +package tpsa; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +public class BFInterpreter { + + static LogManager lm; + public static Logger lg; + static Level lv = Level.OFF; + + static { + lm = LogManager.getLogManager(); + lm.addLogger(Logger.getLogger("BFInterpreter")); + lg = lm.getLogger("BFInterpreter"); + lg.setLevel(lv); + for (Handler h : BFInterpreter.lg.getHandlers()) + h.setLevel(lv); + + for (Handler h : Logger.getLogger("").getHandlers()) + h.setLevel(lv); + + for (Handler h : Logger.getLogger("").getHandlers()) + h.setLevel(lv); + + } + + /** + * @param args + */ + public static void main(String[] args) { + + Thread t = new Thread( + new BFMachine( + "+[,+[-[>+>+<<-]>[<+>-]+>>++++++++[<-------->-]<-[<[-]>>>+[<+<+>>-]<[>+<-]<[<++>\n" + + ">>+[<+<->>-]<[>+<-]]>[<]<]>>[-]<<<[[-]<[>>+>+<<<-]>>[<<+>>-]>>++++++++[<-------\n" + + "->-]<->>++++[<++++++++>-]<-<[>>>+<<[>+>[-]<<-]>[<+>-]>[<<<<<+>>>>++++[<++++++++\n" + + ">-]>-]<<-<-]>[<<<<[-]>>>>[<<<<->>>>-]]<<++++[<<++++++++>>-]<<-[>>+>+<<<-]>>[<<+\n" + + ">>-]+>>+++++[<----->-]<-[<[-]>>>+[<+<->>-]<[>+<-]<[<++>>>+[<+<+>>-]<[>+<-]]>[<]\n" + + "<]>>[-]<<<[[-]<<[>>+>+<<<-]>>[<<+>>-]+>------------[<[-]>>>+[<+<->>-]<[>+<-]<[<\n" + + "++>>>+[<+<+>>-]<[>+<-]]>[<]<]>>[-]<<<<<------------->>[[-]+++++[<<+++++>>-]<<+>\n" + + ">]<[>++++[<<++++++++>>-]<-]>]<[-]++++++++[<++++++++>-]<+>]<.[-]+>>+<]>[[-]<]<]")); + try { + t.start(); + t.join(); + } catch (InterruptedException e) { + BFInterpreter.lg.severe(BFInterpreter.ErrorsFormatter(e)); + } + + } + + public static String ErrorsFormatter(Exception excp) { + StringBuilder sb = new StringBuilder(); + String newLine = System.getProperty("line.separator"); + + sb.append("Exception catched: " + excp.getLocalizedMessage() + newLine); + StackTraceElement[] stack = excp.getStackTrace(); + for (int i = 0; i < stack.length; i++) { + sb.append("StackElement(" + i + ") : " + stack[i].getClassName() + + ", " + stack[i].getFileName() + ":" + + stack[i].getLineNumber() + ", " + + stack[i].getMethodName() + newLine); + } + + return sb.toString(); + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/BFMachine.java b/BFInterpreter/src/main/java/tpsa/BFMachine.java new file mode 100644 index 0000000..53678ee --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/BFMachine.java @@ -0,0 +1,381 @@ +package tpsa; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.Arrays; +import java.util.EmptyStackException; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicBoolean; + +import tpsa.exceptions.BFException; +import tpsa.exceptions.BadCallException; +import tpsa.exceptions.MalformedCodeException; +import tpsa.exceptions.OutOfMemoryPointer; +import tpsa.streams.JTextPaneInputStream; +import tpsa.streams.JTextPaneOutputStream; + +/** + * Brainfuck is an esoteric programming language noted for its extreme minimalism. + * It is a Turing tarpit, designed to challenge and amuse programmers, + * and was not made to be suitable for practical use. It was created in 1993 by Urban Müller. + * The name of the language is generally not capitalized except at the start of a sentence, + * although it is a proper noun. + * + * Język ma 8 mnemoników (patrz mnemoniki assemblera) + * + * > zwiększa wskaźnik o 1 ++p + * < zmniejsza wskaźnik o 1 --p + * + zwiększa o 1 w bieżącej pozycji ++(*p) + * - zmniejsza o 1 w bieżącej pozycji --(*p) + * . wyświetla znak w bieżącej pozycji (ASCII) putchar(*p) + * , pobiera znak i wstawia go w bieżącej pozycji (ASCII) *p=getchar() + * [ skacze bezpośrednio za odpowiadający mu ], jeśli w + * bieżącej pozycji znajduje się 0 while(*p){ + * ] skacze do odpowiadającego mu [ } + * + */ + +/** + * Klasa główna maszyny BF. Jest ona odpowiedzialna, za stworzenie VM dla BF i + * interpretowanie kodu BF. + * + * @author Duga Eye + */ +public class BFMachine implements Runnable { + + public static int defaultMemorySize = 0x10000; + /** + * Kod, dla maszyny wirtualnej BF + */ + private Runnable breakpointHandler; + private byte[] code; + private int codePointer = 0; + private String error; + private InputStreamReader iStreamReader = null; + private InputStream iS = null; + private Object lock = new Object(); + private AtomicBoolean machineWorking = new AtomicBoolean(false); + private byte[] memory; + private int memoryPointer = 0; + private OutputStreamWriter oStream = null; + private AtomicBoolean paused = new AtomicBoolean(false); + private Runnable postHandler; + private Stack stackExecution = new Stack(); + + public void reset() { + memoryPointer = memory.length / 2; + Arrays.fill(memory, (byte) 0); + try { + iS.reset(); + } catch (IOException excp) { + + } + codePointer = 0; + } + + private Runnable stepHandler; + + private AtomicBoolean stepping = new AtomicBoolean(false); + + public BFMachine(JTextPaneInputStream is, JTextPaneOutputStream os, + String code, int sizeOfMemory) { + this.code = code.getBytes(); + memory = new byte[sizeOfMemory]; + memoryPointer = sizeOfMemory / 2; + iS = is; + oStream = new OutputStreamWriter(os); + iStreamReader = new InputStreamReader(is); + } + + /** + * Domyślnie program startuje z wskaźnikiem pamięci w środku pamięci + * operacynej + * + * @param code + * Kod, który wykonywany jest przez maszynę BF + */ + public BFMachine(String code) { + this(code, defaultMemorySize); + } + + /** + * Domyślnie program startuje z wskaźnikiem pamięci w środku pamięci + * operacynej + * + * @param code + * Kod, który wykonywany jest przez maszynę BF + * @param sizeOfMemory + * rozmiar pamięci operacyjnej maszyny BF + */ + public BFMachine(String code, int sizeOfMemory) { + this.code = code.getBytes(); + memory = new byte[sizeOfMemory]; + memoryPointer = sizeOfMemory / 2; + error = ""; + } + + public Runnable getBreakpointHandler() { + return breakpointHandler; + } + + public String getError() { + if (error == null) + return new String(); + return new String(error); + } + + public byte[] getMemory() { + return memory; + } + + public int getMemoryPointer() { + return memoryPointer; + } + + public int getPosition() { + return codePointer; + } + + public Runnable getStepHandler() { + return stepHandler; + } + + public boolean getStepping() { + return stepping.get(); + } + + public boolean isWorking() { + return machineWorking.get(); + } + + /** + * Zatrzymuje pracę maszynę BF + */ + public void pause() { + paused.set(true); + } + + /** + * Wznawia pracę maszyny BF + */ + public void resume() { + paused.set(false); + synchronized (lock) { + lock.notify(); + } + } + + /** + * Przewija wskaźnik kodu na pozycję o 1 dalej niż jest koniec + * odpowiadającemu mu znakowi końca pętli + * + * @throws BadCallException + * W miejscu wskaźnika BF nie ma znaku początku pętli + * @throws MalformedCodeException + * Kod podany dla maszyny BF jest niepoprawny + */ + private void rewindTheLoopToTheEnd() throws BadCallException, + MalformedCodeException { + if (code[codePointer] != '[') + throw new BadCallException("Nie ma '[' w miejscu startu", + codePointer); + // System.err.println("Wchodzę w pozycji: " + codePointer); + int stack = 1; + codePointer++; + + while (stack > 0) { + while (code[codePointer] != '[' && code[codePointer] != ']') { + // System.err.println("Pozycja: " + codePointer + ", char: " + + // code[codePointer]); + if (++codePointer >= code.length) + throw new MalformedCodeException( + "Nie wiem jakim cudem, ale pointer wyszedł poza kod", + codePointer); + } + // System.err.println("Pozycja: " + codePointer + ", char: " + + // code[codePointer]); + switch (code[codePointer]) { + case (byte) 0xcc: + /* + * if (breakpointHandled.get() == false) return; else { + * breakpointHandled.set(true); } + */ + break; + case '[': + ++stack; + break; + case ']': + --stack; + break; + } + codePointer++; + } + // System.err.println("Znak wyjścia: " + codePointer + ", char: " + + // code[codePointer]); + + } + + @Override + public void run() { + memoryPointer = memory.length / 2; + codePointer = 0; + + BFInterpreter.lg.info("Machine starting"); + + BFInterpreter.lg.finest("Code" + new String(code)); + + machineWorking.set(true); + + try { + + while (codePointer < code.length && machineWorking.get()) { + + if (paused.get() || stepping.get()) { + + if (stepHandler != null && stepping.get()) { + BFInterpreter.lg.fine("Stepping item"); + System.err.println("Joł"); + stepHandler.run(); + } + + synchronized (lock) { + lock.wait(); + } + } + + // BFInterpreter.lg.finest("Executing code at " + codePointer); + + switch (code[codePointer]) { + case 'b': + if (breakpointHandler != null) { + breakpointHandler.run(); + synchronized (lock) { + lock.wait(); + } + + } else + return; + break; + case '>': + if (++memoryPointer >= memory.length) + throw new OutOfMemoryPointer( + "Pointer wyszedł poza pamięć", codePointer); + break; + case '<': + if (--memoryPointer < 0) + throw new OutOfMemoryPointer( + "Pointer wyszedł poza pamięć", codePointer); + break; + case '+': + ++memory[memoryPointer]; + break; + case '-': + --memory[memoryPointer]; + break; + case '.': + BFInterpreter.lg.finest("Priting char"); + if (oStream == null) { + System.out.print(memory[memoryPointer]); + } else { + oStream.write(memory[memoryPointer]); + } + break; + case ',': + BFInterpreter.lg.fine("Reading char from input"); + int c = (iStreamReader == null) ? System.in.read() : iStreamReader + .read(); + BFInterpreter.lg.fine("Read input code " + c); + memory[memoryPointer] = (byte) c; + BFInterpreter.lg.fine("Read char"); + break; + case '[': + // System.err.println("Wartość w komórce: " + + // (int)memory[memoryPointer]); + if (memory[memoryPointer] > 0) + stackExecution.push(codePointer); + else { + rewindTheLoopToTheEnd(); + continue; + } + break; + case ']': + int pos = stackExecution.pop(); + codePointer = pos; + continue; + } + codePointer++; + Thread.yield(); + } + if (oStream != null) + oStream.flush(); + // oStream.close(); + } catch (BFException excp) { + String message = excp.getMessage(); + if (message == null) + message = new String("Exception"); + setError(excp.getClass().getName() + ": " + message + ", offset: " + + excp.getOffset()); + + } catch (EmptyStackException excp) { + setError(excp.getClass().getName() + + "Stary, klamerki się nie zgadzają"); + } catch (IOException excp) { + setError("No nic, IOException..., cokolwiek by to nie znaczyło"); + } catch (InterruptedException excp) { + BFInterpreter.lg.severe(BFInterpreter.ErrorsFormatter(excp)); + } + + if (postHandler != null && machineWorking.get() == true) + postHandler.run(); + + machineWorking.set(false); + + BFInterpreter.lg.finest("Machine has ended"); + } + + public void setBreakpointHandler(Runnable breakpointHandler) { + this.breakpointHandler = breakpointHandler; + } + + public void setCode(String code) { + this.code = code.getBytes(); + } + + public void setError(String error) { + this.error = new String(error); + } + + public void setFinishHandler(Runnable runnable) { + this.postHandler = runnable; + + } + + public void setStepHandler(Runnable stepHandler) { + this.stepHandler = stepHandler; + } + + public void setStepping(boolean b) { + this.stepping.set(b); + } + + /** + * Definitywnie kończy pracę maszyny (nie da się wznowić) + * + * @see pause + */ + public void stop() { + // definitywnie kończy pracę, nie da się wznowić + machineWorking.set(false); + } + + public byte[] getCode() { + return code; + } + + public void setCode(byte[] code) { + this.code = code; + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/exceptions/BFException.java b/BFInterpreter/src/main/java/tpsa/exceptions/BFException.java new file mode 100644 index 0000000..c2bc08b --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/exceptions/BFException.java @@ -0,0 +1,27 @@ +package tpsa.exceptions; + +/** + * Ogólny exception dla BF + * + * @author Duga Eye + * + */ +public class BFException extends Exception { + + /** + * + */ + private static final long serialVersionUID = -1955781345162254359L; + + private int offset; + + public BFException(String message, int offset) { + super(message); + this.offset = offset; + } + + public int getOffset() { + return offset; + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/exceptions/BadCallException.java b/BFInterpreter/src/main/java/tpsa/exceptions/BadCallException.java new file mode 100644 index 0000000..7dadb08 --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/exceptions/BadCallException.java @@ -0,0 +1,21 @@ +package tpsa.exceptions; + +/** + * Wyjątek rzucany gdy funkcja rewindTheLoopToTheEnd zostanie wywołana nie + * wtedy, gdy codePointer będzie wskazywał na '[' + * + * @author Duga Eye + * + */ +public class BadCallException extends BFException { + + /** + * + */ + private static final long serialVersionUID = -2134149027354550342L; + + public BadCallException(String message, int offset) { + super(message, offset); + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/exceptions/MalformedCodeException.java b/BFInterpreter/src/main/java/tpsa/exceptions/MalformedCodeException.java new file mode 100644 index 0000000..b1fc834 --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/exceptions/MalformedCodeException.java @@ -0,0 +1,21 @@ +package tpsa.exceptions; + +/** + * W zasadzie ten wyjątek nigdy nie powinien wystąpić: "Nie wiem jakim cudem, + * ale pointer wyszedł poza kod" + * + * @author Duga Eye + * + */ +public class MalformedCodeException extends BFException { + + /** + * + */ + private static final long serialVersionUID = 5072506605351465843L; + + public MalformedCodeException(String message, int offset) { + super(message, offset); + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/exceptions/NotImplementedException.java b/BFInterpreter/src/main/java/tpsa/exceptions/NotImplementedException.java new file mode 100644 index 0000000..96bdf76 --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/exceptions/NotImplementedException.java @@ -0,0 +1,14 @@ +package tpsa.exceptions; + +public class NotImplementedException extends BFException { + + /** + * + */ + private static final long serialVersionUID = 8190551201861687817L; + + public NotImplementedException(String message, int offset) { + super(message, offset); + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/exceptions/OutOfMemoryPointer.java b/BFInterpreter/src/main/java/tpsa/exceptions/OutOfMemoryPointer.java new file mode 100644 index 0000000..408caef --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/exceptions/OutOfMemoryPointer.java @@ -0,0 +1,20 @@ +package tpsa.exceptions; + +/** + * Wskaźnik pamięci wyszedł poza pamięć + * + * @author Duga Eye + * + */ +public class OutOfMemoryPointer extends BFException { + + /** + * + */ + private static final long serialVersionUID = 6323824785754255254L; + + public OutOfMemoryPointer(String message, int offset) { + super(message, offset); + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/streams/JTextPaneInputStream.java b/BFInterpreter/src/main/java/tpsa/streams/JTextPaneInputStream.java new file mode 100644 index 0000000..b200add --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/streams/JTextPaneInputStream.java @@ -0,0 +1,108 @@ +package tpsa.streams; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Semaphore; +import java.util.logging.Logger; + +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyledDocument; + +import tpsa.BFInterpreter; + +/** + * Klasa dająca strumień wejściowy z JTextPane dla BF maszyny + * + * @author Duga Eye + * + */ +public class JTextPaneInputStream extends InputStream implements + DocumentListener { + + private boolean eof = false; + private Logger lg = Logger.getLogger("BF"); + private Semaphore sem = new Semaphore(0); + private StringBuilder string = new StringBuilder(); + + public JTextPaneInputStream() { + } + + public void reset() { + eof = false; + string.replace(0, string.length(), ""); + try { + if (sem.availablePermits() > 0) + sem.acquire(sem.availablePermits()); + } catch (InterruptedException excp) { + + } + } + + @Override + public void changedUpdate(DocumentEvent e) { + + } + + @Override + public void insertUpdate(final DocumentEvent e) { + try { + lg.info("Strumień (handler) otrzymał nowe powiadomienie o wprowadzonych danych"); + String s = e.getDocument().getText(e.getOffset(), e.getLength()); + string.append(s); + sem.release(s.length()); + lg.info("Wartość semafora: " + sem.availablePermits()); + lg.info("Strumień dodał parę bajtów do bufora"); + + final StyledDocument doc = (StyledDocument) e.getDocument(); + final Style set = doc.getStyle("INPUT"); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + doc.setParagraphAttributes(e.getOffset(), e.getLength(), + set, false); + + } + }); + + } catch (BadLocationException excp) { + BFInterpreter.lg.severe(BFInterpreter.ErrorsFormatter(excp)); + } + } + + @Override + public int read() throws IOException { + if (eof == true) { + return -1; + } + + try { + lg.info("Strumień ma dać jeden bajt"); + sem.acquire(); + lg.info("Strumień uzyskał bajt na semaforze"); + String c = string.substring(0, 1); + char c2 = c.charAt(0); + Character c3 = '\u001a'; + if (c3.equals(c2)) { + lg.info("ERROR SUCCESS"); + eof = true; + return -1; + } + string.delete(0, 1); + lg.info("Strumień usunął wczytany bajt z bufora"); + return c2; + } catch (Exception e) { + return -1; + } + } + + @Override + public void removeUpdate(DocumentEvent e) { + + } + +} diff --git a/BFInterpreter/src/main/java/tpsa/streams/JTextPaneOutputStream.java b/BFInterpreter/src/main/java/tpsa/streams/JTextPaneOutputStream.java new file mode 100644 index 0000000..5d67903 --- /dev/null +++ b/BFInterpreter/src/main/java/tpsa/streams/JTextPaneOutputStream.java @@ -0,0 +1,51 @@ +package tpsa.streams; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Logger; + +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyledDocument; + +import tpsa.BFInterpreter; + +/** + * Klasa strumień wyjściowy do JTextPane dla BF maszyny + * + * @author Duga Eye + * + */ +public class JTextPaneOutputStream extends OutputStream { + + private Logger lg = Logger.getLogger("BF"); + private JTextPane pane; + + public JTextPaneOutputStream() { + } + + public JTextPane getPane() { + return pane; + } + + public void setPane(JTextPane pane) { + this.pane = pane; + } + + @Override + public void write(int b) throws IOException { + lg.finest("Writing byte :" + b); + StyledDocument doc = (StyledDocument) pane.getDocument(); + char c = (char) b; + String s = "" + c; + Style style = doc.getStyle("OUTPUT"); + try { + doc.insertString(doc.getLength(), s, style); + } catch (BadLocationException excp) { + BFInterpreter.lg.severe(BFInterpreter.ErrorsFormatter(excp)); + } + + } + +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4541dba --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Róbta co chceta z tym kodem, tylko nie publikujcie tego z moim imieniem, nazwiskiem, czy nickiem. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7db35a9 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Purpose + +It was a test project for java studies (to complete the course). Except of putting it under the gradle control (it +wasn't), whole code is original from those times. I will try to translate original comments, naming convention to +non-polish. + +# Technology + +Ancient swing gui with some custom helpers from other authors + +- https://github.com/sporst/splib +- https://github.com/Konloch/bytecode-viewer + HexEditor (com.jhe.hexed) and other (tv.ports.*) are not mine, just reused. diff --git a/bfIDE/brainfuckprintout.bf b/bfIDE/brainfuckprintout.bf new file mode 100644 index 0000000..737c5ec --- /dev/null +++ b/bfIDE/brainfuckprintout.bf @@ -0,0 +1,2 @@ +Pisze kod, dla inputa, taki że kod wygenerowany generuje wpisany input +,[>>++++++[-<+++++++>]<+<[->.<]>+++.<++++[->++++<]>.>,] diff --git a/bfIDE/build.gradle b/bfIDE/build.gradle new file mode 100644 index 0000000..673a92d --- /dev/null +++ b/bfIDE/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' +apply plugin: 'application' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':BFInterpreter') +} + +mainClassName = 'tpsa.BrainFuckIDE' diff --git a/bfIDE/failure.bf b/bfIDE/failure.bf new file mode 100644 index 0000000..0ce005c --- /dev/null +++ b/bfIDE/failure.bf @@ -0,0 +1 @@ +[]] \ No newline at end of file diff --git a/bfIDE/hacker.bf b/bfIDE/hacker.bf new file mode 100644 index 0000000..fc733bc --- /dev/null +++ b/bfIDE/hacker.bf @@ -0,0 +1,3 @@ ++++[>+++++<-]>[>+>+++>+>++>+++++>++<[++<]>---]>->-.[>++>+<<--]>--.--.+.>>>++.<< +.<------.+.+++++.>>-.<++++.<--.>>>.<<---.<.-->-.>+.[+++++.---<]>>[.--->]<<.<+.+ ++.++>+++[.<][.]<++. diff --git a/bfIDE/hello.bf b/bfIDE/hello.bf new file mode 100644 index 0000000..fd84f54 Binary files /dev/null and b/bfIDE/hello.bf differ diff --git a/bfIDE/infinite.bf b/bfIDE/infinite.bf new file mode 100644 index 0000000..5aae817 --- /dev/null +++ b/bfIDE/infinite.bf @@ -0,0 +1 @@ ++[] \ No newline at end of file diff --git a/bfIDE/memoryOffset.bf b/bfIDE/memoryOffset.bf new file mode 100644 index 0000000..03026da --- /dev/null +++ b/bfIDE/memoryOffset.bf @@ -0,0 +1 @@ ++[>+] \ No newline at end of file diff --git a/bfIDE/squares.bf b/bfIDE/squares.bf new file mode 100644 index 0000000..21483be --- /dev/null +++ b/bfIDE/squares.bf @@ -0,0 +1,3 @@ +++++[>+++++<-]>[<+++++>-]+<+[>[>+>+<<-]++>>[<<+>>-]>>>[-]++>[-]+>>>+[[-]++++++> +>>]<<<[[<++++++++<++>>-]+<.<[>----<-]<]<<[>>>>>[>>>[-]+++++++++<[>-<-]+++++++++ +>[-[<->-]+[<<<]]<[>+<-]>]<<-]<<-] diff --git a/bfIDE/src/main/java/com/jhe/hexed/JHexEditor.java b/bfIDE/src/main/java/com/jhe/hexed/JHexEditor.java new file mode 100644 index 0000000..ad9c86e --- /dev/null +++ b/bfIDE/src/main/java/com/jhe/hexed/JHexEditor.java @@ -0,0 +1,324 @@ +package com.jhe.hexed; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +import javax.swing.JPanel; +import javax.swing.JScrollBar; + +/** + * Created by IntelliJ IDEA. + * User: laullon + * Date: 08-abr-2003 + * Time: 13:21:09 + */ +public class JHexEditor extends JPanel implements FocusListener,AdjustmentListener,MouseWheelListener +{ + /** + * + */ + private static final long serialVersionUID = 2289328616534802372L; + byte[] buff; + public int cursor; + protected static Font font=new Font("Monospaced",0,12); + protected int border=2; + public boolean DEBUG=false; + private JPanel panel; + private JScrollBar sb; + private int inicio=0; + private int lineas=10; + + public JHexEditor(byte[] buff) + { + super(); + this.buff=buff; + + this.addMouseWheelListener(this); + + sb=new JScrollBar(JScrollBar.VERTICAL); + sb.addAdjustmentListener(this); + // sb.setMinimum(0); + // sb.setMaximum(buff.length/getLineas()); + + JPanel p1,p2,p3; + //centro + p1=new JPanel(new BorderLayout(1,1)); + p1.add(new JHexEditorHEX(this),BorderLayout.CENTER); + p1.add(new Columnas(),BorderLayout.NORTH); + + // izq. + p2=new JPanel(new BorderLayout(1,1)); + p2.add(new Filas(),BorderLayout.CENTER); + p2.add(new Caja(),BorderLayout.NORTH); + + // der + p3=new JPanel(new BorderLayout(1,1)); + p3.add(sb,BorderLayout.EAST); + p3.add(new JHexEditorASCII(this),BorderLayout.CENTER); + p3.add(new Caja(),BorderLayout.NORTH); + + panel=new JPanel(); + panel.setLayout(new BorderLayout(1,1)); + panel.add(p1,BorderLayout.CENTER); + panel.add(p2,BorderLayout.WEST); + panel.add(p3,BorderLayout.EAST); + + this.setLayout(new BorderLayout(1,1)); + this.add(panel,BorderLayout.CENTER); + } + + public void paint(Graphics g) + { + FontMetrics fn=getFontMetrics(font); + Rectangle rec=this.getBounds(); + lineas=(rec.height/fn.getHeight())-1; + int n=(buff.length/16)-1; + if(lineas>n) { lineas=n; inicio=0; } + + sb.setValues(getInicio(),+getLineas(),0,buff.length/16); + sb.setValueIsAdjusting(true); + super.paint(g); + } + + protected void actualizaCursor() + { + int n=(cursor/16); + + System.out.print("- "+inicio+"<"+n+"<"+(lineas+inicio)+"("+lineas+")"); + + if(n=inicio+lineas) inicio=n-(lineas-1); + + System.out.println(" - "+inicio+"<"+n+"<"+(lineas+inicio)+"("+lineas+")"); + + repaint(); + } + + protected int getInicio() + { + return inicio; + } + + protected int getLineas() + { + return lineas; + } + + protected void fondo(Graphics g,int x,int y,int s) + { + FontMetrics fn=getFontMetrics(font); + g.fillRect(((fn.stringWidth(" ")+1)*x)+border,(fn.getHeight()*y)+border,((fn.stringWidth(" ")+1)*s),fn.getHeight()+1); + } + + protected void cuadro(Graphics g,int x,int y,int s) + { + FontMetrics fn=getFontMetrics(font); + g.drawRect(((fn.stringWidth(" ")+1)*x)+border,(fn.getHeight()*y)+border,((fn.stringWidth(" ")+1)*s),fn.getHeight()+1); + } + + protected void printString(Graphics g,String s,int x,int y) + { + FontMetrics fn=getFontMetrics(font); + g.drawString(s,((fn.stringWidth(" ")+1)*x)+border,((fn.getHeight()*(y+1))-fn.getMaxDescent())+border); + } + + public void focusGained(FocusEvent e) + { + this.repaint(); + } + + public void focusLost(FocusEvent e) + { + this.repaint(); + } + + public void adjustmentValueChanged(AdjustmentEvent e) + { + inicio=e.getValue(); + if(inicio<0) inicio=0; + repaint(); + } + + public void mouseWheelMoved(MouseWheelEvent e) + { + inicio+=(e.getUnitsToScroll()); + if((inicio+lineas)>=buff.length/16) inicio=(buff.length/16)-lineas; + if(inicio<0) inicio=0; + repaint(); + } + + public void keyPressed(KeyEvent e) + { + switch(e.getKeyCode()) + { + case 33: // rep + if(cursor>=(16*lineas)) cursor-=(16*lineas); + actualizaCursor(); + break; + case 34: // fin + if(cursor<(buff.length-(16*lineas))) cursor+=(16*lineas); + actualizaCursor(); + break; + case 35: // fin + cursor=buff.length-1; + actualizaCursor(); + break; + case 36: // ini + cursor=0; + actualizaCursor(); + break; + case 37: // <-- + if(cursor!=0) cursor--; + actualizaCursor(); + break; + case 38: // <-- + if(cursor>15) cursor-=16; + actualizaCursor(); + break; + case 39: // --> + if(cursor!=(buff.length-1)) cursor++; + actualizaCursor(); + break; + case 40: // --> + if(cursor<(buff.length-16)) cursor+=16; + actualizaCursor(); + break; + } + } + + private class Columnas extends JPanel + { + /** + * + */ + private static final long serialVersionUID = -1734199617526339842L; + + public Columnas() + { + this.setLayout(new BorderLayout(1,1)); + } + public Dimension getPreferredSize() + { + return getMinimumSize(); + } + + public Dimension getMinimumSize() + { + Dimension d=new Dimension(); + FontMetrics fn=getFontMetrics(font); + int h=fn.getHeight(); + int nl=1; + d.setSize(((fn.stringWidth(" ") + 1) * +((16 * 3) - 1)) + + (border * 2) + 1, h * nl + (border * 2) + 1); + return d; + } + + public void paint(Graphics g) + { + Dimension d=getMinimumSize(); + g.setColor(Color.white); + g.fillRect(0,0,d.width,d.height); + g.setColor(Color.black); + g.setFont(font); + + for(int n=0;n<16;n++) + { + if(n==(cursor%16)) cuadro(g,n*3,0,2); + String s="00"+Integer.toHexString(n); + s=s.substring(s.length()-2); + printString(g,s,n*3,0); + } + } + } + + private class Caja extends JPanel + { + /** + * + */ + private static final long serialVersionUID = -6124062720565016834L; + + public Dimension getPreferredSize() + { + return getMinimumSize(); + } + + public Dimension getMinimumSize() + { + Dimension d=new Dimension(); + FontMetrics fn=getFontMetrics(font); + int h=fn.getHeight(); + d.setSize((fn.stringWidth(" ")+1)+(border*2)+1,h+(border*2)+1); + return d; + } + + } + + private class Filas extends JPanel + { + /** + * + */ + private static final long serialVersionUID = 8797347523486018051L; + + public Filas() + { + this.setLayout(new BorderLayout(1,1)); + } + public Dimension getPreferredSize() + { + return getMinimumSize(); + } + + public Dimension getMinimumSize() + { + Dimension d=new Dimension(); + FontMetrics fn=getFontMetrics(font); + int h=fn.getHeight(); + int nl=getLineas(); + d.setSize((fn.stringWidth(" ")+1)*(8)+(border*2)+1,h*nl+(border*2)+1); + return d; + } + + public void paint(Graphics g) + { + Dimension d=getMinimumSize(); + g.setColor(Color.white); + g.fillRect(0,0,d.width,d.height); + g.setColor(Color.black); + g.setFont(font); + + int ini=getInicio(); + int fin=ini+getLineas(); + int y=0; + for(int n=ini;n126)) s=""+(char)16; + he.printString(g,s,(x++),y); + if(x==16) + { + x=0; + y++; + } + } + + } + + private void debug(String s) + { + if(he.DEBUG) System.out.println("JHexEditorASCII ==> "+s); + } + + // calcular la posicion del raton + public int calcularPosicionRaton(int x,int y) + { + FontMetrics fn = getFontMetrics(JHexEditor.font); + x=x/(fn.stringWidth(" ")+1); + y=y/fn.getHeight(); + debug("x="+x+" ,y="+y); + return x+((y+he.getInicio())*16); + } + + // mouselistener + public void mouseClicked(MouseEvent e) + { + debug("mouseClicked("+e+")"); + he.cursor=calcularPosicionRaton(e.getX(),e.getY()); + this.requestFocus(); + he.repaint(); + } + + public void mousePressed(MouseEvent e) + { + } + + public void mouseReleased(MouseEvent e) + { + } + + public void mouseEntered(MouseEvent e) + { + } + + public void mouseExited(MouseEvent e) + { + } + + //KeyListener + public void keyTyped(KeyEvent e) + { + debug("keyTyped("+e+")"); + + he.buff[he.cursor]=(byte)e.getKeyChar(); + + if(he.cursor!=(he.buff.length-1)) he.cursor++; + he.repaint(); + } + + public void keyPressed(KeyEvent e) + { + debug("keyPressed("+e+")"); + he.keyPressed(e); + } + + public void keyReleased(KeyEvent e) + { + debug("keyReleased("+e+")"); + } + + public boolean isFocusTraversable() + { + return true; + } +} diff --git a/bfIDE/src/main/java/com/jhe/hexed/JHexEditorHEX.java b/bfIDE/src/main/java/com/jhe/hexed/JHexEditorHEX.java new file mode 100644 index 0000000..f7c1d14 --- /dev/null +++ b/bfIDE/src/main/java/com/jhe/hexed/JHexEditorHEX.java @@ -0,0 +1,188 @@ +package com.jhe.hexed; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JComponent; + +/** + * Created by IntelliJ IDEA. + * User: laullon + * Date: 09-abr-2003 + * Time: 12:47:32 + */ +public class JHexEditorHEX extends JComponent implements MouseListener,KeyListener +{ + /** + * + */ + private static final long serialVersionUID = -4919858660968739876L; + private JHexEditor he; + private int cursor=0; + + public JHexEditorHEX(JHexEditor he) + { + this.he=he; + addMouseListener(this); + addKeyListener(this); + addFocusListener(he); + } + + public Dimension getPreferredSize() + { + debug("getPreferredSize()"); + return getMinimumSize(); + } + + public Dimension getMaximumSize() + { + debug("getMaximumSize()"); + return getMinimumSize(); + } + + public Dimension getMinimumSize() + { + debug("getMinimumSize()"); + + Dimension d=new Dimension(); + FontMetrics fn = getFontMetrics(JHexEditor.font); + int h=fn.getHeight(); + int nl=he.getLineas(); + d.setSize(((fn.stringWidth(" ")+1)*+((16*3)-1))+(he.border*2)+1,h*nl+(he.border*2)+1); + return d; + } + + public void paint(Graphics g) + { + debug("paint("+g+")"); + debug("cursor="+he.cursor+" buff.length="+he.buff.length); + Dimension d=getMinimumSize(); + g.setColor(Color.white); + g.fillRect(0,0,d.width,d.height); + g.setColor(Color.black); + + g.setFont(JHexEditor.font); + + int ini=he.getInicio()*16; + int fin=ini+(he.getLineas()*16); + if(fin>he.buff.length) fin=he.buff.length; + + //datos hex + int x=0; + int y=0; + for(int n=ini;n "+s); + } + + // calcular la posicion del raton + public int calcularPosicionRaton(int x,int y) + { + FontMetrics fn = getFontMetrics(JHexEditor.font); + x=x/((fn.stringWidth(" ")+1)*3); + y=y/fn.getHeight(); + debug("x="+x+" ,y="+y); + return x+((y+he.getInicio())*16); + } + + // mouselistener + public void mouseClicked(MouseEvent e) + { + debug("mouseClicked("+e+")"); + he.cursor=calcularPosicionRaton(e.getX(),e.getY()); + this.requestFocus(); + he.repaint(); + } + + public void mousePressed(MouseEvent e) + { + } + + public void mouseReleased(MouseEvent e) + { + } + + public void mouseEntered(MouseEvent e) + { + } + + public void mouseExited(MouseEvent e) + { + } + + //KeyListener + public void keyTyped(KeyEvent e) + { + debug("keyTyped("+e+")"); + + char c=e.getKeyChar(); + if(((c>='0')&&(c<='9'))||((c>='A')&&(c<='F'))||((c>='a')&&(c<='f'))) + { + char[] str=new char[2]; + String n="00"+Integer.toHexString((int)he.buff[he.cursor]); + if(n.length()>2) n=n.substring(n.length()-2); + str[1-cursor]=n.charAt(1-cursor); + str[cursor]=e.getKeyChar(); + he.buff[he.cursor]=(byte)Integer.parseInt(new String(str),16); + + if(cursor!=1) cursor=1; + else if(he.cursor!=(he.buff.length-1)){ he.cursor++; cursor=0;} + he.actualizaCursor(); + } + } + + public void keyPressed(KeyEvent e) + { + debug("keyPressed("+e+")"); + he.keyPressed(e); + } + + public void keyReleased(KeyEvent e) + { + debug("keyReleased("+e+")"); + } + + public boolean isFocusTraversable() + { + return true; + } +} diff --git a/bfIDE/src/main/java/com/jhe/hexed/Test.java b/bfIDE/src/main/java/com/jhe/hexed/Test.java new file mode 100644 index 0000000..b58c95b --- /dev/null +++ b/bfIDE/src/main/java/com/jhe/hexed/Test.java @@ -0,0 +1,49 @@ +package com.jhe.hexed; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.Arrays; + +import javax.swing.JFrame; + +/** + * Created by IntelliJ IDEA. + * User: laullon + * Date: 08-abr-2003 + * Time: 13:16:06 + */ +public class Test extends WindowAdapter +{ + private JFrame win; + + public Test() throws IOException + { + byte[] ar; + ar=new byte[16*16*100]; + Arrays.fill(ar,(byte)0); + + //ByteArrayOutputStream bos=new ByteArrayOutputStream(); + //ObjectOutputStream oos=new ObjectOutputStream(bos); + //oos.writeObject("dfasnvcxnz.,mvnmc,xznvmcxzmnvcmxzcccbnxz cz hajk vc jbcvj xbnzvc sbj cvxz,bcxjnzbcvjhs avcjz cxmzncvxz "); + //ar=bos.toByteArray(); + + win=new JFrame(); + win.getContentPane().add(new JHexEditor(ar)); + win.addWindowListener(this); + win.pack(); + win.setVisible(true); + } + + public void windowClosing(WindowEvent e) + { + System.exit(0); + } + + + + public static void main(String arg[]) throws IOException + { + new Test(); + } +} diff --git a/bfIDE/src/main/java/tpsa/BFTab.java b/bfIDE/src/main/java/tpsa/BFTab.java new file mode 100644 index 0000000..349307f --- /dev/null +++ b/bfIDE/src/main/java/tpsa/BFTab.java @@ -0,0 +1,512 @@ +package tpsa; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Event; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; +import java.awt.event.KeyEvent; +import java.io.File; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextPane; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.UndoableEditListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +import tpsa.highlighters.SyntaxHighlighter; +import tpsa.streams.JTextPaneInputStream; +import tpsa.streams.JTextPaneOutputStream; +import tv.porst.jhexview.JHexView; +import tv.porst.jhexview.JHexView.DefinitionStatus; +import tv.porst.jhexview.SimpleDataProvider; + +/** + * Zakładka tego ide. Każda zakładka posiada swoją maszynę + * + * @author Duga Eye + * + */ +public class BFTab extends JPanel implements CaretListener, ComponentListener, + ActionListener, ContainerListener { + /** + * + */ + private static final long serialVersionUID = 3256469085182527862L; + protected static final double startSplitterPosition = 0.5; + /** + * JTextPane w którym umieszczamy kod BF + */ + private JTextPane codePanel; + private File file; + private Style foundStyle; + private SyntaxHighlighter high; + private String lastCodeWorking; + private BFMachine machine; + private JScrollPane pane; + private JTextPane paneResult; + /** + * Label, który pokazuje aktualną pozycję w tekście + */ + private JLabel positionLabel; + private TextLineNumber rows; + private JScrollPane scrollPaneResult; + private JPanel status; + private JLabel statusLabel; + private JTextPane memoryPanel = new JTextPane(); + private JSplitPane splitPane1; + private Timer t; + private JHexView editor; + + /** + * @param undoListener + * undo/redo listener + * @param pauseAction + * pause akcja + * @param resumeAction + * resume akcja + * @param stepAction + * step akcja + * @param stopAction + * stop akcja + * @param runAction + * run akcja + * @param undoAction + * undo akcja + * @param redoAction + * redo akcja + */ + public BFTab(UndoableEditListener undoListener, Action pauseAction, + Action resumeAction, Action stepAction, Action stopAction, + Action runAction, Action undoAction, Action redoAction) { + + t = new Timer(2000, this); + + lastCodeWorking = new String(); + codePanel = new JTextPane(); + pane = new JScrollPane(codePanel); + rows = new TextLineNumber(codePanel); + + status = new JPanel(); + status.setLayout(new BorderLayout()); + + statusLabel = new JLabel("Ready"); + positionLabel = new JLabel("0:0"); + status.add(statusLabel, BorderLayout.WEST); + status.add(positionLabel, BorderLayout.EAST); + + StyledDocument doc = (StyledDocument) codePanel.getDocument(); + + doc.addDocumentListener(rows); + doc.addUndoableEditListener(undoListener); + + codePanel.addCaretListener(this); + + pane.setRowHeaderView(rows); + + pane.setFocusable(true); + + KeyStroke undoKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Z, + Event.CTRL_MASK); + KeyStroke redoKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Y, + Event.CTRL_MASK); + + codePanel.getInputMap().put(undoKeyStroke, "undo"); + codePanel.getInputMap().put(redoKeyStroke, "redo"); + codePanel.getActionMap().put("undo", undoAction); + codePanel.getActionMap().put("redo", redoAction); + KeyStroke runWithoutStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F9, + Event.CTRL_MASK); + + KeyStroke stopStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F4, + Event.CTRL_MASK); + + codePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + runWithoutStroke, "RunWithoutDebug"); + codePanel.getActionMap().put("RunWithoutDebug", runAction); + + codePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + stopStroke, "StopAction"); + codePanel.getActionMap().put("StopAction", stopAction); + + KeyStroke stepStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F6, + Event.CTRL_MASK); + codePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + stepStroke, "stepAction"); + codePanel.getActionMap().put("stepAction", stepAction); + + // panel2.add(pane); + + paneResult = new JTextPane(); + KeyStroke eofStroke = KeyStroke.getKeyStroke(KeyEvent.VK_D, + Event.CTRL_MASK); + paneResult.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + eofStroke, "EOF"); + + KeyStroke pauseStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F5, + Event.CTRL_MASK); + codePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + pauseStroke, "pauseAction"); + codePanel.getActionMap().put("pauseAction", pauseAction); + + KeyStroke resumeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F7, + Event.CTRL_MASK); + codePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + resumeStroke, "resumeAction"); + codePanel.getActionMap().put("resumeAction", resumeAction); + + paneResult.getActionMap().put("EOF", new AbstractAction() { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent e) { + Document doc = paneResult.getDocument(); + String s = "\u001a"; + try { + doc.insertString(doc.getLength(), s, null); + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }); + + StyledDocument doc2 = (StyledDocument) paneResult.getDocument(); + Style sInput = doc2.addStyle("INPUT", null); + StyleConstants.setForeground(sInput, Color.red); + Style sOutput = doc2.addStyle("OUTPUT", null); + StyleConstants.setForeground(sOutput, Color.GREEN); + Style sComment = doc.addStyle("COMMENT", null); + StyleConstants.setItalic(sComment, true); + StyleConstants.setForeground(sComment, Color.BLUE); + Style sCode = doc.addStyle("CODE", null); + StyleConstants.setForeground(sCode, Color.BLACK); + Style sError = doc.addStyle("ERROR", null); + StyleConstants.setUnderline(sError, true); + StyleConstants.setStrikeThrough(sError, true); + StyleConstants.setForeground(sError, Color.RED); + Style sBFCaret = doc.addStyle("BFCaret", null); + StyleConstants.setBackground(sBFCaret, Color.RED); + Style sBreakpoint = doc.addStyle("BREAKPOINT", null); + StyleConstants.setBackground(sBreakpoint, Color.CYAN); + + Style empty = doc.addStyle("empty", null); + StyleConstants.setForeground(empty, Color.YELLOW); + + Style foundStyle = doc.addStyle("found", null); + StyleConstants.setBackground(foundStyle, Color.BLUE); + + high = new SyntaxHighlighter(); + doc.addDocumentListener(high); + + setLayout(new BorderLayout()); + + scrollPaneResult = new JScrollPane(paneResult); + + scrollPaneResult.setPreferredSize(new Dimension(80, 80)); + + add(status, BorderLayout.NORTH); + add(pane, BorderLayout.CENTER); + add(scrollPaneResult, BorderLayout.SOUTH); + + /* + * WordsHighlighter w = new WordsHighlighter(new String[] { "BF", + * "TODO", "FIXME", "XXX", "WARNING", "ERROR", "CRITICAL", "DEBUG", + * "FUNCTION", "PROCEDURE" }); codePanel.setHighlighter(w); + * codePanel.getDocument().addDocumentListener(w); + */ + + final JTextPaneInputStream is = new JTextPaneInputStream(); + final JTextPaneOutputStream os = new JTextPaneOutputStream(); + paneResult.getDocument().addDocumentListener(is); + os.setPane(paneResult); + + machine = new BFMachine(is, os, "", BFMachine.defaultMemorySize); + memoryPanel.setEditable(false); + memoryPanel.setPreferredSize(new Dimension(200, 200)); + + splitPane1 = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + editor = new JHexView(); + editor.setData(new SimpleDataProvider(machine.getMemory())); + editor.setDefinitionStatus(DefinitionStatus.DEFINED); + editor.setEnabled(true); + + // editor.set + splitPane1.add(pane); + splitPane1.add(editor); + splitPane1.setSize(new Dimension(800, 600)); + + addComponentListener(this); + // t.start(); + + add(splitPane1, BorderLayout.CENTER); + splitPane1.setDividerLocation(BFTab.startSplitterPosition); + + } + + @Override + public void caretUpdate(final CaretEvent e) { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + JTextPane pane = (JTextPane) e.getSource(); + BrainFuckIDE.lg.fine("Caret update" + e.getDot()); + + Document doc = pane.getDocument(); + try { + String s = doc.getText(0, e.getDot()); + TextEditorPosition pos = getLineNumer(s); + positionLabel.setText("" + pos.Line + ":" + pos.Offset); + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }); + + } + + public JTextPane getArea() { + return codePanel; + } + + public File getFile() { + return file; + } + + public Style getFoundStyle() { + return foundStyle; + } + + public SyntaxHighlighter getHigh() { + return high; + } + + public String getLastCodeWorking() { + return lastCodeWorking; + } + + protected TextEditorPosition getLineNumer(String s) { + String lineSeparator = System.getProperty("line.separator"); + + int line = 1; + int pos = -1, last = 0; + + BrainFuckIDE.lg.finer("contents: " + s); + + while ((pos = s.indexOf(lineSeparator, pos + lineSeparator.length())) >= 0) { + BrainFuckIDE.lg.finer("found separator: " + pos); + line++; + last = pos + lineSeparator.length(); + } + + return new TextEditorPosition(line, s.length() - last); + } + + public BFMachine getMachine() { + return machine; + } + + public JScrollPane getPane() { + return pane; + } + + public JTextPane getPaneResult() { + return paneResult; + } + + public JLabel getPositionLabel() { + return positionLabel; + } + + public TextLineNumber getRows() { + return rows; + } + + public JScrollPane getScrollPaneResult() { + return scrollPaneResult; + } + + public JPanel getStatus() { + return status; + } + + public JLabel getStatusLabel() { + return statusLabel; + } + + public void setArea(JTextPane area) { + this.codePanel = area; + } + + public void setFile(File file) { + this.file = file; + } + + public void setFoundStyle(Style foundStyle) { + this.foundStyle = foundStyle; + } + + public void setHigh(SyntaxHighlighter high) { + this.high = high; + } + + public void setLastCodeWorking(String lastCodeWorking) { + this.lastCodeWorking = lastCodeWorking; + } + + public void setMachine(BFMachine machine) { + this.machine = machine; + } + + public void setPane(JScrollPane pane) { + this.pane = pane; + } + + public void setPaneResult(JTextPane paneResult) { + this.paneResult = paneResult; + } + + public void setPositionLabel(JLabel positionLabel) { + this.positionLabel = positionLabel; + } + + public void setRows(TextLineNumber rows) { + this.rows = rows; + } + + public void setScrollPaneResult(JScrollPane scrollPaneResult) { + this.scrollPaneResult = scrollPaneResult; + } + + public void setStatus(JPanel status) { + this.status = status; + } + + public void setStatusLabel(JLabel statusLabel) { + this.statusLabel = statusLabel; + } + + @Override + public void componentResized(ComponentEvent e) { + // splitPane1.setDividerLocation(BFTab.startSplitterPosition); + } + + @Override + public void componentMoved(ComponentEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void componentShown(ComponentEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void componentHidden(ComponentEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.fine("Refreshing code syntax"); + StyledDocument doc = codePanel.getStyledDocument(); + high.update(doc, 0, doc.getLength(), machine.isWorking()); + } + + @Override + public void componentAdded(ContainerEvent e) { + if (e.getChild() == this) { + BrainFuckIDE.lg.info("Component added"); + t.start(); + } + + } + + @Override + public void componentRemoved(ContainerEvent e) { + if (e.getChild() == this) { + machine.stop(); + e.getContainer().removeContainerListener(this); + BrainFuckIDE.lg.info("Component removed"); + t.stop(); + } + } + + public JHexView getEditor() { + return editor; + } + + public void setEditor(JHexView editor) { + this.editor = editor; + } + + public JTextPane getCodePanel() { + return codePanel; + } + + public void setCodePanel(JTextPane codePanel) { + this.codePanel = codePanel; + } + + public JTextPane getMemoryPanel() { + return memoryPanel; + } + + public void setMemoryPanel(JTextPane memoryPanel) { + this.memoryPanel = memoryPanel; + } + + public JSplitPane getSplitPane1() { + return splitPane1; + } + + public void setSplitPane1(JSplitPane splitPane1) { + this.splitPane1 = splitPane1; + } + + /** + * Pobiera timer dla syntaxu + * + * @return timer + */ + public Timer getT() { + return t; + } + + /** + * @param t + * Timer + */ + public void setT(Timer t) { + this.t = t; + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tpsa/BrainFuckIDE.java b/bfIDE/src/main/java/tpsa/BrainFuckIDE.java new file mode 100644 index 0000000..d156eba --- /dev/null +++ b/bfIDE/src/main/java/tpsa/BrainFuckIDE.java @@ -0,0 +1,71 @@ +package tpsa; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import javax.swing.UIManager; + +public class BrainFuckIDE { + + static public Logger lg; + // protected static final long milsToWaitBeforeKill = 60000; + static private LogManager lm; + static private Level lv = Level.OFF; + + /** + * @param args + */ + public static void main(String[] args) { + + try { + UIManager + .setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel"); + } catch (Exception excp) { + // TODO Auto-generated catch block + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + BrainFuckIDE.lm = LogManager.getLogManager(); + BrainFuckIDE.lm.addLogger(Logger.getLogger("BFIDE")); + BrainFuckIDE.lg = lm.getLogger("BFIDE"); + BrainFuckIDE.lg.setLevel(lv); + for (Handler h : BrainFuckIDE.lg.getHandlers()) + h.setLevel(lv); + + for (Handler h : Logger.getLogger("").getHandlers()) + h.setLevel(lv); + + BrainFuckIDE.lg.info("Brainfuck IDE is starting"); + + MainFrame mf = new MainFrame(); + mf.setVisible(true); + } + + /** + * @param excp + * Wyjątek ;-) + * @return stringi opisujące ten wyjątek w strawnej postaci + */ + + public static String ErrorsFormatter(Exception excp) { + + // System.setOut(null); + // System.setErr(null); + + StringBuilder sb = new StringBuilder(); + String newLine = System.getProperty("line.separator"); + + sb.append("Exception catched: " + excp.getLocalizedMessage() + newLine); + StackTraceElement[] stack = excp.getStackTrace(); + for (int i = 0; i < stack.length; i++) { + sb.append("StackElement(" + i + ") : " + stack[i].getClassName() + + ", " + stack[i].getFileName() + ":" + + stack[i].getLineNumber() + ", " + + stack[i].getMethodName() + newLine); + } + + return sb.toString(); + } + +} diff --git a/bfIDE/src/main/java/tpsa/ButtonTabComponent.java b/bfIDE/src/main/java/tpsa/ButtonTabComponent.java new file mode 100644 index 0000000..02e7685 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/ButtonTabComponent.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 1995, 2008, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle or the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package tpsa; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.AbstractButton; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.plaf.basic.BasicButtonUI; + +/** + * Component to be used as tabComponent; + * Contains a JLabel to show the text and + * a JButton to close the tab it belongs to + */ +public class ButtonTabComponent extends JPanel { + private class TabButton extends JButton implements ActionListener { + /** + * + */ + private static final long serialVersionUID = -7852819509277461601L; + + public TabButton() { + int size = 17; + setPreferredSize(new Dimension(size, size)); + setToolTipText("close this tab"); + //Make the button looks the same for all Laf's + setUI(new BasicButtonUI()); + //Make it transparent + setContentAreaFilled(false); + //No need to be focusable + setFocusable(false); + setBorder(BorderFactory.createEtchedBorder()); + setBorderPainted(false); + //Making nice rollover effect + //we use the same listener for all buttons + addMouseListener(buttonMouseListener); + setRolloverEnabled(true); + //Close the proper tab by clicking the button + addActionListener(this); + } + + @Override + public void actionPerformed(ActionEvent e) { + int i = pane.indexOfTabComponent(ButtonTabComponent.this); + if (i != -1) { + pane.remove(i); + } + } + + //paint the cross + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + //shift the image for pressed buttons + if (getModel().isPressed()) { + g2.translate(1, 1); + } + g2.setStroke(new BasicStroke(2)); + g2.setColor(Color.BLACK); + if (getModel().isRollover()) { + g2.setColor(Color.MAGENTA); + } + int delta = 6; + g2.drawLine(delta, delta, getWidth() - delta - 1, getHeight() - delta - 1); + g2.drawLine(getWidth() - delta - 1, delta, delta, getHeight() - delta - 1); + g2.dispose(); + } + + //we don't want to update UI for this button + @Override + public void updateUI() { + } + } + private final static MouseListener buttonMouseListener = new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + Component component = e.getComponent(); + if (component instanceof AbstractButton) { + AbstractButton button = (AbstractButton) component; + button.setBorderPainted(true); + } + } + + @Override + public void mouseExited(MouseEvent e) { + Component component = e.getComponent(); + if (component instanceof AbstractButton) { + AbstractButton button = (AbstractButton) component; + button.setBorderPainted(false); + } + } + }; + + /** + * + */ + private static final long serialVersionUID = -2329475802746012869L; + + private final JTabbedPane pane; + + public ButtonTabComponent(final JTabbedPane pane) { + //unset default FlowLayout' gaps + super(new FlowLayout(FlowLayout.LEFT, 0, 0)); + if (pane == null) { + throw new NullPointerException("TabbedPane is null"); + } + this.pane = pane; + setOpaque(false); + + //make JLabel read titles from JTabbedPane + JLabel label = new JLabel() { + /** + * + */ + private static final long serialVersionUID = 6069043534335090073L; + + @Override + public String getText() { + int i = pane.indexOfTabComponent(ButtonTabComponent.this); + if (i != -1) { + return pane.getTitleAt(i); + } + return null; + } + }; + + add(label); + //add more space between the label and the button + label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5)); + //tab button + JButton button = new TabButton(); + add(button); + //add more space to the top of the component + setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0)); + } +} + diff --git a/bfIDE/src/main/java/tpsa/MainFrame.java b/bfIDE/src/main/java/tpsa/MainFrame.java new file mode 100644 index 0000000..a2246c8 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/MainFrame.java @@ -0,0 +1,970 @@ +package tpsa; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Event; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JViewport; +import javax.swing.KeyStroke; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.UndoableEditEvent; +import javax.swing.event.UndoableEditListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultEditorKit; +import javax.swing.text.Document; +import javax.swing.text.Style; +import javax.swing.text.StyledDocument; +import javax.swing.undo.UndoManager; + +import tpsa.dialogs.AboutProgramDialog; +import tpsa.dialogs.FindDialog; +import tpsa.dialogs.HelpDialog; +import tpsa.dialogs.MemorySizeDialog; +import tpsa.dialogs.ReplaceDialog; +import tpsa.dialogs.filters.BFFileFilter; +import tpsa.highlighters.SyntaxHighlighter; + +// DONE liczenie klamerek (bilans) +// DONE uruchomienie ostatniej poprawnej wersji BrainFucka +// DONE zróżnicowanie podświetlenia (prawie zrobione - działa, ale więcej) +// DONE ustalenie skończonej pamięci +// DONE wyświetlanie ładnych wyjątków +// DONE wyróżnienie komentarzy +// DONE debugowanie +// DONE wyświetlanie stanu taśmy +// DONE scenariusze testowe - przykłady działające, i nie działające, poprawnie obsłużyć +// DONE krótki opis BrainFucka + +/** + * Klasa głowna edytora, odpowiedzialna za GUI edytora + * + * @author tpsa + */ +public class MainFrame extends JFrame implements CaretListener, + UndoableEditListener, ChangeListener { + static int lastBFPositionShowing = -1; + + /** + * + */ + private static final long serialVersionUID = -4950512615304159756L; + + /** + * + */ + /** + * JTextPane w którym umieszczamy kod BF + */ + private AbstractAction findAction; + private FindDialog findDialog; + private Action newFileAction; + private AbstractAction openAction; + private AbstractAction pauseAction; + private JMenuItem pauseMachine; + /** + * Label, który pokazuje aktualną pozycję w tekście + */ + private JMenuItem redo; + private RedoAction redoAction; + private AbstractAction replaceAction; + private ReplaceDialog replaceDialog; + private AbstractAction resumeAction; + private JMenuItem resumeMachine; + private AbstractAction runAction; + @SuppressWarnings("unused") + private AbstractAction runLastWorkingCode = null; + private JMenuItem runWithoutDebug; + private AbstractAction saveAction; + private AbstractAction saveKnownAction; + private AbstractAction showLinesAction; + private Action showStatusAction; + + private JMenuItem showStatusLabel; + + private Action stepAction; + private JMenuItem stepMachine; + private AbstractAction stopAction; + private JMenuItem stopMachine; + private JTabbedPane tabs = new JTabbedPane(); + private JMenuItem undo; + + private UndoAction undoAction; + + private UndoManager undoManager = new UndoManager(); + + private AbstractAction runActionWithDebug; + + private AbstractAction showMemoryPanelAction; + + protected byte[] lastWorkingCode; + + /** + * Tworzy instancję klasy MainFrame + */ + public MainFrame() { + super("Brainfuck IDE"); + + BrainFuckIDE.lg.info("Creating main frame of Brainfuck IDE"); + + tabs.addChangeListener(this); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setPreferredSize(new Dimension(800, 600)); + + replaceDialog = new ReplaceDialog(this); + findDialog = new FindDialog(this); + + BrainFuckIDE.lg.fine("Setting actions"); + setActions(); + + JMenuBar menuBar = getMainFrameMenuBar(); + setJMenuBar(menuBar); + + // tab.getArea().addMouseListener(this); + // tab.getArea().addMouseMotionListener(this); + + undoAction = new UndoAction(undoManager, undo, null); + redoAction = new RedoAction(undoManager, redo, undoAction); + undo.setAction(undoAction); + undo.setText("Undo"); + redo.setAction(redoAction); + redo.setText("Redo"); + + undoAction.setRedoAction(redoAction); + + BFTab tab1 = new BFTab(this, pauseAction, resumeAction, stepAction, + stopAction, runAction, undoAction, redoAction); + tabs.addContainerListener(tab1); + + tabs.add(tab1); + int idx = tabs.indexOfComponent(tab1); + tabs.setTitleAt(idx, "Untitled"); + tabs.setTabComponentAt(idx, new ButtonTabComponent(tabs)); + add(tabs, BorderLayout.CENTER); + // add(status, BorderLayout.SOUTH); + + BrainFuckIDE.lg.info("Adding document listener"); + + pack(); + } + + @Override + public void caretUpdate(final CaretEvent e) { + + } + + /** + * Generuje menu programu + * + * @return Zwraca instancję JMenuBar (menu), dla programu + */ + private JMenuBar getMainFrameMenuBar() { + JMenuBar menuBar = new JMenuBar(); + + JMenu file = new JMenu("File"); + JMenu edit = new JMenu("Edit"); + JMenu view = new JMenu("View"); + JMenu run = new JMenu("Run"); + JMenu help = new JMenu("Help"); + + menuBar.add(file); + menuBar.add(edit); + menuBar.add(view); + menuBar.add(run); + menuBar.add(help); + + help.add(new JMenuItem(new AbstractAction("About program") { + + /** + * + */ + private static final long serialVersionUID = 5839320025951821584L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("About program action performed"); + Container c = tabs.getTopLevelAncestor(); + AboutProgramDialog dialog = new AboutProgramDialog((JFrame) c); + dialog.setLocationRelativeTo(c); + dialog.setLocation(c.getWidth() / 2, c.getHeight() / 2); + dialog.showDialog(); + } + })); + + help.add(new JMenuItem(new AbstractAction("Tutorial") { + + /** + * + */ + private static final long serialVersionUID = -6114772901713788593L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Help action performed"); + Container c = tabs.getTopLevelAncestor(); + HelpDialog dialog = new HelpDialog((JFrame) c); + dialog.setLocationRelativeTo(c); + dialog.setLocation(c.getWidth() / 2, c.getHeight() / 2); + dialog.showDialog(); + + } + })); + + JMenuItem newFile = new JMenuItem("New file"); + newFile.setAction(newFileAction); + JMenuItem open = new JMenuItem("Open"); + open.setAction(openAction); + JMenuItem save = new JMenuItem("Save"); + JMenuItem saveAs = new JMenuItem("Save as"); + saveAs.setAction(saveAction); + JMenuItem quit = new JMenuItem("Quit"); + + quit.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + + file.add(newFile); + file.add(open); + save.setAction(saveKnownAction); + file.add(save); + file.add(saveAs); + file.add(quit); + + undo = new JMenuItem("Undo"); + redo = new JMenuItem("Redo"); + + JMenuItem cut = new JMenuItem("Cut"); + cut.addActionListener(new DefaultEditorKit.CutAction()); + // cut.setMnemonic(KeyEvent.VK_X); + + JMenuItem copy = new JMenuItem("Copy"); + copy.addActionListener(new DefaultEditorKit.CopyAction()); + // copy.setMnemonic(KeyEvent.VK_C); + + JMenuItem paste = new JMenuItem("Paste"); + paste.addActionListener(new DefaultEditorKit.PasteAction()); + // paste.setMnemonic(KeyEvent.VK_P); + + JMenuItem find = new JMenuItem("Find"); + find.setAction(findAction); + + KeyStroke findStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F, + Event.CTRL_MASK); + find.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(findStroke, + "find"); + find.getActionMap().put("find", findAction); + + JMenuItem replace = new JMenuItem("Replace"); + replace.setAction(replaceAction); + + KeyStroke replaceStroke = KeyStroke.getKeyStroke(KeyEvent.VK_R, + Event.CTRL_MASK); + find.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(replaceStroke, + "replace"); + find.getActionMap().put("replace", replaceAction); + + KeyStroke newFileStroke = KeyStroke.getKeyStroke(KeyEvent.VK_N, + Event.CTRL_MASK); + newFile.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + newFileStroke, "newFile"); + newFile.getActionMap().put("newFile", newFileAction); + + KeyStroke saveFileStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S, + Event.CTRL_MASK); + save.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(saveFileStroke, + "saveFile"); + save.getActionMap().put("saveFile", saveKnownAction); + + edit.add(undo); + edit.add(redo); + edit.addSeparator(); + edit.add(cut); + edit.add(copy); + edit.add(paste); + edit.addSeparator(); + edit.add(find); + edit.add(replace); + edit.addSeparator(); + edit.add(new JMenuItem(new AbstractAction("Memory preferences") { + + /** + * + */ + private static final long serialVersionUID = 6096201895928552672L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Memory preferences action performed"); + MainFrame frame = (MainFrame) tabs.getTopLevelAncestor(); + + MemorySizeDialog dialog = new MemorySizeDialog(frame); + dialog.setSizeMemory(BFMachine.defaultMemorySize); + if (!dialog.showDialog()) + return; + + BFMachine.defaultMemorySize = dialog.getSizeMemory(); + + } + })); + + JMenuItem showLineNumbers = new JMenuItem("Show line numbers"); + showStatusLabel = new JCheckBoxMenuItem("Show status label"); + + view.add(showLineNumbers); + + showLineNumbers.setAction(showLinesAction); + + view.add(showMemoryPanelAction); + + view.add(showStatusLabel); + + showStatusLabel.setAction(showStatusAction); + + runWithoutDebug = new JMenuItem("Run without debug"); + pauseMachine = new JMenuItem("Pause machine"); + resumeMachine = new JMenuItem("Resume machine"); + stopMachine = new JMenuItem("Stop machine"); + stepMachine = new JMenuItem("Step machine"); + + runWithoutDebug.setAction(runAction); + pauseMachine.setAction(pauseAction); + resumeMachine.setAction(resumeAction); + stopMachine.setAction(stopAction); + stepMachine.setAction(stepAction); + + run.add(runActionWithDebug); + run.add(runWithoutDebug); + // run.add(runLastWorkingCode); + run.add(pauseMachine); + run.add(resumeMachine); + run.add(stopMachine); + run.add(stepAction); + + return menuBar; + } + + /** + * Ustawia akcje np. find, replace Każda akcja ma reakcję + */ + private void setActions() { + + showMemoryPanelAction = new AbstractAction("Show memory panel") { + + /** + * + */ + private static final long serialVersionUID = 2012577125654615111L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Show memory action performed"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + if (tab == null) + return; + + JSplitPane pane = tab.getSplitPane1(); + ArrayList components = new ArrayList( + Arrays.asList(pane.getComponents())); + BrainFuckIDE.lg.finest("Got components"); + + boolean contains = false; + for (Component c : components) + if (c == tab.getEditor()) { + contains = true; + break; + } + + System.err.println(components.size()); + if (contains) { + BrainFuckIDE.lg.info("Removing memory panel"); + pane.remove(tab.getEditor()); + } else { + BrainFuckIDE.lg.info("Adding memory panel"); + pane.add(tab.getEditor(), 1); + pane.setDividerLocation(BFTab.startSplitterPosition); + } + + } + }; + + newFileAction = new AbstractAction("New file") { + + /** + * + */ + private static final long serialVersionUID = 5483399276425267838L; + + @Override + public void actionPerformed(ActionEvent e) { + MainFrame frame = (MainFrame) tabs.getTopLevelAncestor(); + BFTab tab = new BFTab(frame, pauseAction, resumeAction, + stepAction, stopAction, runAction, undoAction, + redoAction); + tabs.addContainerListener(tab); + tabs.add(tab); + int idx = tabs.indexOfComponent(tab); + tabs.setTitleAt(idx, "Untitled"); + tabs.setTabComponentAt(idx, new ButtonTabComponent(tabs)); + + } + }; + + runAction = new AbstractAction("RunWithoutDebug") { + + /** + * + */ + private static final long serialVersionUID = 5839320025951821584L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Run action executed"); + + final BFTab tab = (BFTab) tabs.getSelectedComponent(); + Document doc = tab.getPaneResult().getDocument(); + final BFMachine machine = tab.getMachine(); + + final Thread t = prepareMachine(tab, doc, machine); + if (t == null) + return; + machine.setStepping(false); + t.start(); + + } + }; + + runActionWithDebug = new AbstractAction("Run with debug") { + + /** + * + */ + private static final long serialVersionUID = 6096201895928552672L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Run action executed"); + + final BFTab tab = (BFTab) tabs.getSelectedComponent(); + Document doc = tab.getPaneResult().getDocument(); + final BFMachine machine = tab.getMachine(); + + final Thread t = prepareMachine(tab, doc, machine); + if (t == null) + return; + + machine.setStepping(true); + t.start(); + + } + }; + + showStatusAction = new AbstractAction("ShowStatusLabel") { + + /** + * + */ + private static final long serialVersionUID = 2012577125654615111L; + + @Override + public void actionPerformed(ActionEvent e) { + // BrainFuckIDE.lg.finest("showStatusLabel.isSelected()); + + BFTab tab = (BFTab) tabs.getSelectedComponent(); + Component[] comp = tab.getComponents(); + ArrayList l = new ArrayList( + Arrays.asList(comp)); + + boolean contains = l.contains(tab.getStatus()); + + if (!contains) { + tab.add(tab.getStatus(), BorderLayout.NORTH); + } else + tab.remove(tab.getStatus()); + + } + }; + + showLinesAction = new AbstractAction("ShowLines") { + + /** + * + */ + private static final long serialVersionUID = 2012577125654615111L; + + @Override + public void actionPerformed(ActionEvent e) { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + JScrollPane pane = tab.getPane(); + JViewport port = pane.getRowHeader(); + if (port == null) + pane.setRowHeaderView(tab.getRows()); + else + pane.setRowHeader(null); + + } + }; + + runLastWorkingCode = new AbstractAction("Run last working code") { + + /** + * + */ + private static final long serialVersionUID = -1126695922005433078L; + + @Override + public void actionPerformed(ActionEvent e) { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + if (tab == null) + return; + StyledDocument doc = tab.getArea().getStyledDocument(); + BFMachine machine = tab.getMachine(); + if (machine.isWorking()) { + JOptionPane.showMessageDialog(tabs.getTopLevelAncestor(), + "Machine is working"); + return; + } + Thread t = prepareMachine(tab, doc, machine); + if (lastWorkingCode == null) { + JOptionPane.showMessageDialog(tabs.getTopLevelAncestor(), + "no working code"); + tab.getStatusLabel().setText("Ready"); + return; + } + machine.setCode(lastWorkingCode); + t.start(); + } + }; + + pauseAction = new AbstractAction("Pause machine") { + + /** + * + */ + private static final long serialVersionUID = 6096201895928552672L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Pause action performed"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + BFMachine machine = tab.getMachine(); + + if (!machine.isWorking()) { + JOptionPane.showMessageDialog(null, "No maching running"); + return; + } + machine.pause(); + System.err.println("Paused"); + tab.getStatusLabel().setText("Machine paused"); + } + }; + + resumeAction = new AbstractAction("Resume machine") { + + /** + * + */ + private static final long serialVersionUID = 2012577125654615111L; + + @Override + public void actionPerformed(ActionEvent e) { + + BrainFuckIDE.lg.info("Resume action performed"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + BFMachine machine = tab.getMachine(); + + if (!machine.isWorking()) { + JOptionPane.showMessageDialog(null, "No maching running"); + return; + } + machine.setStepping(false); + machine.resume(); + tab.getStatusLabel().setText("Running..."); + } + }; + + stopAction = new AbstractAction("Stop machine") { + + /** + * + */ + private static final long serialVersionUID = -2599448412251509415L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Stop action performed"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + BFMachine machine = tab.getMachine(); + if (!machine.isWorking()) { + JOptionPane.showMessageDialog(null, "No maching running"); + return; + } + machine.stop(); + tab.getStatusLabel().setText("Machine stopped - ready..."); + tab.getArea().setEditable(true); + } + }; + + stepAction = new AbstractAction("StepAction") { + + /** + * + */ + private static final long serialVersionUID = 5880656879834148166L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Step action performed"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + BFMachine machine = tab.getMachine(); + if (!machine.isWorking()) { + JOptionPane.showMessageDialog(getParent(), + "Machine is not working"); + return; + } + machine.resume(); + machine.setStepping(true); + } + }; + + openAction = new AbstractAction("open") { + + /** + * + */ + private static final long serialVersionUID = 4575053887363265531L; + + @Override + public void actionPerformed(ActionEvent e) { + try { + JFileChooser chooser = new JFileChooser("."); + chooser.setFileFilter(new BFFileFilter()); + if (chooser.showDialog(null, "Otwórz") == JFileChooser.APPROVE_OPTION) { + + File f = chooser.getSelectedFile(); + BufferedReader r = new BufferedReader( + new InputStreamReader(new FileInputStream(f))); + StringBuilder sb = new StringBuilder(); + + char[] buffer = new char[0x1000]; + + while (r.read(buffer) >= 0) + sb.append(buffer); + + r.close(); + + MainFrame frame = (MainFrame) tabs + .getTopLevelAncestor(); + BFTab tab = new BFTab(frame, pauseAction, resumeAction, + stepAction, stopAction, runAction, undoAction, + redoAction); + tabs.add(tab); + int idx = tabs.indexOfComponent(tab); + tab.setFile(f); + tabs.setTitleAt(idx, f.getName()); + tabs.setTabComponentAt(idx, + new ButtonTabComponent(tabs)); + + tabs.setSelectedIndex(idx); + + tab.getArea().setText(sb.toString()); + tab.getArea().setCaretPosition(0); + } + } catch (IOException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }; + + saveKnownAction = new AbstractAction("Save") { + + /** + * + */ + private static final long serialVersionUID = -985908773082887131L; + + @Override + public void actionPerformed(ActionEvent e) { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + if (tab == null) + return; + + File f = tab.getFile(); + if (f == null) { + saveAction.actionPerformed(e); + f = tab.getFile(); + if (f == null) { + JOptionPane + .showMessageDialog(tabs.getTopLevelAncestor(), + "Coś się schrzaniło\nW zasadzie nie wiem, nawet co"); + return; + } + } + + OutputStreamWriter writer; + try { + writer = new OutputStreamWriter(new FileOutputStream(f)); + writer.write(tab.getArea().getText()); + writer.close(); + } catch (FileNotFoundException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } catch (IOException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }; + + saveAction = new AbstractAction("Save as") { + + /** + * + */ + private static final long serialVersionUID = 4575053887363265531L; + + @Override + public void actionPerformed(ActionEvent e) { + try { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + JFileChooser chooser = new JFileChooser("."); + chooser.setFileFilter(new BFFileFilter()); + if (chooser.showDialog(null, "Zapisz") == JFileChooser.APPROVE_OPTION) { + + File f = chooser.getSelectedFile(); + tabs.setTitleAt(tabs.getSelectedIndex(), f.getName()); + tab.setFile(f); + BufferedWriter r = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(f))); + r.write(tab.getArea().getText()); + r.close(); + } + } catch (IOException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }; + + findAction = new AbstractAction("find") { + + /** + * + */ + private static final long serialVersionUID = 8670201600032148368L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Action find performed"); + + findDialog.setVisible(true); + + } + }; + + replaceAction = new AbstractAction("replace") { + + /** + * + */ + private static final long serialVersionUID = 4575053887363265531L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("Action action performed"); + replaceDialog.setVisible(true); + + } + }; + + } + + private void showBFProgress() { + try { + BrainFuckIDE.lg.info("Show BF progress"); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + BFMachine machine = tab.getMachine(); + int pos = machine.getPosition(); + tab.getStatusLabel().setText( + "Stepping pos: " + pos + "(" + + (char) tab.getMachine().getCode()[pos] + + "), memory pos : " + + tab.getMachine().getMemoryPointer()); + tab.getEditor().setCurrentOffset( + tab.getMachine().getMemoryPointer()); + StyledDocument doc = (StyledDocument) tab.getArea().getDocument(); + Style caretStyle = doc.getStyle("BFCaret"); + Style codeStyle = doc.getStyle("CODE"); + Style commentStyle = doc.getStyle("COMMENT"); + + if (lastBFPositionShowing >= 0) { + String s = doc.getText(pos, 1); + Character c = s.charAt(0); + if (c == '[' || c == ']' || c == '\n' || c == '\r' + || SyntaxHighlighter.BFcharacterList.contains(c)) + doc.setCharacterAttributes(lastBFPositionShowing, 1, + codeStyle, true); + else + doc.setCharacterAttributes(lastBFPositionShowing, 1, + commentStyle, true); + } + + doc.setCharacterAttributes(pos, 1, caretStyle, true); + lastBFPositionShowing = pos; + } catch (BadLocationException excp) { + BrainFuckIDE.lg.warning(BrainFuckIDE.ErrorsFormatter(excp)); + } + } + + @Override + public void undoableEditHappened(UndoableEditEvent e) { + BrainFuckIDE.lg.info("Undoable edit happend: " + + e.getEdit().getPresentationName()); + + // FIXME jak na razie, działa, ale nie wiem dlaczego, jeżeli coś się + // sypie z UndoManager tu sprawdzać + + if (e.getEdit().getPresentationName().equals("style change") == false) + undoManager.addEdit(e.getEdit()); + + undoAction.update(); + redoAction.update(); + } + + @Override + public void stateChanged(ChangeEvent e) { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + if (tab == null) + return; + if (tab.getComponentCount() <= 0) + return; + ArrayList components = new ArrayList( + Arrays.asList(tab.getComponents())); + showStatusLabel.setSelected(components.contains(tab.getStatus())); + + } + + /** + * Szykuje maszynę, na nowe zawody (reset) + * + * @param tab + * zakładka + * @param doc + * dokument + * @param machine + * maszyna + * @return + */ + private Thread prepareMachine(final BFTab tab, Document doc, + final BFMachine machine) { + if (machine.isWorking()) { + JOptionPane.showMessageDialog(null, + "Machine is working currently!!!"); + return null; + } + + tab.getArea().setEditable(false); + + BrainFuckIDE.lg + .info("Adding document listener to the input stream of console"); + try { + doc.remove(0, doc.getLength()); + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe("Exception: "); + } + + BrainFuckIDE.lg.fine("Creating BF machine"); + + machine.setCode(tab.getArea().getText()); + machine.setFinishHandler(new Runnable() { + + @Override + public void run() { + BFTab tab = (BFTab) tabs.getSelectedComponent(); + String s = machine.getError(); + if (s != null && !s.equals("")) + JOptionPane.showMessageDialog(tabs.getTopLevelAncestor(), + "Whoops: BF machine" + " complaints: " + s, + "BF error", JOptionPane.ERROR_MESSAGE); + else + lastWorkingCode = tab.getMachine().getCode(); + tab.getEditor().repaint(); + BrainFuckIDE.lg.info("Memory of BF machine is: " + + tab.getMachine().getMemory().length); + tab.getArea().setEditable(true); + tab.getStatusLabel().setText("Ready"); + + } + + }); + + machine.setBreakpointHandler(new Runnable() { + + @Override + public void run() { + showBFProgress(); + tab.getStatusLabel().setText( + tab.getStatusLabel().getText() + + ". Breakpoint reached..."); + } + }); + + machine.setStepHandler(new Runnable() { + + @Override + public void run() { + showBFProgress(); + + } + }); + + final Thread t = new Thread(machine); + tab.getMachine().reset(); + tab.getStatusLabel().setText("Running..."); + return t; + } + + public JTabbedPane getTabs() { + return tabs; + } + + public void setTabs(JTabbedPane tabs) { + this.tabs = tabs; + } + +} diff --git a/bfIDE/src/main/java/tpsa/RedoAction.java b/bfIDE/src/main/java/tpsa/RedoAction.java new file mode 100644 index 0000000..4e5f2fd --- /dev/null +++ b/bfIDE/src/main/java/tpsa/RedoAction.java @@ -0,0 +1,73 @@ +package tpsa; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JMenuItem; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.UndoManager; + +/** + * Klasa umożliwia działanie ReDo + * + * @author Duga Eye + */ +public class RedoAction extends AbstractAction { + + /** + * + */ + private static final long serialVersionUID = -2467991386706847458L; + private JMenuItem redoButton; + private UndoAction undoAction; + private UndoManager undoManager; + + /** + * @param manager + * @param button + * @param action + */ + public RedoAction(UndoManager manager, JMenuItem button, UndoAction action) { + super("Redo"); + setEnabled(false); + undoManager = manager; + redoButton = button; + undoAction = action; + } + + @Override + public void actionPerformed(ActionEvent e) { + try { + undoManager.redo(); + } catch (CannotRedoException ex) { + } + update(); + undoAction.update(); + } + + /** + * @param action + * Akcja analogiczna do ReDo Undo + */ + public void setUndoAction(UndoAction action) { + undoAction = action; + } + + /** + * Uaktualnia historię edycji + */ + protected void update() { + BrainFuckIDE.lg.fine("Updating state of undo/redo action"); + if (undoManager.canRedo()) { + setEnabled(true); + redoButton.setEnabled(true); + putValue(Action.NAME, undoManager.getRedoPresentationName()); + } else { + setEnabled(false); + redoButton.setEnabled(false); + putValue(Action.NAME, "Redo"); + } + } + +} diff --git a/bfIDE/src/main/java/tpsa/TextEditorPosition.java b/bfIDE/src/main/java/tpsa/TextEditorPosition.java new file mode 100644 index 0000000..d3f4fe4 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/TextEditorPosition.java @@ -0,0 +1,33 @@ +package tpsa; + +/** + * Klasa pozycji w dokumencie + * + * @author Duga Eye + * + */ +public class TextEditorPosition { + /** + * Linia w dokumencie + */ + public int Line; + /** + * Offset w linii dokumentu + */ + public int Offset; + + /** + * + */ + public TextEditorPosition() + { + Line = 0; + Offset = 0; + } + + public TextEditorPosition(int line, int offset) { + this.Line = line; + this.Offset = offset; + } + +} diff --git a/bfIDE/src/main/java/tpsa/TextLineNumber.java b/bfIDE/src/main/java/tpsa/TextLineNumber.java new file mode 100644 index 0000000..5fa4a4d --- /dev/null +++ b/bfIDE/src/main/java/tpsa/TextLineNumber.java @@ -0,0 +1,484 @@ +package tpsa; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashMap; + +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.MatteBorder; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; +import javax.swing.text.JTextComponent; +import javax.swing.text.StyleConstants; +import javax.swing.text.Utilities; + +/** + * This class will display line numbers for a related text component. The text + * component must use the same line height for each line. TextLineNumber + * supports wrapped lines and will highlight the line number of the current + * line in the text component. + * + * This class was designed to be used as a component added to the row header + * of a JScrollPane. + */ +public class TextLineNumber extends JPanel + implements CaretListener, DocumentListener, PropertyChangeListener +{ + public final static float CENTER = 0.5f; + private final static int HEIGHT = Integer.MAX_VALUE - 1000000; + public final static float LEFT = 0.0f; + private final static Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY); + + public final static float RIGHT = 1.0f; + + /** + * + */ + private static final long serialVersionUID = 3696719794891436122L; + + // Text component this TextTextLineNumber component is in sync with + + private int borderGap; + + // Properties that can be changed + + private JTextComponent component; + private Color currentLineForeground; + private float digitAlignment; + private HashMap fonts; + private int lastDigits; + + // Keep history information to reduce the number of times the component + // needs to be repainted + + private int lastHeight; + private int lastLine; + private int minimumDisplayDigits; + + private boolean updateFont; + + /** + * Create a line number component for a text component. This minimum + * display width will be based on 3 digits. + * + * @param component the related text component + */ + public TextLineNumber(JTextComponent component) + { + this(component, 3); + } + + /** + * Create a line number component for a text component. + * + * @param component the related text component + * @param minimumDisplayDigits the number of digits used to calculate + * the minimum width of the component + */ + public TextLineNumber(JTextComponent component, int minimumDisplayDigits) + { + this.component = component; + + setFont( component.getFont() ); + + setBorderGap( 5 ); + setCurrentLineForeground( Color.RED ); + setDigitAlignment( RIGHT ); + setMinimumDisplayDigits( minimumDisplayDigits ); + + component.getDocument().addDocumentListener(this); + component.addCaretListener( this ); + component.addPropertyChangeListener("font", this); + } + + // +// Implement CaretListener interface +// + @Override + public void caretUpdate(CaretEvent e) + { + // Get the line the caret is positioned on + + int caretPosition = component.getCaretPosition(); + Element root = component.getDocument().getDefaultRootElement(); + int currentLine = root.getElementIndex( caretPosition ); + + // Need to repaint so the correct line number can be highlighted + + if (lastLine != currentLine) + { + repaint(); + lastLine = currentLine; + } + } + + // +// Implement DocumentListener interface +// + @Override + public void changedUpdate(DocumentEvent e) + { + documentChanged(); + } + + /* + * A document change may affect the number of displayed lines of text. + * Therefore the lines numbers will also change. + */ + private void documentChanged() + { + // Preferred size of the component has not been updated at the time + // the DocumentEvent is fired + + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + int preferredHeight = component.getPreferredSize().height; + + // Document change has caused a change in the number of lines. + // Repaint to reflect the new line numbers + + if (lastHeight != preferredHeight) + { + setPreferredWidth(); + repaint(); + lastHeight = preferredHeight; + } + } + }); + } + + /** + * Gets the border gap + * + * @return the border gap in pixels + */ + public int getBorderGap() + { + return borderGap; + } + + /** + * Gets the current line rendering Color + * + * @return the Color used to render the current line number + */ + public Color getCurrentLineForeground() + { + return currentLineForeground == null ? getForeground() : currentLineForeground; + } + + /** + * Gets the digit alignment + * + * @return the alignment of the painted digits + */ + public float getDigitAlignment() + { + return digitAlignment; + } + + /** + * Gets the minimum display digits + * + * @return the minimum display digits + */ + public int getMinimumDisplayDigits() + { + return minimumDisplayDigits; + } + + /* + * Determine the X offset to properly align the line number when drawn + */ + private int getOffsetX(int availableWidth, int stringWidth) + { + return (int)((availableWidth - stringWidth) * digitAlignment); + } + + /* + * Determine the Y offset for the current row + */ + private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics) + throws BadLocationException + { + // Get the bounding rectangle of the row + + Rectangle r = component.modelToView( rowStartOffset ); + int lineHeight = fontMetrics.getHeight(); + int y = r.y + r.height; + int descent = 0; + + // The text needs to be positioned above the bottom of the bounding + // rectangle based on the descent of the font(s) contained on the row. + + if (r.height == lineHeight) // default font is being used + { + descent = fontMetrics.getDescent(); + } + else // We need to check all the attributes for font changes + { + if (fonts == null) + fonts = new HashMap(); + + Element root = component.getDocument().getDefaultRootElement(); + int index = root.getElementIndex( rowStartOffset ); + Element line = root.getElement( index ); + + for (int i = 0; i < line.getElementCount(); i++) + { + Element child = line.getElement(i); + AttributeSet as = child.getAttributes(); + String fontFamily = (String)as.getAttribute(StyleConstants.FontFamily); + Integer fontSize = (Integer)as.getAttribute(StyleConstants.FontSize); + String key = fontFamily + fontSize; + + FontMetrics fm = fonts.get( key ); + + if (fm == null) + { + Font font = new Font(fontFamily, Font.PLAIN, fontSize); + fm = component.getFontMetrics( font ); + fonts.put(key, fm); + } + + descent = Math.max(descent, fm.getDescent()); + } + } + + return y - descent; + } + + /* + * Get the line number to be drawn. The empty string will be returned + * when a line of text has wrapped. + */ + protected String getTextLineNumber(int rowStartOffset) + { + Element root = component.getDocument().getDefaultRootElement(); + int index = root.getElementIndex( rowStartOffset ); + Element line = root.getElement( index ); + + if (line.getStartOffset() == rowStartOffset) + return String.valueOf(index + 1); + else + return ""; + } + + /** + * Gets the update font property + * + * @return the update font property + */ + public boolean getUpdateFont() + { + return updateFont; + } + + @Override + public void insertUpdate(DocumentEvent e) + { + documentChanged(); + } + + /* + * We need to know if the caret is currently positioned on the line we + * are about to paint so the line number can be highlighted. + */ + private boolean isCurrentLine(int rowStartOffset) + { + int caretPosition = component.getCaretPosition(); + Element root = component.getDocument().getDefaultRootElement(); + + if (root.getElementIndex( rowStartOffset ) == root.getElementIndex(caretPosition)) + return true; + else + return false; + } + + /** + * Draw the line numbers + */ + @Override + public void paintComponent(Graphics g) + { + super.paintComponent(g); + + // Determine the width of the space available to draw the line number + + FontMetrics fontMetrics = component.getFontMetrics( component.getFont() ); + Insets insets = getInsets(); + int availableWidth = getSize().width - insets.left - insets.right; + + // Determine the rows to draw within the clipped bounds. + + Rectangle clip = g.getClipBounds(); + int rowStartOffset = component.viewToModel( new Point(0, clip.y) ); + int endOffset = component.viewToModel( new Point(0, clip.y + clip.height) ); + + while (rowStartOffset <= endOffset) + { + try + { + if (isCurrentLine(rowStartOffset)) + g.setColor( getCurrentLineForeground() ); + else + g.setColor( getForeground() ); + + // Get the line number as a string and then determine the + // "X" and "Y" offsets for drawing the string. + + String lineNumber = getTextLineNumber(rowStartOffset); + int stringWidth = fontMetrics.stringWidth( lineNumber ); + int x = getOffsetX(availableWidth, stringWidth) + insets.left; + int y = getOffsetY(rowStartOffset, fontMetrics); + g.drawString(lineNumber, x, y); + + // Move to the next row + + rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1; + } + catch(Exception e) {} + } + } + + // +// Implement PropertyChangeListener interface +// + @Override + public void propertyChange(PropertyChangeEvent evt) + { + if (evt.getNewValue() instanceof Font) + { + if (updateFont) + { + Font newFont = (Font) evt.getNewValue(); + setFont(newFont); + lastDigits = 0; + setPreferredWidth(); + } + else + { + repaint(); + } + } + } + + @Override + public void removeUpdate(DocumentEvent e) + { + documentChanged(); + } + +/** + * The border gap is used in calculating the left and right insets of the + * border. Default value is 5. + * + * @param borderGap the gap in pixels + */ + public void setBorderGap(int borderGap) + { + this.borderGap = borderGap; + Border inner = new EmptyBorder(0, borderGap, 0, borderGap); + setBorder( new CompoundBorder(OUTER, inner) ); + lastDigits = 0; + setPreferredWidth(); + } + +/** + * The Color used to render the current line digits. Default is Coolor.RED. + * + * @param currentLineForeground the Color used to render the current line + */ + public void setCurrentLineForeground(Color currentLineForeground) + { + this.currentLineForeground = currentLineForeground; + } + + /** + * Specify the horizontal alignment of the digits within the component. + * Common values would be: + *
    + *
  • TextLineNumber.LEFT + *
  • TextLineNumber.CENTER + *
  • TextLineNumber.RIGHT (default) + *
+ * @param currentLineForeground the Color used to render the current line + */ + public void setDigitAlignment(float digitAlignment) + { + this.digitAlignment = + digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment; + } + + /** + * Specify the mimimum number of digits used to calculate the preferred + * width of the component. Default is 3. + * + * @param minimumDisplayDigits the number digits used in the preferred + * width calculation + */ + public void setMinimumDisplayDigits(int minimumDisplayDigits) + { + this.minimumDisplayDigits = minimumDisplayDigits; + setPreferredWidth(); + } + + /** + * Calculate the width needed to display the maximum line number + */ + private void setPreferredWidth() + { + Element root = component.getDocument().getDefaultRootElement(); + int lines = root.getElementCount(); + int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits); + + // Update sizes when number of digits in the line number changes + + if (lastDigits != digits) + { + lastDigits = digits; + FontMetrics fontMetrics = getFontMetrics( getFont() ); + int width = fontMetrics.charWidth( '0' ) * digits; + Insets insets = getInsets(); + int preferredWidth = insets.left + insets.right + width; + + Dimension d = getPreferredSize(); + d.setSize(preferredWidth, HEIGHT); + setPreferredSize( d ); + setSize( d ); + } + } + +/** + * Set the update font property. Indicates whether this Font should be + * updated automatically when the Font of the related text component + * is changed. + * + * @param updateFont when true update the Font and repaint the line + * numbers, otherwise just repaint the line numbers. + */ + public void setUpdateFont(boolean updateFont) + { + this.updateFont = updateFont; + } +} diff --git a/bfIDE/src/main/java/tpsa/UndoAction.java b/bfIDE/src/main/java/tpsa/UndoAction.java new file mode 100644 index 0000000..0847996 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/UndoAction.java @@ -0,0 +1,77 @@ +package tpsa; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JMenuItem; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoManager; + +/** + * Klasa implementująca UnDo + * + * @author Duga Eye + * + */ +public class UndoAction extends AbstractAction { + + /** + * + */ + private static final long serialVersionUID = 5058740336403951058L; + + private RedoAction redoAction; + private JMenuItem undoButton; + + private UndoManager undoManager; + + /** + * @param manager + * @param button + * @param action + */ + public UndoAction(UndoManager manager, JMenuItem button, RedoAction action) { + super("Undo"); + BrainFuckIDE.lg.info("Creating instance of class"); + setEnabled(false); + undoManager = manager; + undoButton = button; + redoAction = action; + } + + @Override + public void actionPerformed(ActionEvent e) { + try { + // System.err.println("YEP"); + undoManager.undo(); + // System.err.println("WORKS"); + } catch (CannotUndoException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + update(); + redoAction.update(); + + } + + /** + * @param action + */ + public void setRedoAction(RedoAction action) { + redoAction = action; + } + + protected void update() { + BrainFuckIDE.lg.fine("Updating undo/redo state"); + if (undoManager.canUndo()) { + setEnabled(true); + undoButton.setEnabled(true); + putValue(Action.NAME, undoManager.getUndoPresentationName()); + } else { + setEnabled(false); + undoButton.setEnabled(false); + putValue(Action.NAME, "Undo"); + } + } + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/AboutProgramDialog.java b/bfIDE/src/main/java/tpsa/dialogs/AboutProgramDialog.java new file mode 100644 index 0000000..590c54c --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/AboutProgramDialog.java @@ -0,0 +1,34 @@ +package tpsa.dialogs; + +import java.awt.Dimension; + +import javax.swing.JDialog; +import javax.swing.JFrame; + +/** + * O programie dialog + * + * @author Duga Eye + * + */ +public class AboutProgramDialog extends JDialog { + + /** + * + */ + private static final long serialVersionUID = -1176985069310756058L; + + public AboutProgramDialog(JFrame frame) + { + super(frame, "About program", ModalityType.APPLICATION_MODAL); + setPreferredSize(new Dimension(500, 400)); + + pack(); + } + + public boolean showDialog() { + setVisible(true); + return true; + } + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/FindDialog.java b/bfIDE/src/main/java/tpsa/dialogs/FindDialog.java new file mode 100644 index 0000000..096f01f --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/FindDialog.java @@ -0,0 +1,190 @@ +package tpsa.dialogs; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.KeyStroke; +import javax.swing.WindowConstants; +import javax.swing.text.BadLocationException; + +import tpsa.BFTab; +import tpsa.BrainFuckIDE; +import tpsa.MainFrame; + +/** + * Dialog wyszukiwania wyrażenia + * + * @author Duga Eye + * + */ +public class FindDialog extends JDialog { + + /** + * + */ + private static final long serialVersionUID = -486529548871883227L; + private JTextField match; + private boolean result; + /** + * @param frame + */ + public FindDialog(final MainFrame frame) { + super(frame); + setTitle("Find string"); + + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + setModalityType(ModalityType.APPLICATION_MODAL); + setPreferredSize(new Dimension(250, 100)); + + Box b = Box.createVerticalBox(); + + JRootPane pane = getRootPane(); + KeyStroke escapeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + KeyStroke enterStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + + pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeStroke, + "escape"); + pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(enterStroke, + "EnterFind"); + pane.getActionMap().put("escape", new AbstractAction() { + + /** + * + */ + private static final long serialVersionUID = -375905298222684550L; + + @Override + public void actionPerformed(ActionEvent e) { + result = false; + setVisible(false); + } + }); + pane.getActionMap().put("EnterFind", new AbstractAction() { + + /** + * + */ + private static final long serialVersionUID = -8373549218006477790L; + + @Override + public void actionPerformed(ActionEvent e) { + + try { + + BrainFuckIDE.lg.fine("Searching..."); + JTabbedPane tabs = frame.getTabs(); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + if (tab == null) + return; + + String s = tab.getArea().getText(); + String search = getMatch(); + int oldPos = tab.getArea().getCaretPosition(); + int pos = -1; + pos = s.indexOf(search, oldPos + 1); + if (pos < 0) { + BrainFuckIDE.lg.info("Rewinding to the beginning"); + pos = s.indexOf(search); + } + + BrainFuckIDE.lg.fine("Searching for: " + search); + + BrainFuckIDE.lg.finest("found: " + pos); + if (pos >= 0) { + Rectangle rect = tab.getArea().modelToView(pos); + BrainFuckIDE.lg.finest("rect: " + rect.toString()); + JTextPane area = tab.getArea(); + area.scrollRectToVisible(rect); + area.setCaretPosition(pos); + } else + JOptionPane.showMessageDialog(frame, + "Not found from cursor phrase: " + search); + + } catch (BadLocationException excp) { + BrainFuckIDE.lg.warning(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + }); + + Box b1 = Box.createHorizontalBox(); + + b1.add(new JLabel("Find: ")); + match = new JTextField(10); + b1.add(match); + + JPanel p2 = new JPanel(); + p2.setLayout(new FlowLayout(FlowLayout.CENTER)); + + JButton find = new JButton("Find"); + // JButton findNext = new JButton("Find next"); + p2.add(find); + // p2.add(findNext); + + b.add(b1); + Component gl = Box.createHorizontalGlue(); + gl.setPreferredSize(new Dimension(10, 10)); + b.add(gl); + b.add(p2); + + find.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + result = true; + setVisible(false); + + } + }); + + // add(find); + // add(findNext); + + Component glue = Box.createVerticalGlue(); + glue.setPreferredSize(new Dimension(Integer.MAX_VALUE, + Integer.MAX_VALUE)); + b.add(glue); + + add(b); + + pack(); + } + + /** + * Wyrażenie do wyszukania + * + * @return Wyrażenie, które użytkownik chce wyszukać + */ + public String getMatch() { + return new String(match.getText()); + } + + /** + * Pokaż dialog + * + * @return Potwierdzenie/odrzucenie zmian + */ + public boolean showDialog() { + result = false; + setVisible(true); + return result; + } + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/HelpDialog.java b/bfIDE/src/main/java/tpsa/dialogs/HelpDialog.java new file mode 100644 index 0000000..51df603 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/HelpDialog.java @@ -0,0 +1,33 @@ +package tpsa.dialogs; + +import java.awt.Dimension; + +import javax.swing.JDialog; +import javax.swing.JFrame; + +/** + * Dialog pomocy + * + * @author Duga Eye + * + */ +public class HelpDialog extends JDialog{ + + /** + * + */ + private static final long serialVersionUID = 5854396908923903514L; + + public HelpDialog(JFrame frame) { + super(frame, "Help", ModalityType.APPLICATION_MODAL); + setPreferredSize(new Dimension(500, 400)); + + pack(); + } + + public boolean showDialog() { + setVisible(true); + return true; + } + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/MemorySizeDialog.java b/bfIDE/src/main/java/tpsa/dialogs/MemorySizeDialog.java new file mode 100644 index 0000000..cf41eab --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/MemorySizeDialog.java @@ -0,0 +1,147 @@ +package tpsa.dialogs; + +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.Box; +import javax.swing.InputMap; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; + +import tpsa.BrainFuckIDE; +import tpsa.MainFrame; + +/** + * Dialog ustalania pamięci VM BrainFucka + * + * @author Duga Eye + * + */ +public class MemorySizeDialog extends JDialog { + + /** + * + */ + private static final long serialVersionUID = -6179258766072502357L; + private Action okAction; + private Action cancelAction; + private JTextField sizeEdit; + private boolean result = false; + + public MemorySizeDialog(final MainFrame frame) { + super(frame); + setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE); + setModalityType(ModalityType.APPLICATION_MODAL); + + setPreferredSize(new Dimension(200, 150)); + + Box b = Box.createVerticalBox(); + + JPanel p1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); + + p1.add(new JLabel("Podaj rozmiar VM Brainfucka: ")); + sizeEdit = new JTextField(8); + p1.add(sizeEdit); + + JButton ok = new JButton("OK"); + JButton cancel = new JButton("Cancel"); + + JPanel p2 = new JPanel(new FlowLayout(FlowLayout.LEFT)); + p2.add(ok); + p2.add(cancel); + + b.add(p1); + b.add(p2); + + okAction = new AbstractAction("OK") { + + /** + * + */ + private static final long serialVersionUID = -5794707237614988673L; + + @Override + public void actionPerformed(ActionEvent e) { + BrainFuckIDE.lg.info("ok action performed"); + if (getSizeMemory() <= 0) { + JOptionPane + .showMessageDialog(frame, + "Wartość rozmiaru pamieci musi być typu Integer i >= 0"); + return; + } + + result = true; + setVisible(false); + + } + }; + + cancelAction = new AbstractAction("Cancel") { + + /** + * + */ + private static final long serialVersionUID = 8441237664658329642L; + + @Override + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }; + + ok.setAction(okAction); + cancel.setAction(cancelAction); + + JRootPane pane = getRootPane(); + KeyStroke acceptStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + KeyStroke cancelStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + + InputMap iMap = pane + .getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + ActionMap aMap = pane.getActionMap(); + + iMap.put(acceptStroke, "accept"); + aMap.put("accept", okAction); + + iMap.put(cancelStroke, "cancel"); + aMap.put("cancel", cancelAction); + + add(b); + + pack(); + + } + + public int getSizeMemory() { + return Integer.parseInt(sizeEdit.getText()); + } + + public void setSizeMemory(int s) { + sizeEdit.setText("" + s); + } + + /** + * Pokazuje ten wspaniały dialog + * + * @return w zasadzie nic, czy użytkownik zaakceptował wprowadzone dane + */ + public boolean showDialog() { + result = false; + setVisible(true); + return result; + } + + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/ReplaceDialog.java b/bfIDE/src/main/java/tpsa/dialogs/ReplaceDialog.java new file mode 100644 index 0000000..a25e096 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/ReplaceDialog.java @@ -0,0 +1,197 @@ +package tpsa.dialogs; + +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JRootPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.KeyStroke; +import javax.swing.text.BadLocationException; + +import tpsa.BFTab; +import tpsa.BrainFuckIDE; +import tpsa.MainFrame; + +/** + * Dialog do zastępowania treści jednej drugą + * + * @author Duga Eye + * + */ +public class ReplaceDialog extends JDialog { + + /** + * + */ + private static final long serialVersionUID = 103076309978197148L; + /** + * Wyrażenie, które ma znaleść + */ + private JTextField match; + /** + * Wyrażenie, którym ma zastąpić match + */ + private JTextField replace; + /** + * Czy użytkownik, zaakceptował dane + */ + private boolean success; + public ReplaceDialog(final MainFrame frame) { + super(frame); + setTitle("Replace"); + setPreferredSize(new Dimension(250, 100)); + setModalityType(ModalityType.APPLICATION_MODAL); + + setTitle("Find"); + + setLayout(new GridLayout(3, 2)); + + JRootPane pane = getRootPane(); + KeyStroke escapeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + + pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeStroke, + "escape"); + pane.getActionMap().put("escape", new AbstractAction() { + + /** + * + */ + private static final long serialVersionUID = -3277168784395957597L; + + @Override + public void actionPerformed(ActionEvent e) { + success = false; + setVisible(false); + } + }); + + add(new JLabel("Find: ")); + match = new JTextField(10); + add(match); + + replace = new JTextField(10); + add(new JLabel("Replace: ")); + add(replace); + + + JButton replace = new JButton("Replace"); + Action replaceAct = new AbstractAction("Replace one") { + + /** + * + */ + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent e) { + try { + JTabbedPane tabs = frame.getTabs(); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + JTextPane area = tab.getArea(); + int cursorPos = area.getCaretPosition(); + StringBuilder s = new StringBuilder(area.getText()); + int posToChange = s.indexOf(getMatch(), cursorPos); + if (posToChange < 0) { + JOptionPane.showMessageDialog(frame, + "Nope, nie znalazłem ciągu do zastąpienia"); + return; + // ретурн; + } + s.replace(posToChange, posToChange + getMatch().length(), + getReplace()); + tab.getArea().setText(s.toString()); + Rectangle rect = tab.getArea().modelToView(posToChange); + BrainFuckIDE.lg.finest("rect: " + rect.toString()); + area.scrollRectToVisible(rect); + area.setCaretPosition(posToChange); + } catch (BadLocationException excp) { + BrainFuckIDE.lg.warning(BrainFuckIDE.ErrorsFormatter(excp)); + JOptionPane.showMessageDialog(frame, + "Coś się stało, i właściwie nie chcę wiedzieć, co", + "Powitanie", JOptionPane.INFORMATION_MESSAGE); + } + } + }; + + replace.setAction(replaceAct); + KeyStroke replaceStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( + replaceStroke, "replace"); + getRootPane().getActionMap().put("replace", replaceAct); + + JButton replaceAll = new JButton("Replace all"); + + replaceAll.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JTabbedPane tabs = frame.getTabs(); + BFTab tab = (BFTab) tabs.getSelectedComponent(); + String s = tab.getArea().getText(); + int count = ReplaceDialog.countOccurences(s, getMatch()); + s = s.replaceAll(getMatch(), getReplace()); + tab.getArea().setText(s); + + JOptionPane.showMessageDialog(frame, "Sucess. Changed all " + + count + " occurences of phrase: " + getMatch(), + "Abnormal error", JOptionPane.INFORMATION_MESSAGE); + + } + }); + + add(replace); + add(replaceAll); + + pack(); + } + + /** + * @return Wyrażenie, które użytkownik chce zastąpić + */ + public String getMatch() { + return new String(match.getText()); + } + + /** + * @return Wyrażenie, którym użytkownik chce zastąpić match + */ + public String getReplace() { + return new String(replace.getText()); + } + + /** + * @return Czy potwierdzono zmiany + */ + public boolean getSuccess() { + return success; + } + + @Override + public void setVisible(boolean b) { + if (b == true) + success = false; + super.setVisible(b); + } + + static int countOccurences(String s, String match) { + int count = 0; + int pos = -1; + while ((pos = s.indexOf(match, pos + 1)) >= 0) + ++count; + return count; + } + +} diff --git a/bfIDE/src/main/java/tpsa/dialogs/filters/BFFileFilter.java b/bfIDE/src/main/java/tpsa/dialogs/filters/BFFileFilter.java new file mode 100644 index 0000000..c02a290 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/dialogs/filters/BFFileFilter.java @@ -0,0 +1,29 @@ +package tpsa.dialogs.filters; + +import java.io.File; + +import javax.swing.filechooser.FileFilter; + +/** + * Filtr rozszerzeń kodów źródłowych BrainFucka + * + * @author Duga Eye + * + */ +public class BFFileFilter extends FileFilter { + + @Override + public boolean accept(File f) { + if (f.isDirectory()) + return true; + if (f.getName().endsWith(".bf")) + return true; + return false; + } + + @Override + public String getDescription() { + return "BrainFuck source code"; + } + +} diff --git a/bfIDE/src/main/java/tpsa/highlighters/StyleHighlight.java b/bfIDE/src/main/java/tpsa/highlighters/StyleHighlight.java new file mode 100644 index 0000000..ef9cf0c --- /dev/null +++ b/bfIDE/src/main/java/tpsa/highlighters/StyleHighlight.java @@ -0,0 +1,62 @@ +package tpsa.highlighters; + +import javax.swing.text.Style; +import javax.swing.text.StyledDocument; + +/** + * Reprezentuje klasę coś ala TODO dla swing utilities co ma odmalować + * odpowiednim kolorem + * + * @author Duga Eye + */ +public class StyleHighlight { + private StyledDocument doc; + + private int length; + + private int offset; + + private Style style; + + public StyleHighlight() { + + } + + public StyleHighlight(StyledDocument doc, int offset, int length, + Style style) { + this.doc = doc; + this.offset = offset; + this.length = length; + this.style = style; + } + + public StyledDocument getDoc() { + return doc; + } + public int getLength() { + return length; + } + public int getOffset() { + return offset; + } + public Style getStyle() { + return style; + } + + public void setDoc(StyledDocument doc) { + this.doc = doc; + } + + public void setLength(int length) { + this.length = length; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public void setStyle(Style style) { + this.style = style; + } + +} diff --git a/bfIDE/src/main/java/tpsa/highlighters/SyntaxHighlighter.java b/bfIDE/src/main/java/tpsa/highlighters/SyntaxHighlighter.java new file mode 100644 index 0000000..e4a9d80 --- /dev/null +++ b/bfIDE/src/main/java/tpsa/highlighters/SyntaxHighlighter.java @@ -0,0 +1,192 @@ +package tpsa.highlighters; + +import java.util.ArrayList; +import java.util.Arrays; + +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyledDocument; + +import tpsa.BrainFuckIDE; + +/** + * Wyróżnia kod od komentarzy + * + * @author Duga Eye + * + */ +public class SyntaxHighlighter implements DocumentListener { + + public static ArrayList BFcharacterList = new ArrayList( + Arrays.asList(new Character[] { '>', '<', '+', '-', '.', ',', '[', + ']', 'b' })); + + final ArrayList list = new ArrayList(); + + public SyntaxHighlighter() { + } + + private void addToTheList(StyledDocument doc, final int offset, + Style comment, int from, final ArrayList list, + int position) { + int length; + + length = position - from; + if (length <= 0) + return; + synchronized (list) { + list.add(new StyleHighlight(doc, offset + from, length, comment)); + } + + } + + @Override + public void changedUpdate(DocumentEvent e) { + // update(e); + + } + + private void checkForBugs(StyledDocument doc) { + try { + String s = doc.getText(0, doc.getLength()); + Style errorStyle = doc.getStyle("ERROR"); + + int indentation = 0; + int idx = 0; + while (idx < s.length()) { + char c = 0; + do { + c = s.charAt(idx++); + } while (c != '[' && c != ']' && idx < s.length()); + switch (c) { + case '[': + indentation++; + break; + case ']': + if (--indentation < 0) { + indentation = 0; + list.add(new StyleHighlight(doc, idx - 1, 1, errorStyle)); + } + } + + } + + updateHighlights(); + + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + } + + @Override + public void insertUpdate(DocumentEvent e) { + update(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + update(e); + + } + + private void setStyleComment(StyledDocument doc, final int offset, + String s, Style comment) { + + BrainFuckIDE.lg.finest("Text: '" + s + "'"); + Style sCode = doc.getStyle("CODE"); + int state = 0; + int from = 0; + synchronized (sCode) { + list.clear(); + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (state) { + case 0: // seria kodu + if (!BFcharacterList.contains(c)) { + addToTheList(doc, offset, sCode, from, list, i); + from = i; + state = 1; + } + + break; + case 1: // seria komentarzy + if (BFcharacterList.contains(c)) { + addToTheList(doc, offset, comment, from, list, i); + from = i; + state = 0; + } + break; + } + } + + if (state == 1) { + addToTheList(doc, offset, comment, from, list, s.length()); + } else { + addToTheList(doc, offset, sCode, from, list, s.length()); + } + + } + + public void update(StyledDocument doc, int offset, int length, + boolean machine) { + Style comment = doc.getStyle("COMMENT"); + String s; + try { + BrainFuckIDE.lg.finer("Change to analyse: " + offset + ", " + + length + ", length: " + doc.getLength()); + + s = doc.getText(offset, Math.min(length, doc.getLength() - offset)); + + setStyleComment(doc, offset, s, comment); + + checkForBugs(doc); + checkBreakpoints(doc); + + updateHighlights(); + + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + } + + private void checkBreakpoints(StyledDocument doc) { + /* + * try { BrainFuckIDE.lg.finer("Check breapoints"); String s = + * doc.getText(0, doc.getLength()); int pos = -1; Style sBreakpoint = + * doc.getStyle("BREAKPOINT"); while ((pos = s.indexOf('b', pos + 1)) >= + * 0) { BrainFuckIDE.lg.finer("Breakpoint pos: " + pos); int i; for (i = + * 0; i < s.length() - pos && s.charAt(i + pos) != 'b'; i++) ; + * + * synchronized (list) { list.add(new StyleHighlight(doc, pos, i - 1, + * sBreakpoint)); } pos += i; } + * + * } catch (BadLocationException excp) { } + */ + } + + private void update(DocumentEvent e) { + update((StyledDocument) e.getDocument(), e.getOffset(), e.getLength(), + false); + + } + + private void updateHighlights() { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + synchronized (list) { + for (StyleHighlight h : list) + h.getDoc().setCharacterAttributes(h.getOffset(), + h.getLength(), h.getStyle(), true); + list.clear(); + } + + } + }); + } +} diff --git a/bfIDE/src/main/java/tpsa/highlighters/WordsHighlighter.java b/bfIDE/src/main/java/tpsa/highlighters/WordsHighlighter.java new file mode 100644 index 0000000..e13fd4f --- /dev/null +++ b/bfIDE/src/main/java/tpsa/highlighters/WordsHighlighter.java @@ -0,0 +1,134 @@ +package tpsa.highlighters; + +import java.awt.Color; +import java.util.Locale; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.Document; + +import tpsa.BrainFuckIDE; + +public class WordsHighlighter extends DefaultHighlighter implements + DocumentListener { + + /** + * + */ + private Long last = 0L; + /** + * Words to highlight + * Words to highlight TODO not implemented + */ + private String[] words; + + /** + * @param word + */ + public WordsHighlighter(String[] words) { + // FIXME shallow copy + this.words = words.clone(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateFrequent(e); + updateNotToFrequent(e); + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateFrequent(e); + updateNotToFrequent(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateFrequent(e); + updateNotToFrequent(e); + } + + /** + * @param s + * @param start + * @param length + * TODO + * @throws BadLocationException + */ + private void searchForOccurance(String s, int start, int length) + throws BadLocationException { + DefaultHighlightPainter todoHighLighter = new DefaultHighlightPainter( + Color.LIGHT_GRAY); + + int pos = -1; + + String copy = new String(s).toUpperCase(Locale.forLanguageTag("pl_PL")); + for (int i = 0; i < words.length; i++) { + pos = -1; + while ((pos = copy.indexOf(words[i], pos + 1)) >= 0) { + int endOfHighlighting = Math.min(pos + words[i].length() + + start, length - 1); + addHighlight(pos + start, endOfHighlighting, + todoHighLighter); + } + } + } + + /** + * @param e + */ + private void update(DocumentEvent e) { + try { + Document doc = e.getDocument(); + String s = doc.getText(0, doc.getLength()); + removeAllHighlights(); + + searchForOccurance(s, 0, doc.getLength()); + + } catch (BadLocationException excp) { + + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + + /** + * @param e + */ + private void updateFrequent(DocumentEvent e) { + int offset = e.getOffset(); + int length = e.getLength(); + Document doc = e.getDocument(); + + try { + int start = Math.max(0, offset - 10); + int end = Math.min(doc.getLength() - start, length + 20); + String s = doc.getText(start, end); + Highlight[] highlights = getHighlights(); + for (int i = 0; i < highlights.length; i++) + if (highlights[i].getStartOffset() >= offset + && highlights[i].getEndOffset() <= offset + length) + removeHighlight(highlights[i]); + + searchForOccurance(s, start, doc.getLength()); + + } catch (BadLocationException excp) { + BrainFuckIDE.lg.severe(BrainFuckIDE.ErrorsFormatter(excp)); + } + + } + + /** + * @param e + */ + private void updateNotToFrequent(DocumentEvent e) { + long now = System.nanoTime(); + long diff = now - last; + if (diff > 2e9) { + last = now; + update(e); + } + } +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/ColoredRange.java b/bfIDE/src/main/java/tv/porst/jhexview/ColoredRange.java new file mode 100644 index 0000000..c8cbb9f --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/ColoredRange.java @@ -0,0 +1,48 @@ +package tv.porst.jhexview; + +import java.awt.Color; + +public class ColoredRange implements Comparable { + + private final Color fcolor; + + private final long start; + + private final int size; + + private final Color bgcolor; + + public ColoredRange(final long start, final int size, final Color fcolor, + final Color bgcolor) { + + this.start = start; + this.size = size; + this.fcolor = fcolor; + this.bgcolor = bgcolor; + } + + @Override + public int compareTo(final ColoredRange arg0) { + return (int) (start - arg0.start); + } + + public boolean containsOffset(final long offset) { + return offset >= start && offset < start + size; + } + + public Color getBackgroundColor() { + return bgcolor; + } + + public Color getColor() { + return fcolor; + } + + public int getSize() { + return size; + } + + public long getStart() { + return start; + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/jhexview/ColoredRangeManager.java b/bfIDE/src/main/java/tv/porst/jhexview/ColoredRangeManager.java new file mode 100644 index 0000000..b162d55 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/ColoredRangeManager.java @@ -0,0 +1,137 @@ +package tv.porst.jhexview; + +import java.util.ArrayList; +import java.util.Collections; + +public final class ColoredRangeManager { + + private final ArrayList ranges = new ArrayList(); + + public void addRange(final ColoredRange range) { + ranges.add(range); + Collections.sort(ranges); + } + + public void clear() { + ranges.clear(); + } + + public ColoredRange findRange(final long offset) { + + for (final ColoredRange range : ranges) { + if (range.getStart() >= offset) { + return range; + } + } + + return null; + } + + public ColoredRange findRangeWith(final long offset) { + + for (final ColoredRange range : ranges) { + if (range.containsOffset(offset)) { + return range; + } + } + + return null; + } + + public void removeRange(final long offset, final int size) { + // Try to find the range that contains the offset + ColoredRange range = findRangeWith(offset); + + // If there is no such range, at least find the range right + // before the offset. + if (range == null) { + range = findRange(offset); + } + + // If there is no such range then there is nothing to remove. + if (range == null) { + return; + } + + // TODO: The <= is a hack; used to be just ==; Check this again + if (offset <= range.getStart()) { + // The offset starts exactly at the range. That means + // we start decolorizing at the beginning of the range. + + if (range.getSize() == size) { + // If the sizes are equal the entire range is decolorized and + // we're done. + + ranges.remove(range); + } else if (range.getSize() < size) { + // If the range is smaller the entire range is decolorized and + // the + // remaining bytes are decolorized in the next step. + + ranges.remove(range); + removeRange(range.getStart() + range.getSize(), + size - range.getSize()); + } else + // if (range.getSize() > size) + { + // If the range is larger than the decolorizing area, ditch the + // range and add a new range that contains the area that stays + // colorized. + + ranges.remove(range); + addRange(new ColoredRange(offset + size, + range.getSize() - size, range.getColor(), + range.getBackgroundColor())); + } + } else + // if (offset > range.getStart()) + { + // We start to decolorize somewhere in the middle of the range. + + if (offset + size == range.getStart() + range.getSize()) { + // If the last offset of the range and the decolorizing area is equal, + // then the latter part of the range is ditched. Get rid of the range + // and create a new range that represents the first part of the area. + + ranges.remove(range); + + final long newStart = range.getStart(); + final int newSize = range.getSize() - size; + // int newSize = (int)(offset - range.getStart()); + addRange(new ColoredRange(newStart, newSize, range.getColor(), range.getBackgroundColor())); + } else if (offset + size < range.getStart() + range.getSize()) { + // Some part is cut out of the middle of the range. Get rid of the old range + // and create two new ranges for the first part and last part of the old range. + + ranges.remove(range); + + final long newStartFirst = range.getStart(); + final int newSizeFirst = (int) (offset - range.getStart()); + + addRange(new ColoredRange(newStartFirst, newSizeFirst, + range.getColor(), range.getBackgroundColor())); + + final long newStartLast = offset + size; + final int newSizeLast = (int) (range.getStart() + + range.getSize() - offset - size); + + addRange(new ColoredRange(newStartLast, newSizeLast, + range.getColor(), range.getBackgroundColor())); + } else + // if (offset + size > range.getStart() + range.getSize()) + { + // More than the current range must be decolorized. That means + // the entire range + // except for the start is ditched. + + ranges.remove(range); + + final long newStart = range.getStart(); + final int newSize = (int) (offset - range.getStart()); + + addRange(new ColoredRange(newStart, newSize, range.getColor(), range.getBackgroundColor())); + removeRange(range.getStart() + range.getSize(), size - (int) (range.getStart() + range.getSize() - offset)); + } + } + } +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/IColormap.java b/bfIDE/src/main/java/tv/porst/jhexview/IColormap.java new file mode 100644 index 0000000..17237e1 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/IColormap.java @@ -0,0 +1,41 @@ +package tv.porst.jhexview; + +import java.awt.Color; + +public interface IColormap { + + /** + * Determines whether the byte at the given offset should be + * colored or not. + * + * @param data The data array that can be used to determine the return value. + * @param currentOffset The offset of the byte in question. + * + * @return True if the byte should be colored. False, otherwise. + */ + boolean colorize(final byte[] data, final long currentOffset); + + /** + * Returns the background color that should be used to color + * the byte at the given offset. + * + * @param data The data array that can be used to determine the return value. + * @param currentOffset The offset of the byte in question. + * + * @return The background color to be used by that byte. Null, if + * the default background color should be used, + */ + Color getBackgroundColor(final byte[] data, final long currentOffset); + + /** + * Returns the foreground color that should be used to color + * the byte at the given offset. + * + * @param data The data array that can be used to determine the return value. + * @param currentOffset The offset of the byte in question. + * + * @return The foreground color to be used by that byte. Null, if + * the default foreground color should be used, + */ + Color getForegroundColor(final byte[] data, final long currentOffset); +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/IDataChangedListener.java b/bfIDE/src/main/java/tv/porst/jhexview/IDataChangedListener.java new file mode 100644 index 0000000..14f5689 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/IDataChangedListener.java @@ -0,0 +1,7 @@ +package tv.porst.jhexview; + +public interface IDataChangedListener { + + void dataChanged(); + +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/IDataProvider.java b/bfIDE/src/main/java/tv/porst/jhexview/IDataProvider.java new file mode 100644 index 0000000..445adf5 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/IDataProvider.java @@ -0,0 +1,22 @@ +package tv.porst.jhexview; + +public interface IDataProvider { + + void addListener(final IDataChangedListener hexView); + + byte[] getData(); + + byte[] getData(long offset, int length); + + int getDataLength(); + + boolean hasData(long start, int length); + + boolean isEditable(); + + boolean keepTrying(); + + void removeListener(IDataChangedListener listener); + + void setData(long offset, byte[] data); +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/IHexViewListener.java b/bfIDE/src/main/java/tv/porst/jhexview/IHexViewListener.java new file mode 100644 index 0000000..b68e7d5 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/IHexViewListener.java @@ -0,0 +1,7 @@ +package tv.porst.jhexview; + +public interface IHexViewListener { + + void selectionChanged(long start, long length); + +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/IMenuCreator.java b/bfIDE/src/main/java/tv/porst/jhexview/IMenuCreator.java new file mode 100644 index 0000000..4b519f1 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/IMenuCreator.java @@ -0,0 +1,22 @@ +package tv.porst.jhexview; + +import javax.swing.JPopupMenu; + +/** + * This interface must be implemented by all classes that want to provide + * context menus for the JHexView control. + * + */ +public interface IMenuCreator { + + /** + * This function is called to generate a popup menu after the user + * right-clicked somewhere in the hex control. + * + * @param offset The offset of the right-click. + * + * @return The popup menu suitable for that offset or null if no popup menu + * should be shown. + */ + JPopupMenu createMenu(long offset); +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/JHexView.java b/bfIDE/src/main/java/tv/porst/jhexview/JHexView.java new file mode 100644 index 0000000..53c5616 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/JHexView.java @@ -0,0 +1,2714 @@ +package tv.porst.jhexview; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.KeyboardFocusManager; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashSet; + +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JPopupMenu; +import javax.swing.JScrollBar; +import javax.swing.KeyStroke; +import javax.swing.Timer; + +import tv.porst.splib.convert.ConvertHelpers; +import tv.porst.splib.gui.GuiHelpers; +import tv.porst.splib.gui.caret.ICaretListener; +import tv.porst.splib.gui.caret.JCaret; + + +/** + * The JHexView component is a Java component that can be used to display + * data in hexadecimal format. + * + * @author sp + * + */ +public final class JHexView extends JComponent { + + private static final long serialVersionUID = -2402458562501988128L; + + /** + * Two characters are needed to display a byte in the hex window. + */ + private static final int CHARACTERS_PER_BYTE = 2; + + /** + * Lookup table to convert byte values into printable strings. + */ + private static final String[] HEX_BYTES = + { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", "1C", "1D", "1E", "1F", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", "3D", "3E", "3F", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4A", "4B", "4C", "4D", "4E", "4F", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", "5E", "5F", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6A", "6B", "6C", "6D", "6E", "6F", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", "7F", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8A", "8B", "8C", "8D", "8E", "8F", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF", "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF", "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF", "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF", "E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF", "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF", }; + + private static final int PADDING_OFFSETVIEW = 20; + + private static final int NIBBLES_PER_BYTE = 2; + + /** + * List of listeners that are notified if something happens in the + * hex panel component. + */ + private final ArrayList m_listeners = new ArrayList(); + + /** + * The data set that is displayed in the component. + */ + private IDataProvider m_dataProvider; + + /** + * Number of bytes shown per row. + */ + private int m_bytesPerRow = 16; + + /** + * Font used to draw the data. + */ + private Font m_font = new Font(GuiHelpers.getMonospaceFont(), 0, 12); + + /** + * Currently selected position. Note that this field + * is twice as large as the length of data because nibbles + * can be selected. + */ + private long m_selectionStart = 0; + + /** + * Current selection length in nibbles. This value can be negative + * if nibbles before the current position are selected. + */ + private long m_selectionLength = 0; + + /** + * Determines the window where the caret is shown. + */ + private Views m_activeView = Views.HEX_VIEW; + + /** + * Width of the hex view in pixels. + */ + private int m_hexViewWidth = 270; + + /** + * Width of the space between columns in pixels. + */ + private int m_columnSpacing = 4; + + /** + * Number of bytes per column. + */ + private int m_bytesPerColumn = 2; + + /** + * Background color of the offset view. + */ + private Color m_bgColorOffset = Color.GRAY; + + /** + * Background color of the hex view. + */ + private Color m_bgColorHex = Color.WHITE; + + /** + * Background color of the ASCII view. + */ + private Color m_bgColorAscii = Color.WHITE; + + /** + * Font color of the offset view. + */ + private Color m_fontColorOffsets = Color.WHITE; + + /** + * Font color of the hex view. + */ + private Color m_fontColorHex1 = Color.BLUE; + + /** + * Font color of the hex view. + */ + private Color m_fontColorHex2 = new Color(0x3399FF); + + /** + * Font color of the ASCII view. + */ + private Color m_fontColorAscii = new Color(0x339900); + + /** + * Used to store the height of a single row. + * This value is updated every time the component is drawn. + */ + private int m_rowHeight = 12; + + /** + * Used to store the width of a single character. + * This value is updated every time the component is drawn. + */ + private int m_charWidth = 8; + + /** + * Scrollbar that is used to scroll through the dataset. + */ + private final JScrollBar m_scrollbar = new JScrollBar(JScrollBar.VERTICAL, 0, 1, 0, 1);; + + /** + * Horizontal scrollbar that is used to scroll through the dataset. + */ + private final JScrollBar m_horizontalScrollbar = new JScrollBar(JScrollBar.HORIZONTAL, 0, 1, 0, 1);; + + /** + * The first visible row. + */ + private int m_firstRow = 0; + + /** + * The first visible column. + */ + private int m_firstColumn = 0; + + /** + * Address of the first offset in the data set. + */ + private long m_baseAddress = 0; + + /** + * Last x-coordinate of the mouse cursor in the component. + */ + private int m_lastMouseX = 0; + + /** + * Last y-coordinate of the mouse cursor in the component. + */ + private int m_lastMouseY = 0; + + /** + * Flag that determines whether the component reacts to user + * input or not. + */ + private boolean m_enabled = false; + + /** + * Color that is used to draw all text in disabled components. + */ + private final Color m_disabledColor = Color.GRAY; + + /** + * Blinking caret of the component. + */ + private final JCaret m_caret = new JCaret(); + + /** + * Left-padding of the hex view in pixels. + */ + private final int m_paddingHexLeft = 10; + + /** + * Left-padding of the ASCII view in pixels. + */ + private final int m_paddingAsciiLeft = 10; + + /** + * Top-padding of all views in pixels. + */ + private final int m_paddingTop = 16; + + /** + * Height of a drawn character in the component. + */ + private int m_charHeight = 8; + + /** + * Color that is used to highlight data when the + * mouse cursor hovers of the data. + */ + private final Color m_colorHighlight = Color.LIGHT_GRAY; + + /** + * Start with an undefined definition status. + */ + private DefinitionStatus m_status = DefinitionStatus.UNDEFINED; + + /** + * The menu creator is used to create popup menus when + * the user right-clicks on the hex view control. + */ + private IMenuCreator m_menuCreator; + + /** + * Current addressing mode (32bit or 64bit) + */ + private AddressMode m_addressMode = AddressMode.BIT32; + + /** + * Width of the offset view part of the component. + */ + private int m_offsetViewWidth; + + /** + * Manager that keeps track of specially colored byte ranges. + */ + private final ColoredRangeManager[] m_coloredRanges = new ColoredRangeManager[10]; + + /** + * Used for double buffering the graphical output. + */ + private Graphics bufferGraphics; + + /** + * Used for double buffering the graphical output. + */ + private BufferedImage img; + + /** + * Timer that is used to refresh the component if no data for the selected + * range is available. + */ + private Timer m_updateTimer; + + /** + * Flag that indicates whether the component is being drawn for the first time. + */ + private boolean m_firstDraw = true; + + /** + * Default internal listener that is used to handle various events. + */ + private final InternalListener m_listener = new InternalListener(); + + /** + * Action that's executed when the user presses the left arrow key. + */ + private final LeftAction m_leftAction = new LeftAction(); + + /** + * Action that's executed when the user presses the right arrow key. + */ + private final RightAction m_rightAction = new RightAction(); + + /** + * Action that's executed when the user presses the up arrow key. + */ + private final UpAction m_upAction = new UpAction(); + + /** + * Action that's executed when the user presses the down arrow key. + */ + private final DownAction m_downAction = new DownAction(); + + /** + * Action that's executed when the user presses the page up key. + */ + private final PageUpAction m_pageUpAction = new PageUpAction(); + + /** + * Action that's executed when the user presses the page down key. + */ + private final PageDownAction m_pageDownAction = new PageDownAction(); + + /** + * Action that's executed when the user presses the tab key. + */ + private final TabAction m_tabAction = new TabAction(); + + private int m_lastHighlightedNibble; + + private IColormap m_colormap; + + private Color m_selectionColor = Color.YELLOW; + + /** + * Determines whether the bytes inside a column are flipped or not. + */ + private boolean m_flipBytes = false; + + /** + * Creates a new hex viewer. + */ + public JHexView() { + + for (int i = 0; i < m_coloredRanges.length; i++) { + m_coloredRanges[i] = new ColoredRangeManager(); + } + + // Necessary to receive input + setFocusable(true); + + setLayout(new BorderLayout()); + + // Set the initial font + setFont(m_font); + + initListeners(); + + initHotkeys(); + + initScrollbar(); + + img = new BufferedImage(getWidth() + 1 - m_scrollbar.getWidth(), getHeight() + 1 - m_horizontalScrollbar.getHeight(), BufferedImage.TYPE_INT_RGB); + bufferGraphics = img.getGraphics(); + + updateOffsetViewWidth(); + + // By default, this component is disabled. + setEnabled(false); + } + + /** + * Calculates current character and row sizes. + */ + private void calculateSizes() { + + m_rowHeight = getRowHeight(bufferGraphics); + m_charHeight = getCharHeight(bufferGraphics); + m_charWidth = getCharacterWidth(bufferGraphics); + } + + private void changeBy(final ActionEvent event, final int length) { + + if (event.getModifiers() == ActionEvent.SHIFT_MASK) { + + if (getSelectionStart() + getSelectionLength() + length < 0) { + setSelectionLength(-getSelectionStart()); + } + else { + + if (getSelectionStart() + getSelectionLength() + length < 2 * m_dataProvider.getDataLength()) { + setSelectionLength(getSelectionLength() + length); + } + else { + setSelectionLength(2 * m_dataProvider.getDataLength() - getSelectionStart()); + } + } + } + else { + + if (getSelectionStart() + getSelectionLength() + length < 0) { + setSelectionStart(0); + } + else if (getSelectionStart() + getSelectionLength() + length < 2 * m_dataProvider.getDataLength()) { + setSelectionStart(getSelectionStart() + getSelectionLength() + length); + } + else { + setSelectionStart(2 * m_dataProvider.getDataLength()); + } + + setSelectionLength(0); + } + + final long newPosition = getSelectionStart() + getSelectionLength(); + + if (newPosition < 2 * getFirstVisibleByte()) { + scrollToPosition(newPosition); + } + else if (newPosition >= 2 * (getFirstVisibleByte() + getMaximumVisibleBytes())) { + + final long invisibleNibbles = newPosition - 2 * (getFirstVisibleByte() + getMaximumVisibleBytes()); + + final long scrollpos = 2 * getFirstVisibleByte() + 2 * m_bytesPerRow + invisibleNibbles; + + scrollToPosition(scrollpos); + } + + m_caret.setVisible(true); + repaint(); + } + + /** + * Draws the content of the ASCII panel. + * + * @param g The graphics context of the hex panel. + */ + private void drawAsciiPanel(final Graphics g) { + + if (isEnabled()) { + // Choose the right color for the ASCII view + g.setColor(m_fontColorAscii); + } + else { + g.setColor(m_disabledColor != m_bgColorAscii ? m_disabledColor : Color.WHITE); + } + + final int characterWidth = getCharacterWidth(g); + + final int initx = getAsciiViewLeft() + m_paddingAsciiLeft; + + int x = initx; + int y = m_paddingTop; + + byte[] data = null; + int bytesToDraw; + + if (m_status == DefinitionStatus.DEFINED) { + bytesToDraw = getBytesToDraw(); + data = m_dataProvider.getData(getFirstVisibleOffset(), bytesToDraw); + } + else { + bytesToDraw = getMaximumVisibleBytes(); + } + + long currentOffset = getFirstVisibleOffset(); + + for (int i = 0; i < bytesToDraw; i++, currentOffset++) { + + ColoredRange range = findColoredRange(currentOffset); + + if (range != null && currentOffset + bytesToDraw < range.getStart()) { + range = null; + } + + if (i != 0 && i % m_bytesPerRow == 0) { + // If the end of a row is reached, reset the + // x-coordinate and increase the y-coordinate. + x = initx; + y += m_rowHeight; + } + + if (m_status == DefinitionStatus.DEFINED) { + + char c = (char) data[i]; + + c = ConvertHelpers.isPrintableCharacter(c) ? c : '.'; + + final String dataString = String.valueOf(c); + + if (isEnabled()) { + + // Fixed: Highlighting im debugger memory window is wrong in regards to the endianess selected + final long normalizedOffset = m_flipBytes ? + (currentOffset & -m_bytesPerColumn) + m_bytesPerColumn - (currentOffset % m_bytesPerColumn) - 1 : + currentOffset; + + if (isSelectedOffset(normalizedOffset)) { + + g.setColor(m_selectionColor); + g.fillRect(x, y - m_charHeight, m_charWidth, m_charHeight + 2); + + // Choose the right color for the ASCII view + g.setColor(m_fontColorAscii); + } + else if (range != null && range.containsOffset(currentOffset)) { + + final Color bgColor = range.getBackgroundColor(); + + if (bgColor != null) { + g.setColor(bgColor); + } + + g.fillRect(x, y - m_charHeight, m_charWidth, m_charHeight + 2); + g.setColor(range.getColor()); + } + else if (m_colormap != null && m_colormap.colorize(data, i)) { + + final Color backgroundColor = m_colormap.getBackgroundColor(data, i); + final Color foregroundColor = m_colormap.getForegroundColor(data, i); + + if (backgroundColor != null) { + + g.setColor(backgroundColor); + g.fillRect(x, y - m_charHeight, m_charWidth, m_charHeight + 2); + } + + if (foregroundColor != null) { + g.setColor(foregroundColor); + } + } + else { + // Choose the right color for the ASCII view + g.setColor(m_fontColorAscii); + } + + } + else { + g.setColor(m_disabledColor != m_bgColorAscii ? m_disabledColor : Color.WHITE); + } + + g.drawString(dataString, x, y); + } + else { + g.drawString("?", x, y); + } + + x += characterWidth; + + if (range != null && range.getStart() + range.getSize() <= currentOffset) { + + range = findColoredRange(currentOffset); + + if (range != null && currentOffset + bytesToDraw < range.getStart()) { + range = null; + } + } + } + } + + /** + * Draws the background of the hex panel. + * + * @param g The graphics context of the hex panel. + */ + private void drawBackground(final Graphics g) { + + // Draw the background of the offset view + g.setColor(m_bgColorOffset); + g.fillRect(-m_firstColumn * m_charWidth, 0, m_offsetViewWidth, getHeight()); + + // Draw the background of the hex view + g.setColor(m_bgColorHex); + g.fillRect(-m_firstColumn * m_charWidth + m_offsetViewWidth, 0, m_hexViewWidth, getHeight()); + + // Draw the background of the ASCII view + g.setColor(m_bgColorAscii); + g.fillRect(-m_firstColumn * m_charWidth + m_hexViewWidth + m_offsetViewWidth, 0, m_firstColumn * m_charWidth + getWidth() - (m_hexViewWidth + m_offsetViewWidth) - m_scrollbar.getWidth(), getHeight() - m_horizontalScrollbar.getHeight()); + + // Draw the lines that separate the individual views + g.setColor(Color.BLACK); + g.drawLine(-m_firstColumn * m_charWidth + m_offsetViewWidth, 0, -m_firstColumn * m_charWidth + m_offsetViewWidth, getHeight()); + g.drawLine(-m_firstColumn * m_charWidth + m_offsetViewWidth + m_hexViewWidth, 0, -m_firstColumn * m_charWidth + m_offsetViewWidth + m_hexViewWidth, getHeight()); + } + + /** + * Draws the caret. + * + * @param g + */ + private void drawCaret(final Graphics g) { + + if (!isEnabled()) { + return; + } + + if (getCurrentOffset() < getFirstVisibleByte() || getCurrentColumn() > getFirstVisibleByte() + getMaximumVisibleBytes()) { + return; + } + + final int characterSize = getCharacterWidth(g); + + if (m_activeView == Views.HEX_VIEW) { + drawCaretHexWindow(g, characterSize, m_rowHeight); + } + else { + drawCaretAsciiWindow(g, characterSize, m_rowHeight); + } + + } + + /** + * Draws the caret in the ASCII window. + * + * @param g The graphic context of the ASCII panel. + * @param characterWidth The width of a single character. + * @param characterHeight The height of a single character. + */ + private void drawCaretAsciiWindow(final Graphics g, final int characterWidth, final int characterHeight) { + + final int currentRow = getCurrentRow() - m_firstRow; + final int currentColumn = getCurrentColumn(); + final int currentCharacter = currentColumn / 2; + + // Calculate the position of the first character in the row + final int startLeft = 9 + m_offsetViewWidth + m_hexViewWidth; + + // Calculate the position of the current character in the row + final int x = -m_firstColumn * m_charWidth + startLeft + currentCharacter * characterWidth; + + // Calculate the position of the row + final int y = 3 + m_paddingTop - characterHeight + characterHeight * currentRow; + + m_caret.draw(g, x, y, characterHeight); + } + + /** + * Draws the caret in the hex window. + * + * @param g The graphic context of the hex panel. + * @param characterWidth The width of a single character. + * @param characterHeight The height of a single character. + */ + private void drawCaretHexWindow(final Graphics g, final int characterWidth, final int characterHeight) { + + final int currentRow = getCurrentRow() - m_firstRow; + final int currentColumn = getCurrentColumn(); + + // Calculate the position of the first character in the row. + final int startLeft = 9 + m_offsetViewWidth; + + // Calculate the extra padding between columns. + final int paddingColumns = currentColumn / (2 * m_bytesPerColumn) * m_columnSpacing; + + // Calculate the position of the character in the row. + final int x = -m_firstColumn * m_charWidth + startLeft + currentColumn * characterWidth + paddingColumns; + + // Calculate the position of the row. + final int y = 3 + m_paddingTop - characterHeight + characterHeight * currentRow; + + m_caret.draw(g, x, y, characterHeight); + } + + /** + * Draws the content of the hex view. + * + * @param g The graphics context of the hex panel. + */ + private void drawHexView(final Graphics g) { + + final int standardSize = 2 * getCharacterWidth(g); + + final int firstX = -m_firstColumn * m_charWidth + m_paddingHexLeft + m_offsetViewWidth; + + int x = firstX; + int y = m_paddingTop; + + boolean evenColumn = true; + + byte[] data = null; + int bytesToDraw; + + if (m_status == DefinitionStatus.DEFINED) { + bytesToDraw = getBytesToDraw(); + data = m_dataProvider.getData(getFirstVisibleOffset(), bytesToDraw); + } + else { + bytesToDraw = getMaximumVisibleBytes(); + } + + long currentOffset = getFirstVisibleOffset(); + + // Iterate over all bytes in the data set and + // print their hex value to the hex view. + for (int i = 0; i < bytesToDraw; i++, currentOffset++) { + + final ColoredRange range = findColoredRange(currentOffset); + + if (i != 0) { + if (i % m_bytesPerRow == 0) { + // If the end of a row was reached, reset the x-coordinate + // and set the y-coordinate to the next row. + + x = firstX; + y += m_rowHeight; + + evenColumn = true; + } + else if (i % m_bytesPerColumn == 0) { + // Add some spacing after each column. + x += m_columnSpacing; + + evenColumn = !evenColumn; + } + } + + if (isEnabled()) { + if (isSelectedOffset(currentOffset)) { + g.setColor(m_selectionColor); + g.fillRect(x, y - m_charHeight, 2 * m_charWidth, m_charHeight + 2); + + // Choose the right color for the hex view + g.setColor(evenColumn ? m_fontColorHex1 : m_fontColorHex2); + } + else if (range != null && range.containsOffset(currentOffset)) { + final Color bgColor = range.getBackgroundColor(); + + if (bgColor != null) { + g.setColor(bgColor); + } + + g.fillRect(x, y - m_charHeight, 2 * m_charWidth, m_charHeight + 2); + g.setColor(range.getColor()); + } + else { + if (m_colormap != null && m_colormap.colorize(data, i)) { + final Color backgroundColor = m_colormap.getBackgroundColor(data, i); + final Color foregroundColor = m_colormap.getForegroundColor(data, i); + + if (backgroundColor != null) { + g.setColor(backgroundColor); + g.fillRect(x, y - m_charHeight, 2 * m_charWidth, m_charHeight + 2); + } + + if (foregroundColor != null) { + g.setColor(foregroundColor); + } + } + else { + // Choose the right color for the hex view + g.setColor(evenColumn ? m_fontColorHex1 : m_fontColorHex2); + } + } + } + else { + g.setColor(m_disabledColor != m_bgColorHex ? m_disabledColor : Color.WHITE); + } + + if (m_status == DefinitionStatus.DEFINED) { + // Number of bytes shown in the current column + final int columnBytes = Math.min(m_dataProvider.getDataLength() - i, m_bytesPerColumn); + + final int dataPosition = m_flipBytes ? (i / m_bytesPerColumn) * m_bytesPerColumn + (columnBytes - (i % columnBytes) - 1) : i; + + // Print the data + g.drawString(HEX_BYTES[data[dataPosition] & 0xFF], x, y); + } + else { + g.drawString("??", x, y); + } + + // Update the position of the x-coordinate + x += standardSize; + } + } + + /** + * Draws highlighting of bytes when the mouse hovers over them. + * + * @param g The graphics context where the highlighting is drawn. + */ + private void drawMouseOverHighlighting(final Graphics g) { + g.setColor(m_colorHighlight); + + m_lastHighlightedNibble = getNibbleAtCoordinate(m_lastMouseX, m_lastMouseY); + + if (m_lastHighlightedNibble == -1) { + return; + } + + // Find out in which view the mouse currently resides. + final Views lastHighlightedView = m_lastMouseX >= getAsciiViewLeft() ? Views.ASCII_VIEW : Views.HEX_VIEW; + + if (lastHighlightedView == Views.HEX_VIEW) { + // If the mouse is in the hex view just one nibble must be highlighted. + final Rectangle r = getNibbleBoundsHex(m_lastHighlightedNibble); + g.fillRect((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight()); + } + else if (lastHighlightedView == Views.ASCII_VIEW) { + + // If the mouse is in the ASCII view it is necessary + // to highlight two nibbles. + + final int first = 2 * m_lastHighlightedNibble / 2; // Don't change. + + Rectangle r = getNibbleBoundsHex(first); + g.fillRect((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight()); + + r = getNibbleBoundsHex(first + 1); + g.fillRect((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight()); + } + + // Highlight the byte in the ASCII panel too. + final Rectangle r = getByteBoundsAscii(m_lastHighlightedNibble); + g.fillRect((int) r.getX(), (int) r.getY(), (int) r.getWidth(), (int) r.getHeight()); + } + + /** + * Draws the offsets in the offset view. + * + * @param g The graphics context of the hex panel. + */ + private void drawOffsets(final Graphics g) { + if (isEnabled()) + { + // Choose the right color for the offset text + g.setColor(m_fontColorOffsets); + } + else + { + g.setColor(m_disabledColor != m_bgColorOffset ? m_disabledColor : Color.WHITE); + } + + final int x = -m_firstColumn * m_charWidth + 10; + + final int bytesToDraw = getMaximumVisibleBytes(); + + final String formatString = m_addressMode == AddressMode.BIT32 ? "%08X" : "%016X"; + + // Iterate over the data and print the offsets + for (int i = 0; i < bytesToDraw; i += m_bytesPerRow) { + + final long address = m_baseAddress + m_firstRow * m_bytesPerRow + i; + + final String offsetString = String.format(formatString, address); + final int currentRow = i / m_bytesPerRow; + + g.drawString(offsetString, x, m_paddingTop + currentRow * m_rowHeight); + } + } + + private ColoredRange findColoredRange(final long currentOffset) { + + for (final ColoredRangeManager element : m_coloredRanges) { + + final ColoredRange range = element.findRangeWith(currentOffset); + + if (range != null) { + return range; + } + } + + return null; + } + + /** + * Returns the left coordinate of the ASCII view. + * + * @return The left coordinate of the ASCII view. + */ + private int getAsciiViewLeft() { + return getHexViewLeft() + getHexViewWidth(); + } + + /** + * Returns the bounds of a byte in the ASCII view. + * + * @param position The index of one of the nibbles that belong to the byte. + * + * @return The bounds of the byte in the ASCII view. + */ + private Rectangle getByteBoundsAscii(final int position) { + + if (position < 2 * getFirstVisibleByte()) { + return new Rectangle(-1, -1, -1, -1); + } + + if (position > 2 * getFirstVisibleByte() + 2 * getMaximumVisibleBytes()) { + return new Rectangle(-1, -1, -1, -1); + } + + final int relativePosition = (position - 2 * getFirstVisibleByte()) / 2; + + final int row = relativePosition / m_bytesPerRow; + final int character = relativePosition % m_bytesPerRow; + + final int x = getAsciiViewLeft() + m_paddingAsciiLeft + character * m_charWidth; + final int y = m_paddingTop - m_charHeight + row * m_rowHeight; + + return new Rectangle(x, y, m_charWidth, m_charHeight); + } + + /** + * Returns the number of bytes that need to be displayed. + * + * @return The number of bytes that need to be displayed. + */ + private int getBytesToDraw() { + + final int firstVisibleByte = getFirstVisibleByte(); + + final int maxBytes = getMaximumVisibleBytes(); + + final int restBytes = m_dataProvider.getDataLength() - firstVisibleByte; + + return Math.min(maxBytes, restBytes); + } + + /** + * Returns the character size of a single character on + * the given graphics context. + * + * @param g The graphics context. + * + * @return The size of a single character. + */ + private int getCharacterWidth(final Graphics g) { + return (int) g.getFontMetrics().getStringBounds("0", g).getWidth(); + } + + /** + * Determines the height of a character in a graphical context. + * + * @param g The graphical context. + * + * @return The height of a character in the graphical context. + */ + private int getCharHeight(final Graphics g) { + return g.getFontMetrics().getAscent(); + } + + /** + * Returns the size of a hex view column in pixels (includes column spacing). + * + * @return The size of a hex view column in pixels. + */ + private int getColumnSize() { + return NIBBLES_PER_BYTE * m_bytesPerColumn * m_charWidth + m_columnSpacing; + } + + /** + * Returns the column of the byte at the current position. + * + * @return The column of the byte at the current position. + */ + private int getCurrentColumn() { + return (int) getCurrentNibble() % (NIBBLES_PER_BYTE * m_bytesPerRow); + } + + /** + * Returns the nibble at the caret position. + * + * @return The nibble at the care position. + */ + private long getCurrentNibble() { + return getSelectionStart() + getSelectionLength(); + } + + /** + * Returns the row of the byte at the current position. + * + * @return The row of the byte at the current position. + */ + private int getCurrentRow() { + return (int) getCurrentNibble() / (NIBBLES_PER_BYTE * m_bytesPerRow); + } + + /** + * Returns the number of bytes before the first visible byte. + * + * @return The number of bytes before the first visible byte. + */ + private int getEarlierBytes() { + return m_firstRow * m_bytesPerRow; + } + + /** + * Returns the first visible byte. + * + * @return The first visible byte. + */ + private int getFirstVisibleByte() { + return m_firstRow * m_bytesPerRow; + } + + /** + * Returns the left position of the hex view. + * + * @return The left position of the hex view. + */ + private int getHexViewLeft() { + return -m_firstColumn * m_charWidth + m_offsetViewWidth; + } + + /** + * Returns the maximum number of visible bytes. + * + * @return The maximum number of visible bytes. + */ + private int getMaximumVisibleBytes() { + return getNumberOfVisibleRows() * m_bytesPerRow; + } + + /** + * Returns the index of the nibble below given coordinates. + * + * @param x The x coordinate. + * @param y The y coordinate. + * + * @return The nibble index at the coordinates or -1 if there is no + * nibble at the coordinates. + */ + private int getNibbleAtCoordinate(final int x, final int y) { + + if (m_dataProvider == null) { + return -1; + } + + if (x < getHexViewLeft() + m_paddingHexLeft) { + return -1; + } + + if (y >= m_paddingTop - m_font.getSize()) { + + if (x >= getHexViewLeft() && x < getHexViewLeft() + getHexViewWidth()) { + // Cursor is in hex view + return getNibbleAtCoordinatesHex(x, y); + } + else if (x >= getAsciiViewLeft()) { + // Cursor is in ASCII view + return getNibbleAtCoordinatesAscii(x, y); + } + } + + return -1; + } + + /** + * Returns the index of the nibble below given coordinates in the ASCII view. + * + * @param x The x coordinate. + * @param y The y coordinate. + * + * @return The nibble index at the coordinates or -1 if there is no + * nibble at the coordinates. + */ + private int getNibbleAtCoordinatesAscii(final int x, final int y) { + + // Normalize the x coordinate to inside the ASCII view + final int normalizedX = x - (getAsciiViewLeft() + m_paddingAsciiLeft); + + if (normalizedX < 0) { + return -1; + } + + // Find the row at the coordinate + final int row = (y - (m_paddingTop - m_charHeight)) / m_rowHeight; + + final int earlierPositions = 2 * getEarlierBytes(); + + if (normalizedX / m_charWidth >= m_bytesPerRow) { + return -1; + } + + final int character = 2 * (normalizedX / m_charWidth); + + final int position = earlierPositions + 2 * row * m_bytesPerRow + character; + + if (position >= 2 * m_dataProvider.getDataLength()) { + return -1; + } + else { + return position; + } + } + + /** + * Returns the index of the nibble below given coordinates in the hex view. + * + * @param x The x coordinate. + * @param y The y coordinate. + * + * @return The nibble index at the coordinates or -1 if there is no + * nibble at the coordinates. + */ + private int getNibbleAtCoordinatesHex(final int x, final int y) { + + // Normalize the x coordinate to inside the hex view + final int normalizedX = x - (getHexViewLeft() + m_paddingHexLeft); + + final int columnSize = getColumnSize(); + + // Find the column at the specified coordinate. + final int column = normalizedX / columnSize; + + // Return if the cursor is at the spacing at the end of a line. + if (column >= m_bytesPerRow / m_bytesPerColumn) { + return -1; + } + + // Find the coordinate relative to the beginning of the column. + final int xInColumn = normalizedX % columnSize; + + // Find the nibble inside the column. + final int nibbleInColumn = xInColumn / m_charWidth; + + // Return if the cursor is in the spacing between columns. + if (nibbleInColumn >= 2 * m_bytesPerColumn) { + return -1; + } + + // Find the row at the coordinate + final int row = (y - (m_paddingTop - m_charHeight)) / m_rowHeight; + + final int earlierPositions = 2 * getEarlierBytes(); + + final int position = earlierPositions + 2 * (row * m_bytesPerRow + column * m_bytesPerColumn) + nibbleInColumn; + + if (position >= 2 * m_dataProvider.getDataLength()) { + return -1; + } + else { + return position; + } + } + + /** + * Returns the bounds of a nibble in the hex view. + * + * @param position The index of the nibble. + * + * @return The bounds of the nibble in the hex view. + */ + private Rectangle getNibbleBoundsHex(final int position) { + + if (position < 2 * getFirstVisibleByte()) { + return new Rectangle(-1, -1, -1, -1); + } + + if (position > 2 * getFirstVisibleByte() + 2 * getMaximumVisibleBytes()) { + return new Rectangle(-1, -1, -1, -1); + } + + final int relativePosition = position - 2 * getFirstVisibleByte(); + + final int columnSize = getColumnSize(); + + final int row = relativePosition / (2 * m_bytesPerRow); + final int column = relativePosition % (2 * m_bytesPerRow) / (2 * m_bytesPerColumn); + final int nibble = relativePosition % (2 * m_bytesPerRow) % (2 * m_bytesPerColumn); + + final int x = getHexViewLeft() + m_paddingHexLeft + column * columnSize + nibble * m_charWidth; + final int y = m_paddingTop - m_charHeight + row * m_rowHeight; + + return new Rectangle(x, y, m_charWidth, m_charHeight); + } + + /** + * Returns the number of visible rows. + * + * @return The number of visible rows. + */ + private int getNumberOfVisibleRows() { + final int rawHeight = getHeight() - m_paddingTop - m_horizontalScrollbar.getHeight(); + return rawHeight / m_rowHeight + (rawHeight % m_rowHeight == 0 ? 0 : 1); + } + + /** + * Determines the height of the current font in a graphical context. + * + * @param g The graphical context. + * + * @return The height of the current font in the graphical context. + */ + private int getRowHeight(final Graphics g) { + return g.getFontMetrics().getHeight(); + } + + private long getSelectionStart() { + return m_selectionStart; + } + + /** + * Initializes the keys that can be used by the user inside + * the component. + */ + private void initHotkeys() { + + // Don't change focus on TAB + setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, new HashSet()); + + final InputMap inputMap = this.getInputMap(); + final ActionMap actionMap = this.getActionMap(); + + inputMap.put(KeyStroke.getKeyStroke("LEFT"), "LEFT"); + actionMap.put("LEFT", m_leftAction); + + inputMap.put(KeyStroke.getKeyStroke("shift LEFT"), "shift LEFT"); + actionMap.put("shift LEFT", m_leftAction); + + inputMap.put(KeyStroke.getKeyStroke("RIGHT"), "RIGHT"); + actionMap.put("RIGHT", m_rightAction); + + inputMap.put(KeyStroke.getKeyStroke("shift RIGHT"), "shift RIGHT"); + actionMap.put("shift RIGHT", m_rightAction); + + inputMap.put(KeyStroke.getKeyStroke("UP"), "UP"); + actionMap.put("UP", m_upAction); + + inputMap.put(KeyStroke.getKeyStroke("shift UP"), "shift UP"); + actionMap.put("shift UP", m_upAction); + + inputMap.put(KeyStroke.getKeyStroke("DOWN"), "DOWN"); + actionMap.put("DOWN", m_downAction); + + inputMap.put(KeyStroke.getKeyStroke("shift DOWN"), "shift DOWN"); + actionMap.put("shift DOWN", m_downAction); + + inputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), "PAGE_DOWN"); + actionMap.put("PAGE_DOWN", m_pageDownAction); + + inputMap.put(KeyStroke.getKeyStroke("shift PAGE_DOWN"), "shift PAGE_DOWN"); + actionMap.put("shift PAGE_DOWN", m_pageDownAction); + + inputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), "PAGE_UP"); + actionMap.put("PAGE_UP", m_pageUpAction); + + inputMap.put(KeyStroke.getKeyStroke("shift PAGE_UP"), "shift PAGE_UP"); + actionMap.put("shift PAGE_UP", m_pageUpAction); + + inputMap.put(KeyStroke.getKeyStroke("TAB"), "TAB"); + actionMap.put("TAB", m_tabAction); + + } + + /** + * Initializes all internal listeners. + */ + private void initListeners() { + + // Add the input listeners + addMouseListener(m_listener); + addMouseMotionListener(m_listener); + addMouseWheelListener(m_listener); + addFocusListener(m_listener); + addComponentListener(m_listener); + addKeyListener(m_listener); + + m_caret.addCaretListener(m_listener); + } + + /** + * Creates and initializes the scroll bar that is used to scroll + * through the data. + */ + private void initScrollbar() { + m_scrollbar.addAdjustmentListener(m_listener); + + add(m_scrollbar, BorderLayout.EAST); + + m_horizontalScrollbar.addAdjustmentListener(m_listener); + + add(m_horizontalScrollbar, BorderLayout.SOUTH); + } + + /** + * Determines whether data to be displayed is available. + * + * @return True, if data is available. False, otherwise. + */ + private boolean isDataAvailable() { + return m_dataProvider != null; + } + + private boolean isInsideAsciiView(final int x, final int y) { + return y >= m_paddingTop - m_font.getSize() && x >= getAsciiViewLeft(); + } + + private boolean isInsideHexView(final int x, final int y) { + return y >= m_paddingTop - m_font.getSize() && x >= getHexViewLeft() && x < getHexViewLeft() + getHexViewWidth(); + } + + /** + * Determines whether a certain position is visible in the view. + * + * @param position The position in question. + * + * @return True, if the position is visible. False, otherwise. + */ + private boolean isPositionVisible(final long position) { + + final int firstVisible = getFirstVisibleByte(); + final int lastVisible = firstVisible + getMaximumVisibleBytes(); + + return position >= 2 * firstVisible && position <= 2 * lastVisible; + } + + private boolean isSelectedOffset(long currentOffset) { + currentOffset = currentOffset - m_baseAddress; + + if (getSelectionLength() == 0) { + return false; + } + else if (getSelectionLength() > 0) { + return currentOffset >= getSelectionStart() / 2 && 2 * currentOffset < getSelectionStart() + getSelectionLength(); + } + else { + return currentOffset >= (getSelectionStart() + getSelectionLength()) / 2 && 2 * currentOffset < getSelectionStart(); + } + } + + /** + * Resets the current graphic buffer and prepares it for another round + * of drawing. + */ + private void resetBufferedGraphic() { + bufferGraphics.clearRect(0, 0, getWidth(), getHeight()); + bufferGraphics.setFont(m_font); + } + + /** + * Scrolls the scroll bar so that it matches the given position. + * + * @param position The position to scroll to. + */ + private void scrollToPosition(final long position) { + m_scrollbar.setValue((int) position / (2 * m_bytesPerRow)); + } + + /** + * Moves the current position of the caret and notifies the listeners + * about the position change. + * + * @param newPosition The new position of the caret. + */ + private void setCurrentPosition(final long newPosition) { + //setSelectionStart(newPosition); + m_selectionStart = newPosition; // Avoid notifying twice + + if (!isPositionVisible(getSelectionStart())) { + scrollToPosition(getSelectionStart()); + } + + for (final IHexViewListener listener : m_listeners) { + listener.selectionChanged(getSelectionStart(), 1); + } + } + + /** + * Updates the maximum scroll range of the scroll bar depending + * on the number of bytes in the current data set. + */ + private void setScrollBarMaximum() { + + if (m_dataProvider == null) { + m_scrollbar.setMaximum(1); + m_horizontalScrollbar.setMaximum(1); + } + else { + final int visibleRows = getNumberOfVisibleRows(); + + final int totalRows = m_dataProvider.getDataLength() / m_bytesPerRow; + int scrollRange = 2 + totalRows - visibleRows; + + if (scrollRange < 0) { + scrollRange = 0; + m_scrollbar.setValue(0); + m_scrollbar.setEnabled(false); + } + else { + m_scrollbar.setEnabled(true); + } + + m_scrollbar.setMaximum(scrollRange); + + final int totalWidth = getAsciiViewLeft() + m_paddingAsciiLeft + m_charWidth * m_bytesPerRow; + + final int realWidth = getWidth() - m_scrollbar.getWidth(); + + if (realWidth >= totalWidth) { + m_horizontalScrollbar.setValue(0); + m_horizontalScrollbar.setEnabled(false); + } + else { + m_horizontalScrollbar.setMaximum((totalWidth - realWidth) / m_charWidth + 1); + m_horizontalScrollbar.setEnabled(true); + } + } + } + + private void setSelectionStart(final long selectionStart) { + m_selectionStart = selectionStart; + + for (final IHexViewListener listener : m_listeners) { + listener.selectionChanged(m_selectionStart, m_selectionLength); + } + } + + private void updateHexViewWidth() { + m_hexViewWidth = 15 + getColumnSize() * getBytesPerRow() / getBytesPerColumn(); + } + + /** + * Calculates and sets the size of the offset view depending on the + * currently selected address mode. + */ + private void updateOffsetViewWidth() { + final int addressBytes = m_addressMode == AddressMode.BIT32 ? 8 : 16; + m_offsetViewWidth = PADDING_OFFSETVIEW + m_charWidth * addressBytes; + } + + /** + * Calculates and sets the preferred size of the component. + */ + private void updatePreferredSize() { + // TODO: Improve this + final int width = m_offsetViewWidth + m_hexViewWidth + 18 * m_charWidth + m_scrollbar.getWidth(); + setPreferredSize(new Dimension(width, getHeight())); + revalidate(); + } + + /** + * Paints the hex window. + */ + @Override + protected void paintComponent(final Graphics gx) { + super.paintComponent(gx); + + // Make room for a new graphic + resetBufferedGraphic(); + + // Calculate current sizes of characters and rows + calculateSizes(); + + updateOffsetViewWidth(); + + if (m_firstDraw) { + m_firstDraw = false; + + // The first time the component is drawn, its size must be set. + updateHexViewWidth(); + updatePreferredSize(); + } + + // Draw the background of the hex panel + drawBackground(bufferGraphics); + + // Draw the offsets column + drawOffsets(bufferGraphics); + + if (isEnabled()) { + // Only draw the cursor "shadow" if the component is enabled. + drawMouseOverHighlighting(bufferGraphics); + } + + // If the component has defined data, it can be drawn. + if (m_status == DefinitionStatus.DEFINED && m_dataProvider != null) { + + final int bytesToDraw = getBytesToDraw(); + + if (bytesToDraw != 0 && !m_dataProvider.hasData(getFirstVisibleOffset(), bytesToDraw)) { + + // At this point the component wants to draw data but the data + // provider does not have the data yet. The hope is that the data + // provider can reload the data. Until this happens, set the + // component's status to UNDEFINED and create a timer that + // periodically rechecks if the missing data is finally available. + + setDefinitionStatus(DefinitionStatus.UNDEFINED); + setEnabled(false); + + if (m_updateTimer != null) { + m_updateTimer.setRepeats(false); + m_updateTimer.stop(); + } + + m_updateTimer = new Timer(1000, new WaitingForDataAction(getFirstVisibleOffset(), bytesToDraw)); + m_updateTimer.setRepeats(true); + m_updateTimer.start(); + + return; + } + } + + if (isDataAvailable() || m_status == DefinitionStatus.UNDEFINED) { + // Draw the hex data + drawHexView(bufferGraphics); + + // Draw the ASCII data + drawAsciiPanel(bufferGraphics); + + // Show the caret if necessary + if (m_caret.isVisible() && hasFocus()) { + drawCaret(bufferGraphics); + } + } + + gx.drawImage(img, 0, 0, this); + } + + /** + * Adds a new event listener to the list of event listeners. + * + * @param listener The new event listener. + * + * @throws NullPointerException Thrown if the listener argument is null. + */ + public void addHexListener(final IHexViewListener listener) { + + if (listener == null) { + throw new NullPointerException("Error: Listener can't be null"); + } + + // Avoid duplicates + if (!m_listeners.contains(listener)) { + m_listeners.add(listener); + } + } + + /** + * Colorizes a range of bytes in special colors. To keep the default text or + * background color, it is possible to pass null as these colors. + * + * @param offset The start offset of the byte range. + * @param size The number of bytes in the range. + * @param color The text color that is used to color that range. + * @param bgcolor The background color that is used to color that range. + * + * @throws IllegalArgumentException Thrown if offset is negative or size is not positive. + */ + public void colorize(final int level, final long offset, final int size, final Color color, final Color bgcolor) { + + if (offset < 0) { + throw new IllegalArgumentException("Error: Offset can't be negative"); + } + + if (size <= 0) { + throw new IllegalArgumentException("Error: Size must be positive"); + } + + if (level < 0 || level >= m_coloredRanges.length) { + throw new IllegalArgumentException("Error: Invalid level"); + } + + m_coloredRanges[level].addRange(new ColoredRange(offset, size, color, bgcolor)); + + repaint(); + } + + public void dispose() { + + removeMouseListener(m_listener); + removeMouseMotionListener(m_listener); + removeMouseWheelListener(m_listener); + removeFocusListener(m_listener); + removeComponentListener(m_listener); + removeKeyListener(m_listener); + + m_caret.removeListener(m_listener); + + m_caret.stop(); + } + + /** + * Returns a a flag that indicates whether the bytes inside a column + * are flipped or not. + * + * @return True, if the bytes are flipped. False, otherwise. + */ + public boolean doFlipBytes() { + return m_flipBytes; + } + + /** + * Returns the currently used address mode. + * + * @return The currently used address mode. + */ + public AddressMode getAddressMode() { + return m_addressMode; + } + + /** + * Returns the current background color of the ASCII view. + * + * @return The current background color of the ASCII view. + */ + public Color getBackgroundColorAsciiView() { + return m_bgColorAscii; + } + + /** + * Returns the current background color of the hex view. + * + * @return The current background color of the hex view. + */ + public Color getBackgroundColorHexView() { + return m_bgColorHex; + } + + /** + * Returns the current background color of the offset view. + * + * @return The current background color of the offset view. + */ + public Color getBackgroundColorOffsetView() { + return m_bgColorOffset; + } + + /** + * Returns the current base address. + * + * @return The current base address. + */ + public long getBaseAddress() { + return m_baseAddress; + } + + /** + * Returns the number of bytes displayed per column. + * + * @return The number of bytes displayed per column. + */ + public int getBytesPerColumn() { + return m_bytesPerColumn; + } + + /** + * Returns the current number of bytes displayed per row. + * + * @return The current number of bytes displayed per row. + */ + public int getBytesPerRow() { + return m_bytesPerRow; + } + + /** + * Returns the spacing between columns in pixels. + * + * @return The spacing between columns. + */ + public int getColumnSpacing() { + return m_columnSpacing; + } + + /** + * Returns the offset at the current caret position. + * + * @return The offset at the current caret position. + */ + public long getCurrentOffset() { + + final long currentOffset = m_baseAddress + getCurrentNibble() / 2; + + return m_flipBytes ? + (currentOffset & -m_bytesPerColumn) + m_bytesPerColumn - (currentOffset % m_bytesPerColumn) - 1 : + currentOffset; + } + + /** + * Returns the currently used data provider. + * + * @return The currently used data provider. + */ + public IDataProvider getData() { + return m_dataProvider; + } + + /** + * Returns the current definition status. + * + * @return The current definition status. + */ + public DefinitionStatus getDefinitionStatus() { + return m_status; + } + + /** + * Returns the first selected offset. + * + * @return The first selected offset. + */ + public long getFirstSelectedOffset() { + + if (m_selectionLength >= 0) { + return (m_baseAddress + m_selectionStart) / 2; + } + else { + return (m_baseAddress + m_selectionStart + m_selectionLength) / 2; + } + } + + /** + * Returns the first visible offset. + * + * @return The first visible offset. + */ + public long getFirstVisibleOffset() { + return getBaseAddress() + getFirstVisibleByte(); + } + + /** + * Returns the current font color of the ASCII view. + * + * @return The current font color of the ASCII view. + */ + public Color getFontColorAsciiView() { + return m_fontColorAscii; + } + + /** + * Returns the current font color of even columns in the hex view. + * + * @return The current font color of even columns in the hex view. + */ + public Color getFontColorHexView1() { + return m_fontColorHex1; + } + + /** + * Returns the current font color of odd columns in the hex view. + * + * @return The current font color of odd columns in the hex view. + */ + public Color getFontColorHexView2() { + return m_fontColorHex2; + } + + /** + * Returns the current font color of the offset view. + * + * @return The current font color of the offset view. + */ + public Color getFontColorOffsetView() { + return m_fontColorOffsets; + } + + /** + * Returns the size of the font that is used to draw all data. + * + * @return The size of the font that is used to draw all data. + */ + public int getFontSize() { + return m_font.getSize(); + } + + /** + * Returns the current width of the hex view. + * + * @return The current width of the hex view. + */ + public int getHexViewWidth() { + return m_hexViewWidth; + } + + public long getLastOffset() { + return getBaseAddress() + m_dataProvider.getDataLength(); + } + + /** + * Returns the last selected offset. + * + * @return The last selected offset. + */ + public long getLastSelectedOffset() { + + // In this method it is necessary to round up. This is because + // half a selected byte counts as a fully selected byte. + + if (m_selectionLength >= 0) { + return (m_baseAddress + m_selectionStart + m_selectionLength) / 2 + (m_baseAddress + m_selectionStart + m_selectionLength) % 2; + } + else { + return (m_baseAddress + m_selectionStart) / 2 + (m_baseAddress + m_selectionStart) % 2; + } + } + + public long getSelectionLength() { + return m_selectionLength; + } + + public int getVisibleBytes() { + + final int visibleBytes = getMaximumVisibleBytes(); + + if (m_dataProvider.getDataLength() - getFirstVisibleByte() >= visibleBytes) { + return visibleBytes; + } + else { + return m_dataProvider.getDataLength() - getFirstVisibleByte(); + } + } + + /** + * Scrolls to a given offset. + * + * @param offset The offset to scroll to. + * + * @throws IllegalArgumentException Thrown if the offset is out of bounds. + */ + public void gotoOffset(final long offset) { + + if (m_dataProvider == null) { + throw new IllegalStateException("Error: No data provider active"); + } + + if (getCurrentOffset() == offset) { + + if (!isPositionVisible(getSelectionStart())) { + scrollToPosition(getSelectionStart()); + } + + return; + } + + final long realOffset = offset - m_baseAddress; + + if (realOffset < 0 || realOffset >= m_dataProvider.getDataLength()) { + throw new IllegalArgumentException("Error: Invalid offset"); + } + + setCurrentPosition(2 * realOffset); + } + + /** + * Returns the status of the component. + * + * @return True, if the component is enabled. False, otherwise. + */ + @Override + public boolean isEnabled() { + return m_enabled; + } + + public void removeHexListener(final IHexViewListener listener) { + + if (listener == null) { + throw new NullPointerException("Internal Error: Listener can't be null"); + } + + if (!m_listeners.remove(listener)) { + throw new IllegalArgumentException("Internal Error: Listener was not listening on object"); + } + } + + /** + * Sets the currently used address mode. + * + * @param mode The new address mode. + * + * @throws NullPointerException Thrown if the new address mode is null. + */ + public void setAddressMode(final AddressMode mode) { + + if (mode == null) { + throw new NullPointerException("Error: Address mode can't be null"); + } + + m_addressMode = mode; + + updateOffsetViewWidth(); + updatePreferredSize(); + } + + /** + * Sets the current background color of the ASCII view. + * + * @param color The new background color of the ASCII view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setBackgroundColorAsciiView(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_bgColorAscii = color; + + repaint(); + } + + /** + * Sets the current background color of the hex view. + * + * @param color The new background color of the hex view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setBackgroundColorHexView(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_bgColorHex = color; + + repaint(); + } + + /** + * Sets the current background color of the offset view. + * + * @param color The new background color of the offset view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setBackgroundColorOffsetView(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_bgColorOffset = color; + + repaint(); + } + + /** + * Sets the current base address. + * + * @param baseAddress The current base address. + * + * @throws IllegalArgumentException Thrown if the new base address is negative. + */ + public void setBaseAddress(final long baseAddress) { + + if (baseAddress < 0) { + throw new IllegalArgumentException("Error: Base address can't be negative"); + } + + this.m_baseAddress = baseAddress; + + repaint(); + } + + /** + * Sets the number of bytes displayed per column. + * + * @param bytes The new number of bytes per column. + * + * @throws IllegalArgumentException Thrown if the new number of bytes is smaller than 1 or bigger than the number of bytes per row. + */ + public void setBytesPerColumn(final int bytes) { + + if (bytes <= 0) { + throw new IllegalArgumentException("Error: Number of bytes must be positive"); + } + + if (bytes > m_bytesPerRow) { + throw new IllegalArgumentException("Error: Number of bytes can't be more than the number of bytes per row"); + } + + m_bytesPerColumn = bytes; + + updateHexViewWidth(); + updatePreferredSize(); + + repaint(); + } + + /** + * Sets the current number of bytes displayed per row. + * + * @param value The new number of bytes displayed per row. + * + * @throws IllegalArgumentException Thrown if the new number is smaller than 1. + */ + public void setBytesPerRow(final int value) { + + if (value <= 0) { + throw new IllegalArgumentException("Error: Value must be positive"); + } + + m_bytesPerRow = value; + + repaint(); + } + + public void setColormap(final IColormap colormap) { + m_colormap = colormap; + + repaint(); + } + + /** + * Sets the spacing between columns. + * + * @param spacing The spacing between columns in pixels. + * + * @throws IllegalArgumentException Thrown if the new spacing is smaller than 1. + */ + public void setColumnSpacing(final int spacing) { + + if (spacing <= 0) { + throw new IllegalArgumentException("Error: Spacing must be positive"); + } + + m_columnSpacing = spacing; + + repaint(); + } + + /** + * Sets the caret to a new offset. + * + * @param offset The new offset. + */ + public void setCurrentOffset(final long offset) { + + if (m_dataProvider == null) { + return; + } + + if (offset < getBaseAddress() || offset > getBaseAddress() + m_dataProvider.getDataLength()) { + throw new IllegalArgumentException("Error: Invalid offset"); + } + + setCurrentPosition(CHARACTERS_PER_BYTE * (offset - m_baseAddress)); + } + + /** + * Sets the current data provider. + * + * It is valid to pass null as the new data provider. This clears the display. + * + * @param data The new data provider. + */ + public void setData(final IDataProvider data) { + + /** + * Remove the data listener from the old data source. + */ + if (m_dataProvider != null) { + m_dataProvider.removeListener(m_listener); + } + + m_dataProvider = data; + + /** + * Add a data listener to the new data source so that the + * component can be updated when the data changes. + */ + if (data != null) { + m_dataProvider.addListener(m_listener); + } + + setCurrentPosition(0); + + setScrollBarMaximum(); + + repaint(); + } + + /** + * Changes the definition status of the JHexView component. + * This flag determines whether real data or ?? are + * displayed. + * + * @param status The new definition status. + * + * @throws NullPointerException Thrown if the new definition status is null. + */ + public void setDefinitionStatus(final DefinitionStatus status) { + + if (status == null) { + throw new NullPointerException("Error: Definition status can't be null"); + } + + m_status = status; + + repaint(); + } + + /** + * Enables or disables the component. + * + * @param enabled True to enable the component, false to disable it. + */ + @Override + public void setEnabled(final boolean enabled) { + + if (enabled == m_enabled) { + return; + } + + this.m_enabled = enabled; + // m_scrollbar.setEnabled(enabled); + repaint(); + } + + public void setFlipBytes(final boolean flip) { + + if (m_flipBytes == flip) { + return; + } + + m_flipBytes = flip; + + repaint(); + } + + /** + * Sets the current font color of the ASCII view. + * + * @param color The new font color of the ASCII view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setFontColorAsciiView(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_fontColorAscii = color; + + repaint(); + } + + /** + * Sets the current font color of even columns in the hex view. + * + * @param color The new font color of even columns in the hex view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setFontColorHexView1(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_fontColorHex1 = color; + + repaint(); + } + + /** + * Sets the current font color of odd columns in the hex view. + * + * @param color The new font color of odd columns in the hex view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setFontColorHexView2(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_fontColorHex2 = color; + + repaint(); + } + + /** + * Sets the current font color of the offset view. + * + * @param color The new font color of the offset view. + * + * @throws NullPointerException Thrown if the new color is null. + */ + public void setFontColorOffsetView(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_fontColorOffsets = color; + + repaint(); + } + + /** + * Sets the size of the font that is used to draw all data. + * + * @param size The size of the font that is used to draw all data. + * + * @throws IllegalArgumentException Thrown if the new font size is smaller than 1. + */ + public void setFontSize(final int size) { + + if (size <= 0) { + throw new IllegalArgumentException("Error: Font size must be positive"); + } + + m_font = new Font(GuiHelpers.getMonospaceFont(), 0, size); + setFont(m_font); + + // The proportions of the hex window change significantly. + // Just start over when the next repaint event comes. + m_firstDraw = true; + + repaint(); + } + + /** + * Sets the width of the hex view. + * + * @param width The new width of the offset view. + * + * @throws IllegalArgumentException Thrown if the new width is smaller than 1. + */ + public void setHexViewWidth(final int width) { + + if (width <= 0) { + throw new IllegalArgumentException("Error: Width must be positive"); + } + + m_hexViewWidth = width; + + repaint(); + } + + /** + * Sets the menu creator of the hex view control. + * + * @param creator The new menu creator. If this parameter is null, no context menu is shown in the component. + */ + public void setMenuCreator(final IMenuCreator creator) { + m_menuCreator = creator; + } + + public void setSelectionColor(final Color color) { + + if (color == null) { + throw new NullPointerException("Error: Color can't be null"); + } + + m_selectionColor = color; + + repaint(); + } + + public void setSelectionLength(final long selectionLength) { + + m_selectionLength = selectionLength; + + for (final IHexViewListener listener : m_listeners) { + listener.selectionChanged(m_selectionStart, m_selectionLength); + } + + repaint(); + } + + /** + * Removes special colorization from a range of bytes. + * + * @param offset The start offset of the byte range. + * @param size The number of bytes in the byte range. + * + * @throws IllegalArgumentException Thrown if offset is negative or size is not positive. + */ + public void uncolorize(final int level, final long offset, final int size) { + + if (offset < 0) { + throw new IllegalArgumentException("Error: Offset can't be negative"); + } + + if (size <= 0) { + throw new IllegalArgumentException("Error: Size must be positive"); + } + + if (level < 0 || level >= m_coloredRanges.length) { + throw new IllegalArgumentException("Error: Invalid level"); + } + + m_coloredRanges[level].removeRange(offset, size); + + repaint(); + } + + public void uncolorizeAll() { + + for (final ColoredRangeManager coloredRange : m_coloredRanges) { + coloredRange.clear(); + } + } + + /** + * Removes all special range colorizations. + */ + public void uncolorizeAll(final int level) { + + m_coloredRanges[level].clear(); + + repaint(); + } + + private class DownAction extends AbstractAction { + + private static final long serialVersionUID = -6501310447863685486L; + + public void actionPerformed(final ActionEvent arg0) { + changeBy(arg0, 2 * m_bytesPerRow); + } + } + + /** + * Event listeners are moved into an internal class to avoid publishing + * the listener methods in the public interface of the JHexView. + * + * @author sp + * + */ + private class InternalListener implements AdjustmentListener, MouseListener, MouseMotionListener, FocusListener, ICaretListener, IDataChangedListener, ComponentListener, KeyListener, MouseWheelListener { + + private void keyPressedInAsciiView(final KeyEvent arg0) { + + final byte[] data = m_dataProvider.getData(getCurrentOffset(), 1); + + if (getSelectionStart() >= m_dataProvider.getDataLength() * 2) { + return; + } + + data[0] = (byte) arg0.getKeyChar(); + + m_dataProvider.setData(getCurrentOffset(), data); + + setSelectionStart(getSelectionStart() + 2); + } + + private void keyPressedInHexView(final KeyEvent key) { + + final byte[] data = m_dataProvider.getData(getCurrentOffset(), 1); + + final long pos = m_baseAddress + getSelectionStart(); + + if (getSelectionStart() >= m_dataProvider.getDataLength() * 2) { + return; + } + + final int value = Character.digit(key.getKeyChar(), 16); + + if (value == -1) { + return; + } + + if (pos % 2 == 0) { + data[0] = (byte) (data[0] & 0x0F | value << 4); + } + else { + data[0] = (byte) (data[0] & 0xF0 | value); + } + + m_dataProvider.setData(getCurrentOffset(), data); + + setSelectionStart(getSelectionStart() + 1); + } + + public void adjustmentValueChanged(final AdjustmentEvent arg0) { + + if (arg0.getSource() == m_scrollbar) { + m_firstRow = arg0.getValue(); + } + else { + m_firstColumn = arg0.getValue(); + } + + repaint(); + } + + public void caretStatusChanged(final JCaret source) { + repaint(); + } + + public void componentHidden(final ComponentEvent arg0) { + } + + public void componentMoved(final ComponentEvent arg0) { + } + + public void componentResized(final ComponentEvent arg0) { + + setScrollBarMaximum(); + + int width = getWidth() + 1 - m_scrollbar.getWidth(); + int height = getHeight() + 1 - m_horizontalScrollbar.getHeight(); + + width = Math.max(1, width); + height = Math.max(1, height); + + img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + bufferGraphics = img.getGraphics(); + } + + public void componentShown(final ComponentEvent arg0) { + } + + public void dataChanged() { + + setScrollBarMaximum(); + + repaint(); + } + + public void focusGained(final FocusEvent arg0) { + m_caret.setVisible(true); + repaint(); + } + + public void focusLost(final FocusEvent arg0) { + repaint(); + } + + public void keyPressed(final KeyEvent arg0) { + + if (!isEnabled()) { + return; + } + + if (m_activeView == Views.HEX_VIEW) { + + if (m_dataProvider.isEditable() && ConvertHelpers.isHexCharacter(arg0.getKeyChar())) { + keyPressedInHexView(arg0); + } + } + else { + + if (m_dataProvider.isEditable() && ConvertHelpers.isPrintableCharacter(arg0.getKeyChar())) { + keyPressedInAsciiView(arg0); + } + } + + repaint(); + } + + public void keyReleased(final KeyEvent arg0) { + } + + public void keyTyped(final KeyEvent arg0) { + } + + public void mouseClicked(final MouseEvent arg0) { + } + + public void mouseDragged(final MouseEvent arg0) { + + if (!isEnabled()) { + return; + } + + final int x = arg0.getX(); + final int y = arg0.getY(); + + if (y < m_paddingTop - (m_rowHeight - m_charHeight)) { + scrollToPosition(2 * getFirstVisibleByte() - 2 * m_bytesPerRow); + + if (getSelectionLength() - 2 * m_bytesPerRow < 0) { + return; + } + + setSelectionLength(getSelectionLength() - 2 * m_bytesPerRow); + } + else if (y >= m_rowHeight * getNumberOfVisibleRows()) { + + scrollToPosition(2 * getFirstVisibleByte() + 2 * m_bytesPerRow); + + if (getSelectionLength() + 2 * m_bytesPerRow > 2 * (m_dataProvider.getDataLength() - getSelectionStart())) { + return; + } + + setSelectionLength(getSelectionLength() + 2 * m_bytesPerRow); + } + else { + final int position = getNibbleAtCoordinate(x, y); + + if (position != -1) { + setSelectionLength(position - getSelectionStart()); + repaint(); + } + } + } + + public void mouseEntered(final MouseEvent arg0) { + } + + public void mouseExited(final MouseEvent arg0) { + } + + public void mouseMoved(final MouseEvent arg0) { + + m_lastMouseX = arg0.getX(); + m_lastMouseY = arg0.getY(); + + repaint(); + } + + public void mousePressed(final MouseEvent event) { + + if (!isEnabled()) { + return; + } + + if (event.getButton() == MouseEvent.BUTTON1 || event.getButton() == MouseEvent.BUTTON3) { + + m_selectionLength = 0; // We don't want the notifiers to kick in here. + // setSelectionLength(0); + + requestFocusInWindow(); + + final int x = event.getX(); + final int y = event.getY(); + + final int position = getNibbleAtCoordinate(x, y); + + if (position != -1) { + + m_caret.setVisible(true); + setCurrentPosition(position); + + if (isInsideHexView(x, y)) { + m_activeView = Views.HEX_VIEW; + } + else if (isInsideAsciiView(x, y)) { + m_activeView = Views.ASCII_VIEW; + } + + repaint(); + } + else { + + // m_selectionLength = 0 must be notified in case the click position is invalid. + + for (final IHexViewListener listener : m_listeners) { + listener.selectionChanged(m_selectionStart, m_selectionLength); + } + + repaint(); + } + } + + if (event.getButton() == MouseEvent.BUTTON3) { + + final int x = event.getX(); + final int y = event.getY(); + + final int position = getNibbleAtCoordinate(x, y); + + if (position != -1) { + + m_caret.setVisible(true); + + if (m_menuCreator != null) { + + final JPopupMenu menu = m_menuCreator.createMenu(getCurrentOffset()); + + if (menu != null) { + menu.show(JHexView.this, x, y); + } + } + + repaint(); + } + + } + } + + public void mouseReleased(final MouseEvent arg0) { + } + + public void mouseWheelMoved(final MouseWheelEvent e) { + + // Mouse wheel support for scrolling + + if (!isEnabled()) + { + return; + } + + final int notches = e.getWheelRotation(); + m_scrollbar.setValue(m_scrollbar.getValue() + notches); + } + } + + private class LeftAction extends AbstractAction { + + private static final long serialVersionUID = -9032577023548944503L; + + public void actionPerformed(final ActionEvent arg0) + { + changeBy(arg0, m_activeView == Views.HEX_VIEW ? -1 : -2); + } + } + + private class PageDownAction extends AbstractAction { + + private static final long serialVersionUID = 490837791577654025L; + + public void actionPerformed(final ActionEvent arg0) { + changeBy(arg0, getNumberOfVisibleRows() * m_bytesPerRow * 2); + } + } + + private class PageUpAction extends AbstractAction { + + private static final long serialVersionUID = -7424423002191015929L; + + public void actionPerformed(final ActionEvent arg0) { + changeBy(arg0, -getNumberOfVisibleRows() * m_bytesPerRow * 2); + } + } + + private class RightAction extends AbstractAction { + + private static final long serialVersionUID = 3857972387525998636L; + + public void actionPerformed(final ActionEvent arg0) { + changeBy(arg0, m_activeView == Views.HEX_VIEW ? 1 : 2); + } + } + + private class TabAction extends AbstractAction { + + private static final long serialVersionUID = -3265020583339369531L; + + public void actionPerformed(final ActionEvent arg0) { + + // Switch between hex and ASCII view + + if (m_activeView == Views.HEX_VIEW) { + m_activeView = Views.ASCII_VIEW; + setSelectionStart(getSelectionStart() - getSelectionStart() % 2); + } + else { + m_activeView = Views.HEX_VIEW; + } + + m_caret.setVisible(true); + repaint(); + } + } + + private class UpAction extends AbstractAction { + + private static final long serialVersionUID = -3513103611571283106L; + + public void actionPerformed(final ActionEvent arg0) { + changeBy(arg0, -2 * m_bytesPerRow); + } + } + + /** + * Enumeration that is used to decide which view of + * the component has the focus. + * + */ + private enum Views { + HEX_VIEW, ASCII_VIEW + } + + private class WaitingForDataAction extends AbstractAction { + + private static final long serialVersionUID = -610823391617272365L; + + private final long m_offset; + + private final int m_size; + + private WaitingForDataAction(final long offset, final int size) { + m_offset = offset; + m_size = size; + } + + public void actionPerformed(final ActionEvent arg0) { + + if (m_dataProvider.hasData(m_offset, m_size)) { + + JHexView.this.setEnabled(true); + setDefinitionStatus(DefinitionStatus.DEFINED); + + ((Timer) arg0.getSource()).stop(); + } + else if (!m_dataProvider.keepTrying()) { + ((Timer) arg0.getSource()).stop(); + } + } + + } + + /** + * Enumeration that is used to switch the output format + * of the offsets between 32 bit mode and 64 bit mode. + * + */ + public enum AddressMode { + BIT32, BIT64 + } + + /** + * Enumeration that is used to decided whether + * real data or ??? is shown. + * + */ + public enum DefinitionStatus { + DEFINED, UNDEFINED + } +} diff --git a/bfIDE/src/main/java/tv/porst/jhexview/SimpleDataProvider.java b/bfIDE/src/main/java/tv/porst/jhexview/SimpleDataProvider.java new file mode 100644 index 0000000..9a73e04 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/jhexview/SimpleDataProvider.java @@ -0,0 +1,64 @@ +package tv.porst.jhexview; + +import java.util.Arrays; + +/** + * Data provider that provides data to the hex view component from a + * static array. Use this data provider if you already have all the data + * in memory and do not have to reload memory from an external source. + */ +public final class SimpleDataProvider implements IDataProvider { + + private final byte[] m_data; + + public SimpleDataProvider(final byte[] data) { + this.m_data = data; + } + + @Override + public void addListener(final IDataChangedListener listener) { + } + + @Override + public byte[] getData() { + return getData(0L, getDataLength()); + } + + @Override + public byte[] getData(final long offset, final int length) { + return Arrays.copyOfRange(this.m_data, (int) offset, (int) (offset + length)); + } + + @Override + public int getDataLength() { + return this.m_data.length; + } + + public long getOffset() { + return 0L; + } + + @Override + public boolean hasData(final long offset, final int length) { + return true; + } + + @Override + public boolean isEditable() { + return true; + } + + @Override + public boolean keepTrying() { + return false; + } + + @Override + public void removeListener(final IDataChangedListener listener) { + } + + @Override + public void setData(final long offset, final byte[] data) { + System.arraycopy(data, 0, this.m_data, (int) offset, data.length); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/arrays/ArrayHelpers.java b/bfIDE/src/main/java/tv/porst/splib/arrays/ArrayHelpers.java new file mode 100644 index 0000000..421cc47 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/arrays/ArrayHelpers.java @@ -0,0 +1,83 @@ +package tv.porst.splib.arrays; + +/** + * Contains helper functions for working with arrays. + */ +public final class ArrayHelpers { + + /** + * Returns a sub array of a given array. + * + * @param source The source array from which the sub array is extracted. + * @param offset The start offset of the sub array. + * @param length The length of the sub array in bytes. + * + * @return The sub array. + */ + public static byte[] getSubArray(final byte[] source, final int offset, final int length) { + + if (source == null) { + throw new IllegalArgumentException("Source argument must not be null."); + } + + if (offset < 0) { + throw new IllegalArgumentException("Start offset must not be negative."); + } + + if (offset >= source.length) { + throw new IllegalArgumentException("Start offset is too big."); + } + + if (length < 0) { + throw new IllegalArgumentException("Length must not be negative."); + } + + if (offset + length > source.length) { + throw new IllegalArgumentException("Range is too big."); + } + + final byte[] subArray = new byte[length]; + + System.arraycopy(source, offset, subArray, 0, length); + + return subArray; + } + + /** + * Removes data from a byte array. + * + * @param data The input byte array. + * @param startOffset The start offset of the data to remove. + * @param length Length in bytes of the data to remove. + * + * @return The new array without the removed data. + */ + public static byte[] removeData(final byte[] data, final int startOffset, final int length) { + final byte[] newData = new byte[data.length - length]; + + System.arraycopy(data, 0, newData, 0, startOffset); + System.arraycopy(data, startOffset + length, newData, startOffset, data.length - startOffset - length); + + return newData; + } + + /** + * Replaces data in a byte array. + * + * @param data The input byte array. + * @param startOffset The start offset of the data to change. + * @param length The length in bytes of the data to change. + * @param replacementValue The new value for each byte in the replacement range. + * + * @return The new array with the replaced data. + */ + public static byte[] replaceData(final byte[] data, final int startOffset, final int length, final byte replacementValue) { + final byte[] newData = data.clone(); + + for (int i=startOffset;i implements IFileElement { + + /** + * Flag that says whether the parsed string was zero-terminated or not. + */ + private final boolean zeroTerminated; + + /** + * Creates a new AsciiString object. + * + * @param bytePosition Byte position of the string in the input stream. + * @param value String value. + * @param zeroTerminated Flag that says whether the parsed string was zero-terminated or not. + */ + public AsciiString(final int bytePosition, final String value, final boolean zeroTerminated) { + + super(bytePosition, (value.length() + (zeroTerminated ? 1 : 0)) * 8, value); + + this.zeroTerminated = zeroTerminated; + } + + /** + * Returns the flag that says whether the parsed string was zero-terminated or not. + * + * @return The flag that says whether the parsed string was zero-terminated or not. + */ + public boolean isZeroTerminated() { + return zeroTerminated; + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParser.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParser.java new file mode 100644 index 0000000..cc68fb0 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParser.java @@ -0,0 +1,531 @@ +package tv.porst.splib.binaryparser; + + +/** + * Class that can be used to parse simple data structures from + * byte streams. + */ +public class BinaryParser { + + /** + * The data to parse. + */ + private final byte[] data; + + /** + * The current byte position. + */ + private int bytePosition = 0; + + /** + * The current bit position. + */ + private int bitPosition = 0; + + /** + * Creates a new binary parser object that parses data from + * the given byte array. + * + * @param data The source array. + */ + public BinaryParser(final byte[] data) { + + if (data == null) { + throw new IllegalArgumentException("Data argument must not be null"); + } + + this.data = data.clone(); + } + + /** + * Aligns the parser to the next byte. + */ + public void align() { + if (bitPosition != 0) { + bytePosition++; + bitPosition = 0; + } + } + + /** + * Returns the current bit position. + * + * @return The current bit position. + */ + public int getBitPosition() { + return bitPosition; + } + + /** + * Returns the current byte position. + * + * @return The current byte position. + */ + public int getBytePosition() { + return bytePosition; + } + + /** + * Returns the length of the byte stream. + * + * @return The length of the byte stream. + */ + public int getLength() { + return data.length; + } + + /** + * Returns whether parsing the whole byte stream is complete. + * + * @return True, if parsing is complete. False, if there is data left to parse. + */ + public boolean isDone() { + return bytePosition == data.length; + } + + /** + * Peeks at the next few bits without moving the current parsing + * position forward. + * + * @param numberOfBits The number of bits to peek at. This value must be between 0 and 32. + * + * @return The peeked bits. + */ + public UBits peekBits(final int numberOfBits) { + + if (numberOfBits < 0 || numberOfBits > 32) { + throw new IllegalArgumentException("Number of bits argument must be between 0 and 32"); + } + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final UBits value = readBits(numberOfBits); + + this.bitPosition = bitPosition; + this.bytePosition = bytePosition; + + return value; + } + + /** + * Peeks at the next UINT16 value without moving the current parsing + * position forward. + * + * @return The peeked UINT16 value. + */ + public UINT16 peekUInt16() { + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final UINT16 value = readUInt16(); + + this.bitPosition = bitPosition; + this.bytePosition = bytePosition; + + return value; + } + + /** + * Peeks at the next UINT32 value without moving the current parsing + * position forward. + * + * @return The peeked UINT32 value. + */ + public UINT32 peekUInt32() { + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final UINT32 value = readUInt32(); + + this.bitPosition = bitPosition; + this.bytePosition = bytePosition; + + return value; + } + + /** + * Peeks at the next UINT8 value without moving the current parsing + * position forward. + * + * @return The peeked UINT8 value. + */ + public UINT8 peekUInt8() { + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final UINT8 value = readUInt8(); + + this.bitPosition = bitPosition; + this.bytePosition = bytePosition; + + return value; + } + + /** + * Reads the next few bits from the byte stream. + * + * @param numberOfBits The number of bits to read. This value must be between 0 and 32. + * + * @return The read bits. + */ + public UBits readBits(final int numberOfBits) { + + if (bytePosition * 8 + bitPosition + numberOfBits > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + final int oldBytePosition = bytePosition; + final int oldBitPosition = bitPosition; + + int value = 0; + + for (int i=0;i> (7 - bitPosition)) & 1); + + bitPosition++; + + if (bitPosition == 8) { + bitPosition = 0; + bytePosition++; + } + } + + return new UBits(8 * oldBytePosition + oldBitPosition, numberOfBits, value); + } + + /** + * Reads the next byte from the byte stream. + * + * @return The read byte. + */ + public byte readByte() { + + if (bytePosition * 8 + bitPosition + 8 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + return data[bytePosition++]; + } + + /** + * Reads the next bit from the byte stream. + * + * @return The read bit. + */ + public Flag readFlag() { + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final UBits value = readBits(1); + + return new Flag(8 * bytePosition + bitPosition, value.value() == 1); + } + + /** + * Reads the next float from the byte stream. + * + * @return The next float. + */ + public Float32 readFloat() { + + final float value = Float.intBitsToFloat(readInt32().value()); + + return new Float32(8 * bytePosition + bitPosition - 32, value); + } + + /** + * Reads the next short float from the byte stream. + * + * @return The next short float. + */ + public Float16 readFloat16() { + readInt16(); + + return new Float16(8 * bytePosition + bitPosition - 16, (float) 0.0); + } + + /** + * Reads the next float from the byte stream. + * + * @return The next float. + */ + public Float64 readFloat64() { + + final double value = Double.longBitsToDouble(readInt64().value()); + + return new Float64(8 * bytePosition + bitPosition - 64, value); + } + + /** + * Reads the next short integer from the byte stream. + * + * @return The next short integer. + */ + public INT16 readInt16() { + + if (bytePosition * 8 + bitPosition + 16 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + + return new INT16(8 * bytePosition + bitPosition - 16, secondByte << 8 | firstByte); + } + + /** + * Reads the next three-byte integer from the byte stream. + * + * @return The next three-byte integer. + */ + public INT24 readInt24() { + + if (bytePosition * 8 + bitPosition + 24 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + final int thirdByte = readByte() & 0xFF; + + int value = thirdByte << 16 | secondByte << 8 | firstByte; + + if (value > 0x7FFFFF) { + value = value - 0x1000000; + } + + return new INT24(8 * bytePosition + bitPosition - 24, value); + } + + /** + * Reads the next integer from the byte stream. + * + * @return The next integer. + */ + public INT32 readInt32() { + + if (bytePosition * 8 + bitPosition + 32 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + final int thirdByte = readByte() & 0xFF; + final int fourthByte = readByte() & 0xFF; + + return new INT32(8 * bytePosition + bitPosition - 32, fourthByte << 24 | thirdByte << 16 | secondByte << 8 | firstByte); + } + + /** + * Reads the next long from the byte stream. + * + * @return The next integer. + */ + public INT64 readInt64() { + + if (bytePosition * 8 + bitPosition + 64 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + final int thirdByte = readByte() & 0xFF; + final int fourthByte = readByte() & 0xFF; + final int fifthByte = readByte() & 0xFF; + final int sixthByte = readByte() & 0xFF; + final int seventhByte = readByte() & 0xFF; + final int eigthByte = readByte() & 0xFF; + + return new INT64(8 * bytePosition + bitPosition - 64, eigthByte << 56 | seventhByte << 48 | sixthByte << 40 | fifthByte << 32 | fourthByte << 24 | thirdByte << 16 | secondByte << 8 | firstByte); + } + + /** + * Reads the next few signed bits from the byte stream. + * + * @param numberOfBits The number of signed bits to read. This value must be between 0 and 32. + * + * @return The read signed bits. + */ + public Bits readSBits(final int numberOfBits) { + + if (bytePosition * 8 + bitPosition + numberOfBits > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + final int oldBytePosition = bytePosition; + final int oldBitPosition = bitPosition; + + int value = 0; + + for (int i=0;i> (7 - bitPosition)) & 1); + + bitPosition++; + + if (bitPosition == 8) { + bitPosition = 0; + bytePosition++; + } + } + + return new Bits(8 * oldBytePosition + oldBitPosition, numberOfBits, value); + } + + /** + * Reads the next 0-terminated ASCII string from the byte stream. + * + * @return The read string. + */ + public AsciiString readString() { + + final int bytePosition = this.bytePosition; + final int bitPosition = this.bitPosition; + + final StringBuffer value = new StringBuffer(); + + byte b; + + while ((b = readByte()) != 0) + { + value.append((char) b); + } + + return new AsciiString(8 * bytePosition + bitPosition, value.toString(), true); + } + + /** + * Reads the next number of bytes from the byte stream into an ASCII string. + * + * @param numberOfBytes The number of bytes to add to the string. + * + * @return The read string. + */ + public AsciiString readString(final int numberOfBytes) { + + if (numberOfBytes < 0) { + throw new IllegalArgumentException("The number of bytes in the string can not be negative"); + } + + final StringBuffer value = new StringBuffer(); + + for (int i=0;i data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + + return new UINT16(8 * bytePosition + bitPosition - 16, secondByte << 8 | firstByte); + } + + /** + * Reads the next unsigned integer from the byte stream. + * + * @return The next unsigned integer. + */ + public UINT32 readUInt32() { + + if (bytePosition * 8 + bitPosition + 32 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + final int secondByte = readByte() & 0xFF; + final int thirdByte = readByte() & 0xFF; + final int fourthByte = readByte() & 0xFF; + + return new UINT32(8 * bytePosition + bitPosition - 32, fourthByte << 24 | thirdByte << 16 | secondByte << 8 | firstByte); + } + + /** + * Reads the next unsigned byte from the byte stream. + * + * @return The next unsigned byte. + */ + public UINT8 readUInt8() { + + if (bytePosition * 8 + bitPosition + 32 > data.length * 8) { + throw new IllegalArgumentException("Not enough data left"); + } + + if (bitPosition != 0) { + throw new IllegalStateException("Parser is not byte aligned"); + } + + final int firstByte = readByte() & 0xFF; + + return new UINT8(8 * bytePosition + bitPosition - 8, firstByte); + } + + /** + * Sets the current read position in the byte stream. + * + * @param bytePosition The new byte position. + * @param bitPosition The new bit position. + */ + public void setPosition(final int bytePosition, final int bitPosition) { + + if (bytePosition < 0) { + throw new IllegalArgumentException("Byte position argument must not be negative"); + } + + if (bitPosition < 0) { + throw new IllegalArgumentException("Bit position argument must not be negative"); + } + + if (bytePosition * 8 + bitPosition > data.length * 8) { + throw new IllegalArgumentException("Can not move read position beyond the end of the input buffer"); + } + + this.bytePosition = bytePosition; + this.bitPosition = bitPosition; + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserException.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserException.java new file mode 100644 index 0000000..8a639e2 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserException.java @@ -0,0 +1,16 @@ +package tv.porst.splib.binaryparser; + +/** + * Exception class used to signal exceptions during binary stream parsing. + */ +public final class BinaryParserException extends RuntimeException { + + /** + * Creates a new exception object. + * + * @param message The exception message. + */ + public BinaryParserException(final String message) { + super(message); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserHelpers.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserHelpers.java new file mode 100644 index 0000000..94dd390 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/BinaryParserHelpers.java @@ -0,0 +1,82 @@ +package tv.porst.splib.binaryparser; + +/** + * Contains helper functions for working with the binary parser. + */ +public final class BinaryParserHelpers { + + /** + * Checks whether a certain number of bits can be read from the input stream before + * the end of the stream is reached. + * + * @param parser The parser whose stream is checked. + * @param numberOfBits The number of bits to check for. + * + * @return True, if there are the requested number of bits left in the input stream. False, otherwise. + */ + public static boolean hasBitsLeft(final BinaryParser parser, final long numberOfBits) { + + if (parser == null) { + throw new IllegalArgumentException("Parser argument must not be null"); + } + + if (numberOfBits < 0) { + throw new IllegalArgumentException("Number of requested bits can not be negative"); + } + + return parser.getBytePosition() * 8 + parser.getBitPosition() + numberOfBits <= parser.getLength() * 8; + } + + /** + * Checks whether a certain number of bytes can be read from the input stream before + * the end of the stream is reached. + * + * @param parser The parser whose stream is checked. + * @param numberOfBytes The number of bytes to check for. + * + * @return True, if there are the requested number of bytes left in the input stream. False, otherwise. + */ + public static boolean hasBytesLeft(final BinaryParser parser, final long numberOfBytes) { + + if (parser == null) { + throw new IllegalArgumentException("Parser argument must not be null"); + } + + if (numberOfBytes < 0) { + throw new IllegalArgumentException("Number of requested bytes can not be negative"); + } + + return hasBitsLeft(parser, numberOfBytes * 8); + } + + /** + * Reads a byte array from the input stream of a parser. + * + * @param parser The parser from which the byte array is read. + * @param length The length of the byte array to read. + * + * @return The byte array. + */ + public static byte[] readByteArray(final BinaryParser parser, final int length) { + + if (parser == null) { + throw new IllegalArgumentException("Parser argument must not be null"); + } + + if (length < 0) { + throw new IllegalArgumentException("The length of the byte array must not be negative"); + } + + if (!hasBytesLeft(parser, length)) { + throw new IllegalArgumentException("Not enough bytes left in the input stream"); + } + + final byte[] array = new byte[length]; + + for (int i=0;i { + + /** + * Creates a new bit field object. + * + * @param bitPosition Bit position of the bit field in the input stream. + * @param bitLength Length of the field in bits. + * @param value Bit field value. + */ + public Bits(final int bitPosition, final int bitLength, final Integer value) { + super(bitPosition, bitLength, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/Flag.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Flag.java new file mode 100644 index 0000000..41bec19 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Flag.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed flag. + */ +public final class Flag extends ParsedType { + + /** + * Creates a new flag object. + * + * @param bitPosition Bit position of the flag in the input stream. + * @param value Floag value. + */ + public Flag(final int bitPosition, final boolean value) { + super(bitPosition, 1, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float16.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float16.java new file mode 100644 index 0000000..1fd8916 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float16.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed short float. + */ +public class Float16 extends ParsedType { + + /** + * Creates a new short float object. + * + * @param bytePosition Byte position of the short float in the input stream. + * @param value Float value. + */ + public Float16(final int bytePosition, final Float value) { + super(bytePosition, 16, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float32.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float32.java new file mode 100644 index 0000000..4e3df51 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float32.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed float. + */ +public final class Float32 extends ParsedType { + + /** + * Creates a new float object. + * + * @param bytePosition Byte position of the float in the input stream. + * @param value Float value. + */ + public Float32(final int bytePosition, final Float value) { + super(bytePosition, 32, value); + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float64.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float64.java new file mode 100644 index 0000000..612f8fb --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/Float64.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed float. + */ +public final class Float64 extends ParsedType { + + /** + * Creates a new float object. + * + * @param bytePosition Byte position of the float in the input stream. + * @param value Float value. + */ + public Float64(final int bytePosition, final Double value) { + super(bytePosition, 64, value); + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/IFileElement.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/IFileElement.java new file mode 100644 index 0000000..3f35311 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/IFileElement.java @@ -0,0 +1,21 @@ +package tv.porst.splib.binaryparser; + +/** + * Interface to be implemented by all parsed types. + */ +public interface IFileElement { + + /** + * Returns the length of the element in bits. + * + * @return The length of the element in bits. + */ + int getBitLength(); + + /** + * Returns the position of the element in bits. + * + * @return The position of the element in bits. + */ + int getBitPosition(); +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT16.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT16.java new file mode 100644 index 0000000..4e14417 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT16.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed short int. + */ +public final class INT16 extends ParsedType { + + /** + * Creates a new short int object. + * + * @param bytePosition Byte position of the short int in the input stream. + * @param value Integer value. + */ + public INT16(final int bytePosition, final int value) { + super(bytePosition, 16, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT24.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT24.java new file mode 100644 index 0000000..3320a41 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT24.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed 3-byte int. + */ +public final class INT24 extends ParsedType { + + /** + * Creates a new 3-byte int object. + * + * @param bytePosition Byte position of the 3-byte int in the input stream. + * @param value Integer value. + */ + public INT24(final int bytePosition, final int value) { + super(bytePosition, 24, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT32.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT32.java new file mode 100644 index 0000000..52919dc --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT32.java @@ -0,0 +1,22 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed int. + */ +public final class INT32 extends ParsedType implements IParsedINTElement { + + /** + * Length of the type in bytes. + */ + public static final int BYTE_LENGTH = 4; + + /** + * Creates a new short int object. + * + * @param bytePosition Byte position of the int in the input stream. + * @param value Integer value. + */ + public INT32(final int bytePosition, final int value) { + super(bytePosition, 32, value); + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT64.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT64.java new file mode 100644 index 0000000..efba9ea --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/INT64.java @@ -0,0 +1,17 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed long. + */ +public final class INT64 extends ParsedType implements IParsedINTElement { + + /** + * Creates a new long object. + * + * @param bytePosition Byte position of the long in the input stream. + * @param value Long value. + */ + public INT64(final int bytePosition, final long value) { + super(bytePosition, 64, value); + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/IParsedINTElement.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/IParsedINTElement.java new file mode 100644 index 0000000..cc8d0d8 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/IParsedINTElement.java @@ -0,0 +1,8 @@ +package tv.porst.splib.binaryparser; + +/** + * Interface to be implemented by parsed integer values. + */ +public interface IParsedINTElement extends IFileElement { + +} diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/ParsedType.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/ParsedType.java new file mode 100644 index 0000000..75494da --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/ParsedType.java @@ -0,0 +1,70 @@ +package tv.porst.splib.binaryparser; + +/** + * Base class for all parsed types. + * + * @param Type of the parsed value. + */ +public abstract class ParsedType implements IFileElement { + + /** + * Bit position of the parsed value in the input stream. + */ + private final int bitPosition; + + /** + * Parsed value. + */ + private final ValueType value; + + /** + * Length of the field in bits. + */ + private final int bitLength; + + /** + * Creates a new parsed type object. + * + * @param bitPosition Bit position of the parsed value in the input stream. + * @param bitLength Length of the field in bits. + * @param value Parsed value. + */ + public ParsedType(final int bitPosition, final int bitLength, final ValueType value) { + + if (bitPosition < 0) { + throw new IllegalArgumentException("Byte position must not be negative"); + } + + if (value == null) { + throw new IllegalArgumentException("Value argument must not be null"); + } + + this.bitPosition = bitPosition; + this.bitLength = bitLength; + this.value = value; + } + + @Override + public int getBitLength() { + return bitLength; + } + + /** + * Returns the byte position of the parsed value in the input stream. + * + * @return The byte position of the parsed value in the input stream. + */ + @Override + public int getBitPosition() { + return bitPosition; + } + + /** + * Returns the parsed value. + * + * @return The parsed value. + */ + public ValueType value() { + return value; + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/UBits.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UBits.java new file mode 100644 index 0000000..d54970c --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UBits.java @@ -0,0 +1,18 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed unsigned bit field. + */ +public final class UBits extends ParsedType { + + /** + * Creates a new unsigned bit field object. + * + * @param bitPosition Bit position of the unsigned bit field in the input stream. + * @param bitLength Length of the field in bits. + * @param value Unsigned bit field value. + */ + public UBits(final int bitPosition, final int bitLength, final Integer value) { + super(bitPosition, bitLength, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT16.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT16.java new file mode 100644 index 0000000..5daa35c --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT16.java @@ -0,0 +1,22 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed unsigned short int. + */ +public final class UINT16 extends ParsedType implements IParsedINTElement { + + /** + * Length of the type in bytes. + */ + public static final int BYTE_LENGTH = 2; + + /** + * Creates a new unsigned short int object. + * + * @param bytePosition Byte position of the unsigned short int in the input stream. + * @param value Integer value. + */ + public UINT16(final int bytePosition, final int value) { + super(bytePosition, 16, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT32.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT32.java new file mode 100644 index 0000000..ecdbb02 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT32.java @@ -0,0 +1,22 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed unsigned int. + */ +public class UINT32 extends ParsedType implements IParsedINTElement { + + /** + * Length of the type in bytes. + */ + public static final int BYTE_LENGTH = 4; + + /** + * Creates a new unsigned int object. + * + * @param bytePosition Byte position of the unsigned int in the input stream. + * @param value Integer value. + */ + public UINT32(final int bytePosition, final long value) { + super(bytePosition, 32, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT8.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT8.java new file mode 100644 index 0000000..ff38987 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/UINT8.java @@ -0,0 +1,22 @@ +package tv.porst.splib.binaryparser; + +/** + * Represents a parsed unsigned byte. + */ +public final class UINT8 extends ParsedType implements IParsedINTElement { + + /** + * Length of the type in bytes. + */ + public static final int BYTE_LENGTH = 1; + + /** + * Creates a new unsigned byte object. + * + * @param bytePosition Byte position of the unsigned byte in the input stream. + * @param value Integer value. + */ + public UINT8(final int bytePosition, final int value) { + super(bytePosition, 8, value); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/binaryparser/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/binaryparser/_Doc.java new file mode 100644 index 0000000..cdc7cf7 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/binaryparser/_Doc.java @@ -0,0 +1,7 @@ +package tv.porst.splib.binaryparser; + +/** + * This package contains classes for parsing binary data. The main class + * of this package is {@link BinaryParser}. Using this class you can parse + * binary data into the types defined in the other classes. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/convert/ConvertHelpers.java b/bfIDE/src/main/java/tv/porst/splib/convert/ConvertHelpers.java new file mode 100644 index 0000000..d1fee6c --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/convert/ConvertHelpers.java @@ -0,0 +1,51 @@ +package tv.porst.splib.convert; + +import java.awt.event.KeyEvent; + +/** + * Converts helper functions for working with different data types. + */ +public final class ConvertHelpers { + + /** + * Tests whether a given character is a valid decimal character. + * + * @param c The character to test. + * + * @return True, if the given character is a valid decimal character. + */ + public static boolean isDecCharacter(final char c) + { + return c >= '0' && c <= '9'; + } + + /** + * Tests whether a character is a valid character of a hexadecimal string. + * + * @param c The character to test. + * + * @return True, if the character is a hex character. False, otherwise. + */ + public static boolean isHexCharacter(final char c) + { + return isDecCharacter(c) || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; + } + + /** + * Tests whether a character is a printable ASCII character. + * + * @param c The character to test. + * + * @return True, if the character is a printable ASCII character. False, + * otherwise. + */ + public static boolean isPrintableCharacter(final char c) + { + final Character.UnicodeBlock block = Character.UnicodeBlock.of(c); + + return !Character.isISOControl(c) && + c != KeyEvent.CHAR_UNDEFINED && + block != null && + block != Character.UnicodeBlock.SPECIALS; + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/convert/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/convert/_Doc.java new file mode 100644 index 0000000..1333f6f --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/convert/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.convert; + +/** + * This package contains classes for converting between types. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/file/DirectoryTraverser.java b/bfIDE/src/main/java/tv/porst/splib/file/DirectoryTraverser.java new file mode 100644 index 0000000..f8263d5 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/file/DirectoryTraverser.java @@ -0,0 +1,33 @@ +package tv.porst.splib.file; + +import java.io.File; + +/** + * Class for recursive directory traversal. + */ +public final class DirectoryTraverser { + + /** + * Traverses a directory. + * + * @param directory The directory to traverse. + * @param callback The callback object to invoked for each encountered file. + */ + public static void traverse(final File directory, final IDirectoryTraversalVisitor callback) { + + if (directory.isDirectory()) { + for (final File file : directory.listFiles()) { + if (!file.isDirectory()) { + callback.visit(file); + } + } + + for (final File file : directory.listFiles()) { + if (file.isDirectory()) { + traverse(file, callback); + } + } + } + + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/file/FileHelpers.java b/bfIDE/src/main/java/tv/porst/splib/file/FileHelpers.java new file mode 100644 index 0000000..b62f39b --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/file/FileHelpers.java @@ -0,0 +1,111 @@ +package tv.porst.splib.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Contains helper functions for working with files. + */ +public final class FileHelpers { + + /** + * Extracts the filename from a full path. + * + * @param fullPath The full input path. + * + * @return The raw filename. + */ + public static String extractFilename(final String fullPath) { + + final int index = fullPath.lastIndexOf(File.separator); + + return index == -1 ? fullPath : fullPath.substring(index + 1); + } + + /** + * Reads a whole file into a byte array. + * + * @param file The input file. + * + * @return The bytes read from the file. + * + * @throws IOException Thrown if the file could not be read. + */ + public static byte[] readFile(final File file) throws IOException { + + if (file == null) { + throw new IllegalArgumentException("File argument must not be null"); + } + + final FileInputStream inputStream = new FileInputStream(file); + + final byte[] data = new byte[(int) file.length()]; + inputStream.read(data); + + return data; + } + + /** + * Reads a whole file into a byte array. + * + * @param file The input file. + * + * @return The bytes read from the file. + * + * @throws IOException Thrown if the file could not be read. + */ + public static byte[] readFile(final String file) throws IOException { + + if (file == null) { + throw new IllegalArgumentException("File argument must not be null"); + } + + return readFile(new File(file)); + } + + /** + * Writes binary data to a file. + * + * @param file The file to write to. + * @param data The data to write. + * + * @throws IOException Thrown if writing the data to the byte failed. + */ + public static void writeFile(final File file, final byte[] data) throws IOException { + + if (file == null) { + throw new IllegalArgumentException("File argument must not be null"); + } + + final FileOutputStream inputStream = new FileOutputStream(file); + + inputStream.write(data); + + inputStream.close(); + } + + /** + * Writes binary data to a file. + * + * @param fileName The name of the file to write to. + * @param data The data to write. + * + * @throws IOException Thrown if writing the data to the byte failed. + */ + public static void writeFile(final String fileName, final byte[] data) throws IOException { + + if (fileName == null) { + throw new IllegalArgumentException("File name argument must not be null"); + } + + final File file = new File(fileName); + + final FileOutputStream inputStream = new FileOutputStream(file); + + inputStream.write(data); + + inputStream.close(); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/file/IDirectoryTraversalVisitor.java b/bfIDE/src/main/java/tv/porst/splib/file/IDirectoryTraversalVisitor.java new file mode 100644 index 0000000..cf8a030 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/file/IDirectoryTraversalVisitor.java @@ -0,0 +1,16 @@ +package tv.porst.splib.file; + +import java.io.File; + +/** + * Interface to be implemented by all callback objects for directory traversal. + */ +public interface IDirectoryTraversalVisitor { + + /** + * Invoked when a new file is found during traversal. + * + * @param file The next file. + */ + void visit(File file); +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/file/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/file/_Doc.java new file mode 100644 index 0000000..dc20aa3 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/file/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.file; + +/** + * This package contains classes for working with files. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/general/ListenerProvider.java b/bfIDE/src/main/java/tv/porst/splib/general/ListenerProvider.java new file mode 100644 index 0000000..0406775 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/general/ListenerProvider.java @@ -0,0 +1,41 @@ +package tv.porst.splib.general; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Class that provides a generic interface for working with listeners. + * + * @param Type of the listeners managed by the class. + */ +public final class ListenerProvider implements Iterable { + + /** + * Currently added listeners. + */ + private final List listeners = new ArrayList(); + + /** + * Adds a new listener to the provider. + * + * @param listener The new listener object to add. + */ + public void add(final ListenerType listener) { + listeners.add(listener); + } + + @Override + public Iterator iterator() { + return listeners.iterator(); + } + + /** + * Removes a listener from the provider. + * + * @param listener The listener to remove. + */ + public void remove(final ListenerType listener) { + listeners.remove(listener); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/general/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/general/_Doc.java new file mode 100644 index 0000000..9c8e342 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/general/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.general; + +/** + * This package contains classes that do not fit into any of the other packages. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/GuiHelpers.java b/bfIDE/src/main/java/tv/porst/splib/gui/GuiHelpers.java new file mode 100644 index 0000000..2bc6df3 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/GuiHelpers.java @@ -0,0 +1,27 @@ +package tv.porst.splib.gui; + +import java.awt.Font; +import java.awt.GraphicsEnvironment; + +/** + * Helper class that contains GUI functions. + */ +public final class GuiHelpers { + + /** + * Returns the system monospace font. + * + * @return The name of the system monospace font. + */ + public static String getMonospaceFont() { + + final GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + final Font[] fonts = localGraphicsEnvironment.getAllFonts(); + for (final Font font : fonts) { + if (font.getName().equals("Courier New")) { + return "Courier New"; + } + } + return "Monospaced"; + } +} diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/gui/_Doc.java new file mode 100644 index 0000000..652fde7 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.gui; + +/** + * This package contains classes for working with GUI components. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/caret/ICaretListener.java b/bfIDE/src/main/java/tv/porst/splib/gui/caret/ICaretListener.java new file mode 100644 index 0000000..acffcfc --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/caret/ICaretListener.java @@ -0,0 +1,15 @@ +package tv.porst.splib.gui.caret; + +/** + * Interface to be implemented by classes that want to be notified about + * changed in the caret. + */ +public interface ICaretListener +{ + /** + * Invoked after the caret status changed. + * + * @param caret The caret whose status changed. + */ + public void caretStatusChanged(JCaret caret); +} diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/caret/JCaret.java b/bfIDE/src/main/java/tv/porst/splib/gui/caret/JCaret.java new file mode 100644 index 0000000..644c829 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/caret/JCaret.java @@ -0,0 +1,245 @@ +package tv.porst.splib.gui.caret; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Timer; + +/** + * Caret class that can be used to draw carets in custom GUI components. + */ +public class JCaret +{ + /** + * The default blink time of the caret in milliseconds. + */ + private static final int DEFAULT_BLINK_TIME = 500; + + /** + * The default color of the caret. + */ + private static final Color DEFAULT_CARET_COLOR = Color.RED; + + /** + * List of listeners that are notified when the caret status changes. + */ + private final List m_listeners = new ArrayList(); + + /** + * Timer that is used to make the caret blink. + */ + private final Timer m_caretTimer; + + /** + * Flag that determines whether the caret is visible or not. + */ + private boolean m_isCaretVisible = false; + + /** + * The color of the caret. + */ + private final Color m_caretColor = Color.RED; + + /** + * Listeners that are notified about changes in the caret. + */ + private final InternalListener m_listener = new InternalListener(); + + /** + * Creates a new caret with a default blink period of 500ms and + * the default caret color red. + */ + public JCaret() + { + this(DEFAULT_BLINK_TIME, DEFAULT_CARET_COLOR); + } + + /** + * Creates a new caret with the default blink period and a custom caret color. + * + * @param caretColor The color of the caret. + * + * @throws NullPointerException Thrown if the color is null. + */ + public JCaret(final Color caretColor) + { + this(DEFAULT_BLINK_TIME, caretColor); + } + + /** + * Creates a new caret with a custom blink period and the default caret color red. + * + * @param blinkPeriod The blink period in milliseconds. + * + * @throws IllegalArgumentException Thrown if the blink period is negative. + */ + public JCaret(final int blinkPeriod) + { + this(blinkPeriod, DEFAULT_CARET_COLOR); + } + + /** + * Creates a new caret with a custom blink period and a custom + * caret color. + * + * @param blinkPeriod The blink period in milliseconds. + * @param caretColor The color of the caret. + + * @throws IllegalArgumentException Thrown if the blink period is negative. + * @throws NullPointerException Thrown if the color is null. + */ + public JCaret(final int blinkPeriod, final Color caretColor) + { + + if (blinkPeriod < 0) + { + throw new IllegalArgumentException("Error: Blink period can't be negative"); + } + + if (caretColor == null) + { + throw new NullPointerException("Error: Caret color can't be null"); + } + + // Initialize the timer that makes the caret blink + m_caretTimer = new Timer(blinkPeriod, m_listener); + m_caretTimer.setRepeats(true); + m_caretTimer.start(); + } + + /** + * Notifies all listeners of a status change of the caret. + */ + private void notifyListeners() + { + for (final ICaretListener listener : m_listeners) + { + listener.caretStatusChanged(JCaret.this); + } + } + + /** + * Adds a new status change listener to the list of listeners. + * + * @param listener The new listener. + * + * @throws NullPointerException Thrown if the passed listener is null. + */ + public void addCaretListener(final ICaretListener listener) + { + + if (listener == null) + { + throw new NullPointerException("Error: Listener can't be null"); + } + + // No duplicate listeners + if (!m_listeners.contains(listener)) + { + m_listeners.add(listener); + } + } + + /** + * Draws the caret on a given graphics surface. + * + * @param g The graphics surface where the caret is drawn. + * @param x The x coordinate of the caret. + * @param y The y coordinate of the caret. + * @param height The height of the caret. + * + * @throws NullPointerException Thrown if the graphics context is null. + */ + public void draw(final Graphics g, final int x, final int y, final int height) + { + + if (g == null) + { + throw new NullPointerException("Error: Graphics context can't be null"); + } + + if (isVisible()) + { + + // Save the old color. + final Color oldColor = g.getColor(); + + // Draw the caret + g.setColor(m_caretColor); + g.drawLine(x, y, x, y + height - 1); + + // Restore the old color. + g.setColor(oldColor); + } + } + + /** + * Determines whether the caret is currently visible nor not. + * + * @return True, if the caret is visible. False, otherwise. + */ + public boolean isVisible() + { + return m_isCaretVisible; + } + + /** + * Removes a registered listeners. + * + * @param listener The listener to remove. + */ + public void removeListener(final ICaretListener listener) + { + m_listeners.remove(listener); + } + + /** + * Sets the visibility status of the caret. + * + * @param isCaretVisible The new visibility status of the caret. + */ + public void setVisible(final boolean isCaretVisible) + { + this.m_isCaretVisible = isCaretVisible; + + notifyListeners(); + } + + /** + * Stops the caret from blinking. + */ + public void stop() + { + m_caretTimer.stop(); + m_caretTimer.removeActionListener(m_listener); + + setVisible(false); + } + + /** + * Class of internal listeners that are used to hide + * the callback functions from the public interface. + */ + private class InternalListener implements ActionListener + { + + /** + * This function is called every time the caret timer + * ticks. + */ + @Override + public void actionPerformed(final ActionEvent arg0) + { + + // Switch the caret status at every timer click. + m_isCaretVisible = !m_isCaretVisible; + + notifyListeners(); + } + } + +} diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/caret/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/gui/caret/_Doc.java new file mode 100644 index 0000000..aaed7c1 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/caret/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.gui.caret; + +/** + * This package contains classes that simulate a caret in custom components. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconNode.java b/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconNode.java new file mode 100644 index 0000000..2d8223f --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconNode.java @@ -0,0 +1,76 @@ +package tv.porst.splib.gui.tree; + +import javax.swing.Icon; +import javax.swing.tree.DefaultMutableTreeNode; + +/** + * Represents a node in an icon tree. + */ +public class IconNode extends DefaultMutableTreeNode { + + /** + * The icon shown for the node. + */ + protected Icon icon; + + /** + * Name of the icon. + */ + protected String iconName; + + /** + * Creates a new icon node. + */ + public IconNode() { + this(null); + } + + /** + * Creates a new icon node. + * + * @param userObject The user object associated with the node. + */ + public IconNode(final Object userObject) { + this(userObject, true, null); + } + + /** + * Creates a new icon node. + * + * @param userObject The user object associated with the node. + * @param allowsChildren Flag that says whether the node can have children or not. + * @param icon The icon shown in the node. + */ + public IconNode(final Object userObject, final boolean allowsChildren, final Icon icon) { + super(userObject, allowsChildren); + this.icon = icon; + } + + /** + * Returns the icon of the node. + * + * @return The icon of the node. + */ + public Icon getIcon() { + return icon; + } + + /** + * Returns the icon name of the node. + * + * @return The icon name of the node. + */ + public String getIconName() { + if (iconName != null) { + return iconName; + } else { + final String str = userObject.toString(); + int index = str.lastIndexOf("."); + if (index != -1) { + return str.substring(++index); + } else { + return null; + } + } + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconTree.java b/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconTree.java new file mode 100644 index 0000000..f770684 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/tree/IconTree.java @@ -0,0 +1,63 @@ +package tv.porst.splib.gui.tree; + +import java.awt.Component; + +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeModel; + +/** + * Renderer used to display icons in tree nodes. + */ +class CustomTreeCellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { + if((value instanceof IconNode) && (value != null)) { + setIcon(((IconNode)value).getIcon()); + } + + //we can not call super.getTreeCellRendererComponent method, since it overrides our setIcon call and cause rendering of labels to '...' when node expansion is done + + //so, we copy (and modify logic little bit) from super class method: + + final String stringValue = tree.convertValueToText(value, sel, + expanded, leaf, row, hasFocus); + + this.hasFocus = hasFocus; + setText(stringValue); + if(sel) { + setForeground(getTextSelectionColor()); + } else { + setForeground(getTextNonSelectionColor()); + } + + if (!tree.isEnabled()) { + setEnabled(false); + } + else { + setEnabled(true); + } + setComponentOrientation(tree.getComponentOrientation()); + selected = sel; + return this; + + } +} + +/** + * Represents a tree where the nodes are custom icons. + */ +public class IconTree extends JTree { + + /** + * Creates a new icon tree object. + * + * @param model The model of the icon tree. + */ + public IconTree(final TreeModel model) { + super(model); + + setCellRenderer(new CustomTreeCellRenderer()); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/gui/tree/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/gui/tree/_Doc.java new file mode 100644 index 0000000..f654173 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/gui/tree/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.gui.tree; + +/** + * This package contains classes for working with trees. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/maps/MapHelpers.java b/bfIDE/src/main/java/tv/porst/splib/maps/MapHelpers.java new file mode 100644 index 0000000..4cf2dc4 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/maps/MapHelpers.java @@ -0,0 +1,41 @@ +package tv.porst.splib.maps; + +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Contains helper functions for working with maps. + */ +public final class MapHelpers { + + /** + * Sorts a map by its values. + * + * @param Type of map keys. + * @param Type of map values. + * @param map The map to sort. + * + * @return The sorted map. + */ + public static > Map sortByValue(final Map map) { + + final List> list = new LinkedList>(map.entrySet()); + Collections.sort(list, new Comparator>() { + @Override + public int compare(final Map.Entry o1, final Map.Entry o2) { + return o1.getValue().compareTo(o2.getValue()); + } + }); + + final Map result = new LinkedHashMap(); + for (final Map.Entry element : list) { + result.put(element.getKey(), element.getValue()); + } + return result; + } + +} diff --git a/bfIDE/src/main/java/tv/porst/splib/maps/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/maps/_Doc.java new file mode 100644 index 0000000..cfba53e --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/maps/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.maps; + +/** + * This package contains classes for working with maps. + */ diff --git a/bfIDE/src/main/java/tv/porst/splib/strings/StringHelpers.java b/bfIDE/src/main/java/tv/porst/splib/strings/StringHelpers.java new file mode 100644 index 0000000..5555838 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/strings/StringHelpers.java @@ -0,0 +1,60 @@ +package tv.porst.splib.strings; + +/** + * Contains helper functions for working with strings. + */ +public final class StringHelpers { + + /** + * Joins a bunch of strings into a single string. + * + * @param parts The string parts to join. + * @param delimiter The delimiter to add between strings. + * + * @return The joined string. + */ + public static String join(final String[] parts, final String delimiter) { + final StringBuilder builder = new StringBuilder(); + + for (final String part : parts) { + if (part == parts[0] && (part == null || part.equals(""))) { + continue; + } + + if (builder.length() != 0) { + builder.append(delimiter); + } + + builder.append(part); + } + + return builder.toString(); + } + + /** + * Creates a new string by repeating a given string a number of times. + * + * @param string The string to repeat. + * @param repeats Number of repeats. + * + * @return The new string. + */ + public static String repeat(final String string, final int repeats) { + + if (repeats < 0) { + throw new IllegalArgumentException("Number of repeats can not be null"); + } + + if (repeats == 0) { + return ""; + } + + final StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < repeats; i++) { + builder.append(string); + } + + return builder.toString(); + } +} \ No newline at end of file diff --git a/bfIDE/src/main/java/tv/porst/splib/strings/_Doc.java b/bfIDE/src/main/java/tv/porst/splib/strings/_Doc.java new file mode 100644 index 0000000..49e20a6 --- /dev/null +++ b/bfIDE/src/main/java/tv/porst/splib/strings/_Doc.java @@ -0,0 +1,5 @@ +package tv.porst.splib.strings; + +/** + * This package contains classes for working with strings. + */ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dbc16a3 --- /dev/null +++ b/build.gradle @@ -0,0 +1,5 @@ +subprojects { + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..be52383 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6d8604f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'brainfuck-ide' +include ':bfIDE', ':BFInterpreter'