Skip to content

Commit 79efddd

Browse files
committed
Replace xset in axis with detectable auto repeat.
1 parent c588367 commit 79efddd

3 files changed

Lines changed: 203 additions & 15 deletions

File tree

src/emc/usr_intf/axis/Submakefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11

22
EMCMODULESRCS := emc/usr_intf/axis/extensions/emcmodule.cc
33
TOGLMODULESRCS := emc/usr_intf/axis/extensions/_toglmodule.c
4-
PYSRCS += $(EMCMODULESRCS) $(TOGLMODULESRCS)
4+
TKDARMODULESRCS := emc/usr_intf/axis/extensions/tkdarmodule.c
5+
PYSRCS += $(EMCMODULESRCS) $(TOGLMODULESRCS) $(TKDARMODULESRCS)
56

67
EMCMODULE := ../lib/python/linuxcnc.so
78
TOGLMODULE := ../lib/python/_togl.so
9+
TKDARMODULE := ../lib/python/tkdar.so
810

911
$(call TOOBJSDEPS, $(TOGLMODULESRCS)) : EXTRAFLAGS = $(ULFLAGS) $(TCL_CFLAGS)
1012

1113
$(call TOOBJSDEPS, $(EMCMODULESRCS)) : Makefile.inc
1214

15+
$(call TOOBJSDEPS, $(TKDARMODULESRCS)) : EXTRAFLAGS = $(TCL_CFLAGS)
16+
1317
$(EMCMODULE): $(call TOOBJS, $(EMCMODULESRCS)) ../lib/liblinuxcnc.a ../lib/libnml.so.0 \
1418
../lib/liblinuxcncini.so ../lib/libtooldata.so.0
1519
$(ECHO) Linking python module $(notdir $@)
@@ -19,7 +23,11 @@ $(TOGLMODULE): $(call TOOBJS, $(TOGLMODULESRCS))
1923
$(ECHO) Linking python module $(notdir $@)
2024
$(Q)$(CC) $(LDFLAGS) -shared -o $@ $(TCL_CFLAGS) $^ -L/usr/X11R6/lib -lX11 -lepoxy -lXmu $(TCL_LIBS)
2125

22-
PYTARGETS += $(EMCMODULE) $(TOGLMODULE)
26+
$(TKDARMODULE): $(call TOOBJS, $(TKDARMODULESRCS))
27+
$(ECHO) Linking python module $(notdir $@)
28+
$(Q)$(CC) $(LDFLAGS) -shared -o $@ $(TCL_CFLAGS) $^ -lX11 $(TCL_LIBS)
29+
30+
PYTARGETS += $(EMCMODULE) $(TOGLMODULE) $(TKDARMODULE)
2331

2432
PYSCRIPTS := axis.py axis-remote.py linuxcnctop.py hal_manualtoolchange.py \
2533
mdi.py image-to-gcode.py lintini.py debuglevel.py teach-in.py tracking-test.py
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//
2+
// tkdar - Tk/Tkinter Detectable Auto Repeat for Python
3+
// Copyright 2026 B.Stultiens
4+
//
5+
// This program is free software; you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation; either version 2 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program; if not, write to the Free Software
17+
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18+
//
19+
20+
//
21+
// Switch the X server's detectable auto repeat feature (if supported).
22+
// Normal auto repeat sends a KeyRelease/KeyPress event sequence
23+
// resulting in:
24+
// press - release, press - release, press - ... - release
25+
//
26+
// Detectable auto repeat modifies the event sequence into:
27+
// press - press - press - ... - release
28+
//
29+
// The first KeyPress event is the initial press of the button and the
30+
// final KeyRelease event is the actual physical release of the button.
31+
//
32+
// This code was inspired by the example found at:
33+
// https://wiki.tcl-lang.org/page/Disable+autorepeat+under+X11
34+
//
35+
//
36+
// Usage in Python/Tkinter:
37+
/*
38+
import tkinter
39+
import tkdar # exposes tkdar.enable() and tkdar.disable()
40+
41+
pressed_keys = [] # Current list if pressed keys
42+
43+
def keypress(event):
44+
if event.keysym in pressed_keys:
45+
return # already pressed, ignore repeats
46+
pressed_keys.append(event.keysym)
47+
print("Press ", event.keysym)
48+
49+
def keyrelease(event):
50+
# KeyRelease without KeyPress may happen when a modifier is active when
51+
# the key is pressed without a KeyPress handler. No KeyPress event is
52+
# generated, but releasing the actual key while still holding the modifier
53+
# generates a KeyRelease event that may be handled if there is a handler
54+
# installed. Therefore, we test the list to prevent an exception.
55+
if ev.keysym in pressed_keys:
56+
pressed_keys.remove(event.keysym)
57+
print("Release", event.keysym)
58+
59+
rootwin = tkinter.Tk(className="KeyRepeater")
60+
rootwin.title = "Key-repeat tester"
61+
rootwin.minsize(640, 400);
62+
63+
tkdar.enable(rootwin) # Set detectable auto repeat
64+
65+
for key in ["Up", "Down", "Left", "Right"]:
66+
rootwin.bind("<KeyPress-{}>".format(key), keypress)
67+
rootwin.bind("<KeyRelease-{}>".format(key), keyrelease)
68+
69+
rootwin.mainloop()
70+
*/
71+
72+
#include <Python.h>
73+
#include <tk.h>
74+
#include <X11/XKBlib.h>
75+
76+
static PyObject *tkdar(PyObject *arg, Bool enable)
77+
{
78+
// Retrieve the Tcl interpreter instance
79+
PyObject *interpaddrobj = PyObject_CallMethod(arg, "interpaddr", NULL);
80+
if(!interpaddrobj) {
81+
PyErr_SetString(PyExc_TypeError, "get_interpreter: 'interpaddr' call returned NULL");
82+
return NULL;
83+
}
84+
Tcl_Interp *interp = (Tcl_Interp *)PyLong_AsVoidPtr(interpaddrobj);
85+
Py_DECREF(interpaddrobj);
86+
if(interp == (void*)-1) {
87+
PyErr_SetString(PyExc_TypeError, "get_interpreter: 'interpaddrobj' returned NULL");
88+
return NULL;
89+
}
90+
91+
// Get the X server display via the main Tk window of the interpreter
92+
Tk_Window tkwin = Tk_MainWindow(interp);
93+
if(!tkwin) {
94+
PyErr_SetString(PyExc_RuntimeError, "Error while getting Tk_MainWindow");
95+
return NULL;
96+
}
97+
Display *display = Tk_Display(tkwin);
98+
if(!display) {
99+
PyErr_SetString(PyExc_RuntimeError, "Error while getting display connection to X server");
100+
return NULL;
101+
}
102+
103+
// Set the intended detectable auto repeat
104+
Bool supported = 1;
105+
Bool result = XkbSetDetectableAutoRepeat(display, enable, &supported);
106+
XFlush(display);
107+
108+
if(!supported) {
109+
PyErr_SetString(PyExc_NotImplementedError, "Setting detectable auto repeat not supported by X server");
110+
return NULL;
111+
}
112+
if(enable != result) {
113+
PyErr_SetString(PyExc_RuntimeError, "Could not set detectable auto repeat");
114+
return NULL;
115+
}
116+
117+
Py_INCREF(Py_None);
118+
return Py_None;
119+
}
120+
121+
// Python function tkdar.enable() handler
122+
static PyObject *tkdar_ena(PyObject *s, PyObject *arg)
123+
{
124+
(void)s;
125+
return tkdar(arg, 1);
126+
}
127+
128+
// Python function tkdar.disable() handler
129+
static PyObject *tkdar_dis(PyObject *s, PyObject *arg)
130+
{
131+
(void)s;
132+
return tkdar(arg, 0);
133+
}
134+
135+
static PyMethodDef tkdar_methods[] = {
136+
{"enable", (PyCFunction)tkdar_ena, METH_O, "Enable detectable auto repeat"},
137+
{"disable", (PyCFunction)tkdar_dis, METH_O, "Disable detectable auto repeat"},
138+
{}
139+
};
140+
141+
static struct PyModuleDef tkdar_moduledef = {
142+
.m_base = PyModuleDef_HEAD_INIT,
143+
.m_name = "tkdar",
144+
.m_doc = "Detectable auto repeat extension for Tk/Tkinter",
145+
.m_size = -1,
146+
.m_methods = tkdar_methods,
147+
};
148+
149+
PyMODINIT_FUNC PyInit_tkdar(void);
150+
PyMODINIT_FUNC PyInit_tkdar(void)
151+
{
152+
PyObject *m = PyModule_Create(&tkdar_moduledef);
153+
return m;
154+
}
155+
// vim: ts=4 shiftwidth=4

src/emc/usr_intf/axis/scripts/axis.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import traceback
3838

3939
import tkinter as Tkinter
40+
import tkdar
4041
import _thread
4142
gettext.install("linuxcnc", localedir=os.path.join(BASE, "share", "locale"))
4243

@@ -119,9 +120,26 @@ def putpref(self, option, value, type=bool):
119120

120121
ap = AxisPreferences()
121122

123+
# Handle repeated key press events
124+
pressed_keys_list = []
125+
def key_pressed(ev):
126+
if ev.keysym in pressed_keys_list:
127+
return True
128+
pressed_keys_list.append(ev.keysym)
129+
return False
130+
131+
def key_released(ev):
132+
# KeyRelease without KeyPress may happen when a modifier is active when
133+
# the key is pressed without a KeyPress handler. No KeyPress event is
134+
# generated, but releasing the actual key while still holding the modifier
135+
# generates a KeyRelease event that may be handled if there is a handler
136+
# installed. Therefore, we test the list to prevent an exception.
137+
if ev.keysym in pressed_keys_list:
138+
pressed_keys_list.remove(ev.keysym)
139+
122140
os.system("xhost -SI:localuser:gdm -SI:localuser:root > /dev/null 2>&1")
123-
os.system("xset r off")
124141
root_window = Tkinter.Tk(className="Axis")
142+
tkdar.enable(root_window) # Set detectable key repeat
125143
dpi_value = root_window.winfo_fpixels('1i')
126144
root_window.tk.call('tk', 'scaling', '-displayof', '.', dpi_value / 72.0)
127145
root_window.withdraw()
@@ -154,7 +172,6 @@ def putpref(self, option, value, type=bool):
154172
def General_Halt():
155173
text = _("Do you really want to close LinuxCNC?")
156174
if not root_window.tk.call("nf_dialog", ".error", _("Confirm Close"), text, "warning", 1, _("Yes"), _("No")):
157-
os.system("xset r on")
158175
root_window.destroy()
159176

160177
root_window.protocol("WM_DELETE_WINDOW", General_Halt)
@@ -2685,17 +2702,22 @@ def toggle_show_pyvcppanel(*event):
26852702

26862703
# The next three don't have 'manual_ok' because that's done in jog_on /
26872704
# jog_off
2688-
def jog_plus(incr=False):
2705+
def jog_plus(event):
2706+
if key_pressed(event):
2707+
return # Ignore repeated press events
26892708
a = ja_from_rbutton()
26902709
speed = get_jog_speed(a)
26912710
jog_on(a, speed)
26922711

2693-
def jog_minus(incr=False):
2712+
def jog_minus(event):
2713+
if key_pressed(event):
2714+
return # Ignore repeated press events
26942715
a = ja_from_rbutton()
26952716
speed = get_jog_speed(a)
26962717
jog_on(a, -speed)
26972718

2698-
def jog_stop(event=None):
2719+
def jog_stop(event):
2720+
key_released(event)
26992721
a = ja_from_rbutton()
27002722
jog_off(a)
27012723

@@ -3304,7 +3326,9 @@ def jog_off_all():
33043326
if jogging[i]:
33053327
jog_off_actual(i)
33063328

3307-
def jog_on_map(num, speed):
3329+
def jog_on_map(ev, num, speed):
3330+
if key_pressed(ev):
3331+
return # Ignore repeated press events
33083332
if not get_jog_mode():
33093333
if num >= len(jog_order): return
33103334
axis_letter = jog_order[num]
@@ -3322,7 +3346,8 @@ def jog_on_map(num, speed):
33223346
if axis_letter in jog_invert: speed = -speed
33233347
return jog_on(num, speed)
33243348

3325-
def jog_off_map(num):
3349+
def jog_off_map(ev, num):
3350+
key_released(ev)
33263351
if not get_jog_mode():
33273352
if num >= len(jog_order): return
33283353
num = "XYZABCUVW".index(jog_order[num])
@@ -3337,12 +3362,12 @@ def jog_off_map(num):
33373362
return jog_off(num)
33383363

33393364
def bind_axis(a, b, d):
3340-
root_window.bind("<KeyPress-%s>" % a, kp_wrap(lambda e: jog_on_map(d, -get_jog_speed_map(d)), "KeyPress"))
3341-
root_window.bind("<KeyPress-%s>" % b, kp_wrap(lambda e: jog_on_map(d, get_jog_speed_map(d)), "KeyPress"))
3342-
root_window.bind("<Shift-KeyPress-%s>" % a, lambda e: jog_on_map(d, -get_max_jog_speed_map(d)))
3343-
root_window.bind("<Shift-KeyPress-%s>" % b, lambda e: jog_on_map(d, get_max_jog_speed_map(d)))
3344-
root_window.bind("<KeyRelease-%s>" % a, lambda e: jog_off_map(d))
3345-
root_window.bind("<KeyRelease-%s>" % b, lambda e: jog_off_map(d))
3365+
root_window.bind("<KeyPress-%s>" % a, kp_wrap(lambda e: jog_on_map(e, d, -get_jog_speed_map(d)), "KeyPress"))
3366+
root_window.bind("<KeyPress-%s>" % b, kp_wrap(lambda e: jog_on_map(e, d, get_jog_speed_map(d)), "KeyPress"))
3367+
root_window.bind("<Shift-KeyPress-%s>" % a, lambda e: jog_on_map(e, d, -get_max_jog_speed_map(d)))
3368+
root_window.bind("<Shift-KeyPress-%s>" % b, lambda e: jog_on_map(e, d, get_max_jog_speed_map(d)))
3369+
root_window.bind("<KeyRelease-%s>" % a, lambda e: jog_off_map(e, d))
3370+
root_window.bind("<KeyRelease-%s>" % b, lambda e: jog_off_map(e, d))
33463371

33473372
root_window.bind("<FocusOut>", lambda e: str(e.widget) == "." and jog_off_all())
33483373

0 commit comments

Comments
 (0)