From a38b4e89fbed787ccc795faf601f259821dab88c Mon Sep 17 00:00:00 2001 From: Akarshit Wal Date: Mon, 29 Feb 2016 21:34:17 +0530 Subject: [PATCH 01/17] Reapplied blendMode --- core/src/processing/awt/PGraphicsJava2D.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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(); /* From 9c5579b2338d4e2dab08b596334fa60949938bdd Mon Sep 17 00:00:00 2001 From: Ben Fry Date: Mon, 11 Apr 2016 17:03:59 -0400 Subject: [PATCH 02/17] several fixes, cleanups, and speed improvements to dictionary classes --- core/src/processing/data/FloatDict.java | 300 ++++++++--------------- core/src/processing/data/IntDict.java | 180 +++++++------- core/src/processing/data/StringDict.java | 100 ++++---- core/todo.txt | 9 + 4 files changed, 244 insertions(+), 345 deletions(-) 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/todo.txt b/core/todo.txt
index 87ac0fbba..744241f2c 100644
--- a/core/todo.txt
+++ b/core/todo.txt
@@ -1,4 +1,13 @@
 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?
 

From 205b73bf891dcec9fe2c8bbceed141c2ab98f351 Mon Sep 17 00:00:00 2001
From: codeanticode 
Date: Wed, 13 Apr 2016 16:49:24 -0400
Subject: [PATCH 03/17] flushes after drawing the source texture, fixes #4404

---
 core/src/processing/opengl/FontTexture.java |  8 +-------
 core/src/processing/opengl/Texture.java     | 19 ++++++++++++++-----
 2 files changed, 15 insertions(+), 12 deletions(-)

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/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);
   }

From 1cd8bd7b07a5030a13a8161dfe08f05832bd3893 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 12:58:38 +0200
Subject: [PATCH 04/17] Fix drawing lines with tabs

Fixes #3975
---
 .../app/syntax/TextAreaPainter.java           | 14 +++++++++++---
 .../mode/java/pdex/JavaTextAreaPainter.java   | 19 ++++++++-----------
 2 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/app/src/processing/app/syntax/TextAreaPainter.java b/app/src/processing/app/syntax/TextAreaPainter.java
index bdfded94d..9de9be7aa 100644
--- a/app/src/processing/app/syntax/TextAreaPainter.java
+++ b/app/src/processing/app/syntax/TextAreaPainter.java
@@ -20,6 +20,7 @@ import javax.swing.JComponent;
 
 import processing.app.Preferences;
 import processing.app.syntax.im.CompositionTextPainter;
+import processing.app.ui.Editor;
 
 
 /**
@@ -670,14 +671,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 +750,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 +779,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/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
index 0749bf3ca..571aae4ab 100644
--- a/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
+++ b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java
@@ -389,20 +389,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 (marker.getType() == LineMarker.WARNING) {

From 9739c6202c16c209373838de148741215b3d1870 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 14:35:46 +0200
Subject: [PATCH 05/17] FX: cursors

Fixes #4405
---
 core/src/processing/javafx/PSurfaceFX.java | 37 +++++++++++++++++-----
 1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/core/src/processing/javafx/PSurfaceFX.java b/core/src/processing/javafx/PSurfaceFX.java
index 29fa59861..6c1992a1c 100644
--- a/core/src/processing/javafx/PSurfaceFX.java
+++ b/core/src/processing/javafx/PSurfaceFX.java
@@ -38,9 +38,13 @@ 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.PixelFormat;
+import javafx.scene.image.WritableImage;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
@@ -590,28 +594,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);
   }
 
 

From b5e09b474aaaa2a4170d23e2a5c3eb6e4b8f0fd5 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 14:36:01 +0200
Subject: [PATCH 06/17] FX: window icons

---
 core/src/processing/javafx/PSurfaceFX.java | 43 +++++++++++++++++++++-
 1 file changed, 42 insertions(+), 1 deletion(-)

diff --git a/core/src/processing/javafx/PSurfaceFX.java b/core/src/processing/javafx/PSurfaceFX.java
index 6c1992a1c..088646f3d 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;
@@ -43,6 +46,7 @@ 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;
@@ -356,6 +360,8 @@ public class PSurfaceFX implements PSurface {
         Thread.sleep(5);
       } catch (InterruptedException e) { }
     }
+
+    setProcessingIcon(stage);
   }
 
 
@@ -394,7 +400,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
   }
 
 

From eec809091e7a129647cbd289e35a8df6f1336295 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 14:59:15 +0200
Subject: [PATCH 07/17] FX: make sure dispose() is called on exit

---
 core/src/processing/javafx/PSurfaceFX.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/core/src/processing/javafx/PSurfaceFX.java b/core/src/processing/javafx/PSurfaceFX.java
index 088646f3d..9cd5baa11 100644
--- a/core/src/processing/javafx/PSurfaceFX.java
+++ b/core/src/processing/javafx/PSurfaceFX.java
@@ -337,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();
+    }
   }
 
 

From 179c6052b0ad6e97e60fbf4f9568263e035867e9 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 15:02:13 +0200
Subject: [PATCH 08/17] Make sure PImage.parent is set in loadImage()

---
 core/src/processing/core/PApplet.java | 1 +
 1 file changed, 1 insertion(+)

diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java
index 63613c995..3998ece88 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;
         }
       }

From ddf5231d67d0df2d3a580f9722477aa706a12da5 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 15:04:37 +0200
Subject: [PATCH 09/17] Remove accidental import

---
 app/src/processing/app/syntax/TextAreaPainter.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/app/src/processing/app/syntax/TextAreaPainter.java b/app/src/processing/app/syntax/TextAreaPainter.java
index 9de9be7aa..54eb80f1c 100644
--- a/app/src/processing/app/syntax/TextAreaPainter.java
+++ b/app/src/processing/app/syntax/TextAreaPainter.java
@@ -20,7 +20,6 @@ import javax.swing.JComponent;
 
 import processing.app.Preferences;
 import processing.app.syntax.im.CompositionTextPainter;
-import processing.app.ui.Editor;
 
 
 /**

From 1e539dc87096e5700103df3a2017dcac363149fa Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Sun, 17 Apr 2016 16:08:24 +0200
Subject: [PATCH 10/17] Make TextAreaPainter load tab size from Preferences

---
 app/src/processing/app/syntax/TextAreaPainter.java | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/app/src/processing/app/syntax/TextAreaPainter.java b/app/src/processing/app/syntax/TextAreaPainter.java
index 54eb80f1c..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());

From f77a71d8621435ea4cc06c25923fe67a50c066a3 Mon Sep 17 00:00:00 2001
From: Ben Fry 
Date: Mon, 18 Apr 2016 12:16:24 -0400
Subject: [PATCH 11/17] note Jakub fixes and merges

---
 core/todo.txt | 8 ++++++++
 todo.txt      | 7 ++++---
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/core/todo.txt b/core/todo.txt
index 744241f2c..602bc3f61 100644
--- a/core/todo.txt
+++ b/core/todo.txt
@@ -11,6 +11,14 @@ 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
diff --git a/todo.txt b/todo.txt
index e7a03b40d..36a9bd4ad 100644
--- a/todo.txt
+++ b/todo.txt
@@ -22,14 +22,15 @@ 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
 
+_ 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
 

From e2fe0bf40cf1816cb50691a6346f917bb9cfaa52 Mon Sep 17 00:00:00 2001
From: Jakub Valtar 
Date: Mon, 25 Apr 2016 01:15:43 +0200
Subject: [PATCH 12/17] Fix file path decoding

URLDecoder was being used for path decoding, even though it can't handle
RFC2396 encoding. This resulted in plus characters being removed and
possibly other weirdness.

See https://docs.oracle.com/javase/8/docs/api/java/net/URL.html
"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(). The URLEncoder and URLDecoder classes can also be used, but
only for HTML form encoding, which is not the same as the encoding
scheme defined in RFC2396."

Fixes #4417
---
 app/src/processing/app/Platform.java  | 18 ++++++++++++-----
 core/src/processing/core/PApplet.java | 28 ++++++++++++++++++---------
 2 files changed, 32 insertions(+), 14 deletions(-)

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/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java
index 3998ece88..d11b44e73 100644
--- a/core/src/processing/core/PApplet.java
+++ b/core/src/processing/core/PApplet.java
@@ -7389,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
@@ -7543,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);
     }
@@ -7633,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");

From 957f744d65b5bc0b70fca03c6813d814431487db Mon Sep 17 00:00:00 2001
From: Ben Fry 
Date: Sun, 24 Apr 2016 20:20:02 -0400
Subject: [PATCH 13/17] update Java version, other notes from bug reports

---
 build/build.xml |  4 ++--
 core/todo.txt   | 13 +++++++++++++
 todo.txt        |  5 +++--
 3 files changed, 18 insertions(+), 4 deletions(-)

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/todo.txt b/core/todo.txt
index 602bc3f61..c93f6f7b2 100644
--- a/core/todo.txt
+++ b/core/todo.txt
@@ -22,6 +22,19 @@ 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/todo.txt b/todo.txt
index 36a9bd4ad..489d8bc38 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
@@ -872,8 +875,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?

From 9afb080684bce3d5e344852311d0fa430628e5ba Mon Sep 17 00:00:00 2001
From: Ben Fry 
Date: Wed, 27 Apr 2016 10:19:35 -0400
Subject: [PATCH 14/17] remove unused variables to prevent warnings

---
 .../app/syntax/im/InputMethodSupport.java     | 40 ++++++++-----------
 1 file changed, 17 insertions(+), 23 deletions(-)

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());

From a00567888e1add1801fcbe03d2b3fddf59527dc5 Mon Sep 17 00:00:00 2001
From: Ben Fry 
Date: Sun, 1 May 2016 09:10:08 -0400
Subject: [PATCH 15/17] notes re: recent merge

---
 todo.txt | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/todo.txt b/todo.txt
index 489d8bc38..34c716156 100644
--- a/todo.txt
+++ b/todo.txt
@@ -28,6 +28,11 @@ 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
 

From 38dc5d31f0a9d51c683ab45596df28ca3bb1a8dc Mon Sep 17 00:00:00 2001
From: codeanticode 
Date: Wed, 4 May 2016 10:50:05 -0400
Subject: [PATCH 16/17] save previous texture in tessellator only if tex cache
 in non-null

---
 core/src/processing/opengl/PGraphicsOpenGL.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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;
     }
 
     // -----------------------------------------------------------------

From f870930ed6ff3882d0c3bd2745beaf5053b7fa2a Mon Sep 17 00:00:00 2001
From: codeanticode 
Date: Wed, 4 May 2016 21:53:09 -0400
Subject: [PATCH 17/17] null buffer check

---
 core/src/processing/opengl/PGL.java | 2 ++
 1 file changed, 2 insertions(+)

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);