diff --git a/src/emc/usr_intf/axis/Submakefile b/src/emc/usr_intf/axis/Submakefile index 72e590a959d..f2bcd4b1142 100644 --- a/src/emc/usr_intf/axis/Submakefile +++ b/src/emc/usr_intf/axis/Submakefile @@ -1,15 +1,19 @@ EMCMODULESRCS := emc/usr_intf/axis/extensions/emcmodule.cc TOGLMODULESRCS := emc/usr_intf/axis/extensions/_toglmodule.c -PYSRCS += $(EMCMODULESRCS) $(TOGLMODULESRCS) +TKDARMODULESRCS := emc/usr_intf/axis/extensions/tkdarmodule.c +PYSRCS += $(EMCMODULESRCS) $(TOGLMODULESRCS) $(TKDARMODULESRCS) EMCMODULE := ../lib/python/linuxcnc.so TOGLMODULE := ../lib/python/_togl.so +TKDARMODULE := ../lib/python/tkdar.so $(call TOOBJSDEPS, $(TOGLMODULESRCS)) : EXTRAFLAGS = $(ULFLAGS) $(TCL_CFLAGS) $(call TOOBJSDEPS, $(EMCMODULESRCS)) : Makefile.inc +$(call TOOBJSDEPS, $(TKDARMODULESRCS)) : EXTRAFLAGS = $(TCL_CFLAGS) + $(EMCMODULE): $(call TOOBJS, $(EMCMODULESRCS)) ../lib/liblinuxcnc.a ../lib/libnml.so.0 \ ../lib/liblinuxcncini.so ../lib/libtooldata.so.0 $(ECHO) Linking python module $(notdir $@) @@ -19,7 +23,11 @@ $(TOGLMODULE): $(call TOOBJS, $(TOGLMODULESRCS)) $(ECHO) Linking python module $(notdir $@) $(Q)$(CC) $(LDFLAGS) -shared -o $@ $(TCL_CFLAGS) $^ -L/usr/X11R6/lib -lX11 -lepoxy -lXmu $(TCL_LIBS) -PYTARGETS += $(EMCMODULE) $(TOGLMODULE) +$(TKDARMODULE): $(call TOOBJS, $(TKDARMODULESRCS)) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CC) $(LDFLAGS) -shared -o $@ $(TCL_CFLAGS) $^ -lX11 $(TCL_LIBS) + +PYTARGETS += $(EMCMODULE) $(TOGLMODULE) $(TKDARMODULE) PYSCRIPTS := axis.py axis-remote.py linuxcnctop.py hal_manualtoolchange.py \ mdi.py image-to-gcode.py lintini.py debuglevel.py teach-in.py tracking-test.py diff --git a/src/emc/usr_intf/axis/extensions/tkdarmodule.c b/src/emc/usr_intf/axis/extensions/tkdarmodule.c new file mode 100644 index 00000000000..b2c95cf8033 --- /dev/null +++ b/src/emc/usr_intf/axis/extensions/tkdarmodule.c @@ -0,0 +1,155 @@ +// +// tkdar - Tk/Tkinter Detectable Auto Repeat for Python +// Copyright 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +// +// Switch the X server's detectable auto repeat feature (if supported). +// Normal auto repeat sends a KeyRelease/KeyPress event sequence +// resulting in: +// press - release, press - release, press - ... - release +// +// Detectable auto repeat modifies the event sequence into: +// press - press - press - ... - release +// +// The first KeyPress event is the initial press of the button and the +// final KeyRelease event is the actual physical release of the button. +// +// This code was inspired by the example found at: +// https://wiki.tcl-lang.org/page/Disable+autorepeat+under+X11 +// +// +// Usage in Python/Tkinter: +/* +import tkinter +import tkdar # exposes tkdar.enable() and tkdar.disable() + +pressed_keys = [] # Current list if pressed keys + +def keypress(event): + if event.keysym in pressed_keys: + return # already pressed, ignore repeats + pressed_keys.append(event.keysym) + print("Press ", event.keysym) + +def keyrelease(event): + # KeyRelease without KeyPress may happen when a modifier is active when + # the key is pressed without a KeyPress handler. No KeyPress event is + # generated, but releasing the actual key while still holding the modifier + # generates a KeyRelease event that may be handled if there is a handler + # installed. Therefore, we test the list to prevent an exception. + if ev.keysym in pressed_keys: + pressed_keys.remove(event.keysym) + print("Release", event.keysym) + +rootwin = tkinter.Tk(className="KeyRepeater") +rootwin.title = "Key-repeat tester" +rootwin.minsize(640, 400); + +tkdar.enable(rootwin) # Set detectable auto repeat + +for key in ["Up", "Down", "Left", "Right"]: + rootwin.bind("".format(key), keypress) + rootwin.bind("".format(key), keyrelease) + +rootwin.mainloop() +*/ + +#include +#include +#include + +static PyObject *tkdar(PyObject *arg, Bool enable) +{ + // Retrieve the Tcl interpreter instance + PyObject *interpaddrobj = PyObject_CallMethod(arg, "interpaddr", NULL); + if(!interpaddrobj) { + PyErr_SetString(PyExc_TypeError, "get_interpreter: 'interpaddr' call returned NULL"); + return NULL; + } + Tcl_Interp *interp = (Tcl_Interp *)PyLong_AsVoidPtr(interpaddrobj); + Py_DECREF(interpaddrobj); + if(interp == (void*)-1) { + PyErr_SetString(PyExc_TypeError, "get_interpreter: 'interpaddrobj' returned NULL"); + return NULL; + } + + // Get the X server display via the main Tk window of the interpreter + Tk_Window tkwin = Tk_MainWindow(interp); + if(!tkwin) { + PyErr_SetString(PyExc_RuntimeError, "Error while getting Tk_MainWindow"); + return NULL; + } + Display *display = Tk_Display(tkwin); + if(!display) { + PyErr_SetString(PyExc_RuntimeError, "Error while getting display connection to X server"); + return NULL; + } + + // Set the intended detectable auto repeat + Bool supported = 1; + Bool result = XkbSetDetectableAutoRepeat(display, enable, &supported); + XFlush(display); + + if(!supported) { + PyErr_SetString(PyExc_NotImplementedError, "Setting detectable auto repeat not supported by X server"); + return NULL; + } + if(enable != result) { + PyErr_SetString(PyExc_RuntimeError, "Could not set detectable auto repeat"); + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; +} + +// Python function tkdar.enable() handler +static PyObject *tkdar_ena(PyObject *s, PyObject *arg) +{ + (void)s; + return tkdar(arg, 1); +} + +// Python function tkdar.disable() handler +static PyObject *tkdar_dis(PyObject *s, PyObject *arg) +{ + (void)s; + return tkdar(arg, 0); +} + +static PyMethodDef tkdar_methods[] = { + {"enable", (PyCFunction)tkdar_ena, METH_O, "Enable detectable auto repeat"}, + {"disable", (PyCFunction)tkdar_dis, METH_O, "Disable detectable auto repeat"}, + {} +}; + +static struct PyModuleDef tkdar_moduledef = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "tkdar", + .m_doc = "Detectable auto repeat extension for Tk/Tkinter", + .m_size = -1, + .m_methods = tkdar_methods, +}; + +PyMODINIT_FUNC PyInit_tkdar(void); +PyMODINIT_FUNC PyInit_tkdar(void) +{ + PyObject *m = PyModule_Create(&tkdar_moduledef); + return m; +} +// vim: ts=4 shiftwidth=4 diff --git a/src/emc/usr_intf/axis/scripts/axis.py b/src/emc/usr_intf/axis/scripts/axis.py index 0ce22d4ed94..e8b428fb893 100755 --- a/src/emc/usr_intf/axis/scripts/axis.py +++ b/src/emc/usr_intf/axis/scripts/axis.py @@ -37,6 +37,7 @@ import traceback import tkinter as Tkinter +import tkdar import _thread gettext.install("linuxcnc", localedir=os.path.join(BASE, "share", "locale")) @@ -119,9 +120,26 @@ def putpref(self, option, value, type=bool): ap = AxisPreferences() +# Handle repeated key press events +pressed_keys_list = [] +def key_pressed(ev): + if ev.keysym in pressed_keys_list: + return True + pressed_keys_list.append(ev.keysym) + return False + +def key_released(ev): + # KeyRelease without KeyPress may happen when a modifier is active when + # the key is pressed without a KeyPress handler. No KeyPress event is + # generated, but releasing the actual key while still holding the modifier + # generates a KeyRelease event that may be handled if there is a handler + # installed. Therefore, we test the list to prevent an exception. + if ev.keysym in pressed_keys_list: + pressed_keys_list.remove(ev.keysym) + os.system("xhost -SI:localuser:gdm -SI:localuser:root > /dev/null 2>&1") -os.system("xset r off") root_window = Tkinter.Tk(className="Axis") +tkdar.enable(root_window) # Set detectable key repeat dpi_value = root_window.winfo_fpixels('1i') root_window.tk.call('tk', 'scaling', '-displayof', '.', dpi_value / 72.0) root_window.withdraw() @@ -154,7 +172,6 @@ def putpref(self, option, value, type=bool): def General_Halt(): text = _("Do you really want to close LinuxCNC?") if not root_window.tk.call("nf_dialog", ".error", _("Confirm Close"), text, "warning", 1, _("Yes"), _("No")): - os.system("xset r on") root_window.destroy() root_window.protocol("WM_DELETE_WINDOW", General_Halt) @@ -2685,17 +2702,22 @@ def toggle_show_pyvcppanel(*event): # The next three don't have 'manual_ok' because that's done in jog_on / # jog_off - def jog_plus(incr=False): + def jog_plus(event): + if key_pressed(event): + return # Ignore repeated press events a = ja_from_rbutton() speed = get_jog_speed(a) jog_on(a, speed) - def jog_minus(incr=False): + def jog_minus(event): + if key_pressed(event): + return # Ignore repeated press events a = ja_from_rbutton() speed = get_jog_speed(a) jog_on(a, -speed) - def jog_stop(event=None): + def jog_stop(event): + key_released(event) a = ja_from_rbutton() jog_off(a) @@ -3304,7 +3326,9 @@ def jog_off_all(): if jogging[i]: jog_off_actual(i) -def jog_on_map(num, speed): +def jog_on_map(ev, num, speed): + if key_pressed(ev): + return # Ignore repeated press events if not get_jog_mode(): if num >= len(jog_order): return axis_letter = jog_order[num] @@ -3322,7 +3346,8 @@ def jog_on_map(num, speed): if axis_letter in jog_invert: speed = -speed return jog_on(num, speed) -def jog_off_map(num): +def jog_off_map(ev, num): + key_released(ev) if not get_jog_mode(): if num >= len(jog_order): return num = "XYZABCUVW".index(jog_order[num]) @@ -3337,12 +3362,12 @@ def jog_off_map(num): return jog_off(num) def bind_axis(a, b, d): - root_window.bind("" % a, kp_wrap(lambda e: jog_on_map(d, -get_jog_speed_map(d)), "KeyPress")) - root_window.bind("" % b, kp_wrap(lambda e: jog_on_map(d, get_jog_speed_map(d)), "KeyPress")) - root_window.bind("" % a, lambda e: jog_on_map(d, -get_max_jog_speed_map(d))) - root_window.bind("" % b, lambda e: jog_on_map(d, get_max_jog_speed_map(d))) - root_window.bind("" % a, lambda e: jog_off_map(d)) - root_window.bind("" % b, lambda e: jog_off_map(d)) + root_window.bind("" % a, kp_wrap(lambda e: jog_on_map(e, d, -get_jog_speed_map(d)), "KeyPress")) + root_window.bind("" % b, kp_wrap(lambda e: jog_on_map(e, d, get_jog_speed_map(d)), "KeyPress")) + root_window.bind("" % a, lambda e: jog_on_map(e, d, -get_max_jog_speed_map(d))) + root_window.bind("" % b, lambda e: jog_on_map(e, d, get_max_jog_speed_map(d))) + root_window.bind("" % a, lambda e: jog_off_map(e, d)) + root_window.bind("" % b, lambda e: jog_off_map(e, d)) root_window.bind("", lambda e: str(e.widget) == "." and jog_off_all())