All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gnu.app.puppet.Puppet Maven / Gradle / Ivy

Go to download

Escher is a collection of libraries for X Window System written purely in Java.

The newest version!
package gnu.app.puppet;

import gnu.x11.*;
import gnu.x11.event.*;
import gnu.x11.Input;           // shadow gnu.x11.event.Input
import gnu.x11.Error;           // shadow java.lang.Error
import gnu.x11.extension.XTest;
import gnu.x11.extension.NotFoundException;
import gnu.x11.keysym.Misc;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;


/**
 * Window manager.
 *
 *
 * 
 * 

In my PC 104 keyboard system, *

 *   META (MOD1) = "alt" next to spacebar 
 *   ALT (MOD4) = "window" next to meta
 *   SUPER (MOD3) = "menu" next to right CONTROL
 * 
* Please see Xmodmap. * * * *

Bindings

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
KEY SEQUENCECOMMAND
basic
[enter]finish
[kp-enter]finish
[escape]finish or abort-switch-focus
[(control delete) (meta DIGIT)]lanuch-command DIGIT
[(control delete) S]*search-window-backward
[(control delete) s]*search-window-forward
focus
[(meta tab)]switch-focus-ignore-class-forward
[(shift meta tab)]switch-focus-ignore-class-backward
[(alt tab)]switch-focus-same-class-forward
[(shift alt tab)]switch-focus-same-class-backward
[(super tab)]switch-focus-different-class-forward
[(super tab)]switch-focus-different-class-backward
window
[(control delete) ARROW]*move
[(control delete) (shift ARROW)]*move faster
[(control delete) (control ARROW)]*resize
[(control delete) (control ARROW)]*resize faster
[(control delete) pageup]raise
[(control delete) page-down]lower
[(control delete) d]!dump-info
[(control delete) f]toggle-focus
[(control delete) F]grant-all-focus
[(control delete) h]!hide-which
[(control delete) H]!unhide-which
[(control delete) m]minimize
[(control delete) M]!maximize
[(control delete) r]restore-size
[(control delete) R]save-size
[(control delete) w]!warp-pointer
[(control delete) z]*relocate
register
[(control delete) space KEY]client-to-register KEY
[(control delete) j KEY]jump-to-register KEY
[(control delete) (control DIGIT)]jump-to-register DIGIT
critical
[(control delete) backspace d]delete-window
[(control delete) backspace k]kill-window
[(control delete) backspace Q]quit-puppet
geometry
[(control delete) g ARROW]*gravitate
[(control delete) g (shift ARROW)]*gravitate-absolute
[(control delete) g ?2]*scale-two-third
[(control delete) g ?3]*scale-one-and-half
[(control delete) g ?d]*scale-double
[(control delete) g ?h]*scale-half
mouse
[(meta BUTTON) on root]lanuch-on-root
[BUTTON on root]pointer-root-focus
[(control button1)]focus-with-raise
[(control button2)]focus-without-raise
[(control button3)]lower-behind
[(control meta button1)]delete-window
[(control meta button3)]kill-window
mouse key
[(control delete) KP_ARROW]*move-pointer
[(control delete) (shift KP_ARROW)]*move-pointer faster
[(control delete) (control shift KP_ARROW)]*move-pointer even faster
[(control delete) kp-begin]click-button1
[(control delete) (shift kp-begin)]double-click-button1
[(control delete) kp-divide]click-button1
[(control delete) (shift kp-divide)]double-click-button2
[(control delete) kp-multiply]click-button1
[(control delete) (shift kp-multiply)]double-click-button3
* * *
*
* *
persistent command * *
! *
command accepting argument * *
ARROW *
up, down, left, or right * *
BUTTON *
BUTTON1, BUTTON2, BUTTON3 * *
DIGIT *
0, 1, 2, 3, 4, 5, 6, 7, 8, or 9 * *
KEY *
any key (including modifier) * *
KP_ARROW *
kb_up, kb_down, kb_left, kb_right, kb_home, kb_end, kb_pageup, * kb_pagedown *
* * * *

Persistent Command

* * There are two types of commands: persistent commands, and * non-persistent commands. Non-persistent commands such as * `delete-window' will exit active keyboard state (ungrab keyboard) when * their actions are executed. On the other hand, persistent commands such * as `relocate' will stay in active keyboard state (do not ungrab * keyboard) even when their actions are executed. This feature allows * users execute similar commands in a sequence without repeatively * pressing the system key. * *

Note, however, there is no visual feedback for persistent commands. * Users may feel their keyboards are locked, while actually this window * manager is expecting more keystrokes. Remember to press `finish' key to * finish a persistent command. * * * *

Argument

* A prefix argument mechanism similar to that in Emacs is employed as * follows. After pressing system key and before pressing the command key, * pressing any digit (or '-' for negative) will become the argument for * the command. There can be more than one digits, which will be formed as * an integer argument in decimal. * * * * * * * * * * * * * * * * * *
COMMANDARGUMENTDESCRIPTION
dump-infonothingdump-basic-info
negativedump-hidden-windows
otherdump-all-windows
hide-whichnothinghide-focus
negativehide-others
otherhide-same-class
unhide-whichnothingundo-hide
negativeunhide-all
otherunhide-same-class
maximizenothingmaximize-user-space
othermaximize-full-screen
* * * *

Customization

* * There aren't much to customized about! In fact, you have to edit Java * source code and recompile to customize anything at all. Check * gnu/app/puppet/Preference.java and key_process_*() in * gnu/app/puppet/Puppet.java. * * *

Focus Policy - Keyboard

* * Basic switch-focus ([(meta tab)] and [(meta shift tab)]) (similar * to Microsoft Windows (tm)): to change focus to the most recently used * normal window, press [(meta tab)]; to change focus to the least recently * used normal window, press [(meta shift tab)]. To navigate all normal * windows, continuously press and release TAB while holding META. Normal * windows are those in state {@link #NORMAL} which is the * default state for freshly mapped windows. Windows that are hidden or * denied focus by user, or unmapped by application are not considered * normal. * *

Class-based switch-focus ([(alt tab)] and [(super tab)]): * they are exactly the same as [(meta tab)] counterparts, except that the * selection criteria are WM-CLASS sensitive. WM-CLASS is an classification * of applications defined in icccm 4.1.2.5. While [(alt tab)] considers * all normal windows, WINDOW-TAB considers normal windows that are of the * same WM_CLASS as the window under current focus; and [(super tab)] * considers those of different WM_CLASS. Note pressing SHIFT will reverse * the direction. * *

Find Window (`search-window-forward' and * `search-window-backward'): you can search and focus a window by pressing * its first character of its WM-CLASS string. To go to next matching * window, press the same character again, for as many times as you want. * Press [enter] or [escape] when done (as it is a persistent command). To * search among all managed windows (including normal, hidden and * focus-denied windows), hold SHIFT while pressing the character, that is, * press its corresponding capital letter. * *

Register (`client-to-register' and `jump-to-register') * (similar to Emacs buffer register): you can store a window to a * register, and then later restore the focus to that window in the * register. A register is any key (including control, alt, shift....!) on * the keyboard. `client-to-register stores a window to a register, while * `jump-to-register' later will unhide and raise (if necessary) the window * in the register, and then give it the input focus. * *

Fall-back: this policy decides which window to focus if the * current window is being hidden, unmapped, or denied focus. Unlike * Microsoft Windows (tm) which unhelpfully gives the focus back to * nowhere, focus will be fallen back to the most recently used normal * window (as if [(meta tab)] is pressed). * * * *

Focus Policy - Mouse

* * A policy similar to CLICK-TO-FOCUS in other window managers is * employed as follows. To raise and focus a window, press BUTTON1 while * holding CONTROL; to focus without raising, press BUTTON2 while holding * CONTROL; to lower and window and give focus to some other window * (happens to contain the pointer after re-stacking), press BUTTON3 while * holding CONTROL. * *

Intercepting button press events is discouraged (prohibited?) in icccm, * but there is not much choice as we don't do re-parenting. * * * *

Mouse Key

* * A simple mechanism is employed to support moving the pointer with * keyboard. Please check the above section, "Binding", for details. * *

Supposedly, X server comes with XKB extension that supports MouseKey * as a built-in feature. However, personally I find it inconvenient to use * (num-lock for on/off and default click keys) and not highly customizable * (acceleration time and delay). Moreover, there is a serious bug in * reference implementations of X server as pointed out by Stephen * Montgomery-Smith ([email protected]) * here. He also wrote some customization tool ( Xkkset) * for many XKB features. In the meantime, Puppet supports its own mouse * key binding as a supplementary mechanism. * * @see * screenshot * * @see * help output */ public class Puppet extends Application { public static final int SYSTEM_MODIFIER = Input.CONTROL_MASK; public static final int SYSTEM_KEYSYM = Misc.DELETE; public static final int SWITCH_KEYSYM = Misc.TAB; // internal state public static final int UNMANAGED = 0; public static final int NORMAL = 1; public static final int HIDDEN = 2; public static final int NO_FOCUS = 3; // delta for resize and relocate public static final int DELTA_LARGE = 20; public static final int DELTA_SMALL = 2; /** see Focus Policy - Keyboard. */ public Client [] registers; /** * A list of all managed children of root window, that is, our clients. * We need to frequently remove and add elements to maintain focus order * - any better/faster data structure? * * @see #scan_children() */ public Vector clients = new Vector (); /** * The active window when FOCUS-KEY (ALT-TAB, WINDOW-TAB, or MENU-TAB) * is pressed. Used to aid prevent looping and maintain focus order. * * @see #next_client(Client, boolean) */ public Client focus_base; public Vector focus_so_far = new Vector (); // useful atoms (from twm), not all implemented yet Atom _mit_priority_colors, wm_change_state, wm_state, wm_colormap_windows, wm_protocols, wm_take_focus, wm_save_yourself, wm_delete_window, sm_client_id, wm_client_leader, wm_window_role; public Preference pref = new Preference (); public Rectangle space; public Window root; // just an alias public Client focus, last_hide; public XTest xtest; public boolean print_event; public Puppet (String [] args) throws NotFoundException { super (args); print_event = option.booleann ("print-event", "dump all events for debug", false); space = option.rectangle ("space", "workspace for normal windows", pref.space ()); about ("0.1", "puppet window manager", "Stephen Tse ", "http://escher.sourceforge.net/", "\nFor bindings, check http://escher.sourceforge.net/" + "current/doc/gnu/app/puppet/Puppet.html."); if (help_option) return; /** * addShutdownHook in IBM JDK 1.3 does not seem to work for processing * Control-C. It simply ignores TERM signal instead of calling shutdown * hooks and then exiting. */ // Runtime.getRuntime ().addShutdownHook (new Thread () { // public void run () { System.err.println ("shutting down..."); }}); root = display.default_root; Window.NONE.display = display; // for move pointer xtest = new XTest (display); // for press button registers = new Client [display.input.max_keycode - display.input.min_keycode]; _mit_priority_colors = (Atom) Atom.intern (display, "_MIT_PRIORITY_COLORS"); wm_change_state = (Atom) Atom.intern (display, "WM_CHANGE_STATE"); wm_state = (Atom) Atom.intern (display, "WM_STATE"); wm_colormap_windows = (Atom) Atom.intern (display, "WM_COLORMAP_WINDOWS"); wm_protocols = (Atom) Atom.intern (display, "WM_PROTOCOLS"); wm_take_focus = (Atom) Atom.intern (display, "WM_TAKE_FOCUS"); wm_save_yourself = (Atom) Atom.intern (display, "WM_SAVE_YOURSELF"); wm_delete_window = (Atom) Atom.intern (display, "WM_DELETE_WINDOW"); sm_client_id = (Atom) Atom.intern (display, "SM_CLIENT_ID"); wm_client_leader = (Atom) Atom.intern (display, "WM_CLIENT_LEADER"); wm_window_role = (Atom) Atom.intern (display, "WM_WINDOW_ROLE"); control_root_window (); scan_children (); focus = (Client) Client.intern (root.query_pointer ().child); focus.set_input_focus (); grab_keybut (); System.out.println ("Initialization completed."); while (!exit_now) read_and_dispatch_event (); } public void alert_user (String message) { System.out.println (message); display.bell (-50); // not too loud, please } public void client_to_register (Client client, int keycode) { int index = client.register = keycode - display.input.min_keycode; Client old = registers [index]; if (old != null) old.register = -1; registers [index] = client; } public void control_root_window () { try { root.select_input ( Event.BUTTON_PRESS_MASK // unmap, destroy notify | Event.SUBSTRUCTURE_NOTIFY_MASK // map, configure, circulate request | Event.SUBSTRUCTURE_REDIRECT_MASK // icccm properties (wm name, hints, normal hints) | Event.PROPERTY_CHANGE_MASK); display.check_error (); } catch (Error e) { if (e.code == Error.BAD_ACCESS && e.bad == root.id) throw new RuntimeException ( "Failed to access root window\nAnother WM is running?"); else throw e; } } public void deny_focus (Client client) { client.state = NO_FOCUS; give_up_focus (client); } public void focus_client_first_char (Client client, boolean reverse, char c, boolean all) { // support only alphabetic window class if (c < 'A' || c > 'z') return; Client next = next_client_first_char (client, reverse, c, all); if (next == null) alert_user ("Client not found: " + c); // possibly hidden and no-focus, as we search all windows // BUT do not grant focus, leave it as before if (all && next != null) unhide (next); set_focus (next, true); } public void give_up_focus (Client client) { if (client == focus) set_focus (next_client_normal (client, true), false); } public void grab_keybut () { // system key root.grab_key_ignore_locks (SYSTEM_KEYSYM, SYSTEM_MODIFIER, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); // rotate to next normal window - ignore class root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.META_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.META_MASK|Input.SHIFT_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); // rotate to next normal window - different class root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.SUPER_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.SUPER_MASK|Input.SHIFT_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); // rotate to next normal window - same class root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.ALT_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); root.grab_key_ignore_locks (SWITCH_KEYSYM, Input.ALT_MASK|Input.SHIFT_MASK, true, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS); // CLICK-TO-FOCUS root.grab_button_ignore_locks (Window.ANY_BUTTON, Input.CONTROL_MASK, true, Event.BUTTON_PRESS_MASK, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS, Window.NONE, Cursor.NONE); // close / delete root.grab_button_ignore_locks (Window.ANY_BUTTON, Input.CONTROL_MASK | Input.META_MASK, true, Event.BUTTON_PRESS_MASK, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS, Window.NONE, Cursor.NONE); } public void grant_all_focus () { for (Iterator it=clients.iterator (); it.hasNext ();) { grant_focus ((Client) it.next ()); } } public void grant_focus (Client client) { if (client.state == NO_FOCUS) client.state = NORMAL; } public boolean grant_preference (Client client) { if (client.class_hint == null) return false; String id = client.class_hint.res + ":"; Object value; // no focus if (pref.no_focus (id)) client.state = Puppet.NO_FOCUS; // register int keysym = '0' + pref.register (id); if (keysym >= '0') { int keycode = display.input.keysym_to_keycode (keysym); client.register = keycode - display.input.min_keycode; registers [client.register] = client; } int max_width = display.default_screen.width; int max_height = display.default_screen.height; // position Rectangle r = pref.position (id); if (r != null) { // similar to geometry parameter processing in X toolkit r.x = (r.x + max_width) % max_width; r.y = (r.y + max_height) % max_height; client.move_resize (r); return true; // user-specified position } return false; } public void hide (Client client) { if (client.state == HIDDEN) return; /* Set this state to give hint to {@link * #when_unmap_notify(UnmapNotify)}. * *

Do it before client.unmap (). */ client.state = HIDDEN; client.set_wm_state (Window.WMState.ICONIC); client.unmap (); last_hide = client; } public void hide_others (Client client) { for (Iterator it=clients.iterator (); it.hasNext ();) { Client c = (Client) it.next (); if (c.state == NORMAL && c != client) hide (c); } } public void hide_same_class (Client client) { for (Iterator it=clients.iterator (); it.hasNext ();) { Client c = (Client) it.next (); if (c.state == NORMAL && c.class_hint != null && c.class_hint.class_equals (client.class_hint)) hide (c); } } public void jump_to_register (int keycode) { int index = keycode - display.input.min_keycode; Client client = registers [index]; if (client != null) { // do not grant focus, leave it as before unhide (client); // de-iconify, if neceesary set_focus (client, true); } else alert_user ("Register not defined: " + keycode); } public boolean key_do_change_geometry () { int delta = shift_down ? 0 : DELTA_LARGE; switch (keysym) { case Misc.LEFT: // `gravitate' or `gravitate-absolute' focus.x = space.x + delta; focus.move (); return false; case Misc.RIGHT: focus.x = space.x + space.width - focus.width - delta; focus.move (); return false; case Misc.UP: focus.y = space.y + delta; focus.move (); return false; case Misc.DOWN: focus.y = space.y + space.height - focus.height - delta; focus.move (); return false; case '2': // `scale-two-third' scale_size (focus, 0.67); return false; case '3': // `scale-one-and-half' scale_size (focus, 1.5); return false; case 'd': // `scale-double' scale_size (focus, 2.0); return false; case 'h': // `scale-half' scale_size (focus, 0.5); return false; default: alert_user ("Undefined keysym for 'change geometry': " + keysym); return false; } } public boolean key_do_critical_operation () { switch (keysym) { case 'd': // `delete-window' focus.delete (); return true; case 'k': // `kill-window' focus.kill (); return true; case 'Q': // `quit-puppet' when_quit (); exit (); return true; default: alert_user ("Undefined keysym for 'critical operation': " + keysym); return false; } } public boolean key_do_jump_lanuch_or_argument () { if (control_down) { // `jump-to-register DIGIT' jump_to_register (keycode); return true; } else if (meta_down) { // `lanuch-command' String command = pref.launch (keysym - '0'); if (command == null) { alert_user ("Command not defined: " + (keysym - '0')); return true; } try { Runtime.getRuntime ().exec (command); } catch (java.io.IOException e) { alert_user ("Failed to launch " + command + ": " + e); } return true; } else { // argument argument_present = true; if (keysym == '-') { argument_negative = true; return false; } int i = ((char) keysym) - '0'; argument = argument * 10 + i; return false; } } public void key_dump_info () { System.out.println ("input focus: " + focus); System.out.println ("mouse at: " + root.query_pointer ().root_position ()); if (!argument_present) return; // `dump-basic-info' if (argument_negative) { // `dump-hidden-windows'" System.out.println ("all hidden clients: "); for (Iterator it=clients.iterator (); it.hasNext ();) { Client c = (Client) it.next (); if (c.state == HIDDEN) System.out.println (c); } } else // `dump-all-windows' System.out.println ("all clients: " + clients); } public void key_hide_which () { if (!argument_present) // `hide-focus' hide (focus); else if (argument_negative) // `hide-others' hide_others (focus); else // `hide-same-class' hide_same_class (focus); } public void key_unhide_which () { if (!argument_present) // `undo-hide' unhide (last_hide); else if (argument_negative) // `unhide-all' unhide_all (); else // `unhide-same-class' unhide_same_class (focus); } public void key_move_or_resize (int x_direction, int y_direction) { // TODO send synthetic ConfigureNotify? int delta = shift_down ? DELTA_LARGE : DELTA_SMALL; if (control_down) { // `resize' focus.width += x_direction * delta; focus.height += y_direction * delta; focus.resize (); } else { // `move' focus.x += x_direction * delta; focus.y += y_direction * delta; focus.move (); } } public void key_move_pointer (int x_direction, int y_direction) { // `move-pointer' int scale = (shift_down ? DELTA_LARGE : DELTA_SMALL) * (meta_down ? DELTA_LARGE : DELTA_SMALL); int x = x_direction * scale; int y = y_direction * scale; Window.NONE.warp_pointer (x, y); } public boolean key_process () { /* These are global to cases with any prefix. * * RETURN, KP_ENTER, and ESCAPE will terminate key press sequence and * ungrab keyboard in all circumstances. * * Lock modifiers (SHIFT, CONTROL, META, ALT, SUPER, and HYPER) will be * ignored if they are pressed without other keys, except when defining * or jumping to register. */ switch (keysym) { case Misc.RETURN: // `finish' case Misc.KP_ENTER: case Misc.ESCAPE: return true; case Misc.SHIFT_L: // ignore case Misc.SHIFT_R: case Misc.CONTROL_L: case Misc.CONTROL_R: case Misc.META_L: case Misc.META_R: case Misc.ALT_L: case Misc.ALT_R: case Misc.SUPER_L: case Misc.SUPER_R: case Misc.HYPER_L: case Misc.HYPER_R: if (prefix0 != ' ' && prefix0 != 'j') return false; } if (prefix0 == 0) return key_process_no_prefix (); if (prefix1 == 0) return key_process_prefix0 (); return key_process_prefix1 (); } public boolean key_process_no_prefix () { switch (keysym) { case Misc.BACKSPACE: // prefix of critical operation case ' ': // prefix of client to register case 'j': // prefix of jump to register case 'g': // prefix of change geometry case 's': // prefix of search client case 'S': // prefix of search client prefix0 = keysym; return false; case Misc.PAGE_UP: // `raise' focus.raise (); return true; case Misc.PAGE_DOWN: // `lower' focus.lower (); return true; case Misc.LEFT: key_move_or_resize (-1, 0); return false; case Misc.RIGHT: key_move_or_resize (1, 0); return false; case Misc.UP: key_move_or_resize (0, -1); return false; case Misc.DOWN: key_move_or_resize (0, 1); return false; case Misc.KP_HOME: key_move_pointer (-1, -1); return false; case Misc.KP_UP: key_move_pointer (0, -1); return false; case Misc.KP_PAGE_UP: key_move_pointer (1, -1); return false; case Misc.KP_LEFT: key_move_pointer (-1, 0); return false; case Misc.KP_RIGHT: key_move_pointer (1, 0); return false; case Misc.KP_END: key_move_pointer (-1, 1); return false; case Misc.KP_DOWN: key_move_pointer (0, 1); return false; case Misc.KP_PAGE_DOWN: key_move_pointer (1, 1); return false; case Misc.KP_BEGIN: key_click_button (Input.BUTTON1); return true; case Misc.KP_DIVIDE: key_click_button (Input.BUTTON2); return true; case Misc.KP_MULTIPLY: key_click_button (Input.BUTTON3); return true; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': case '-': return key_do_jump_lanuch_or_argument (); case 'd': // `dump-info' key_dump_info (); return true; case 'f': // `toggle-focus' toggle_focus (focus); return true; case 'F': // `grant-all-focus' grant_all_focus (); return true; case 'h': // `hide-which' key_hide_which (); return true; case 'H': // `unhide-which' key_unhide_which (); return true; case 'm': // `minimize' minimize (focus); return true; case 'M': // `maximize', `maximize-user-space', `maximize-full-screen' maximize (focus, argument_present); return true; case 'r': // `restore-size' restore_size (focus); return true; case 'R': // `save-size' save_size (focus); return true; case 'w': // `warp-pointer' key_warp_pointer (); return true; case 'z': relocate (focus); return false; default: alert_user ("Undefined keysym: " + keysym); return false; } } public boolean key_process_prefix0 () { switch (prefix0) { case Misc.BACKSPACE: return key_do_critical_operation (); case ' ': // `client-to-register KEY' client_to_register (focus, keycode); return true; case 'j': // `jump-to-register KEY' jump_to_register (keycode); return true; case 'g': return key_do_change_geometry (); case 'S': // search-window-backward case 's': // search-window-forward key_search_client (); return false; default: throw new java.lang.Error ("Unhandled prefix0: " + prefix0); } } public boolean key_process_prefix1 () { throw new java.lang.Error ("Unhandled prefix1: " + prefix1); } public void key_click_button (int button) { /* Click = Press + Release. We must fake both ButtonPress and * ButtonRelease, or some programs would think we are dragging. */ // `click-button1' // `click-button2' // `click-button3' xtest.fake_button_event (button, true, 0); xtest.fake_button_event (button, false, 0); // `double-click-button1' // `double-click-button2' // `double-click-button3' if (shift_down) { xtest.fake_button_event (button, true, 0); xtest.fake_button_event (button, false, 0); } } public void key_search_client () { focus_client_first_char (focus, false, (char) keysym, shift_down); } public void key_switch_focus () { if (!focus_key_pressed) return; // `switch-focus-ignore-class-forward' // `switch-focus-ignore-class-backward' if (meta_down) set_focus (next_client_normal (focus, !shift_down), true); // `switch-focus-different-class-forward' // `switch-focus-different-class-backward' else if (alt_down) set_focus (next_client_different_class (focus, !shift_down), true); // `switch-focus-same-class-forward' // `switch-focus-same-class-backward' else if (super_down) set_focus (next_client_same_class (focus, !shift_down), true); } public void key_warp_pointer () { if (!argument_present) { focus.warp_pointer (10, 10); return; } Point position = pref.warp_position (argument); if (position == null) alert_user ("Warp position not defined: " + argument); else root.warp_pointer (position); } public void manage (Client client) { if (!clients.contains (client)) clients.add (client); // ready for move and resize client.get_geometry (); // ready for next focus and preference client.class_hint = client.wm_class_hint (); // ready for minimize client.size_hints = client.wm_normal_hints (); // ready for info client.name = client.wm_name (); client.change_save_set (false); if (!grant_preference (client)) // no position preference, do bounding box client.move_resize (client.rectangle ().within (space)); } public void maximize (Client client, boolean full_screen) { if (full_screen) client.move_resize (0, 0, display.default_screen.width, display.default_screen.height); else client.move_resize (space); } public void minimize (Client client) { client.width = client.size_hints == null || client.size_hints.min_width () == 0 ? client.width/2 : client.size_hints.min_width (); client.height = client.size_hints == null || client.size_hints.min_height () == 0 ? client.height/2 : client.size_hints.min_height (); client.resize (); relocate (client); } public Client next_client (Client from, boolean reverse) { int index = clients.indexOf (from); int n = clients.size (); // + n for no negative value // % n for cycle index = (reverse ? index-1+n : index+1+n) % n; Client next = (Client) clients.elementAt (index); /* Detect looping. * * Case !focus_key_pressed: focus is always at the top of * clients, i.e., the end of the vector. next == focus means we * are searching in a loop. We return null to signal it. Callers should * quit recursive searching, and return null to set_focus, which does * nothing (as search fails). * * Case focus_key_pressed: focus_base is the last active window. * Note orders of windows in clients stays constants during * focus_key_pressed, and thus focus cannot be used to detect * looping. next == focus_base means we are searching in a loop. * We return null to signal it. Callers should quit recursive * searching, and return null to set_focus, which, however, sets focus * to focus_base, to allow circular tabbing. * * Note that returning null (instead of focus_base) is critical in * next_* () methods as, say, next_client_normal () will be called by, * say, next_client_first_char (), returning null in next_client_normal * () prevents looping in next_client_first_char (). */ if (focus_key_pressed && next == focus_base) return null; if (!focus_key_pressed && next == focus) return null; if (next.early_unmapped || next.early_destroyed) return next_client (next, reverse); else return next; } /** * @see #next_client(Client, boolean) */ public Client next_client_normal (Client from, boolean reverse) { Client next = next_client (from, reverse); if (next == null) return null; // prevent looping if (next.state == NORMAL) return next; return next_client_normal (next, reverse); // recursive } /** * @see #next_client(Client, boolean) */ public Client next_client_same_class (Client from, boolean reverse) { Client next = next_client_normal (from, reverse); if (next == null) return null; // prevent looping if (focus_base.class_hint != null && next.class_hint != null && focus_base.class_hint.class_equals (next.class_hint)) return next; return next_client_same_class (next, reverse); // recursive } /** * @see #next_client(Client, boolean) */ public Client next_client_different_class (Client from, boolean reverse) { Client next = next_client_normal (from, reverse); if (next == null) return null; // prevent looping /* Check if WM-CLASS of next window is different from all we have * processed so far since first pressing SUPER-TAB. * * Respect null res_class. */ String res_class = ""; if (next.class_hint != null) res_class = next.class_hint.res_class (); if (!focus_so_far.contains (res_class)) { focus_so_far.add (res_class); return next; } return next_client_different_class (next, reverse); // recursive } /** * @see #next_client(Client, boolean) */ public Client next_client_first_char (Client from, boolean reverse, char c, boolean all) { Client next = next_client (from, reverse); if (next == null) return null; // prevent looping boolean state = all ? // no UNMAPPED next.state == NORMAL | next.state == HIDDEN | next.state == NO_FOCUS : next.state == NORMAL; if (next.class_hint != null) { String c1 = next.class_hint.res.substring (0, 1); String c2 = String.valueOf (c); if (state && next.class_hint.res.length () > 0 && c1.equalsIgnoreCase (c2)) return next; } return next_client_first_char (next, reverse, c, all); // recursive } public boolean system_key_pressed, focus_key_pressed; public void when (Event event) { if (print_event) System.out.println (event); switch (event.code ()) { case ButtonPress.CODE: when_button_press ((ButtonPress) event); break; case ClientMessage.CODE: // un-avoidable when_client_message ((ClientMessage) event); break; case ConfigureRequest.CODE: // Event.SUBSTRUCTURE_NOTIFY when_configure_request ((ConfigureRequest) event); break; case DestroyNotify.CODE: // Event.SUBSTRUCTURE_NOTIFY when_destroy_notify ((DestroyNotify) event); break; case KeyPress.CODE: // grab key when_key_press ((KeyPress) event); break; case KeyRelease.CODE: // grab key when_key_release ((KeyRelease) event); break; case PropertyNotify.CODE: // Event.PROPERTY_CHANGE when_property_notify ((PropertyNotify) event); break; case MapRequest.CODE: // Event.SUBSTRUCTURE_REDIRECT when_map_request ((MapRequest) event); break; case MapNotify.CODE: // Event.SUBSTRUCTURE_NOTIFY when_map_notify ((MapNotify) event); break; case UnmapNotify.CODE: // Event.SUBSTRUCTURE_NOTIFY when_unmap_notify ((UnmapNotify) event); break; case ConfigureNotify.CODE: // Event.SUBSTRUCTURE_NOTIFY, ignored case CreateNotify.CODE: // Event.SUBSTRUCTURE_NOTIFY, ignored case MappingNotify.CODE: // un-avoidable, ignored TODO break; default: alert_user ("Unhandled event: " + event); } } public void read_and_dispatch_event () { Event first_event = display.next_event (); /* Race conditions: while reading an event of a window, the window may * have already unmapped or even destroyed. UnmapNotify or * DestroyNotify of the window may come later in the event queue, * or they are just being generated in the server and not yet reach our * queue. * * Solution: We MUST grab the server to halt further event generations * during processing events. Before acting on any event with {@link * #when(Event)}, we pull all events in the network and check for * UnmapNotify and DestroyNotify. * Note we force X server to flush all pending events for us using the * technique described {@link Connection#pull_all_events() here}. We then * set {@link Client#early_unmapped} and {@link Client#early_destroyed} * accordingly. Event handling routines may check these client flags * and decide if they should abort the operations, as the window may * have already unmapped/destroyed. * * For example, {@link #when_key_press(KeyPress)} depends on the * visibility of a window and thus aborts the operations if the window * is already unmapped or destroyed. On the other hand, {@link * #when_property_notify(PropertyNotify)} depends only on the * liveness of a window and thus aborts the operations only when the * window is already destroyed. * * To street test the correctness of this method (or that of other * window manager), drag around any icon in Netscape so that Netscape * will quickly map, unmap, and destroy windows. For instance, remove * display.check_error (); here and run the test, Puppet * will produce an {@link Error#BAD_WINDOW} error. */ display.grab_server (); display.check_error (); List other_events = display.in.pull_all_events (); for (Iterator it=other_events.iterator (); it.hasNext ();) { Event event = (Event) it.next (); if (event.code () == DestroyNotify.CODE) { Client client = (Client) Client.intern (display, ((DestroyNotify) event).window_id); client.early_destroyed = true; } else if (event.code () == UnmapNotify.CODE) { Client client = (Client) Client.intern (display, ((UnmapNotify) event).window_id); client.early_unmapped = true; } } when (first_event); /* Grabbing server significantly hinders responsiveness of this * window manager. While we have to trade speed for correctness, * we can aggregate grabbing and ungrabbing by processing all events in * queue at once between grabbings. */ for (Iterator it=other_events.iterator (); it.hasNext ();) when ((Event) it.next ()); /* We must flush all requests before un-grabbing the server, so that * windows in the requests are guaranteed to be alive. */ display.flush (); display.ungrab_server (); } // find another client of same res_name and res_class public void register_fall_back (Client client) { if (client.register == -1) return; Client fall_back = null; if (client.class_hint != null) for (Iterator it=clients.iterator (); it.hasNext ();) { Client c = (Client) it.next (); if (c != client // another! && c.class_hint.equals (client.class_hint) && !c.early_unmapped && !c.early_destroyed) { fall_back = c; break; } } registers [client.register] = fall_back; client.register = -1; } public static final java.util.Random random = new java.util.Random (); public void relocate (Client client) { int x_range = space.width - client.width; int y_range = space.height - client.height; if (x_range < 0) x_range = 0; if (y_range < 0) y_range = 0; client.x = x_range == 0 ? 0 : random.nextInt (x_range); client.y = y_range == 0 ? 0 : random.nextInt (y_range); // origin of space client.x += space.x; client.y += space.y; client.move (); } public void restore_size (Client client) { client.resize (client.saved_width, client.saved_height); } public void save_size (Client client) { client.saved_width = client.width; client.saved_height = client.height; } public void scale_size (Client client, double factor) { client.width *= factor; client.height *= factor; client.width = Math.min (client.width, space.width); client.height = Math.min (client.height, space.height); client.resize (); relocate (client); } public void scan_children () { // query all top-level windows Window[] children = root.tree ().children (); for (Window w : children) { Client client = (Client) Client.intern (display, w.id); // get override_redirect and map_state client.attributes = client.attributes (); /* Children of root to be managed: (1) not override redirect (pop-up * and transient windows). (2) VIEWABLE (previously mapped) */ if (client.attributes.override_redirect () || client.attributes.map_state () != Window.AttributesReply.VIEWABLE) continue; // not managed Window.WMState wm_state = client.wm_state (); if (wm_state != null && wm_state.state () == Window.WMState.ICONIC) { hide (client); // respect its iconic state } else { // maintain our state variable client.state = NORMAL; // in case someone screws this up client.set_wm_state (Window.WMState.NORMAL); } if (client.state == NORMAL || client.state == HIDDEN) manage (client); } } public void set_focus (Client client, boolean warp_pointer) { /* Process return value from next_client_* () to prevent looping. * * Case 1 = !focus_key_pressed && client == null. next_client_* fails * to find a matching client. Operation aborts and this method simply * returns. * * Case 2 = focus_key_pressed && client == null. next_client_* detects * a cycle (start pressing TAB on client A, navigate all matching * clients, and come back to client A). Cycling operation restarts and * this method continues with client = focus_base. * * @see #next_client(Client, boolean) * @see #focus_so_far */ if (!focus_key_pressed && client == null) return; if (focus_key_pressed && client == null) { client = focus_base; focus_so_far.clear (); if (focus_base.class_hint != null) focus_so_far.add (focus_base.class_hint.res_class ()); } client.raise (); // we choose to do it focus = client; /* At this point, and only at this point, we are 100% sure that the * window is mapped and hence can be assigned focus to. Otherwise, an * {@link Error#BAD_WINDOW} will result. */ focus.set_input_focus (); // give hint if (warp_pointer) client.warp_pointer (10, 10); /* For changing focus in focus_key_pressed mode, do not update client order * until the user decides which window to stay with and releases focus * key modifier. * * @see #when_key_release(KeyRelease) */ if (!focus_key_pressed) update_client_order (focus); } public void when_button_press (ButtonPress event) { /* Because we are doing grabbing on root window, we should not check * event.window() since it always equals to * {@link #root}. Instead, check event.child(). */ boolean control_down = (event.state () & Input.CONTROL_MASK) != 0; boolean meta_down = (event.state () & Input.META_MASK) != 0; boolean on_root = event.child_id () == 0; if (meta_down && on_root) { // `lanuch-on-root' try { Runtime.getRuntime ().exec (pref.launch_on_root ()); } catch (java.io.IOException e) { alert_user ("Failed to launch " + pref.launch_on_root () + ": " + e); } return; } if (!meta_down && on_root) { // `pointer-root-focus' root.set_input_focus (); return; } Client client = (Client) Client.intern (display, event.child_id ()); if (client.early_unmapped || client.early_destroyed) return; if (!control_down) return; int button = event.detail (); switch (button) { case Input.BUTTON1: if (meta_down) // `delete-window' client.delete (); else { // `focus-with-raise' // same as set_focus () client.raise (); focus = client; focus.set_input_focus (); update_client_order (focus); } break; case Input.BUTTON2: // `focus-without-raise' focus = client; focus.set_input_focus (); update_client_order (focus); break; case Input.BUTTON3: if (meta_down) client.kill (); // `kill-window' else { // `lower-behind' // give focus to some other window (under pointer) client.lower (); focus = (Client) Client.intern (root.query_pointer ().child); focus.set_input_focus (); update_client_order (focus); // != client } break; } } public void when_client_message (ClientMessage event) { // client asks to change window state from normal to iconic Client client = (Client) Client.intern (display, event.window_id); if (client.early_unmapped || client.early_destroyed) return; Atom type = event.type (); if (event.format () == 32 && type.name.equals ("WM_CHANGE_STATE") && event.wm_data () == Window.WMHints.ICONIC) { hide (client); } else alert_user ("Unhandled client message: " + type); } public void when_configure_request (ConfigureRequest event) { // client asks to change window configuration // @see icccm/sec-4.html#s-4.1.5 // TODO find space in screen to display window? Client client = (Client) Client.intern (display, event.window_id); if (client.early_unmapped || client.early_destroyed) return; if (client.class_hint != null) { String id = client.class_hint.res + ":"; if (pref.no_geometry_change (id)) return; } /* Should I send a synthetic ConfigureNotify instead of actually * do a configure request on the window? We do not re-parent, and thus, * according to icccm, a ConfigureNotify will be fine. But xterm * relies on a window manager to honour its ConfigureRequest to * configure a window, or it falls back to width = height = 1. A mere * ConfigureNotify seems not sufficient. (Other clients does not * have this problems?) */ client.configure (event.changes ()); client.set_geometry_cache (event.rectangle ()); // we choose to give it focus if it is normal and it raises if (client.state == NORMAL && event.stack_mode () == Window.Changes.ABOVE) set_focus (client, false); } public void when_destroy_notify (DestroyNotify event) { Client client = (Client) Client.intern (display, event.window_id); give_up_focus (client); register_fall_back (client); client.unintern (); clients.remove (client); } /* The following variables are global initialized in * #when_key_press. They are supposed to be global to all * key_* methods such that these critical information are available to * those methods without passing lots of variables. */ public int keycode, keysym; public boolean alt_down, control_down, meta_down, shift_down, super_down; public int prefix0, prefix1; public int argument; public boolean argument_present; public boolean argument_negative; public void when_key_press (KeyPress event) { keycode = event.detail (); int keystate = event.state (); keysym = display.input.keycode_to_keysym (keycode, keystate); shift_down = (keystate & Input.SHIFT_MASK) != 0; control_down = (keystate & Input.CONTROL_MASK) != 0; meta_down = (keystate & Input.META_MASK) != 0; alt_down = (keystate & Input.ALT_MASK) != 0; super_down = (keystate & Input.SUPER_MASK) != 0; if (!system_key_pressed && !focus_key_pressed) { int status = root.grab_keyboard (false, Window.ASYNCHRONOUS, Window.ASYNCHRONOUS, display.CURRENT_TIME); if (status != Window.SUCCESS) throw new RuntimeException ("Failed to grab keyboard"); focus_key_pressed = keysym == Misc.TAB; system_key_pressed = !focus_key_pressed; if (system_key_pressed) return; // if tab (see #next_client) focus_base = focus; focus_so_far.clear (); if (focus_base.class_hint != null) focus_so_far.add (focus.class_hint.res_class ()); } if (focus_key_pressed) { if (keysym == Misc.ESCAPE) { // abort-switch-focus focus_key_pressed = false; set_focus (focus_base, true); } else if (keysym == Misc.TAB) key_switch_focus (); return; } if (key_process ()) { // done display.input.ungrab_keyboard (); system_key_pressed = false; prefix0 = prefix1 = 0; argument = 0; argument_negative = false; argument_present = false; } } /** Handle *-TAB key release. */ public void when_key_release (KeyRelease event) { keycode = event.detail (); int keystate = event.state (); keysym = display.input.keycode_to_keysym (keycode, keystate); if (!focus_key_pressed || !(keysym == Misc.META_L || keysym == Misc.META_R || keysym == Misc.ALT_L || keysym == Misc.ALT_R || keysym == Misc.SUPER_L || keysym == Misc.SUPER_R)) return; display.input.ungrab_keyboard (); focus_key_pressed = false; // finally user has chosen a new active window update_client_order (focus); } public void when_property_notify (PropertyNotify event) { Atom atom = event.atom (display); Client client = (Client) Client.intern (display, event.window_id); if (client.early_destroyed) return; if (atom == wm_colormap_windows || atom == wm_protocols) throw new java.lang.Error ("unhandled property notfiy: " + atom); switch (atom.id) { case Atom.WM_HINTS_ID: // TODO any action? client.wm_hints (); break; case Atom.WM_NORMAL_HINTS_ID: // TODO any action? client.size_hints = client.wm_normal_hints (); break; case Atom.WM_NAME_ID: client.name = client.wm_name (); break; case Atom.WM_ICON_NAME_ID: // fall through case Atom.WM_TRANSIENT_FOR_ID: // ignore (normal window manager should handle these) break; } } public void when_map_request (MapRequest event) { // client asks to change window state from withdrawn to normal/iconic, // or from iconic to normal Client client = (Client) Client.intern (display, event.window_id); if (client.early_unmapped || client.early_destroyed) return; // get override_redirect and map_state client.attributes = client.attributes (); if (client.attributes.override_redirect ()) return; manage (client); Window.WMHints wm_hints = client.wm_hints (); // assume NORMAL if initial_state not specified if (wm_hints == null || (wm_hints.flags () & Window.WMHints.STATE_HINT_MASK) == 0 || wm_hints.initial_state () == Window.WMHints.NORMAL) { /* Do not do any visible operations on the window such as focusing * and warping pointer, until a window is actually map, ie. * MapNotify. * * @see #when_map_notify(MapNotify) */ client.map (); } else { // must be iconic client.state = HIDDEN; client.set_wm_state (Window.WMState.ICONIC); } } public void when_map_notify (MapNotify event) { Client client = (Client) Client.intern (display, event.window_id); if (client.early_unmapped || client.early_destroyed) return; /* Get override_redirect and map_state. * * Case 1. !override_redirect (). MapRequest is generated and * client.attributes is already initialized in when (MapRequest) * * Case 2. override_redirect (). NO MapRequest is generated and * client.attributes == null - it must be initialized here. * * @see XMapWindow */ if (client.attributes == null) client.attributes = client.attributes (); if (client.attributes.override_redirect ()) return; /* Now and only now sets the window state to NORMAL (except during * initialization). Setting this earlier gives false impression that * the window is mapped, but it does not happen until MapNotify. * Note window.raise () and window.map () do not guarantee the * visibility of a window (due to map request and configure request * redirection of wm). Hence, any operations that depends on visibility * (warp pointer and set input focus) should check window.state. * * state == NO_FOCUS from grant_preference (). */ if (client.state != NO_FOCUS) client.state = NORMAL; client.set_wm_state (Window.WMState.NORMAL); /* We can set focus to a window only when it is ready, ie. * MapNotify, not MapRequest. */ if (client.state != NO_FOCUS) set_focus (client, false); } public void when_quit () { // so that I can keep typing focus.warp_pointer (10, 10); // set_input_focus () to POINTER_ROOT to restore the default focus // (every wm should do this)? Window.POINTER_ROOT.display = display; Window.POINTER_ROOT.set_input_focus (); } public void when_unmap_notify (UnmapNotify event) { /* Unmapped != unmanaged, since it can be iconify-ing (or hiding in our * case). We unmanage a window when it is destroyed. * * @see #when_destroy_notify(DestroyNotify) */ Client client = (Client) Client.intern (display, event.window_id); if (client.early_destroyed) return; client.early_unmapped = false; // handled here give_up_focus (client); // they withdraw it if (client.state != HIDDEN) { /* From icccm 4.1.4: For compatibility with obsolete clients, window * managers should trigger the transition to the Withdrawn state on * the real UnmapNotify rather than waiting for the synthetic one. * They should also trigger the transition if they receive a * synthetic UnmapNotify on a window for which they have not yet * received a real UnmapNotify. * * Then, what's the use of synthetic UnmapNotify event? */ client.state = UNMANAGED; client.set_wm_state (Window.WMState.WITHDRAWN); client.change_save_set (true); } } public void toggle_focus (Client client) { if (client.state == NORMAL) deny_focus (client); else if (client.state == NO_FOCUS) grant_focus (client); } public void unhide (Client client) { if (client.state != HIDDEN) return; /* Do not set client.state here. Do it right in * {@link #when_map_notify(MapNotify)}. */ client.map (); } public void unhide_all () { for (Iterator it=clients.iterator (); it.hasNext ();) { unhide ((Client) it.next ()); } } public void unhide_same_class (Client client) { for (Iterator it=clients.iterator (); it.hasNext ();) { Client c = (Client) it.next (); if (c.state == HIDDEN && c.class_hint != null && c.class_hint.class_equals (client.class_hint)) unhide (c); } } public void update_client_order (Client client) { clients.remove (client); clients.add (client); } public static void main (String [] args) throws NotFoundException { new Puppet (args); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy