diff --git a/openide.windows/src/org/netbeans/modules/openide/windows/ApplicationActivationManagerImpl.java b/openide.windows/src/org/netbeans/modules/openide/windows/ApplicationActivationManagerImpl.java new file mode 100644 --- /dev/null +++ b/openide.windows/src/org/netbeans/modules/openide/windows/ApplicationActivationManagerImpl.java @@ -0,0 +1,477 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): Tim Boudreau + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.netbeans.modules.openide.windows; + +import java.awt.AWTEvent; +import java.awt.EventQueue; +import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.AWTEventListener; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.openide.util.Parameters; +import org.openide.util.RequestProcessor; +import org.openide.util.RequestProcessor.Task; +import org.openide.util.lookup.ServiceProvider; +import org.openide.windows.ApplicationActivationManager; + +/** + * Default implementation of ApplicationActivationManager. + * + * @author Tim Boudreau + */ +@ServiceProvider(service = ApplicationActivationManager.class) +public class ApplicationActivationManagerImpl extends ApplicationActivationManager { + + private static final RequestProcessor rp = new RequestProcessor(ApplicationActivationManagerImpl.class); + private static final int DEFAULT_ACTIVATION_DELAY = 800; + private static final int DEFAULT_IDLE_DELAY = 120000; //two minutes - *really* idle + private final Set listeners = Collections.synchronizedSet(new LinkedHashSet()); + private final Object stateLock = new Object(); + private final Object eventLock = new Object(); + private final Object idleLock = new Object(); + private final Set state = EnumSet.noneOf(State.class); + private final List events = new LinkedList(); + private final AtomicBoolean inNotificationLoop = new AtomicBoolean(); + //avoid rapid transiently switching to the application, for example + //moving between virtual desktops, to trigger work + private final IdleListener idleListener = new IdleListener(); + private final Task addIdleStateTask = rp.create(idleListener); + private final KeyboardFocusListener keyboardFocusManagerListener = new KeyboardFocusListener(); + private final Task updateActiveStateTask = rp.create(keyboardFocusManagerListener); + private volatile boolean stateIsIdle; + private volatile boolean listeningForIdle; + private int idleDelay; + private int activationDelay; + private static final String IDLE_SYSTEM_PROPERTY = "application.idle.delay"; + private static final String ACTIVATION_SYSTEM_PROPERTY = "application.idle.delay"; + + public ApplicationActivationManagerImpl() { + //check system properties + try { + String prop = System.getProperty(IDLE_SYSTEM_PROPERTY); + if (prop != null) { + idleDelay = Integer.parseInt(prop); + if (idleDelay <= 0) { + throw new NumberFormatException("Negative " + IDLE_SYSTEM_PROPERTY); + } + } else { + idleDelay = DEFAULT_IDLE_DELAY; + } + } catch (NumberFormatException nfe) { + Logger.getLogger(ApplicationActivationManagerImpl.class.getName()).log(Level.WARNING, IDLE_SYSTEM_PROPERTY, nfe); + idleDelay = DEFAULT_IDLE_DELAY; + } + try { + String prop = System.getProperty(ACTIVATION_SYSTEM_PROPERTY); + if (prop != null) { + activationDelay = Integer.parseInt(prop); + } else { + activationDelay = DEFAULT_ACTIVATION_DELAY; + } + if (activationDelay <= 0) { + throw new NumberFormatException("Negative " + ACTIVATION_SYSTEM_PROPERTY); + } + } catch (NumberFormatException nfe) { + Logger.getLogger(ApplicationActivationManagerImpl.class.getName()).log(Level.WARNING, ACTIVATION_SYSTEM_PROPERTY, nfe); + activationDelay = DEFAULT_ACTIVATION_DELAY; + } + //attach our listener + EventQueue.invokeLater(new Runnable() { + + @Override + public void run() { + KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusedWindow", keyboardFocusManagerListener); + boolean active = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow() != null; + if (active) { + setState(EnumSet.of(State.ACTIVE)); + } + } + }); + } + + @Override + public Set getState() { + synchronized (stateLock) { + return EnumSet.copyOf(state); + } + } + + void setState(Set newState) { + Set oldState; + synchronized (stateLock) { + oldState = getState(); + if (newState.equals(oldState)) { + return; + } + this.state.clear(); + this.state.addAll(newState); + onChange(this.state); + } + notify(oldState, newState); + } + + private void notify(Set old, Set nue) { + if (old != null) { + synchronized (eventLock) { + events.add(new StateEvent(old, nue)); + } + } + boolean notify = inNotificationLoop.compareAndSet(false, true); + //another caller can already be performing notifications, depending on + //which thread last triggered a state change. If so, they will be + //delivered + if (notify) { + //list of things to run on whatever thread this is not + final List notifyOnOtherThread = new LinkedList(); + boolean inEQ = EventQueue.isDispatchThread(); + try { + final List notifyEvents = new LinkedList(); + synchronized (eventLock) { + notifyEvents.addAll(this.events); + this.events.clear(); + } + while (!notifyEvents.isEmpty()) { + for (StateEvent evt : notifyEvents) { + for (Iterator it = listeners.iterator(); it.hasNext();) { + ListenerHolder holder = it.next(); + Listener l = holder.getListener(); + if (l == null) { + it.remove(); + } else { + //Filter out states this listener does not care about + Set oldState = holder.filter(evt.old); + Set newState = holder.filter(evt.nue); + //If the result is no change, do not notify + if (!oldState.equals(newState)) { + //Synchronously notify whichever listeners want + //to be notified on this thread + if (holder.notifyInBackground != inEQ) { + try { + l.onChange(oldState, newState); + } catch (RuntimeException e) { + Logger.getLogger(ApplicationActivationManagerImpl.class.getName()).log(Level.SEVERE, null, e); + } + } else { + //add the rest to the queued list, for + //notification on a background thread + notifyOnOtherThread.add(new Notification(l, new StateEvent(oldState, newState))); + } + } + } + } + } + //Collect any events that have arrived while + //we have been looping + synchronized (eventLock) { + notifyEvents.clear(); + notifyEvents.addAll(this.events); + this.events.clear(); + } + } + } finally { + inNotificationLoop.set(false); + } + boolean renotify; + synchronized (eventLock) { + //it is possible for another thread to have entered while the + //loop variable was set to false - so do a quick check to make + //sure we don't have events that should be delivered but won't be + //unless we call ourselves again + renotify = !events.isEmpty(); + } + if (!notifyOnOtherThread.isEmpty()) { + //Asynchronously notify any listeners we are on the wrong + //thread for + Runnable r = new Runnable() { + + @Override + public void run() { + for (Notification l : notifyOnOtherThread) { + try { + l.notifyListener(); + } catch (RuntimeException e) { + Logger.getLogger(ApplicationActivationManagerImpl.class.getName()).log(Level.SEVERE, null, e); + } + } + } + }; + if (inEQ) { + rp.post(r); + } else { + EventQueue.invokeLater(r); + } + } + if (renotify) { + notify(null, null); + } + } + } + + private void startListeningForIdleEvents() { + if (!listeningForIdle) { + //ensure we don't have a diff between the value of listeningForIdle + //and whether or not we are actually listening + synchronized (idleLock) { + if (!listeningForIdle) { + listeningForIdle = true; + Toolkit.getDefaultToolkit().addAWTEventListener(idleListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK); + addIdleStateTask.schedule(idleDelay); + } + } + } + } + + private void stopListeningForIdleEvents() { + if (listeningForIdle) { + synchronized (idleLock) { + if (listeningForIdle) { + addIdleStateTask.cancel(); + Toolkit.getDefaultToolkit().removeAWTEventListener(idleListener); + listeningForIdle = false; + } + } + } + } + + private final class IdleListener implements AWTEventListener, Runnable { + + private volatile long lastEventTime = System.currentTimeMillis(); + + @Override + public void eventDispatched(AWTEvent event) { + int id = event.getID(); + //ignore mouse motion + if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_ENTERED || id == MouseEvent.MOUSE_EXITED || id == MouseEvent.MOUSE_DRAGGED) { + return; + } + lastEventTime = System.currentTimeMillis(); + //postpone the idle task + addIdleStateTask.schedule(idleDelay); + if (stateIsIdle) { //avoids the lock in the common case that the state is not idle + synchronized (stateLock) { + Set states = getState(); + states.remove(State.IDLE); + //will run notifications under lock + setState(states); + } + } + } + + @Override + public void run() { + //run after the idle delay + long timeSinceLastEvent = System.currentTimeMillis() - lastEventTime; + if (timeSinceLastEvent > idleDelay && getState().equals(EnumSet.of(State.ACTIVE))) { + //add the idle state + changeState(State.IDLE); + } + } + } + + private void onChange(Set states) { + //update the stateIsIdle value so we avoid taking a lock on every + //keystroke / mouse click + stateIsIdle = states.contains(State.IDLE); + if (states.contains(State.ACTIVE)) { + startListeningForIdleEvents(); + } else { + stopListeningForIdleEvents(); + } + } + + private static final class StateEvent { + + private final Set old; + private final Set nue; + + public StateEvent(Set old, Set nue) { + assert !old.equals(nue); + this.old = old; + this.nue = nue; + } + + public String toString() { + return old + " to " + nue; + } + } + + private static final class Notification { + + private final Listener listener; + private final StateEvent evt; + + public Notification(Listener listener, StateEvent evt) { + this.listener = listener; + this.evt = evt; + } + + void notifyListener() { + listener.onChange(evt.old, evt.nue); + } + } + + @Override + public void addListener(Listener listener, boolean notifyInForeground, State... interestedIn) { + Parameters.notNull("listener", listener); + if (interestedIn.length == 0) { + throw new IllegalArgumentException("No states"); + } + listeners.add(new ListenerHolder(listener, notifyInForeground, + EnumSet.copyOf(Arrays.asList(interestedIn)))); + } + + private static final class ListenerHolder { + + private final Reference listenerRef; + private final boolean notifyInBackground; + private final Set interestedIn; + + public ListenerHolder(Listener listener, boolean notifyInBackground, Set interestedIn) { + this.notifyInBackground = notifyInBackground; + this.interestedIn = interestedIn; + this.listenerRef = new WeakReference(listener); + } + + Listener getListener() { + return listenerRef.get(); + } + + Set filter(Set states) { + Set nue = EnumSet.copyOf(states); + nue.retainAll(interestedIn); + return nue; + } + } + + private boolean changeState(State toAdd, State... toRemove) { + //add one state and optionally remove others, then update the state + Set oldState; + Set newState; + synchronized (stateLock) { + oldState = getState(); + newState = EnumSet.copyOf(oldState); + if (toAdd != null) { + newState.add(toAdd); + } + newState.removeAll(Arrays.asList(toRemove)); + if (newState.equals(oldState)) { + return false; + } + this.state.clear(); + this.state.addAll(newState); + onChange(this.state); + } + notify(oldState, newState); + return true; + } + + private final class KeyboardFocusListener implements PropertyChangeListener, Runnable { + + volatile boolean active; + State destState; + + @Override + public void propertyChange(PropertyChangeEvent evt) { + //Note one weirdness I've never been able to decipher: In a regular + //swing application, when you show a dialog, focus goes from the + //parent window to the dialog. In NetBeans, it goes from the + //parent window to null to the dialog, and the same when closing. + //This causes a plethora of extra BECOMING_ACTIVE / BECOMING_INACTIVE + //events which are unavoidable + Window old = (Window) evt.getOldValue(); + Window nue = (Window) evt.getNewValue(); + if ((old == null) != (nue == null)) { + active = nue != null; + if (!active) { + //add the becoming inactive state, remove becoming active if present + changeState(State.BECOMING_INACTIVE, State.BECOMING_ACTIVE); + //set our single destination state + synchronized (this) { + destState = null; + } + //reschedule updating the final state + updateActiveStateTask.schedule(activationDelay); + } else { + //add the becoming active state, remove the becoming inactive state + changeState(State.BECOMING_ACTIVE, State.BECOMING_INACTIVE); + //set our single destination state + synchronized (this) { + destState = State.ACTIVE; + } + //reschedule the task + updateActiveStateTask.schedule(activationDelay); + } + } + } + + @Override + public void run() { + //Called when the state has been BECOMING_* for a sufficient + //amount of time + State dest; + synchronized (this) { + dest = this.destState; + } + if (dest == null) { + changeState(null, State.BECOMING_INACTIVE, State.ACTIVE); + } else { + changeState(destState, State.BECOMING_ACTIVE); + } + } + } +} diff --git a/openide.windows/src/org/openide/windows/ApplicationActivationManager.java b/openide.windows/src/org/openide/windows/ApplicationActivationManager.java new file mode 100644 --- /dev/null +++ b/openide.windows/src/org/openide/windows/ApplicationActivationManager.java @@ -0,0 +1,190 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): Tim Boudreau + * + * Portions Copyrighted 2011 Sun Microsystems, Inc. + */ +package org.openide.windows; + +import java.awt.Window; +import java.util.EnumSet; +import java.util.Set; +import org.openide.util.Lookup; + +/** + * Lifecycle hook for determining whether the application is "active" (the user + * is using it) or not, and whether or not it is "idle" (active but not in use + * for a given period of time), and listening for changes in that state. + *

+ * Usage: If you need to perform some work (such as checking for external + * changes) when the application becomes active, this class can help. Another + * case is if you have a long-running operation to perform (such as scanning + * remote resources for changes) which must be done periodically, and you + * do not want to interfere with normal operation - the idle state is useful + * for this. + *

+ * The application achieves the "ACTIVE" state when it has been given focus + * and not lost focus again for a sufficient amount of time; it achieves the + * "IDLE" state when it has focus but no user input has been received for + * a sufficient amount of time. By listening for changes in the ACTIVE state + * you can perform work only when the application has really been reactivated, + * avoiding notification because of very brief reactivation (for example, a + * user navigating between virtual desktops or applications). + * + * @author Tim Boudreau + */ +public abstract class ApplicationActivationManager { + + public static ApplicationActivationManager getDefault() { + ApplicationActivationManager result = Lookup.getDefault().lookup(ApplicationActivationManager.class); + if (result == null) { + return new DummyImpl(); + } + return result; + } + + /** + * Get the current set of states associated with the application. + * + * @return A set of states, which may be empty if the application is + * inactive + */ + public abstract Set getState(); + + /** + * Add a listener to be notified on changes in state. The listener will + * be notified on a background thread or the AWT event thread, depending + * on the notifyInForeground parameter, when the application + * changes state and the old or new set of states include a state in the + * array of states passed to this method. + *

+ * The caller must hold a strong reference to the listener or it + * may be immediately garbage collected. + * + * @param listener The listener + * @param notifyInForeground If true, the listener will be notified in the + * AWT event thread; if false, the listener will be notified on a background + * thread. + * @param interestedIn The set of states this listener is interested in. + * May not be empty. + */ + public abstract void addListener(Listener listener, boolean notifyInForeground, State... interestedIn); + + /** + * Determine if the application is active. + * + * @return Whether or not the application is active + */ + public static boolean isActive() { + return getDefault().getState().contains(State.ACTIVE); + } + + /** + * Determine if the application is currently idle (active but no keyboard + * input or non-motion-related mouse input for a period of time). + * @return Whether or not the application is idle + */ + public static boolean isIdle() { + return getDefault().getState().contains(State.IDLE); + } + + /** + * Callback which is notified when the application state changes + */ + public interface Listener { + + /** + * Called when the application state changes + * @param from The old set of states + * @param to The new set of states + */ + public void onChange(Set from, Set to); + } + + /** + * States the application can have, relating to whether it is active + * or idle. + */ + public enum State { + + /** + * The application is definitely active + */ + ACTIVE, + /** + * The application has received focus, and if it still has focus after + * a suitable delay, will be determined to be active. + *

+ * Note that this state can change very frequently, for example, + * because a dialog was shown. + */ + BECOMING_ACTIVE, + /** + * The application has lost focus, and if it still does not have focus + * after a suitable delay, will be determined to be inactive. + *

+ * Note that this state can change very frequently, for example, + * because a dialog was shown. + */ + BECOMING_INACTIVE, + /* + * The application is active but no input events have been + * received for longer than a suitable delay, so the application is + * presumed to be idle (the user is not using the machine). + */ + IDLE + } + + private static final class DummyImpl extends ApplicationActivationManager { + + @Override + public Set getState() { + for (Window w : Window.getWindows()) { + if (w.isActive() && w.isFocusOwner()) { + return EnumSet.of(State.ACTIVE); + } + } + return EnumSet.noneOf(State.class); + } + + @Override + public void addListener(Listener listener, boolean notifyInForeground, State... interestedIn) { + //do nothing + } + } +}