Allora: ho fatto delle prove e i risultati sono questi, vedi il sorgente sotto.
Alcune note:
1) osserva
dove ho messo il return true nel dispatchKeyEvent. L'ho messo solo quando è Control+TAB, NON Control+Tab
released. Il motivo è semplice: il TAB (indipendentemente dal Ctrl) fa spostare di componente. Quindi Ctrl+TAB va "consumato"
anche per il
pressed. Se non lo consumi, il risultato è che sposta di componente PRIMA di fare lo switch di finestra.
2) Nel activateWindow al fondo NON ho fatto né requestFocus() né requestFocusInWindow(). Il motivo è che sono proprio
questi che danno il focus alla finestra ma tolgono il focus al componente interno. Questo è il risultato che mi avevi detto, cioè "sparisce" il focus dal componente (e non è il comportamento voluto né standard).
Ora: è sufficiente fare toFront() ? Sì, anzi
nì. La documentazione di toFront() è molto chiara:
If this Window is visible, brings this Window to the front and may make it the focused Window.
E c'è tutta la spiegazione sotto. Insomma, non è detto "universalmente" che portare "sù" una finestra voglia dire dargli il focus e attivarla. Questi purtroppo sono aspetti che dipendono dai vari S.O.
Io ho fatto le prove su Windows 10 e su una VM Xubuntu e funziona correttamente come mi aspettavo. Non ho modo di provare su Mac o altri SO.
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class FrameProva extends JFrame {
public FrameProva(String title, int x, int y) {
super(title);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 200);
setLocation(x, y);
setLayout(new FlowLayout());
add(new JTextField("A", 6));
add(new JTextField("B", 6));
add(new JTextField("C", 6));
add(new JTextField("D", 6));
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
WindowSwitcher.install();
FrameProva frame1 = new FrameProva("Frame 1", 100, 100);
FrameProva frame2 = new FrameProva("Frame 2", 150, 150);
FrameProva frame3 = new FrameProva("Frame 3", 200, 200);
FrameProva frame4 = new FrameProva("Frame 4", 250, 250);
JDialog dialog4 = new JDialog(frame4, "Dialog di Frame 4", false);
dialog4.setSize(300, 200);
dialog4.setLocationRelativeTo(frame4);
dialog4.setVisible(true);
});
}
}
class WindowSwitcher implements KeyEventDispatcher {
private static WindowSwitcher instance;
public static synchronized boolean install() {
if (instance == null) {
instance = new WindowSwitcher();
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(instance);
return true;
}
return false;
}
// TODO uninstall
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (e.isControlDown()) {
if (e.getKeyCode() == KeyEvent.VK_TAB) {
if (e.getID() == KeyEvent.KEY_RELEASED) {
if (e.isShiftDown()) {
WindowUtilities.activateNextWindow(false);
} else {
WindowUtilities.activateNextWindow(true);
}
}
return true;
}
}
return false;
}
}
class WindowUtilities {
private WindowUtilities() {}
public static boolean activateNextWindow(boolean forward) {
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
Window[] windows = Window.getWindows();
for (int i = 0; i < windows.length; i++) {
if (windows[i] == focusedWindow) {
for (int k = 0; k < windows.length; k++) {
i = ((forward ? ++i : --i) + windows.length) % windows.length;
if (activateWindow(windows[i])) {
return true;
}
}
}
}
return false;
}
public static boolean activateWindow(Window window) {
if (window.isEnabled() && window.isVisible() && window.isFocusableWindow()) {
if (window instanceof Frame) {
((Frame) window).setState(Frame.NORMAL);
}
window.toFront();
return true;
}
return false;
}
}
Se nel activateWindow si volesse fare window.requestFocus() dopo il toFront() sarebbe anche (più) corretto ma così toglie il focus da un qualunque componente nella finestra.
Ho trovato comunque un modo per sistemare anche questo scenario, basta usare un WindowFocusListener che sia "stateful" (che mantiene uno stato):
class WindowComponentFocusKeeper implements WindowFocusListener {
private Component lastFocusedComp;
@Override
public void windowGainedFocus(WindowEvent e) {
if (lastFocusedComp != null) {
lastFocusedComp.requestFocusInWindow();
}
}
@Override
public void windowLostFocus(WindowEvent e) {
lastFocusedComp = e.getWindow().getMostRecentFocusOwner();
}
}
Funziona grazie al quel getMostRecentFocusOwner() (di cui non avevo mai fatto molto caso...). Al lostFocus salva il componente che aveva il focus e al gainedFocus gli ridà il focus.
L'unica questione/difetto è che bisogna registrare un
distinto (perché stateful!) WindowComponentFocusKeeper per ciascuna delle tue finestre che crei e di cui hai il controllo.
Alternativa:
Invece di usare un WindowFocusListener, ho trovato un'altra possibilità che è più semplice:
public static boolean activateWindow(Window window) {
if (window.isEnabled() && window.isVisible() && window.isFocusableWindow()) {
if (window instanceof Frame) {
((Frame) window).setState(Frame.NORMAL);
}
window.toFront();
Component lastFocusedComp = window.getMostRecentFocusOwner();
window.requestFocus();
if (lastFocusedComp != null) {
SwingUtilities.invokeLater(() -> {
lastFocusedComp.requestFocusInWindow();
});
}
return true;
}
return false;
}
Si invoca window.requestFocus() però attenzione, questa è solo la
richiesta, la applicazione della richiesta avverrà dopo la terminazione di quel evento. Quindi è necessario schedulare più avanti (con il solito invokeLater) il requestFocusInWindow del componente "last focused".
P.S. nota come ho anche separato i vari concetti.