diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java index b08f62bd0..3578e0a7d 100644 --- a/app/src/processing/app/Platform.java +++ b/app/src/processing/app/Platform.java @@ -26,6 +26,8 @@ package processing.app; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -281,9 +283,16 @@ public class Platform { static public File getContentFile(String name) { if (processingRoot == null) { // Get the path to the .jar file that contains Base.class - String path = Base.class.getProtectionDomain().getCodeSource().getLocation().getPath(); - // Path may have URL encoding, so remove it - String decodedPath = PApplet.urlDecode(path); + URL pathURL = + Base.class.getProtectionDomain().getCodeSource().getLocation(); + // Decode URL + String decodedPath; + try { + decodedPath = pathURL.toURI().getPath(); + } catch (URISyntaxException e) { + e.printStackTrace(); + return null; + } if (decodedPath.contains("/app/bin")) { // This means we're in Eclipse final File build = new File(decodedPath, "../../build").getAbsoluteFile(); @@ -311,8 +320,7 @@ public class Platform { System.err.println("Could not find lib folder via " + jarFolder.getAbsolutePath() + ", switching to user.dir"); - final String userDir = System.getProperty("user.dir"); - processingRoot = new File(PApplet.urlDecode(userDir)); + processingRoot = new File(""); // resolves to "user.dir" } } } diff --git a/app/src/processing/app/syntax/TextAreaPainter.java b/app/src/processing/app/syntax/TextAreaPainter.java index bdfded94d..34da0c35e 100644 --- a/app/src/processing/app/syntax/TextAreaPainter.java +++ b/app/src/processing/app/syntax/TextAreaPainter.java @@ -133,6 +133,7 @@ public class TextAreaPainter extends JComponent implements TabExpander { // moved from setFont() override (never quite comfortable w/ that override) fm = super.getFontMetrics(plainFont); + tabSize = fm.charWidth(' ') * Preferences.getInteger("editor.tabs.size"); textArea.recalculateVisibleLines(); // fgcolor = mode.getColor("editor.fgcolor"); @@ -465,8 +466,6 @@ public class TextAreaPainter extends JComponent implements TabExpander { // g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, // RenderingHints.VALUE_FRACTIONALMETRICS_ON); - tabSize = fm.charWidth(' ') * ((Integer)textArea.getDocument().getProperty(PlainDocument.tabSizeAttribute)).intValue(); - Rectangle clipRect = gfx.getClipBounds(); gfx.setColor(getBackground()); @@ -670,14 +669,18 @@ public class TextAreaPainter extends JComponent implements TabExpander { // gfx.setFont(defaultFont); // gfx.setColor(defaultColor); + int x0 = x - textArea.getHorizontalOffset(); + y += fm.getHeight(); // doesn't respect fixed width like it should // x = Utilities.drawTabbedText(currentLine, x, y, gfx, this, 0); // int w = fm.charWidth(' '); for (int i = 0; i < currentLine.count; i++) { gfx.drawChars(currentLine.array, currentLine.offset+i, 1, x, y); - x = currentLine.array[currentLine.offset + i] == '\t' ? (int)nextTabStop(x, i) : + x = currentLine.array[currentLine.offset + i] == '\t' ? + x0 + (int)nextTabStop(x - x0, i) : x + fm.charWidth(currentLine.array[currentLine.offset+i]); + textArea.offsetToX(line, currentLine.offset + i); } // Draw characters via input method. @@ -745,6 +748,8 @@ public class TextAreaPainter extends JComponent implements TabExpander { // Font defaultFont = gfx.getFont(); // Color defaultColor = gfx.getColor(); + int x0 = x - textArea.getHorizontalOffset(); + // for (byte id = tokens.id; id != Token.END; tokens = tokens.next) { for (;;) { byte id = tokens.id; @@ -772,8 +777,9 @@ public class TextAreaPainter extends JComponent implements TabExpander { // int w = fm.charWidth(' '); for (int i = 0; i < line.count; i++) { gfx.drawChars(line.array, line.offset+i, 1, x, y); - x = line.array[line.offset + i] == '\t' ? (int)nextTabStop(x, i) : - x + fm.charWidth(line.array[line.offset+i]); + x = line.array[line.offset + i] == '\t' ? + x0 + (int)nextTabStop(x - x0, i) : + x + fm.charWidth(line.array[line.offset+i]); } //x += fm.charsWidth(line.array, line.offset, line.count); //x += fm.charWidth(' ') * line.count; diff --git a/app/src/processing/app/syntax/im/InputMethodSupport.java b/app/src/processing/app/syntax/im/InputMethodSupport.java index 921cfc83b..9df319b99 100644 --- a/app/src/processing/app/syntax/im/InputMethodSupport.java +++ b/app/src/processing/app/syntax/im/InputMethodSupport.java @@ -34,7 +34,7 @@ import processing.app.syntax.TextAreaPainter; * @see Bug 1531 : Can't input full-width space when Japanese IME is on. * @see http://docs.oracle.com/javase/8/docs/technotes/guides/imf/index.html * @see http://docs.oracle.com/javase/tutorial/2d/text/index.html - * + * * @author Takashi Maekawa (takachin@generative.info) * @author Satoshi Okita */ @@ -44,11 +44,9 @@ public class InputMethodSupport implements InputMethodRequests, private static final Attribute[] CUSTOM_IM_ATTRIBUTES = { TextAttribute.INPUT_METHOD_HIGHLIGHT, }; - + private int committed_count = 0; - private TextHitInfo caret; private JEditTextArea textArea; - private AttributedCharacterIterator composedText; private AttributedString composedTextString; public InputMethodSupport(JEditTextArea textArea) { @@ -70,10 +68,10 @@ public class InputMethodSupport implements InputMethodRequests, // '+1' mean textArea.lineToY(line) + textArea.getPainter().getFontMetrics().getHeight(). // TextLayout#draw method need at least one height of font. Rectangle rectangle = new Rectangle(textArea.offsetToX(line, offsetX), textArea.lineToY(line + 1), 0, 0); - + Point location = textArea.getPainter().getLocationOnScreen(); rectangle.translate(location.x, location.y); - + return rectangle; } @@ -87,7 +85,7 @@ public class InputMethodSupport implements InputMethodRequests, public int getInsertPositionOffset() { return textArea.getCaretPosition() * -1; } - + @Override public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) { @@ -118,12 +116,11 @@ public class InputMethodSupport implements InputMethodRequests, ///////////////////////////////////////////////////////////////////////////// /** * Handles events from InputMethod. - * + * * @param event event from Input Method. */ @Override - public void inputMethodTextChanged(InputMethodEvent event) { - composedText = null; + public void inputMethodTextChanged(InputMethodEvent event) { if (Base.DEBUG) { StringBuilder sb = new StringBuilder(); sb.append("#Called inputMethodTextChanged"); @@ -132,16 +129,16 @@ public class InputMethodSupport implements InputMethodRequests, sb.append("\t parmString: " + event.paramString()); Messages.log(sb.toString()); } - + AttributedCharacterIterator text = event.getText(); // text = composedText + commitedText committed_count = event.getCommittedCharacterCount(); - - + + // The caret for Input Method. // if you type a character by a input method, original caret become off. // a JEditTextArea is not implemented by the AttributedStirng and TextLayout. // so JEditTextArea Caret On-off logic. - // + // // japanese : if the enter key pressed, event.getText is null. // japanese : if first space key pressed, event.getText is null. // chinese(pinin) : if a space key pressed, event.getText is null. @@ -168,7 +165,7 @@ public class InputMethodSupport implements InputMethodRequests, this.insertCharacter(c); c = text.next(); } - + CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter(); if (Base.DEBUG) { Messages.log(" textArea.getCaretPosition() + committed_count: " + (textArea.getCaretPosition() + committed_count)); @@ -181,15 +178,14 @@ public class InputMethodSupport implements InputMethodRequests, compositionPainter.setComposedTextLayout(null, 0); compositionPainter.setCaret(null); } - caret = event.getCaret(); event.consume(); - textArea.repaint(); + textArea.repaint(); } - + private TextLayout getTextLayout(AttributedCharacterIterator text, int committedCount) { boolean antialias = Preferences.getBoolean("editor.smooth"); TextAreaPainter painter = textArea.getPainter(); - + // create attributed string with font info. //if (text.getEndIndex() - (text.getBeginIndex() + committedCharacterCount) > 0) { if (text.getEndIndex() - (text.getBeginIndex() + committedCount) > 0) { @@ -197,12 +193,11 @@ public class InputMethodSupport implements InputMethodRequests, Font font = painter.getFontMetrics().getFont(); composedTextString.addAttribute(TextAttribute.FONT, font); composedTextString.addAttribute(TextAttribute.BACKGROUND, Color.WHITE); - composedText = composedTextString.getIterator(); } else { composedTextString = new AttributedString(""); return null; } - + // set hint of antialiasing to render target. Graphics2D g2d = (Graphics2D)painter.getGraphics(); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, @@ -219,10 +214,9 @@ public class InputMethodSupport implements InputMethodRequests, @Override public void caretPositionChanged(InputMethodEvent event) { - caret = event.getCaret(); event.consume(); } - + private void insertCharacter(char c) { if (Base.DEBUG) { Messages.log("debug: insertCharacter(char c) textArea.getCaretPosition()=" + textArea.getCaretPosition()); diff --git a/build/build.xml b/build/build.xml index af8df134a..c0cf4734d 100644 --- a/build/build.xml +++ b/build/build.xml @@ -59,8 +59,8 @@ - - + + diff --git a/core/src/processing/awt/PGraphicsJava2D.java b/core/src/processing/awt/PGraphicsJava2D.java index 7a70f8898..213dfbbfe 100644 --- a/core/src/processing/awt/PGraphicsJava2D.java +++ b/core/src/processing/awt/PGraphicsJava2D.java @@ -337,7 +337,10 @@ public class PGraphicsJava2D extends PGraphics { if (fontObject != null) { g2.setFont(fontObject); } - + // https://github.com/processing/processing/issues/4019 + if (blendMode != 0) { + blendMode(blendMode); + } handleSmooth(); /* diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 63613c995..d11b44e73 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -5333,6 +5333,7 @@ public class PApplet implements PConstants { // if (params != null) { // image.setParams(g, params); // } + image.parent = this; return image; } } @@ -7388,10 +7389,10 @@ public class PApplet implements PConstants { try { folder = System.getProperty("user.dir"); - String jarPath = - PApplet.class.getProtectionDomain().getCodeSource().getLocation().getPath(); - // The jarPath from above will be URL encoded (%20 for spaces) - jarPath = urlDecode(jarPath); + URL jarURL = + PApplet.class.getProtectionDomain().getCodeSource().getLocation(); + // Decode URL + String jarPath = jarURL.toURI().getPath(); // Workaround for bug in Java for OS X from Oracle (7u51) // https://github.com/processing/processing/issues/2181 @@ -7542,12 +7543,17 @@ public class PApplet implements PConstants { File why = new File(where); if (why.isAbsolute()) return why; - String jarPath = - getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); + URL jarURL = getClass().getProtectionDomain().getCodeSource().getLocation(); + // Decode URL + String jarPath; + try { + jarPath = jarURL.toURI().getPath(); + } catch (URISyntaxException e) { + e.printStackTrace(); + return null; + } if (jarPath.contains("Contents/Java/")) { - // The path will be URL encoded (%20 for spaces) coming from above - // http://code.google.com/p/processing/issues/detail?id=1073 - File containingFolder = new File(urlDecode(jarPath)).getParentFile(); + File containingFolder = new File(jarPath).getParentFile(); File dataFolder = new File(containingFolder, "data"); return new File(dataFolder, where); } @@ -7632,6 +7638,11 @@ public class PApplet implements PConstants { } + // DO NOT use for file paths, URLDecoder can't handle RFC2396 + // "The recommended way to manage the encoding and decoding of + // URLs is to use URI, and to convert between these two classes + // using toURI() and URI.toURL()." + // https://docs.oracle.com/javase/8/docs/api/java/net/URL.html static public String urlDecode(String str) { try { return URLDecoder.decode(str, "UTF-8"); diff --git a/core/src/processing/data/FloatDict.java b/core/src/processing/data/FloatDict.java index cb06c2b15..5fcc50737 100644 --- a/core/src/processing/data/FloatDict.java +++ b/core/src/processing/data/FloatDict.java @@ -69,6 +69,22 @@ public class FloatDict { } + /** + * @nowebref + */ + public FloatDict(String[] keys, float[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("key and value arrays must be the same length"); + } + this.keys = keys; + this.values = values; + count = keys.length; + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + /** * Constructor to allow (more intuitive) inline initialization, e.g.: *
@@ -90,21 +106,6 @@ public class FloatDict {
   }
 
 
-  /**
-   * @nowebref
-   */
-  public FloatDict(String[] keys, float[] values) {
-    if (keys.length != values.length) {
-      throw new IllegalArgumentException("key and value arrays must be the same length");
-    }
-    this.keys = keys;
-    this.values = values;
-    count = keys.length;
-    for (int i = 0; i < count; i++) {
-      indices.put(keys[i], i);
-    }
-  }
-
   /**
    * @webref floatdict:method
    * @brief Returns the number of key/value pairs
@@ -139,71 +140,35 @@ public class FloatDict {
   }
 
 
-//  /**
-//   * Return the internal array being used to store the keys. Allocated but
-//   * unused entries will be removed. This array should not be modified.
-//   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-  /**
-   * @webref floatdict:method
-   * @brief Return the internal array being used to store the keys
-   */
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
-
-          public void remove() {
-            removeIndex(index);
-          }
-
-          public String next() {
-            return key(++index);
-          }
-
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+        return keyIterator();
       }
     };
   }
+  
+  
+  // Use this to iterate when you want to be able to remove elements along the way
+  public Iterator keyIterator() {
+    return new Iterator() {
+      int index = -1;
 
+      public void remove() {
+        removeIndex(index);
+      }
 
-  /*
-  static class KeyIterator implements Iterator {
-    FloatHash parent;
-    int index;
+      public String next() {
+        return key(++index);
+      }
 
-    public KeyIterator(FloatHash parent) {
-      this.parent = parent;
-      index = -1;
-    }
-
-    public void remove() {
-      parent.removeIndex(index);
-    }
-
-    public String next() {
-      return parent.key(++index);
-    }
-
-    public boolean hasNext() {
-      return index+1 < parent.size();
-    }
-
-    public void reset() {
-      index = -1;
-    }
+      public boolean hasNext() {
+        return index+1 < size();
+      }
+    };
   }
-  */
 
 
   /**
@@ -213,6 +178,7 @@ public class FloatDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -231,11 +197,6 @@ public class FloatDict {
   }
 
 
-//  public float[] values() {
-//    crop();
-//    return values;
-//  }
-
   /**
    * @webref floatdict:method
    * @brief Return the internal array being used to store the values
@@ -245,21 +206,26 @@ public class FloatDict {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return valueIterator();
+      }
+    };
+  }
+  
+  
+  public Iterator valueIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public void remove() {
-            removeIndex(index);
-          }
+      public void remove() {
+        removeIndex(index);
+      }
 
-          public Float next() {
-            return value(++index);
-          }
+      public Float next() {
+        return value(++index);
+      }
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -272,6 +238,7 @@ public class FloatDict {
    * @brief Create a new array and copy each of the values into it
    */
   public float[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -337,18 +304,6 @@ public class FloatDict {
   }
 
 
-//  /** Increase the value of a specific key by 1. */
-//  public void inc(String key) {
-//    inc(key, 1);
-////    int index = index(key);
-////    if (index == -1) {
-////      create(key, 1);
-////    } else {
-////      values[index]++;
-////    }
-//  }
-
-
   /**
    * @webref floatdict:method
    * @brief Add to a value
@@ -363,12 +318,6 @@ public class FloatDict {
   }
 
 
-//  /** Decrease the value of a key by 1. */
-//  public void dec(String key) {
-//    inc(key, -1);
-//  }
-
-
   /**
    * @webref floatdict:method
    * @brief Subtract from a value
@@ -417,8 +366,10 @@ public class FloatDict {
    * @brief Return the smallest value
    */
   public int minIndex() {
-    checkMinMax("minIndex");
-    // Will still return NaN if there is 1 or more entries, and they're all NaN
+    //checkMinMax("minIndex");
+    if (count == 0) return -1;
+    
+    // Will still return NaN if there are 1 or more entries, and they're all NaN
     float m = Float.NaN;
     int mi = -1;
     for (int i = 0; i < count; i++) {
@@ -430,7 +381,7 @@ public class FloatDict {
         // calculate the rest
         for (int j = i+1; j < count; j++) {
           float d = values[j];
-          if (!Float.isNaN(d) && (d < m)) {
+          if ((d == d) && (d < m)) {
             m = values[j];
             mi = j;
           }
@@ -442,6 +393,7 @@ public class FloatDict {
   }
 
 
+  // return the key for the minimum value
   public String minKey() {
     checkMinMax("minKey");
     int index = minIndex();
@@ -452,6 +404,7 @@ public class FloatDict {
   }
 
 
+  // return the minimum value, or throw an error if there are no values
   public float minValue() {
     checkMinMax("minValue");
     int index = minIndex();
@@ -468,7 +421,10 @@ public class FloatDict {
    */
   // The index of the entry that has the max value. Reference above is incorrect.
   public int maxIndex() {
-    checkMinMax("maxIndex");
+    //checkMinMax("maxIndex");
+    if (count == 0) {
+      return -1;
+    }
     // Will still return NaN if there is 1 or more entries, and they're all NaN
     float m = Float.NaN;
     int mi = -1;
@@ -493,9 +449,9 @@ public class FloatDict {
   }
 
 
-  /** The key for a max value, or null if everything is NaN (no max). */
+  /** The key for a max value; null if empty or everything is NaN (no max). */
   public String maxKey() {
-    checkMinMax("maxKey");
+    //checkMinMax("maxKey");
     int index = maxIndex();
     if (index == -1) {
       return null;
@@ -504,9 +460,9 @@ public class FloatDict {
   }
 
 
-  /** The max value. (Or NaN if they're all NaN.) */
+  /** The max value. (Or NaN if no entries or they're all NaN.) */
   public float maxValue() {
-    checkMinMax("maxValue");
+    //checkMinMax("maxValue");
     int index = maxIndex();
     if (index == -1) {
       return Float.NaN;
@@ -573,24 +529,11 @@ public class FloatDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
-//  abstract class InternalSort extends Sort {
-//    @Override
-//    public int size() {
-//      return count;
-//    }
-//
-//    @Override
-//    public void swap(int a, int b) {
-//      FloatHash.this.swap(a, b);
-//    }
-//  }
-
-
   /**
    * Sort the keys alphabetically (ignoring case). Uses the value as a
    * tie-breaker (only really possible with a key that has a case change).
@@ -599,36 +542,16 @@ public class FloatDict {
    * @brief Sort the keys alphabetically
    */
   public void sortKeys() {
-    sortImpl(true, false);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        int result = keys[a].compareToIgnoreCase(keys[b]);
-//        if (result != 0) {
-//          return result;
-//        }
-//        return values[b] - values[a];
-//      }
-//    }.run();
+    sortImpl(true, false, true);
   }
 
 
   /**
    * @webref floatdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
-    sortImpl(true, true);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        int result = keys[b].compareToIgnoreCase(keys[a]);
-//        if (result != 0) {
-//          return result;
-//        }
-//        return values[a] - values[b];
-//      }
-//    }.run();
+    sortImpl(true, true, true);
   }
 
 
@@ -639,13 +562,17 @@ public class FloatDict {
    * @brief Sort by values in ascending order
    */
   public void sortValues() {
-    sortImpl(false, false);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//
-//      }
-//    }.run();
+    sortValues(true);
+  }
+
+
+  /**
+   * Set true to ensure that the order returned is identical. Slightly
+   * slower because the tie-breaker for identical values compares the keys.
+   * @param stable
+   */
+  public void sortValues(boolean stable) {
+    sortImpl(false, false, stable);
   }
 
 
@@ -654,50 +581,17 @@ public class FloatDict {
    * @brief Sort by values in descending order
    */
   public void sortValuesReverse() {
-    sortImpl(false, true);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        float diff = values[b] - values[a];
-//        if (diff == 0 && keys[a] != null && keys[b] != null) {
-//          diff = keys[a].compareToIgnoreCase(keys[b]);
-//        }
-//        return descending ? diff : -diff;
-//      }
-//    }.run();
+    sortValuesReverse(true);
   }
 
 
-//  // ascending puts the largest value at the end
-//  // descending puts the largest value at 0
-//  public void sortValues(final boolean descending, final boolean tiebreaker) {
-//    Sort s = new Sort() {
-//      @Override
-//      public int size() {
-//        return count;
-//      }
-//
-//      @Override
-//      public float compare(int a, int b) {
-//        float diff = values[b] - values[a];
-//        if (tiebreaker) {
-//          if (diff == 0) {
-//            diff = keys[a].compareToIgnoreCase(keys[b]);
-//          }
-//        }
-//        return descending ? diff : -diff;
-//      }
-//
-//      @Override
-//      public void swap(int a, int b) {
-//        FloatHash.this.swap(a, b);
-//      }
-//    };
-//    s.run();
-//  }
+  public void sortValuesReverse(boolean stable) {
+    sortImpl(false, true, stable);
+  }
 
 
-  protected void sortImpl(final boolean useKeys, final boolean reverse) {
+  protected void sortImpl(final boolean useKeys, final boolean reverse, 
+                          final boolean stable) {
     Sort s = new Sort() {
       @Override
       public int size() {
@@ -731,11 +625,11 @@ public class FloatDict {
         if (useKeys) {
           diff = keys[a].compareToIgnoreCase(keys[b]);
           if (diff == 0) {
-            return values[a] - values[b];
+            diff = values[a] - values[b];
           }
         } else {  // sort values
           diff = values[a] - values[b];
-          if (diff == 0) {
+          if (diff == 0 && stable) {
             diff = keys[a].compareToIgnoreCase(keys[b]);
           }
         }
@@ -748,18 +642,24 @@ public class FloatDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
   /**
    * Sum all of the values in this dictionary, then return a new FloatDict of
    * each key, divided by the total sum. The total for all values will be ~1.0.
-   * @return a Dict with the original keys, mapped to their pct of the total
+   * @return a FloatDict with the original keys, mapped to their pct of the total
    */
   public FloatDict getPercent() {
     double sum = 0;
-    for (float value : valueArray()) {
-      sum += value;
+    for (int i = 0; i < count; i++) {
+      sum += values[i];
     }
     FloatDict outgoing = new FloatDict();
     for (int i = 0; i < size(); i++) {
diff --git a/core/src/processing/data/IntDict.java b/core/src/processing/data/IntDict.java
index d33f22130..316f4a73d 100644
--- a/core/src/processing/data/IntDict.java
+++ b/core/src/processing/data/IntDict.java
@@ -133,74 +133,20 @@ public class IntDict {
   }
 
 
-//  private void crop() {
-//    if (count != keys.length) {
-//      keys = PApplet.subset(keys, 0, count);
-//      values = PApplet.subset(values, 0, count);
-//    }
-//  }
+  protected void crop() {
+    if (count != keys.length) {
+      keys = PApplet.subset(keys, 0, count);
+      values = PApplet.subset(values, 0, count);
+    }
+  }
 
 
-  /**
-   * Return the internal array being used to store the keys. Allocated but
-   * unused entries will be removed. This array should not be modified.
-   *
-   * @webref intdict:method
-   * @brief Return the internal array being used to store the keys
-   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-
-//  public Iterable keys() {
-//    return new Iterable() {
-//
-//      @Override
-//      public Iterator iterator() {
-//        return new Iterator() {
-//          int index = -1;
-//
-//          public void remove() {
-//            removeIndex(index);
-//          }
-//
-//          public String next() {
-//            return key(++index);
-//          }
-//
-//          public boolean hasNext() {
-//            return index+1 < size();
-//          }
-//        };
-//      }
-//    };
-//  }
-
-
-  // Use this with 'for' loops
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
         return keyIterator();
-//        return new Iterator() {
-//          int index = -1;
-//
-//          public void remove() {
-//            removeIndex(index);
-//          }
-//
-//          public String next() {
-//            return key(++index);
-//          }
-//
-//          public boolean hasNext() {
-//            return index+1 < size();
-//          }
-//        };
       }
     };
   }
@@ -233,6 +179,7 @@ public class IntDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -292,6 +239,7 @@ public class IntDict {
    * @brief Create a new array and copy each of the values into it
    */
   public int[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -428,7 +376,9 @@ public class IntDict {
 
   // return the index of the minimum value
   public int minIndex() {
-    checkMinMax("minIndex");
+    //checkMinMax("minIndex");
+    if (count == 0) return -1;
+
     int index = 0;
     int value = values[0];
     for (int i = 1; i < count; i++) {
@@ -441,23 +391,30 @@ public class IntDict {
   }
 
 
-  // return the minimum value
+  // return the key for the minimum value
+  public String minKey() {
+    checkMinMax("minKey");
+    int index = minIndex();
+    if (index == -1) {
+      return null;
+    }
+    return keys[index];
+  }
+
+
+  // return the minimum value, or throw an error if there are no values
   public int minValue() {
     checkMinMax("minValue");
     return values[minIndex()];
   }
 
 
-  // return the key for the minimum value
-  public String minKey() {
-    checkMinMax("minKey");
-    return keys[minIndex()];
-  }
-
-
   // return the index of the max value
   public int maxIndex() {
-    checkMinMax("maxIndex");
+    //checkMinMax("maxIndex");
+    if (count == 0) {
+      return -1;
+    }
     int index = 0;
     int value = values[0];
     for (int i = 1; i < count; i++) {
@@ -470,17 +427,21 @@ public class IntDict {
   }
 
 
-  // return the maximum value
-  public int maxValue() {
-    checkMinMax("maxValue");
-    return values[maxIndex()];
+  /** return the key corresponding to the maximum value or null if no entries */
+  public String maxKey() {
+    //checkMinMax("maxKey");
+    int index = maxIndex();
+    if (index == -1) {
+      return null;
+    }
+    return keys[index];
   }
 
 
-  // return the key corresponding to the maximum value
-  public String maxKey() {
-    checkMinMax("maxKey");
-    return keys[maxIndex()];
+  // return the maximum value or throw an error if zero length
+  public int maxValue() {
+    checkMinMax("maxIndex");
+    return values[maxIndex()];
   }
 
 
@@ -541,8 +502,8 @@ public class IntDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
@@ -554,7 +515,7 @@ public class IntDict {
    * @brief Sort the keys alphabetically
    */
   public void sortKeys() {
-    sortImpl(true, false);
+    sortImpl(true, false, true);
   }
 
   /**
@@ -562,10 +523,10 @@ public class IntDict {
    * tie-breaker (only really possible with a key that has a case change).
    *
    * @webref intdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
-    sortImpl(true, true);
+    sortImpl(true, true, true);
   }
 
 
@@ -576,9 +537,20 @@ public class IntDict {
    * @brief Sort by values in ascending order
    */
   public void sortValues() {
-    sortImpl(false, false);
+    sortValues(true);
   }
 
+
+  /**
+   * Set true to ensure that the order returned is identical. Slightly
+   * slower because the tie-breaker for identical values compares the keys.
+   * @param stable
+   */
+  public void sortValues(boolean stable) {
+    sortImpl(false, false, stable);
+  }
+
+
   /**
    * Sort by values in descending order. The largest value will be at [0].
    *
@@ -586,11 +558,17 @@ public class IntDict {
    * @brief Sort by values in descending order
    */
   public void sortValuesReverse() {
-    sortImpl(false, true);
+    sortValuesReverse(true);
   }
 
 
-  protected void sortImpl(final boolean useKeys, final boolean reverse) {
+  public void sortValuesReverse(boolean stable) {
+    sortImpl(false, true, stable);
+  }
+
+
+  protected void sortImpl(final boolean useKeys, final boolean reverse,
+                          final boolean stable) {
     Sort s = new Sort() {
       @Override
       public int size() {
@@ -603,11 +581,11 @@ public class IntDict {
         if (useKeys) {
           diff = keys[a].compareToIgnoreCase(keys[b]);
           if (diff == 0) {
-            return values[a] - values[b];
+            diff = values[a] - values[b];
           }
         } else {  // sort values
           diff = values[a] - values[b];
-          if (diff == 0) {
+          if (diff == 0 && stable) {
             diff = keys[a].compareToIgnoreCase(keys[b]);
           }
         }
@@ -620,18 +598,24 @@ public class IntDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
   /**
    * Sum all of the values in this dictionary, then return a new FloatDict of
    * each key, divided by the total sum. The total for all values will be ~1.0.
-   * @return a Dict with the original keys, mapped to their pct of the total
+   * @return an IntDict with the original keys, mapped to their pct of the total
    */
   public FloatDict getPercent() {
     double sum = 0;
-    for (int value : valueArray()) {
-      sum += value;
+    for (int i = 0; i < count; i++) {
+      sum += values[i];
     }
     FloatDict outgoing = new FloatDict();
     for (int i = 0; i < size(); i++) {
@@ -655,6 +639,13 @@ public class IntDict {
   }
 
 
+  public void print() {
+    for (int i = 0; i < size(); i++) {
+      System.out.println(keys[i] + " = " + values[i]);
+    }
+  }
+
+
   /**
    * Write tab-delimited entries out to
    * @param writer
@@ -667,13 +658,6 @@ public class IntDict {
   }
 
 
-  public void print() {
-    for (int i = 0; i < size(); i++) {
-      System.out.println(keys[i] + " = " + values[i]);
-    }
-  }
-
-
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
diff --git a/core/src/processing/data/StringDict.java b/core/src/processing/data/StringDict.java
index 69fb4b716..330d362c9 100644
--- a/core/src/processing/data/StringDict.java
+++ b/core/src/processing/data/StringDict.java
@@ -142,39 +142,32 @@ public class StringDict {
   }
 
 
-//  /**
-//   * Return the internal array being used to store the keys. Allocated but
-//   * unused entries will be removed. This array should not be modified.
-//   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-  /**
-   * @webref stringdict:method
-   * @brief Return the internal array being used to store the keys
-   */
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return keyIterator();
+      }
+    };
+  }
 
-          public void remove() {
-            removeIndex(index);
-          }
 
-          public String next() {
-            return key(++index);
-          }
+  // Use this to iterate when you want to be able to remove elements along the way
+  public Iterator keyIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public void remove() {
+        removeIndex(index);
+      }
+
+      public String next() {
+        return key(++index);
+      }
+
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -187,6 +180,7 @@ public class StringDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -213,21 +207,26 @@ public class StringDict {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return valueIterator();
+      }
+    };
+  }
 
-          public void remove() {
-            removeIndex(index);
-          }
 
-          public String next() {
-            return value(++index);
-          }
+  public Iterator valueIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public void remove() {
+        removeIndex(index);
+      }
+
+      public String next() {
+        return value(++index);
+      }
+
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -240,6 +239,7 @@ public class StringDict {
    * @brief Create a new array and copy each of the values into it
    */
   public String[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -357,8 +357,8 @@ public class StringDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
@@ -375,7 +375,7 @@ public class StringDict {
 
   /**
    * @webref stringdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
     sortImpl(true, true);
@@ -432,6 +432,12 @@ public class StringDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
@@ -448,6 +454,13 @@ public class StringDict {
   }
 
 
+  public void print() {
+    for (int i = 0; i < size(); i++) {
+      System.out.println(keys[i] + " = " + values[i]);
+    }
+  }
+
+
   /**
    * Write tab-delimited entries out to
    * @param writer
@@ -460,13 +473,6 @@ public class StringDict {
   }
 
 
-  public void print() {
-    for (int i = 0; i < size(); i++) {
-      System.out.println(keys[i] + " = " + values[i]);
-    }
-  }
-
-
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
diff --git a/core/src/processing/javafx/PSurfaceFX.java b/core/src/processing/javafx/PSurfaceFX.java
index 29fa59861..9cd5baa11 100644
--- a/core/src/processing/javafx/PSurfaceFX.java
+++ b/core/src/processing/javafx/PSurfaceFX.java
@@ -25,7 +25,10 @@ package processing.javafx;
 import java.awt.GraphicsDevice;
 import java.awt.GraphicsEnvironment;
 import java.awt.Rectangle;
+import java.net.URL;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javafx.animation.Animation;
@@ -38,9 +41,14 @@ import javafx.beans.value.ObservableValue;
 import javafx.event.ActionEvent;
 import javafx.event.EventHandler;
 import javafx.event.EventType;
+import javafx.scene.Cursor;
+import javafx.scene.ImageCursor;
 import javafx.scene.Scene;
 import javafx.scene.SceneAntialiasing;
 import javafx.scene.canvas.Canvas;
+import javafx.scene.image.Image;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritableImage;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
@@ -329,6 +337,11 @@ public class PSurfaceFX implements PSurface {
       // the stage, assign it only when it is all set up
       surface.stage = stage;
     }
+
+    @Override
+    public void stop() throws Exception {
+      surface.sketch.dispose();
+    }
   }
 
 
@@ -352,6 +365,8 @@ public class PSurfaceFX implements PSurface {
         Thread.sleep(5);
       } catch (InterruptedException e) { }
     }
+
+    setProcessingIcon(stage);
   }
 
 
@@ -390,7 +405,42 @@ public class PSurfaceFX implements PSurface {
 
 
   public void setIcon(PImage icon) {
-    // TODO implement this in JavaFX
+    int w = icon.pixelWidth;
+    int h = icon.pixelHeight;
+    WritableImage im = new WritableImage(w, h);
+    im.getPixelWriter().setPixels(0, 0, w, h,
+                                  PixelFormat.getIntArgbInstance(),
+                                  icon.pixels,
+                                  0, w);
+
+    Stage stage = (Stage) canvas.getScene().getWindow();
+    stage.getIcons().clear();
+    stage.getIcons().add(im);
+  }
+
+
+  List iconImages;
+
+  protected void setProcessingIcon(Stage stage) {
+    // Adapted from PSurfaceAWT
+    // Note: FX chooses wrong icon size, should be fixed in Java 9, see:
+    // https://bugs.openjdk.java.net/browse/JDK-8091186
+    // Removing smaller sizes helps a bit, but big ones are downsized
+    try {
+      if (iconImages == null) {
+        iconImages = new ArrayList<>();
+        final int[] sizes = { 48, 64, 128, 256, 512 };
+
+        for (int sz : sizes) {
+          URL url = PApplet.class.getResource("/icon/icon-" + sz + ".png");
+          Image image = new Image(url.toString());
+          iconImages.add(image);
+        }
+      }
+      List icons = stage.getIcons();
+      icons.clear();
+      icons.addAll(iconImages);
+    } catch (Exception e) { }  // harmless; keep this to ourselves
   }
 
 
@@ -590,28 +640,45 @@ public class PSurfaceFX implements PSurface {
 //    canvas.requestFocus();
 //  }
 
+  Cursor lastCursor = Cursor.DEFAULT;
 
   public void setCursor(int kind) {
-    // TODO Auto-generated method stub
-
+    Cursor c;
+    switch (kind) {
+      case PConstants.ARROW: c = Cursor.DEFAULT; break;
+      case PConstants.CROSS: c = Cursor.CROSSHAIR; break;
+      case PConstants.HAND: c = Cursor.HAND; break;
+      case PConstants.MOVE: c = Cursor.MOVE; break;
+      case PConstants.TEXT: c = Cursor.TEXT; break;
+      case PConstants.WAIT: c = Cursor.WAIT; break;
+      default: c = Cursor.DEFAULT; break;
+    }
+    lastCursor = c;
+    canvas.getScene().setCursor(c);
   }
 
 
   public void setCursor(PImage image, int hotspotX, int hotspotY) {
-    // TODO Auto-generated method stub
-
+    int w = image.pixelWidth;
+    int h = image.pixelHeight;
+    WritableImage im = new WritableImage(w, h);
+    im.getPixelWriter().setPixels(0, 0, w, h,
+                                  PixelFormat.getIntArgbInstance(),
+                                  image.pixels,
+                                  0, w);
+    ImageCursor c = new ImageCursor(im, hotspotX, hotspotY);
+    lastCursor = c;
+    canvas.getScene().setCursor(c);
   }
 
 
   public void showCursor() {
-    // TODO Auto-generated method stub
-
+    canvas.getScene().setCursor(lastCursor);
   }
 
 
   public void hideCursor() {
-    // TODO Auto-generated method stub
-
+    canvas.getScene().setCursor(Cursor.NONE);
   }
 
 
diff --git a/core/src/processing/opengl/FontTexture.java b/core/src/processing/opengl/FontTexture.java
index c4c207b73..af28ad9f3 100644
--- a/core/src/processing/opengl/FontTexture.java
+++ b/core/src/processing/opengl/FontTexture.java
@@ -149,15 +149,9 @@ class FontTexture implements PConstants {
     } else if (resize) {
       // Replacing old smaller texture with larger one.
       // But first we must copy the contents of the older
-      // texture into the new one. Setting blend mode to
-      // REPLACE to preserve color of transparent pixels.
+      // texture into the new one.
       Texture tex0 = textures[lastTex];
-
-      tex.pg.pushStyle();
-      tex.pg.blendMode(REPLACE);
       tex.put(tex0);
-      tex.pg.popStyle();
-
       textures[lastTex] = tex;
 
       pg.setCache(images[lastTex], tex);
diff --git a/core/src/processing/opengl/PGL.java b/core/src/processing/opengl/PGL.java
index 8b471751c..96d7929b7 100644
--- a/core/src/processing/opengl/PGL.java
+++ b/core/src/processing/opengl/PGL.java
@@ -942,6 +942,8 @@ public abstract class PGL {
 
 
   protected void restoreFirstFrame() {
+    if (firstFrame == null) return;
+
     IntBuffer tex = allocateIntBuffer(1);
     genTextures(1, tex);
 
diff --git a/core/src/processing/opengl/PGraphicsOpenGL.java b/core/src/processing/opengl/PGraphicsOpenGL.java
index 2e11c9b03..2903fa561 100644
--- a/core/src/processing/opengl/PGraphicsOpenGL.java
+++ b/core/src/processing/opengl/PGraphicsOpenGL.java
@@ -12481,8 +12481,8 @@ public class PGraphicsOpenGL extends PGraphics {
         } else {
           texCache.setLastIndex(lastIndex, lastCache);
         }
+        prevTexImage = newTexImage;
       }
-      prevTexImage = newTexImage;
     }
 
     // -----------------------------------------------------------------
diff --git a/core/src/processing/opengl/Texture.java b/core/src/processing/opengl/Texture.java
index 5d8a2a8d3..c2f75f3f2 100644
--- a/core/src/processing/opengl/Texture.java
+++ b/core/src/processing/opengl/Texture.java
@@ -1224,10 +1224,10 @@ public class Texture implements PConstants {
     // FBO copy:
     pg.pushFramebuffer();
     pg.setFramebuffer(tempFbo);
-    // Clear the color buffer to make sure that the alpha channel is set to
-    // full transparency
-    pgl.clearColor(0, 0, 0, 0);
-    pgl.clear(PGL.COLOR_BUFFER_BIT);
+    // Replaces anything that this texture might contain in the area being
+    // replaced by the new one.
+    pg.pushStyle();
+    pg.blendMode(REPLACE);
     if (scale) {
       // Rendering tex into "this", and scaling the source rectangle
       // to cover the entire destination region.
@@ -1243,8 +1243,10 @@ public class Texture implements PConstants {
                       0, 0, tempFbo.width, tempFbo.height, 1,
                       x, y, x + w, y + h, x, y, x + w, y + h);
     }
+    pgl.flush(); // Needed to make sure that the change in this texture is
+                 // available immediately.
+    pg.popStyle();
     pg.popFramebuffer();
-
     updateTexels(x, y, w, h);
   }
 
@@ -1264,6 +1266,10 @@ public class Texture implements PConstants {
     // FBO copy:
     pg.pushFramebuffer();
     pg.setFramebuffer(tempFbo);
+    // Replaces anything that this texture might contain in the area being
+    // replaced by the new one.
+    pg.pushStyle();
+    pg.blendMode(REPLACE);
     if (scale) {
       // Rendering tex into "this", and scaling the source rectangle
       // to cover the entire destination region.
@@ -1279,6 +1285,9 @@ public class Texture implements PConstants {
                       0, 0, tempFbo.width, tempFbo.height,
                       x, y, w, h, x, y, w, h);
     }
+    pgl.flush(); // Needed to make sure that the change in this texture is
+                 // available immediately.
+    pg.popStyle();
     pg.popFramebuffer();
     updateTexels(x, y, w, h);
   }
diff --git a/core/todo.txt b/core/todo.txt
index 87ac0fbba..c93f6f7b2 100644
--- a/core/todo.txt
+++ b/core/todo.txt
@@ -1,10 +1,40 @@
 0249 (3.0.3)
+X Float/IntDict changes
+X   minIndex() and maxIndex() return -1 when count is zero (rather than ex)
+X   bug fix to reverse sorts
+X   2x performance increase for sorting
+X   added optional "stable" parameter for sorting by values
+X     set to false for higher performance
+X   call crop() in keyArray() and valueArray() functions
+X     they're duplicates, their length is an implementation detail
+X   normalize features and error handling between all of them
 
 _ add push() and pop() methods to mirror js?
 
+jakub
+X several JavaFX fixes
+X   https://github.com/processing/processing/pull/4411
+X cursor() and noCursor() not working on FX2D
+X   https://github.com/processing/processing/issues/4405
+X Make sure PImage.parent is set in loadImage()
+X   https://github.com/processing/processing/pull/4412
+
 andres
 X Change convention for directional lights in OpenGL-Binding for GLSL
 X   https://github.com/processing/processing/issues/4275
+X internal texture copy does not update immediately
+X   https://github.com/processing/processing/issues/4404
+X Font corruption issue in OpenGL
+X   https://github.com/processing/processing/issues/4392
+X setStroke() does not work with imported OBJ Pshapes
+X   https://github.com/processing/processing/issues/4377
+
+contribs
+X blendMode() resetting with getGraphics()
+X   https://github.com/processing/processing/issues/4019
+X   https://github.com/processing/processing/pull/4341
+X   https://github.com/processing/processing/issues/4376
+
 
 _ textAlign(CENTER) and pixelDensity(2) aligning incorrectly with Java2D
 _   https://github.com/processing/processing/issues/4020
diff --git a/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
index 4d891893b..24a69f857 100644
--- a/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
+++ b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
@@ -301,20 +301,17 @@ public class JavaTextAreaPainter extends TextAreaPainter
           return;
         }
 
-        // Take care of offsets
-        int aw = fm.stringWidth(trimRight(badCode)) + textArea.getHorizontalOffset();
-        // to the left of line + text
-        // width
-        int rw = fm.stringWidth(badCode.trim()); // real width
-        int x1 = fm.stringWidth(goodCode) + (aw - rw);
-        int y1 = y + fm.getHeight() - 2, x2 = x1 + rw;
+        int trimmedLength = badCode.trim().length();
+        int rightTrimmedLength = trimRight(badCode).length();
+        int leftTrimLength = rightTrimmedLength - trimmedLength;
+
+        int x1 = textArea.offsetToX(line, goodCode.length() + leftTrimLength);
+        int x2 = textArea.offsetToX(line, goodCode.length() + rightTrimmedLength);
+        int y1 = y + fm.getHeight() - 2;
 
         if (line != problem.getLineNumber()) {
-          x1 = 0; // on the following lines, wiggle extends to the left border
+          x1 = Editor.LEFT_GUTTER; // on the following lines, wiggle extends to the left border
         }
-        // Adding offsets for the gutter
-        x1 += Editor.LEFT_GUTTER;
-        x2 += Editor.LEFT_GUTTER;
 
         gfx.setColor(errorUnderlineColor);
         if (problem.isWarning()) {
diff --git a/todo.txt b/todo.txt
index e7a03b40d..34c716156 100644
--- a/todo.txt
+++ b/todo.txt
@@ -10,6 +10,9 @@ X   https://github.com/processing/processing/pull/4325
 X Fix non-ARM Linux deb build process
 X   https://github.com/processing/processing/issues/4308
 X   https://github.com/processing/processing/pull/4309
+_ processing-java output as UTF-8 makes Windows unhappy
+_   https://github.com/processing/processing/issues/1633
+_   includes possible fix for Windows
 
 jakub
 X Update app to Java 8
@@ -22,14 +25,20 @@ X update JDT to 4.5.2
 X   https://github.com/processing/processing/pull/4387
 X JavaMode cleanup
 X   https://github.com/processing/processing/pull/4390
+X tabs aren't working properly (several bugs?)
+X   https://github.com/processing/processing/issues/3975
+X   https://github.com/processing/processing/pull/4410
+X file paths not decoding properly
+X   https://github.com/processing/processing/issues/4417
+X   https://github.com/processing/processing/pull/4426
+_   double check that this is working on OS X
 
 
+_ arrow keys aren't working in the examples window
+
 _ createPreprocessor() added to JavaEditor
 https://github.com/processing/processing/commit/2ecdc36ac7c680eb36e271d17ad80b657b3ae6a0
 
-_ tabs aren't working properly (several bugs?)
-_   https://github.com/processing/processing/issues/3975
-
 _ possible infinite loop on modified externally
 _   https://github.com/processing/processing/issues/3965
 
@@ -871,8 +880,6 @@ DIST / Windows
 
 _ PDE and sketches are 2x smaller on high-res Windows 8 machines
 _   https://github.com/processing/processing/issues/2411
-_ processing-java output as UTF-8 makes Windows unhappy
-_   https://github.com/processing/processing/issues/1633
 _ does launching p5 from inside the .zip folder cause it to quit immediately?
 _   how can we provide an error message here?
 _ how to handle double-clicked files on windows?