Swing, like most UI frameworks, is event driven.  Swing, like most UI frameworks, is also single threaded and not thread safe.
This means three things...
- You should never perform long running or blocking operations from within the context of the Event Dispatching Thread (like endless loops)
- You should not update the state of the UI from outside the context of the EDT
- You should use some kind of observer pattern to take action when some kind of state changes.
Since you're already using a Swing Timer and have mentioned using Key Bindings, you're already on the right track, the link you're missing is, the Action associated with the key binding should stop the Timer.
Or, as demonstrated in this example, the Action should notify some other interested party (i.e. a observer), that the Action has been triggered and the observer should take appropriate action.
import com.sun.glass.events.KeyEvent;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
    public static void main(String[] args) {
        new Test();
    }
    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }
                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
    public interface Engine {
        public void stop();
        public void start();
    }
    public class TestPane extends JPanel implements Engine {
        private Timer timer;
        private JLabel label = new JLabel("Waiting");
        public TestPane() {
            setLayout(new GridBagLayout());
            add(label);
            timer = new Timer(1000, new ActionListener() {
                private int counter = 0;
                @Override
                public void actionPerformed(ActionEvent e) {
                    counter++;
                    label.setText(Integer.toString(counter));
                }
            });
            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "stop");
            actionMap.put("stop", new StopAction(this));
            start();
        }
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }
        @Override
        public void stop() {
            timer.stop();
            label.setText("Stopped");
        }
        @Override
        public void start() {
            timer.start();
        }
    }
    public class StopAction extends AbstractAction {
        private Engine engine;
        public StopAction(Engine engine) {
            this.engine = engine;
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            engine.stop();
        }
    }
}