1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org>
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
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.
17 package org.jwz.xscreensaver;
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;
39 import java.nio.ByteBuffer;
41 import java.io.InputStream;
42 import java.io.FileOutputStream;
43 import java.lang.Runnable;
44 import java.lang.Thread;
45 import android.database.Cursor;
46 import android.provider.MediaStore;
47 import android.provider.MediaStore.MediaColumns;
48 import android.media.ExifInterface;
49 import org.jwz.xscreensaver.TTFAnalyzer;
50 import android.util.Log;
54 private void LOG (String fmt, Object... args) {
55 Log.d ("xscreensaver",
56 this.getClass().getSimpleName() + ": " +
57 String.format (fmt, args));
60 private long nativeRunningHackPtr;
66 public final static int STYLE_BOLD = 1;
67 public final static int STYLE_ITALIC = 2;
68 public final static int STYLE_MONOSPACE = 4;
70 public final static int FONT_FAMILY = 0;
71 public final static int FONT_FACE = 1;
72 public final static int FONT_RANDOM = 2;
74 SharedPreferences prefs;
75 Hashtable<String, String> defaults = new Hashtable<String, String>();
78 // Maps font names to either: String (system font) or Typeface (bundled).
79 private Hashtable<String, Object> all_fonts =
80 new Hashtable<String, Object>();
83 // These are defined in jwxyz-android.c:
85 private native void nativeInit (String hack,
86 Hashtable<String,String> defaults,
88 public native void nativeResize (int w, int h, double rot);
89 public native long nativeRender ();
90 public native void nativeDone ();
91 public native void sendButtonEvent (int x, int y, boolean down);
92 public native void sendMotionEvent (int x, int y);
93 public native void sendKeyEvent (boolean down_p, int code, int mods);
96 public jwxyz (String hack, Context app, Bitmap screenshot, int w, int h) {
100 this.screenshot = screenshot;
102 // nativeInit populates 'defaults' with the default values for keys
103 // that are not overridden by SharedPreferences.
105 prefs = app.getSharedPreferences (hack, 0);
107 nativeInit (hack, defaults, w, h);
110 /* TODO: Can't do this yet; nativeDone requires the OpenGL context to be set.
111 protected void finalize() {
112 if (nativeRunningHackPtr != 0) {
118 public String getStringResource (String name) {
120 name = hack + "_" + name;
122 if (prefs.contains(name)) {
124 // SharedPreferences is very picky that you request the exact type that
125 // was stored: if it is a float and you ask for a string, you get an
126 // exception instead of the float converted to a string.
129 try { return prefs.getString (name, "");
130 } catch (Exception e) { }
132 try { return Float.toString (prefs.getFloat (name, 0));
133 } catch (Exception e) { }
135 try { return Long.toString (prefs.getLong (name, 0));
136 } catch (Exception e) { }
138 try { return Integer.toString (prefs.getInt (name, 0));
139 } catch (Exception e) { }
141 try { return (prefs.getBoolean (name, false) ? "true" : "false");
142 } catch (Exception e) { }
145 // If we got to here, it's not in there, so return the default.
146 return defaults.get (name);
150 private String mungeFontName (String name) {
151 // Roboto-ThinItalic => RobotoThin
152 // AndroidCock Regular => AndroidClock
153 String tails[] = { "Bold", "Italic", "Oblique", "Regular" };
154 for (String tail : tails) {
155 String pres[] = { " ", "-", "_", "" };
156 for (String pre : pres) {
157 int i = name.indexOf(pre + tail);
158 if (i > 0) name = name.substring (0, i);
165 private void scanSystemFonts() {
167 // First parse the system font directories for the global fonts.
169 String[] fontdirs = { "/system/fonts", "/system/font", "/data/fonts" };
170 TTFAnalyzer analyzer = new TTFAnalyzer();
171 for (String fontdir : fontdirs) {
172 File dir = new File(fontdir);
175 File[] files = dir.listFiles();
179 for (File file : files) {
180 String name = analyzer.getTtfFontName (file.getAbsolutePath());
182 // LOG ("unparsable system font: %s", file);
184 name = mungeFontName (name);
185 if (! all_fonts.contains (name)) {
186 // LOG ("system font \"%s\" %s", name, file);
187 all_fonts.put (name, name);
193 // Now parse our assets, for our bundled fonts.
195 AssetManager am = app.getAssets();
196 String dir = "fonts";
197 String[] files = null;
198 try { files = am.list(dir); }
199 catch (Exception e) { LOG("listing assets: %s", e.toString()); }
201 for (String fn : files) {
202 String fn2 = dir + "/" + fn;
203 Typeface t = Typeface.createFromAsset (am, fn2);
207 tmpfile = new File(app.getCacheDir(), fn);
208 if (tmpfile.createNewFile() == false) {
210 tmpfile.createNewFile();
213 InputStream in = am.open (fn2);
214 FileOutputStream out = new FileOutputStream (tmpfile);
215 byte[] buffer = new byte[1024 * 512];
216 while (in.read(buffer, 0, 1024 * 512) != -1) {
222 String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath());
225 name = mungeFontName (name);
226 all_fonts.put (name, t);
227 // LOG ("asset font \"%s\" %s", name, fn);
228 } catch (Exception e) {
229 if (tmpfile != null) tmpfile.delete();
230 LOG ("error: %s", e.toString());
236 // Parses family names from X Logical Font Descriptions, including a few
237 // standard X font names that aren't handled by try_xlfd_font().
238 // Returns [ String name, Typeface ]
239 private Object[] parseXLFD (int mask, int traits,
240 String name, int name_type) {
241 boolean fixed = false;
242 boolean serif = false;
244 int style_jwxyz = mask & traits;
246 if (name_type != FONT_RANDOM) {
247 if ((style_jwxyz & STYLE_BOLD) != 0 ||
248 name.equals("fixed") ||
249 name.equals("courier") ||
250 name.equals("console") ||
251 name.equals("lucidatypewriter") ||
252 name.equals("monospace")) {
254 } else if (name.equals("times") ||
255 name.equals("georgia") ||
256 name.equals("serif")) {
258 } else if (name.equals("serif-monospace")) {
263 Random r = new Random();
264 serif = r.nextBoolean(); // Not much to randomize here...
265 fixed = (r.nextInt(8) == 0);
269 ? (serif ? "serif-monospace" : "monospace")
270 : (serif ? "serif" : "sans-serif"));
272 int style_android = 0;
273 if ((style_jwxyz & STYLE_BOLD) != 0)
274 style_android |= Typeface.BOLD;
275 if ((style_jwxyz & STYLE_ITALIC) != 0)
276 style_android |= Typeface.ITALIC;
278 return new Object[] { name, Typeface.create(name, style_android) };
282 // Parses "Native Font Name One 12, Native Font Name Two 14".
283 // Returns [ String name, Typeface ]
284 private Object[] parseNativeFont (String name) {
285 Object font2 = all_fonts.get (name);
286 if (font2 instanceof String)
287 font2 = Typeface.create (name, Typeface.NORMAL);
288 return new Object[] { name, (Typeface)font2 };
292 // Returns [ Paint paint, String family_name, Float ascent, Float descent ]
293 public Object[] loadFont(int mask, int traits, String name, int name_type,
297 if (name_type != FONT_RANDOM && name.equals("")) return null;
299 if (name_type == FONT_FACE) {
300 pair = parseNativeFont (name);
302 pair = parseXLFD (mask, traits, name, name_type);
305 String name2 = (String) pair[0];
306 Typeface font = (Typeface) pair[1];
310 String suffix = (font.isBold() && font.isItalic() ? " bold italic" :
311 font.isBold() ? " bold" :
312 font.isItalic() ? " italic" :
314 Paint paint = new Paint();
315 paint.setTypeface (font);
316 paint.setTextSize (size);
317 paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
319 LOG ("load font \"%s\" = \"%s %.1f\"", name, name2 + suffix, size);
321 FontMetrics fm = paint.getFontMetrics();
322 return new Object[] { paint, name2, -fm.ascent, fm.descent };
326 /* Returns a byte[] array containing XCharStruct with an optional
327 bitmap appended to it.
328 lbearing, rbearing, width, ascent, descent: 2 bytes each.
329 Followed by a WxH pixmap, 32 bits per pixel.
331 public ByteBuffer renderText (Paint paint, String text, boolean render_p) {
338 /* Font metric terminology, as used by X11:
340 "lbearing" is the distance from the logical origin to the leftmost
341 pixel. If a character's ink extends to the left of the origin, it is
344 "rbearing" is the distance from the logical origin to the rightmost
347 "descent" is the distance from the logical origin to the bottommost
348 pixel. For characters with descenders, it is positive. For
349 superscripts, it is negative.
351 "ascent" is the distance from the logical origin to the topmost pixel.
352 It is the number of pixels above the baseline.
354 "width" is the distance from the logical origin to the position where
355 the logical origin of the next character should be placed.
357 If "rbearing" is greater than "width", then this character overlaps the
358 following character. If smaller, then there is trailing blank space.
360 The bbox coordinates returned by getTextBounds grow down and right:
361 for a character with ink both above and below the baseline, top is
362 negative and bottom is positive.
364 FontMetrics fm = paint.getFontMetrics();
365 Rect bbox = new Rect();
366 paint.getTextBounds (text, 0, text.length(), bbox);
368 /* The bbox returned by getTextBounds measures from the logical origin
369 with right and down being positive. This means most characters have
370 a negative top, and characters with descenders have a positive bottom.
372 int lbearing = bbox.left;
373 int rbearing = bbox.right;
374 int ascent = -bbox.top;
375 int descent = bbox.bottom;
376 int width = (int) paint.measureText (text);
378 int w = rbearing - lbearing;
379 int h = ascent + descent;
380 int size = 5 * 2 + (render_p ? w * h * 4 : 0);
382 ByteBuffer bits = ByteBuffer.allocateDirect (size);
384 bits.put ((byte) ((lbearing >> 8) & 0xFF));
385 bits.put ((byte) ( lbearing & 0xFF));
386 bits.put ((byte) ((rbearing >> 8) & 0xFF));
387 bits.put ((byte) ( rbearing & 0xFF));
388 bits.put ((byte) ((width >> 8) & 0xFF));
389 bits.put ((byte) ( width & 0xFF));
390 bits.put ((byte) ((ascent >> 8) & 0xFF));
391 bits.put ((byte) ( ascent & 0xFF));
392 bits.put ((byte) ((descent >> 8) & 0xFF));
393 bits.put ((byte) ( descent & 0xFF));
395 if (render_p && w > 0 && h > 0) {
396 Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
397 Canvas canvas = new Canvas (bitmap);
398 canvas.drawText (text, -lbearing, ascent, paint);
399 bitmap.copyPixelsToBuffer (bits);
407 /* Returns the contents of the URL.
408 Loads the URL in a background thread: if the URL has not yet loaded,
409 this will return null. Once the URL has completely loaded, the full
410 contents will be returned. Calling this again after that starts the
413 private String loading_url = null;
414 private ByteBuffer loaded_url_body = null;
416 public synchronized ByteBuffer loadURL (String url) {
418 if (loaded_url_body != null) { // Thread finished
420 // LOG ("textclient finished %s", loading_url);
422 ByteBuffer bb = loaded_url_body;
424 loaded_url_body = null;
427 } else if (loading_url != null) { // Waiting on thread
428 // LOG ("textclient waiting...");
431 } else { // Launch thread
434 LOG ("textclient launching %s...", url);
436 new Thread (new Runnable() {
441 ByteBuffer body = ByteBuffer.allocateDirect (size);
444 URL u = new URL (loading_url);
445 // LOG ("textclient thread loading: %s", u.toString());
446 InputStream s = u.openStream();
447 byte buf[] = new byte[10240];
449 int n = s.read (buf);
451 // LOG ("textclient thread read %d", n);
452 if (count + n + 1 >= size) {
453 int size2 = (int) (size * 1.2 + size0);
454 // LOG ("textclient thread expand %d -> %d", size, size2);
455 ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
458 body2.position (count);
462 body.put (buf, 0, n);
465 } catch (Exception e) {
466 LOG ("load URL error: %s", e.toString());
468 body.put (e.toString().getBytes());
472 // LOG ("textclient thread finished %s (%d)", loading_url, size);
473 loaded_url_body = body;
482 private ByteBuffer convertBitmap (String name, Bitmap bitmap,
483 int target_width, int target_height,
486 if (bitmap == null) return null;
490 int width = bitmap.getWidth();
491 int height = bitmap.getHeight();
493 LOG ("read image %s: %d x %d", name, width, height);
495 // First rotate the image as per EXIF.
499 switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
500 ExifInterface.ORIENTATION_NORMAL)) {
501 case ExifInterface.ORIENTATION_ROTATE_90: deg = 90; break;
502 case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
503 case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
506 LOG ("%s: EXIF rotate %d", name, deg);
507 Matrix matrix = new Matrix();
508 matrix.preRotate (deg);
509 bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
511 width = bitmap.getWidth();
512 height = bitmap.getHeight();
516 // If the caller requested that we rotate the image to best fit the
517 // screen, rotate it again. (Could combine this with the above and
518 // avoid copying the image again, but I'm lazy.)
521 (width > height) != (target_width > target_height)) {
522 LOG ("%s: rotated to fit screen", name);
523 Matrix matrix = new Matrix();
524 matrix.preRotate (90);
525 bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
527 width = bitmap.getWidth();
528 height = bitmap.getHeight();
531 // Resize the image to be not larger than the screen, potentially
532 // copying it for the third time.
533 // Actually, always scale it, scaling up if necessary.
535 // if (width > target_width || height > target_height)
537 float r1 = target_width / (float) width;
538 float r2 = target_height / (float) height;
539 float r = (r1 > r2 ? r2 : r1);
540 LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
541 r, width, height, (int) (width * r), (int) (height * r));
542 width = (int) (width * r);
543 height = (int) (height * r);
544 bitmap = Bitmap.createScaledBitmap (bitmap, width, height, true);
545 width = bitmap.getWidth();
546 height = bitmap.getHeight();
549 // Now convert it to a ByteBuffer in the form expected by the C caller.
551 byte[] nameb = name.getBytes("UTF-8");
552 int size = bitmap.getByteCount() + 4 + nameb.length + 1;
554 ByteBuffer bits = ByteBuffer.allocateDirect (size);
556 bits.put ((byte) ((width >> 8) & 0xFF));
557 bits.put ((byte) ( width & 0xFF));
558 bits.put ((byte) ((height >> 8) & 0xFF));
559 bits.put ((byte) ( height & 0xFF));
563 // The fourth of five copies. Good thing these are supercomputers.
564 bitmap.copyPixelsToBuffer (bits);
568 } catch (Exception e) {
569 LOG ("image %s unreadable: %s", name, e.toString());
576 public ByteBuffer loadRandomImage (int target_width, int target_height,
580 int max_size = 0x7FFF;
582 ArrayList<String> imgs = new ArrayList<String>();
584 ContentResolver cr = app.getContentResolver();
585 String[] cols = { MediaColumns.DATA,
586 MediaColumns.MIME_TYPE,
588 MediaColumns.HEIGHT };
590 android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
591 android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
593 for (int i = 0; i < uris.length; i++) {
594 Cursor cursor = cr.query (uris[i], cols, null, null, null);
596 int path_col = cursor.getColumnIndexOrThrow (cols[j++]);
597 int type_col = cursor.getColumnIndexOrThrow (cols[j++]);
598 int width_col = cursor.getColumnIndexOrThrow (cols[j++]);
599 int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
600 while (cursor.moveToNext()) {
601 String path = cursor.getString(path_col);
602 String type = cursor.getString(type_col);
603 int w = Integer.parseInt (cursor.getString(width_col));
604 int h = Integer.parseInt (cursor.getString(height_col));
605 if (type.startsWith("image/") &&
606 w > min_size && h > min_size &&
607 w < max_size && h < max_size) {
616 int count = imgs.size();
622 int i = new Random().nextInt (count);
623 which = imgs.get (i);
624 LOG ("picked image %d of %d: %s", i, count, which);
626 Uri uri = Uri.fromFile (new File (which));
627 String name = uri.getLastPathSegment();
628 Bitmap bitmap = null;
629 ExifInterface exif = null;
632 bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
633 } catch (Exception e) {
634 LOG ("image %s unloadable: %s", which, e.toString());
639 exif = new ExifInterface (uri.getPath()); // If it fails, who cares
640 } catch (Exception e) {
643 ByteBuffer bits = convertBitmap (name, bitmap,
644 target_width, target_height,
651 public ByteBuffer getScreenshot (int target_width, int target_height,
653 return convertBitmap ("Screenshot", screenshot,
654 target_width, target_height,
659 // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
661 private static final int ShiftMask = (1<<0);
662 private static final int LockMask = (1<<1);
663 private static final int ControlMask = (1<<2);
664 private static final int Mod1Mask = (1<<3);
665 private static final int Mod2Mask = (1<<4);
666 private static final int Mod3Mask = (1<<5);
667 private static final int Mod4Mask = (1<<6);
668 private static final int Mod5Mask = (1<<7);
669 private static final int Button1Mask = (1<<8);
670 private static final int Button2Mask = (1<<9);
671 private static final int Button3Mask = (1<<10);
672 private static final int Button4Mask = (1<<11);
673 private static final int Button5Mask = (1<<12);
675 private static final int XK_Shift_L = 0xFFE1;
676 private static final int XK_Shift_R = 0xFFE2;
677 private static final int XK_Control_L = 0xFFE3;
678 private static final int XK_Control_R = 0xFFE4;
679 private static final int XK_Caps_Lock = 0xFFE5;
680 private static final int XK_Shift_Lock = 0xFFE6;
681 private static final int XK_Meta_L = 0xFFE7;
682 private static final int XK_Meta_R = 0xFFE8;
683 private static final int XK_Alt_L = 0xFFE9;
684 private static final int XK_Alt_R = 0xFFEA;
685 private static final int XK_Super_L = 0xFFEB;
686 private static final int XK_Super_R = 0xFFEC;
687 private static final int XK_Hyper_L = 0xFFED;
688 private static final int XK_Hyper_R = 0xFFEE;
690 private static final int XK_Home = 0xFF50;
691 private static final int XK_Left = 0xFF51;
692 private static final int XK_Up = 0xFF52;
693 private static final int XK_Right = 0xFF53;
694 private static final int XK_Down = 0xFF54;
695 private static final int XK_Prior = 0xFF55;
696 private static final int XK_Page_Up = 0xFF55;
697 private static final int XK_Next = 0xFF56;
698 private static final int XK_Page_Down = 0xFF56;
699 private static final int XK_End = 0xFF57;
700 private static final int XK_Begin = 0xFF58;
702 private static final int XK_F1 = 0xFFBE;
703 private static final int XK_F2 = 0xFFBF;
704 private static final int XK_F3 = 0xFFC0;
705 private static final int XK_F4 = 0xFFC1;
706 private static final int XK_F5 = 0xFFC2;
707 private static final int XK_F6 = 0xFFC3;
708 private static final int XK_F7 = 0xFFC4;
709 private static final int XK_F8 = 0xFFC5;
710 private static final int XK_F9 = 0xFFC6;
711 private static final int XK_F10 = 0xFFC7;
712 private static final int XK_F11 = 0xFFC8;
713 private static final int XK_F12 = 0xFFC9;
715 public void sendKeyEvent (KeyEvent event) {
716 int uc = event.getUnicodeChar();
717 int jcode = event.getKeyCode();
718 int jmods = event.getModifiers();
723 case KeyEvent.KEYCODE_SHIFT_LEFT: xcode = XK_Shift_L; break;
724 case KeyEvent.KEYCODE_SHIFT_RIGHT: xcode = XK_Shift_R; break;
725 case KeyEvent.KEYCODE_CTRL_LEFT: xcode = XK_Control_L; break;
726 case KeyEvent.KEYCODE_CTRL_RIGHT: xcode = XK_Control_R; break;
727 case KeyEvent.KEYCODE_CAPS_LOCK: xcode = XK_Caps_Lock; break;
728 case KeyEvent.KEYCODE_META_LEFT: xcode = XK_Meta_L; break;
729 case KeyEvent.KEYCODE_META_RIGHT: xcode = XK_Meta_R; break;
730 case KeyEvent.KEYCODE_ALT_LEFT: xcode = XK_Alt_L; break;
731 case KeyEvent.KEYCODE_ALT_RIGHT: xcode = XK_Alt_R; break;
733 case KeyEvent.KEYCODE_HOME: xcode = XK_Home; break;
734 case KeyEvent.KEYCODE_DPAD_LEFT: xcode = XK_Left; break;
735 case KeyEvent.KEYCODE_DPAD_UP: xcode = XK_Up; break;
736 case KeyEvent.KEYCODE_DPAD_RIGHT: xcode = XK_Right; break;
737 case KeyEvent.KEYCODE_DPAD_DOWN: xcode = XK_Down; break;
738 //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior; break;
739 case KeyEvent.KEYCODE_PAGE_UP: xcode = XK_Page_Up; break;
740 //case KeyEvent.KEYCODE_NAVIGATE_NEXT: xcode = XK_Next; break;
741 case KeyEvent.KEYCODE_PAGE_DOWN: xcode = XK_Page_Down; break;
742 case KeyEvent.KEYCODE_MOVE_END: xcode = XK_End; break;
743 case KeyEvent.KEYCODE_MOVE_HOME: xcode = XK_Begin; break;
745 case KeyEvent.KEYCODE_F1: xcode = XK_F1; break;
746 case KeyEvent.KEYCODE_F2: xcode = XK_F2; break;
747 case KeyEvent.KEYCODE_F3: xcode = XK_F3; break;
748 case KeyEvent.KEYCODE_F4: xcode = XK_F4; break;
749 case KeyEvent.KEYCODE_F5: xcode = XK_F5; break;
750 case KeyEvent.KEYCODE_F6: xcode = XK_F6; break;
751 case KeyEvent.KEYCODE_F7: xcode = XK_F7; break;
752 case KeyEvent.KEYCODE_F8: xcode = XK_F8; break;
753 case KeyEvent.KEYCODE_F9: xcode = XK_F9; break;
754 case KeyEvent.KEYCODE_F10: xcode = XK_F10; break;
755 case KeyEvent.KEYCODE_F11: xcode = XK_F11; break;
756 case KeyEvent.KEYCODE_F12: xcode = XK_F12; break;
757 default: xcode = uc; break;
760 if (0 != (jmods & KeyEvent.META_SHIFT_ON)) xmods |= ShiftMask;
761 if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
762 if (0 != (jmods & KeyEvent.META_CTRL_MASK)) xmods |= ControlMask;
763 if (0 != (jmods & KeyEvent.META_ALT_MASK)) xmods |= Mod1Mask;
764 if (0 != (jmods & KeyEvent.META_META_ON)) xmods |= Mod1Mask;
765 if (0 != (jmods & KeyEvent.META_SYM_ON)) xmods |= Mod2Mask;
766 if (0 != (jmods & KeyEvent.META_FUNCTION_ON)) xmods |= Mod3Mask;
768 /* If you touch and release Shift, you get no events.
769 If you type Shift-A, you get Shift down, A down, A up, Shift up.
770 So let's just ignore all lone modifier key events.
772 if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
775 boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
776 sendKeyEvent (down_p, xcode, xmods);