From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / android / project / xscreensaver / src / org / jwz / xscreensaver / jwxyz.java
1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org>
3  *
4  * Permission to use, copy, modify, distribute, and sell this software and its
5  * documentation for any purpose is hereby granted without fee, provided that
6  * the above copyright notice appear in all copies and that both that
7  * copyright notice and this permission notice appear in supporting
8  * documentation.  No representations are made about the suitability of this
9  * software for any purpose.  It is provided "as is" without express or 
10  * implied warranty.
11  *
12  * This class is how the C implementation of jwxyz calls back into Java
13  * to do things that OpenGL does not have access to without Java-based APIs.
14  * It is the Java companion to jwxyz-android.c and screenhack-android.c.
15  */
16
17 package org.jwz.xscreensaver;
18
19 import java.util.Map;
20 import java.util.HashMap;
21 import java.util.Hashtable;
22 import java.util.ArrayList;
23 import java.util.Random;
24 import android.view.KeyEvent;
25 import android.content.SharedPreferences;
26 import android.content.Context;
27 import android.content.ContentResolver;
28 import android.content.res.AssetManager;
29 import android.graphics.Typeface;
30 import android.graphics.Rect;
31 import android.graphics.Paint;
32 import android.graphics.Paint.FontMetrics;
33 import android.graphics.Bitmap;
34 import android.graphics.Canvas;
35 import android.graphics.Color;
36 import android.graphics.Matrix;
37 import android.net.Uri;
38 import java.net.URL;
39 import java.nio.ByteBuffer;
40 import java.io.File;
41 import java.io.InputStream;
42 import java.io.FileOutputStream;
43 import android.database.Cursor;
44 import android.provider.MediaStore;
45 import android.provider.MediaStore.MediaColumns;
46 import android.media.ExifInterface;
47 import org.jwz.xscreensaver.TTFAnalyzer;
48 import android.util.Log;
49
50 public class jwxyz {
51
52   private void LOG (String fmt, Object... args) {
53     Log.d ("xscreensaver",
54            this.getClass().getSimpleName() + ": " +
55            String.format (fmt, args));
56   }
57
58   private long nativeRunningHackPtr;
59
60   String hack;
61   Context app;
62   public final static int API_XLIB = 0;
63   public final static int API_GL   = 1;
64   Bitmap screenshot;
65
66   SharedPreferences prefs;
67   Hashtable<String, String> defaults = new Hashtable<String, String>();
68
69   // Maps numeric IDs to Java objects, so that C code can reference them
70   // even if GC relocates them.
71   private Hashtable<Long, Object> rootset = new Hashtable<Long, Object>();
72   private long rootset_id = 1000;
73
74
75   // Maps font names to either: String (system font) or Typeface (bundled).
76   private Hashtable<String, Object> all_fonts =
77     new Hashtable<String, Object>();
78
79
80   // These are defined in jwxyz-android.c:
81   //
82   private native void nativeInit (String hack, int api,
83                                   Hashtable<String,String> defaults,
84                                   int w, int h);
85   public native void nativeResize (int w, int h, double rot);
86   public native void nativeRender ();
87   public native void nativeDone ();
88   public native void sendButtonEvent (int x, int y, boolean down);
89   public native void sendMotionEvent (int x, int y);
90   public native void sendKeyEvent (boolean down_p, int code, int mods);
91
92   // Constructor
93   public jwxyz (String hack, int api, Context app, Bitmap screenshot,
94                 int w, int h) {
95
96     this.hack = hack;
97     this.app  = app;
98     this.screenshot = screenshot;
99
100     // nativeInit populates 'defaults' with the default values for keys
101     // that are not overridden by SharedPreferences.
102
103     prefs = app.getSharedPreferences (hack, 0);
104     scanSystemFonts();
105     nativeInit (hack, api, defaults, w, h);
106   }
107
108 /*  TODO: Can't do this yet; nativeDone requires the OpenGL context to be set.
109   protected void finalize() {
110     if (nativeRunningHackPtr != 0) {
111       nativeDone();
112     }
113   } */
114
115
116   public String getStringResource (String name) {
117
118     name = hack + "_" + name;
119
120     if (prefs.contains(name)) {
121
122       // SharedPreferences is very picky that you request the exact type that
123       // was stored: if it is a float and you ask for a string, you get an
124       // exception instead of the float converted to a string.
125
126       String s = null;
127       try { return prefs.getString (name, "");
128       } catch (Exception e) { }
129
130       try { return Float.toString (prefs.getFloat (name, 0));
131       } catch (Exception e) { }
132
133       try { return Long.toString (prefs.getLong (name, 0));
134       } catch (Exception e) { }
135
136       try { return Integer.toString (prefs.getInt (name, 0));
137       } catch (Exception e) { }
138
139       try { return (prefs.getBoolean (name, false) ? "true" : "false");
140       } catch (Exception e) { }
141     }
142
143     // If we got to here, it's not in there, so return the default.
144     return defaults.get (name);
145   }
146
147
148   private String mungeFontName (String name) {
149     // Roboto-ThinItalic => RobotoThin
150     // AndroidCock Regular => AndroidClock
151     String tails[] = { "Bold", "Italic", "Oblique", "Regular" };
152     for (String tail : tails) {
153       String pres[] = { " ", "-", "_", "" };
154       for (String pre : pres) {
155         int i = name.indexOf(pre + tail);
156         if (i > 0) name = name.substring (0, i);
157       }
158     }
159     return name;
160   }
161
162
163   private void scanSystemFonts() {
164
165     // First parse the system font directories for the global fonts.
166
167     String[] fontdirs = { "/system/fonts", "/system/font", "/data/fonts" };
168     TTFAnalyzer analyzer = new TTFAnalyzer();
169     for (String fontdir : fontdirs) {
170       File dir = new File(fontdir);
171       if (!dir.exists())
172         continue;
173       File[] files = dir.listFiles();
174       if (files == null)
175         continue;
176
177       for (File file : files) {
178         String name = analyzer.getTtfFontName (file.getAbsolutePath());
179         if (name == null) {
180           // LOG ("unparsable system font: %s", file);
181         } else {
182           name = mungeFontName (name);
183           if (! all_fonts.contains (name)) {
184             // LOG ("system font \"%s\" %s", name, file);
185             all_fonts.put (name, name);
186           }
187         }
188       }
189     }
190
191     // Now parse our assets, for our bundled fonts.
192
193     AssetManager am = app.getAssets();
194     String dir = "fonts";
195     String[] files = null;
196     try { files = am.list(dir); }
197     catch (Exception e) { LOG("listing assets: %s", e.toString()); }
198
199     for (String fn : files) {
200       String fn2 = dir + "/" + fn;
201       Typeface t = Typeface.createFromAsset (am, fn2);
202
203       File tmpfile = null;
204       try {
205         tmpfile = new File(app.getCacheDir(), fn);
206         if (tmpfile.createNewFile() == false) {
207           tmpfile.delete();
208           tmpfile.createNewFile();
209         }
210
211         InputStream in = am.open (fn2);
212         FileOutputStream out = new FileOutputStream (tmpfile);
213         byte[] buffer = new byte[1024 * 512];
214         while (in.read(buffer, 0, 1024 * 512) != -1) {
215           out.write(buffer);
216         }
217         out.close();
218         in.close();
219
220         String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath());
221         tmpfile.delete();
222
223         name = mungeFontName (name);
224         all_fonts.put (name, t);
225         // LOG ("asset font \"%s\" %s", name, fn);
226       } catch (Exception e) {
227         if (tmpfile != null) tmpfile.delete();
228         LOG ("error: %s", e.toString());
229       }
230     }
231   }
232
233
234   // Parses X Logical Font Descriptions, and a few standard X font names.
235   // Returns [ String name, Float size, Typeface ]
236   private Object[] parseXLFD (String name) {
237     float   size   = 12;
238     boolean bold   = false;
239     boolean italic = false;
240     boolean fixed  = false;
241     boolean serif  = false;
242
243     if      (name.equals("6x10"))     { size = 8;  fixed = true; }
244     else if (name.equals("6x10bold")) { size = 8;  fixed = true; bold = true; }
245     else if (name.equals("fixed"))    { size = 12; fixed = true; }
246     else if (name.equals("9x15"))     { size = 12; fixed = true; }
247     else if (name.equals("9x15bold")) { size = 12; fixed = true; bold = true; }
248     else if (name.equals("vga"))      { size = 12; fixed = true; }
249     else if (name.equals("console"))  { size = 12; fixed = true; }
250     else if (name.equals("gallant"))  { size = 12; fixed = true; }
251     else {
252       String[] tokens = name.split("-");        // XLFD
253       int L = tokens.length;
254       int i = 1;
255       String foundry  = (i < L ? tokens[i++] : "");
256       String family   = (i < L ? tokens[i++] : "");
257       String weight   = (i < L ? tokens[i++] : "");
258       String slant    = (i < L ? tokens[i++] : "");
259       String setwidth = (i < L ? tokens[i++] : "");
260       String adstyle  = (i < L ? tokens[i++] : "");
261       String pxsize   = (i < L ? tokens[i++] : "");
262       String ptsize   = (i < L ? tokens[i++] : "");
263       String resx     = (i < L ? tokens[i++] : "");
264       String resy     = (i < L ? tokens[i++] : "");
265       String spacing  = (i < L ? tokens[i++] : "");
266       String avgw     = (i < L ? tokens[i++] : "");
267       String charset  = (i < L ? tokens[i++] : "");
268       String registry = (i < L ? tokens[i++] : "");
269
270       if (spacing.equals("m") ||
271           family.equals("fixed") ||
272           family.equals("courier") ||
273           family.equals("console") ||
274           family.equals("lucidatypewriter")) {
275         fixed = true;
276       } else if (family.equals("times") ||
277                  family.equals("georgia")) {
278         serif = true;
279       }
280
281       if (weight.equals("bold") || weight.equals("demibold")) {
282         bold = true; 
283       }
284
285       if (slant.equals("i") || slant.equals("o")) {
286         italic = true;
287       }
288
289       // -*-courier-bold-r-*-*-14-*-*-*-*-*-*-*         14 px
290       // -*-courier-bold-r-*-*-*-140-*-*-m-*-*-*        14 pt
291       // -*-courier-bold-r-*-*-140-*                    14 pt, via wildcard
292       // -*-courier-bold-r-*-140-*                      14 pt, not handled
293       // -*-courier-bold-r-*-*-14-180-*-*-*-*-*-*       error
294
295       if (!ptsize.equals("") && !ptsize.equals("*")) {
296         // It was in the ptsize field, so that's definitely what it is.
297         size = Float.valueOf(ptsize) / 10.0f;
298       } else if (!pxsize.equals("") && !pxsize.equals("*")) {
299         size = Float.valueOf(pxsize);
300         // If it's a fully qualified XLFD, then this really is the pxsize.
301         // Otherwise, this is probably point size with a multi-field wildcard.
302         if (registry.equals(""))   // not a fully qualified XLFD
303           size /= 10.0f;
304       }
305     }
306
307     if (name.equals("random")) {
308       Random r = new Random();
309       serif = r.nextBoolean();      // Not much to randomize here...
310       fixed = (r.nextInt(8) == 0);
311     }
312
313     name = (fixed
314             ? (serif ? "serif-monospace" : "monospace")
315             : (serif ? "serif" : "sans-serif"));
316
317     Typeface font = Typeface.create (name,
318                                      (bold && italic ? Typeface.BOLD_ITALIC :
319                                       bold   ? Typeface.BOLD :
320                                       italic ? Typeface.ITALIC :
321                                       Typeface.NORMAL));
322
323     Object ret[] = { name, new Float(size), font };
324     return ret;
325   }
326
327
328   // Parses "Native Font Name One 12, Native Font Name Two 14".
329   // Returns [ String name, Float size, Typeface ]
330   private Object[] parseNativeFont (String names) {
331     for (String name : names.split(",")) {
332       float size = 0;
333       name = name.trim();
334       if (name.equals("")) continue;
335       int spc = name.lastIndexOf(" ");
336       if (spc > 0) {
337         size = Float.valueOf (name.substring (spc + 1));
338         name = name.substring (0, spc);
339       }
340       if (size <= 0)
341         size = 12;
342
343       Object font = all_fonts.get (name);
344       if (font instanceof String)
345         font = Typeface.create (name, Typeface.NORMAL);
346
347       if (font != null) {
348         Object ret[] = { name, size, (Typeface) font };
349         return ret;
350       }
351     }
352
353     return null;
354   }
355
356
357   // Returns [ Long font_id, String name, Float size, ascent, descent ]
358   public Object[] loadFont(String name) {
359     Object pair[];
360
361     if (name.equals("")) return null;
362
363     if (name.contains(" ")) {
364       pair = parseNativeFont (name);
365     } else {
366       pair = parseXLFD (name);
367     }
368
369     if (pair == null) return null;
370
371     String name2  = (String)   pair[0];
372     float size    = (Float)    pair[1];
373     Typeface font = (Typeface) pair[2];
374
375     size *= 2;
376
377     name2 += (font.isBold() && font.isItalic() ? " bold italic" :
378               font.isBold()   ? " bold"   :
379               font.isItalic() ? " italic" :
380               "");
381     Paint paint = new Paint();
382     paint.setTypeface (font);
383     paint.setTextSize (size);
384     paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
385
386     Long font_key = new Long(rootset_id++);
387     rootset.put (font_key, paint);
388
389     LOG ("load font %s, \"%s\" = \"%s %.1f\"",
390          font_key.toString(), name, name2, size);
391
392     FontMetrics fm = paint.getFontMetrics();
393     Object ret[] = { font_key, name2, new Float(size),
394                      new Float(-fm.ascent), new Float(fm.descent) };
395     return ret;
396   }
397
398
399   public void releaseFont(long font_id) {
400     rootset.remove (new Long(font_id));
401   }
402
403   /* Returns a byte[] array containing XCharStruct with an optional
404      bitmap appended to it.
405      lbearing, rbearing, width, ascent, descent: 2 bytes each.
406      Followed by a WxH pixmap, 32 bits per pixel.
407    */
408   public ByteBuffer renderText (long font_id, String text, boolean render_p) {
409
410     Paint paint = (Paint) rootset.get(new Long(font_id));
411
412     if (paint == null) {
413       LOG ("no font %d", font_id);
414       return null;
415     }
416
417     /* Font metric terminology, as used by X11:
418
419        "lbearing" is the distance from the logical origin to the leftmost
420        pixel.  If a character's ink extends to the left of the origin, it is
421        negative.
422
423        "rbearing" is the distance from the logical origin to the rightmost
424        pixel.
425
426        "descent" is the distance from the logical origin to the bottommost
427        pixel.  For characters with descenders, it is positive.  For
428        superscripts, it is negative.
429
430        "ascent" is the distance from the logical origin to the topmost pixel.
431        It is the number of pixels above the baseline.
432
433        "width" is the distance from the logical origin to the position where
434        the logical origin of the next character should be placed.
435
436        If "rbearing" is greater than "width", then this character overlaps the
437        following character.  If smaller, then there is trailing blank space.
438
439        The bbox coordinates returned by getTextBounds grow down and right:
440        for a character with ink both above and below the baseline, top is
441        negative and bottom is positive.
442      */
443     FontMetrics fm = paint.getFontMetrics();
444     Rect bbox = new Rect();
445     paint.getTextBounds (text, 0, text.length(), bbox);
446
447     /* The bbox returned by getTextBounds measures from the logical origin
448        with right and down being positive.  This means most characters have
449        a negative top, and characters with descenders have a positive bottom.
450      */
451     int lbearing  =  bbox.left;
452     int rbearing  =  bbox.right;
453     int ascent    = -bbox.top;
454     int descent   =  bbox.bottom;
455     int width     = (int) paint.measureText (text);
456
457     int w = rbearing - lbearing;
458     int h = ascent + descent;
459     int size = 5 * 2 + (render_p ? w * h * 4 : 0);
460
461     ByteBuffer bits = ByteBuffer.allocateDirect (size);
462
463     bits.put ((byte) ((lbearing >> 8) & 0xFF));
464     bits.put ((byte) ( lbearing       & 0xFF));
465     bits.put ((byte) ((rbearing >> 8) & 0xFF));
466     bits.put ((byte) ( rbearing       & 0xFF));
467     bits.put ((byte) ((width    >> 8) & 0xFF));
468     bits.put ((byte) ( width          & 0xFF));
469     bits.put ((byte) ((ascent   >> 8) & 0xFF));
470     bits.put ((byte) ( ascent         & 0xFF));
471     bits.put ((byte) ((descent  >> 8) & 0xFF));
472     bits.put ((byte) ( descent        & 0xFF));
473
474     if (render_p && w > 0 && h > 0) {
475       Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
476       Canvas canvas = new Canvas (bitmap);
477       canvas.drawText (text, -lbearing, ascent, paint);
478       bitmap.copyPixelsToBuffer (bits);
479       bitmap.recycle();
480     }
481
482     return bits;
483   }
484
485
486   // Returns the contents of the URL.  Blocking load.
487   public ByteBuffer loadURL (String url) {
488
489     int size0 = 10240;
490     int size = size0;
491     int count = 0;
492     ByteBuffer body = ByteBuffer.allocateDirect (size);
493
494     try {
495       LOG ("load URL: %s", url);
496       URL u = new URL(url);
497       InputStream s = u.openStream();
498       byte buf[] = new byte[10240];
499       while (true) {
500         int n = s.read (buf);
501         if (n == -1) break;
502         // LOG ("read %d", n);
503         if (count + n + 1 >= size) {
504           int size2 = (int) (size * 1.2 + size0);
505           // LOG ("expand %d -> %d", size, size2);
506           ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
507           body.rewind();
508           body2.put (body);
509           body2.position (count);
510           body = body2;
511           size = size2;
512         }
513         body.put (buf, 0, n);
514         count += n;
515       }
516     } catch (Exception e) {
517       LOG ("load URL error: %s", e.toString());
518       body.clear();
519       body.put (e.toString().getBytes());
520       body.put ((byte) 0);
521     }
522
523     return body;
524   }
525
526
527   private ByteBuffer convertBitmap (String name, Bitmap bitmap,
528                                     int target_width, int target_height,
529                                     ExifInterface exif,
530                                     boolean rotate_p) {
531     if (bitmap == null) return null;
532
533     try {
534
535       int width  = bitmap.getWidth();
536       int height = bitmap.getHeight();
537
538       LOG ("read image %s: %d x %d", name, width, height);
539
540       // First rotate the image as per EXIF.
541
542       if (exif != null) {
543         int deg = 0;
544         switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
545                                       ExifInterface.ORIENTATION_NORMAL)) {
546         case ExifInterface.ORIENTATION_ROTATE_90:  deg = 90;  break;
547         case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
548         case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
549         }
550         if (deg != 0) {
551           LOG ("%s: EXIF rotate %d", name, deg);
552           Matrix matrix = new Matrix();
553           matrix.preRotate (deg);
554           bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
555                                         matrix, true);
556           width  = bitmap.getWidth();
557           height = bitmap.getHeight();
558         }
559       }
560
561       // If the caller requested that we rotate the image to best fit the
562       // screen, rotate it again.  (Could combine this with the above and
563       // avoid copying the image again, but I'm lazy.)
564
565       if (rotate_p &&
566           (width > height) != (target_width > target_height)) {
567         LOG ("%s: rotated to fit screen", name);
568         Matrix matrix = new Matrix();
569         matrix.preRotate (90);
570         bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
571                                       matrix, true);
572         width  = bitmap.getWidth();
573         height = bitmap.getHeight();
574       }
575
576       // Resize the image to be not larger than the screen, potentially
577       // copying it for the third time.
578       // Actually, always scale it, scaling up if necessary.
579
580 //    if (width > target_width || height > target_height)
581       {
582         float r1 = target_width  / (float) width;
583         float r2 = target_height / (float) height;
584         float r = (r1 > r2 ? r2 : r1);
585         LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
586              r, width, height, (int) (width * r), (int) (height * r));
587         width  = (int) (width * r);
588         height = (int) (height * r);
589         bitmap = Bitmap.createScaledBitmap (bitmap, width, height, true);
590         width  = bitmap.getWidth();
591         height = bitmap.getHeight();
592       }
593
594       // Now convert it to a ByteBuffer in the form expected by the C caller.
595
596       byte[] nameb = name.getBytes("UTF-8");
597       int size     = bitmap.getByteCount() + 4 + nameb.length + 1;
598
599       ByteBuffer bits = ByteBuffer.allocateDirect (size);
600
601       bits.put ((byte) ((width  >> 8) & 0xFF));
602       bits.put ((byte) ( width        & 0xFF));
603       bits.put ((byte) ((height >> 8) & 0xFF));
604       bits.put ((byte) ( height       & 0xFF));
605       bits.put (nameb);
606       bits.put ((byte) 0);
607
608       // The fourth of five copies.  Good thing these are supercomputers.
609       bitmap.copyPixelsToBuffer (bits);
610
611       return bits;
612
613     } catch (Exception e) {
614       LOG ("image %s unreadable: %s", name, e.toString());
615     }
616
617     return null;
618   }
619
620
621   public ByteBuffer loadRandomImage (int target_width, int target_height,
622                                      boolean rotate_p) {
623
624     int min_size = 480;
625     int max_size = 0x7FFF;
626
627     ArrayList<String> imgs = new ArrayList<String>();
628
629     ContentResolver cr = app.getContentResolver();
630     String[] cols = { MediaColumns.DATA,
631                       MediaColumns.MIME_TYPE,
632                       MediaColumns.WIDTH,
633                       MediaColumns.HEIGHT };
634     Uri uris[] = {
635       android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
636       android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
637
638     for (int i = 0; i < uris.length; i++) {
639       Cursor cursor = cr.query (uris[i], cols, null, null, null);
640       int j = 0;
641       int path_col   = cursor.getColumnIndexOrThrow (cols[j++]);
642       int type_col   = cursor.getColumnIndexOrThrow (cols[j++]);
643       int width_col  = cursor.getColumnIndexOrThrow (cols[j++]);
644       int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
645       while (cursor.moveToNext()) {
646         String path = cursor.getString(path_col);
647         String type = cursor.getString(type_col);
648         int w = Integer.parseInt (cursor.getString(width_col));
649         int h = Integer.parseInt (cursor.getString(height_col));
650         if (type.startsWith("image/") &&
651             w > min_size && h > min_size &&
652             w < max_size && h < max_size) {
653           imgs.add (path);
654         }
655       }
656       cursor.close();
657     }
658
659     String which = null;
660
661     int count = imgs.size();
662     if (count == 0) {
663       LOG ("no images");
664       return null;
665     }
666
667     int i = new Random().nextInt (count);
668     which = imgs.get (i);
669     LOG ("picked image %d of %d: %s", i, count, which);
670
671     Uri uri = Uri.fromFile (new File (which));
672     String name = uri.getLastPathSegment();
673     Bitmap bitmap = null;
674     ExifInterface exif = null;
675
676     try {
677       bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
678     } catch (Exception e) {
679       LOG ("image %s unloadable: %s", which, e.toString());
680       return null;
681     }
682
683     try {
684       exif = new ExifInterface (uri.getPath());  // If it fails, who cares
685     } catch (Exception e) {
686     }
687
688     ByteBuffer bits = convertBitmap (name, bitmap,
689                                      target_width, target_height,
690                                      exif, rotate_p);
691     bitmap.recycle();
692     return bits;
693   }
694
695
696   public ByteBuffer getScreenshot (int target_width, int target_height,
697                                    boolean rotate_p) {
698     return convertBitmap ("Screenshot", screenshot,
699                           target_width, target_height,
700                           null, rotate_p);
701   }
702
703
704   // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
705   //
706   private static final int ShiftMask =     (1<<0);
707   private static final int LockMask =      (1<<1);
708   private static final int ControlMask =   (1<<2);
709   private static final int Mod1Mask =      (1<<3);
710   private static final int Mod2Mask =      (1<<4);
711   private static final int Mod3Mask =      (1<<5);
712   private static final int Mod4Mask =      (1<<6);
713   private static final int Mod5Mask =      (1<<7);
714   private static final int Button1Mask =   (1<<8);
715   private static final int Button2Mask =   (1<<9);
716   private static final int Button3Mask =   (1<<10);
717   private static final int Button4Mask =   (1<<11);
718   private static final int Button5Mask =   (1<<12);
719
720   private static final int XK_Shift_L =    0xFFE1;
721   private static final int XK_Shift_R =    0xFFE2;
722   private static final int XK_Control_L =  0xFFE3;
723   private static final int XK_Control_R =  0xFFE4;
724   private static final int XK_Caps_Lock =  0xFFE5;
725   private static final int XK_Shift_Lock = 0xFFE6;
726   private static final int XK_Meta_L =     0xFFE7;
727   private static final int XK_Meta_R =     0xFFE8;
728   private static final int XK_Alt_L =      0xFFE9;
729   private static final int XK_Alt_R =      0xFFEA;
730   private static final int XK_Super_L =    0xFFEB;
731   private static final int XK_Super_R =    0xFFEC;
732   private static final int XK_Hyper_L =    0xFFED;
733   private static final int XK_Hyper_R =    0xFFEE;
734
735   private static final int XK_Home =       0xFF50;
736   private static final int XK_Left =       0xFF51;
737   private static final int XK_Up =         0xFF52;
738   private static final int XK_Right =      0xFF53;
739   private static final int XK_Down =       0xFF54;
740   private static final int XK_Prior =      0xFF55;
741   private static final int XK_Page_Up =    0xFF55;
742   private static final int XK_Next =       0xFF56;
743   private static final int XK_Page_Down =  0xFF56;
744   private static final int XK_End =        0xFF57;
745   private static final int XK_Begin =      0xFF58;
746
747   private static final int XK_F1 =         0xFFBE;
748   private static final int XK_F2 =         0xFFBF;
749   private static final int XK_F3 =         0xFFC0;
750   private static final int XK_F4 =         0xFFC1;
751   private static final int XK_F5 =         0xFFC2;
752   private static final int XK_F6 =         0xFFC3;
753   private static final int XK_F7 =         0xFFC4;
754   private static final int XK_F8 =         0xFFC5;
755   private static final int XK_F9 =         0xFFC6;
756   private static final int XK_F10 =        0xFFC7;
757   private static final int XK_F11 =        0xFFC8;
758   private static final int XK_F12 =        0xFFC9;
759
760   public void sendKeyEvent (KeyEvent event) {
761     int uc    = event.getUnicodeChar();
762     int jcode = event.getKeyCode();
763     int jmods = event.getModifiers();
764     int xcode = 0;
765     int xmods = 0;
766
767     switch (jcode) {
768     case KeyEvent.KEYCODE_SHIFT_LEFT:        xcode = XK_Shift_L;   break;
769     case KeyEvent.KEYCODE_SHIFT_RIGHT:       xcode = XK_Shift_R;   break;
770     case KeyEvent.KEYCODE_CTRL_LEFT:         xcode = XK_Control_L; break;
771     case KeyEvent.KEYCODE_CTRL_RIGHT:        xcode = XK_Control_R; break;
772     case KeyEvent.KEYCODE_CAPS_LOCK:         xcode = XK_Caps_Lock; break;
773     case KeyEvent.KEYCODE_META_LEFT:         xcode = XK_Meta_L;    break;
774     case KeyEvent.KEYCODE_META_RIGHT:        xcode = XK_Meta_R;    break;
775     case KeyEvent.KEYCODE_ALT_LEFT:          xcode = XK_Alt_L;     break;
776     case KeyEvent.KEYCODE_ALT_RIGHT:         xcode = XK_Alt_R;     break;
777
778     case KeyEvent.KEYCODE_HOME:              xcode = XK_Home;      break;
779     case KeyEvent.KEYCODE_DPAD_LEFT:         xcode = XK_Left;      break;
780     case KeyEvent.KEYCODE_DPAD_UP:           xcode = XK_Up;        break;
781     case KeyEvent.KEYCODE_DPAD_RIGHT:        xcode = XK_Right;     break;
782     case KeyEvent.KEYCODE_DPAD_DOWN:         xcode = XK_Down;      break;
783   //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior;     break;
784     case KeyEvent.KEYCODE_PAGE_UP:           xcode = XK_Page_Up;   break;
785   //case KeyEvent.KEYCODE_NAVIGATE_NEXT:     xcode = XK_Next;      break;
786     case KeyEvent.KEYCODE_PAGE_DOWN:         xcode = XK_Page_Down; break;
787     case KeyEvent.KEYCODE_MOVE_END:          xcode = XK_End;       break;
788     case KeyEvent.KEYCODE_MOVE_HOME:         xcode = XK_Begin;     break;
789
790     case KeyEvent.KEYCODE_F1:                xcode = XK_F1;        break;
791     case KeyEvent.KEYCODE_F2:                xcode = XK_F2;        break;
792     case KeyEvent.KEYCODE_F3:                xcode = XK_F3;        break;
793     case KeyEvent.KEYCODE_F4:                xcode = XK_F4;        break;
794     case KeyEvent.KEYCODE_F5:                xcode = XK_F5;        break;
795     case KeyEvent.KEYCODE_F6:                xcode = XK_F6;        break;
796     case KeyEvent.KEYCODE_F7:                xcode = XK_F7;        break;
797     case KeyEvent.KEYCODE_F8:                xcode = XK_F8;        break;
798     case KeyEvent.KEYCODE_F9:                xcode = XK_F9;        break;
799     case KeyEvent.KEYCODE_F10:               xcode = XK_F10;       break;
800     case KeyEvent.KEYCODE_F11:               xcode = XK_F11;       break;
801     case KeyEvent.KEYCODE_F12:               xcode = XK_F12;       break;
802     default:                                 xcode = uc;           break;
803     }
804
805     if (0 != (jmods & KeyEvent.META_SHIFT_ON))     xmods |= ShiftMask;
806     if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
807     if (0 != (jmods & KeyEvent.META_CTRL_MASK))    xmods |= ControlMask;
808     if (0 != (jmods & KeyEvent.META_ALT_MASK))     xmods |= Mod1Mask;
809     if (0 != (jmods & KeyEvent.META_META_ON))      xmods |= Mod1Mask;
810     if (0 != (jmods & KeyEvent.META_SYM_ON))       xmods |= Mod2Mask;
811     if (0 != (jmods & KeyEvent.META_FUNCTION_ON))  xmods |= Mod3Mask;
812
813     /* If you touch and release Shift, you get no events.
814        If you type Shift-A, you get Shift down, A down, A up, Shift up.
815        So let's just ignore all lone modifier key events.
816      */
817     if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
818       return;
819
820     boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
821     sendKeyEvent (down_p, xcode, xmods);
822   }
823
824 }