Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/emc/usr_intf/axis/Submakefile
Original file line number Diff line number Diff line change
@@ -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 $@)
Expand All @@ -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
Expand Down
155 changes: 155 additions & 0 deletions src/emc/usr_intf/axis/extensions/tkdarmodule.c
Original file line number Diff line number Diff line change
@@ -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("<KeyPress-{}>".format(key), keypress)
rootwin.bind("<KeyRelease-{}>".format(key), keyrelease)

rootwin.mainloop()
*/

#include <Python.h>
#include <tk.h>
#include <X11/XKBlib.h>

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
51 changes: 38 additions & 13 deletions src/emc/usr_intf/axis/scripts/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import traceback

import tkinter as Tkinter
import tkdar
import _thread
gettext.install("linuxcnc", localedir=os.path.join(BASE, "share", "locale"))

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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]
Expand All @@ -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])
Expand All @@ -3337,12 +3362,12 @@ def jog_off_map(num):
return jog_off(num)

def bind_axis(a, b, d):
root_window.bind("<KeyPress-%s>" % a, kp_wrap(lambda e: jog_on_map(d, -get_jog_speed_map(d)), "KeyPress"))
root_window.bind("<KeyPress-%s>" % b, kp_wrap(lambda e: jog_on_map(d, get_jog_speed_map(d)), "KeyPress"))
root_window.bind("<Shift-KeyPress-%s>" % a, lambda e: jog_on_map(d, -get_max_jog_speed_map(d)))
root_window.bind("<Shift-KeyPress-%s>" % b, lambda e: jog_on_map(d, get_max_jog_speed_map(d)))
root_window.bind("<KeyRelease-%s>" % a, lambda e: jog_off_map(d))
root_window.bind("<KeyRelease-%s>" % b, lambda e: jog_off_map(d))
root_window.bind("<KeyPress-%s>" % a, kp_wrap(lambda e: jog_on_map(e, d, -get_jog_speed_map(d)), "KeyPress"))
root_window.bind("<KeyPress-%s>" % b, kp_wrap(lambda e: jog_on_map(e, d, get_jog_speed_map(d)), "KeyPress"))
root_window.bind("<Shift-KeyPress-%s>" % a, lambda e: jog_on_map(e, d, -get_max_jog_speed_map(d)))
root_window.bind("<Shift-KeyPress-%s>" % b, lambda e: jog_on_map(e, d, get_max_jog_speed_map(d)))
root_window.bind("<KeyRelease-%s>" % a, lambda e: jog_off_map(e, d))
root_window.bind("<KeyRelease-%s>" % b, lambda e: jog_off_map(e, d))

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

Expand Down
Loading