Skip to content

Commit 306d7a7

Browse files
gh-152325: Add curses.has_mouse() and curses.window.mouse_trafo() (GH-152484)
has_mouse() reports whether the mouse driver was successfully initialized. window.mouse_trafo(y, x, to_screen) converts a coordinate pair between window-relative and screen-relative coordinates, returning the (y, x) pair or None if it lies outside the window. Together these complete the curses mouse interface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d831a69 commit 306d7a7

6 files changed

Lines changed: 183 additions & 2 deletions

File tree

Doc/library/curses.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,13 @@ The module :mod:`!curses` defines the following functions:
346346
a key with that value.
347347

348348

349+
.. function:: has_mouse()
350+
351+
Return ``True`` if the mouse driver has been successfully initialized.
352+
353+
.. versionadded:: next
354+
355+
349356
.. function:: define_key(definition, keycode)
350357

351358
Define an escape sequence *definition*, a string, as a key that generates
@@ -1309,6 +1316,18 @@ Window objects
13091316
Previously it returned ``1`` or ``0`` instead of ``True`` or ``False``.
13101317

13111318

1319+
.. method:: window.mouse_trafo(y, x, to_screen)
1320+
1321+
Convert between window-relative and screen-relative (``stdscr``-relative) character-cell coordinates.
1322+
If *to_screen* is true, convert the window-relative coordinates *y*, *x* to screen-relative coordinates;
1323+
otherwise convert in the opposite direction.
1324+
The two coordinate systems differ when lines are reserved on the screen, for example for soft labels.
1325+
1326+
Return the converted coordinates as a ``(y, x)`` tuple, or ``None`` if they lie outside the window.
1327+
1328+
.. versionadded:: next
1329+
1330+
13121331
.. attribute:: window.encoding
13131332

13141333
Encoding used to encode method arguments (Unicode strings and characters).

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ curses
192192
against an ncurses with ``NCURSES_EXT_FUNCS``.
193193
(Contributed by Serhiy Storchaka in :gh:`152334`.)
194194

195+
* Add the :func:`curses.has_mouse` function and the
196+
:meth:`curses.window.mouse_trafo` method, completing the :mod:`curses`
197+
mouse interface.
198+
(Contributed by Serhiy Storchaka in :gh:`152325`.)
199+
195200
* :class:`curses.textpad.Textbox` now supports entering and reading back the
196201
full Unicode range, including combining characters, when curses is built with
197202
wide-character support.

Lib/test/test_curses.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,22 @@ def test_enclose(self):
13051305
self.assertIs(win.enclose(7, 19), False)
13061306
self.assertIs(win.enclose(6, 20), False)
13071307

1308+
@requires_curses_window_meth('mouse_trafo')
1309+
def test_mouse_trafo(self):
1310+
win = curses.newwin(5, 15, 2, 5)
1311+
# to_screen=True: window-relative -> stdscr-relative.
1312+
self.assertEqual(win.mouse_trafo(0, 0, True), (2, 5))
1313+
self.assertEqual(win.mouse_trafo(3, 10, True), (5, 15))
1314+
self.assertEqual(win.mouse_trafo(4, 14, True), (6, 19))
1315+
# A coordinate outside the window has no counterpart.
1316+
self.assertIsNone(win.mouse_trafo(5, 0, True))
1317+
self.assertIsNone(win.mouse_trafo(0, 15, True))
1318+
# to_screen=False is the inverse: stdscr-relative -> window-relative.
1319+
self.assertEqual(win.mouse_trafo(2, 5, False), (0, 0))
1320+
self.assertEqual(win.mouse_trafo(6, 19, False), (4, 14))
1321+
self.assertIsNone(win.mouse_trafo(1, 5, False))
1322+
self.assertIsNone(win.mouse_trafo(7, 19, False))
1323+
13081324
def test_putwin(self):
13091325
win = curses.newwin(5, 12, 1, 2)
13101326
win.addstr(2, 1, 'Lorem ipsum')
@@ -1824,6 +1840,11 @@ def test_has_colors(self):
18241840
self.assertIsInstance(curses.has_colors(), bool)
18251841
self.assertIsInstance(curses.can_change_color(), bool)
18261842

1843+
@requires_curses_func('has_mouse')
1844+
def test_has_mouse(self):
1845+
# Whether a mouse is available depends on the terminal.
1846+
self.assertIsInstance(curses.has_mouse(), bool)
1847+
18271848
def test_start_color(self):
18281849
if not curses.has_colors():
18291850
self.skipTest('requires colors support')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :func:`curses.has_mouse` function and the
2+
:meth:`curses.window.mouse_trafo` method.

Modules/_cursesmodule.c

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
del_curterm mcprint mvcur restartterm
4444
ripoffline set_curterm setterm
4545
tgetent tgetflag tgetnum tgetstr tgoto tputs
46-
vidattr vidputs wmouse_trafo
46+
vidattr vidputs
4747
4848
Low-priority:
4949
slk_attr slk_attr_off slk_attr_on slk_attr_set slk_attroff
@@ -3007,6 +3007,36 @@ _curses_window_enclose_impl(PyCursesWindowObject *self, int y, int x)
30073007
{
30083008
return PyBool_FromLong(wenclose(self->win, y, x));
30093009
}
3010+
3011+
/*[clinic input]
3012+
_curses.window.mouse_trafo
3013+
3014+
y: int
3015+
Y-coordinate.
3016+
x: int
3017+
X-coordinate.
3018+
to_screen: bool
3019+
If True, convert window-relative coordinates to
3020+
stdscr-relative ones; otherwise convert the other way.
3021+
/
3022+
3023+
Convert coordinates between window-relative and screen-relative.
3024+
3025+
Return the converted (y, x) coordinates, or None if they are
3026+
outside the window.
3027+
[clinic start generated code]*/
3028+
3029+
static PyObject *
3030+
_curses_window_mouse_trafo_impl(PyCursesWindowObject *self, int y, int x,
3031+
int to_screen)
3032+
/*[clinic end generated code: output=b21572fa3524c15d input=c51fd793af7f6965]*/
3033+
{
3034+
int ry = y, rx = x;
3035+
if (!wmouse_trafo(self->win, &ry, &rx, to_screen)) {
3036+
Py_RETURN_NONE;
3037+
}
3038+
return Py_BuildValue("(ii)", ry, rx);
3039+
}
30103040
#endif
30113041

30123042
/*[clinic input]
@@ -4836,6 +4866,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
48364866
_CURSES_WINDOW_DUPWIN_METHODDEF
48374867
_CURSES_WINDOW_ECHOCHAR_METHODDEF
48384868
_CURSES_WINDOW_ENCLOSE_METHODDEF
4869+
_CURSES_WINDOW_MOUSE_TRAFO_METHODDEF
48394870
{"erase", PyCursesWindow_werase, METH_NOARGS,
48404871
"erase($self, /)\n--\n\n"
48414872
"Clear the window."},
@@ -6995,6 +7026,21 @@ _curses_meta_impl(PyObject *module, int yes)
69957026
}
69967027

69977028
#ifdef NCURSES_MOUSE_VERSION
7029+
/*[clinic input]
7030+
_curses.has_mouse
7031+
7032+
Return True if the mouse driver has been successfully initialized.
7033+
[clinic start generated code]*/
7034+
7035+
static PyObject *
7036+
_curses_has_mouse_impl(PyObject *module)
7037+
/*[clinic end generated code: output=7901cc34069e4f57 input=94682101a11c4f30]*/
7038+
{
7039+
PyCursesStatefulInitialised(module);
7040+
7041+
return PyBool_FromLong(has_mouse());
7042+
}
7043+
69987044
/*[clinic input]
69997045
_curses.mouseinterval
70007046
@@ -8204,6 +8250,7 @@ static PyMethodDef cursesmodule_methods[] = {
82048250
_CURSES_HAS_IC_METHODDEF
82058251
_CURSES_HAS_IL_METHODDEF
82068252
_CURSES_HAS_KEY_METHODDEF
8253+
_CURSES_HAS_MOUSE_METHODDEF
82078254
_CURSES_DEFINE_KEY_METHODDEF
82088255
_CURSES_KEY_DEFINED_METHODDEF
82098256
_CURSES_KEYOK_METHODDEF

Modules/clinic/_cursesmodule.c.h

Lines changed: 88 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)