diff -r d3c7ebdc71bb Doc/library/curses.menu.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Doc/library/curses.menu.rst Thu Oct 18 00:28:49 2012 -0500 @@ -0,0 +1,613 @@ +:mod:`curses.menu` --- A menu extension for curses +================================================== + +.. module:: curses.menu + :synopsis: A curses extension that adds menu boxes to curses. +.. sectionauthor:: Ben Trofatter + + +Menu description. + +The :mod:`curses.menu` module provides an interface to the curses menu +extension library. + + +.. _curses-menu-functions: + +Functions +--------- + +The module :mod:`curses.menu` defines the following functions: + + +.. function:: new_item(name[, description=None]) + + Return an item object. + + +.. function:: new_menu(*items) + + Return a menu object associated with the given items. By default, the + menu will be tied to the ``stdscr``, but this can be overridden globally + using :func:`set_menu_win` and :func:`set_menu_sub` or on a per-menu + basis with :meth:`win` and :meth:`sub` as described below. + + +.. function:: set_menu_win(win) + + Set the default window used to display the title and border of subsequently + created menus. + + +.. function:: set_menu_win(win) + + Set the default window used to display the items of subsequently created + menus. + + +.. function:: set_menu_mark(mark) + + Set the default menu mark string. This can be overridden with + :meth:`set_mark` as described below. + + +.. function:: request_by_name(request) + + Return the printable name of a menu request code. + + +.. function:: request_name(name) + + Return the menu request code associated with the given name. + + +.. _curses-menu-item-objects: + +Item Objects +------------ + +Item objects, as returned by :func:`new_item` above, have the following +methods: + + +.. method:: Item.description() + + Return the description string of the item if it is set else ``None``. + + +.. method:: Item.index() + + Return the index of the item in its menu's item list. + + +.. method:: Item.name() + + Return the name string of the item. + + +.. method:: Item.opts() + + Return the item's currently set option bits. Currently, there is only one + defined item option, :const:`O_SELECTABLE`, which when set allows the item to + be selected from its menu. This option is on by default. + + +.. method:: Item.opts_off(opts) + + Turn off the given, logically-OR'ed option bits, leaving the rest alone. + + +.. method:: Item.opts_on() + + Turn on the given, logically-OR'ed option bits, leaving the rest alone. + + +.. method:: Item.set_opts(opts) + + Set the item's options bits by logically-OR'ing options together. Currently, + there is only one defined item option, :const:`O_SELECTABLE`, which when set + allows the item to be selected from its menu. This option is on by default. + + +.. method:: Item.set_userptr(obj) + + Set the item's user point to `obj`. This is used to associate an arbitrary + piece of data with the item, and can be any Python object. + + +.. method:: Item.set_value(selected) + + In a multi-valued menu, you can select or deselect the item by passing + ``True`` or ``False``, respectively. + + +.. method:: Item.userptr() + + Return the user pointer for the item. + + +.. method:: Item.value() + + In a multi-valued menu, returns ``True`` if the item is currently selected + or ``False`` if it isn't. + + +.. method:: Item.visible() + + Return ``True`` if the item is currently mapped onto the screen or + ``False`` if it is not. + + +.. _curses-menu-menu-objects: + +Menu Objects +------------ + +Menu objects, as returned by :func:`new_menu` above, have the following +methods: + + +.. method:: Menu.back() + + Return the background attribute. This is the highlight used for selectable + menu items that are not currently selected. The default is + :const:`curses.A_NORMAL`. + + +.. method:: Menu.current_item() + + Return the currently active menu item. + + +.. method:: Menu.driver(c) + + Once the menu has been posted, user input should be passed to + :meth:`driver`. This routine has three major input cases: + + Menu navigation requests + See :ref:`curses-menu-requests` for available values and their meanings. + + Printable characters + The character is appended to the pattern buffer. If the buffer does not + match any items in the menu, the character is subsequently removed. + + A :const:`KEY_MOUSE` request associated with a mouse event + The driver translates the click event into one of the previously + mentioned menu navigation requests. + + If you click above the display region of the menu: + + +--------------+-------------------------+ + | Mouse Event | Menu Navigation Request | + +==============+=========================+ + | Single Click | :const:`REQ_SCR_ULINE` | + +--------------+-------------------------+ + | Double Click | :const:`REQ_SCR_UPAGE` | + +--------------+-------------------------+ + | Triple Click | :const:`REQ_FIRST_ITEM` | + +--------------+-------------------------+ + + If you click below the display region of the menu: + + +--------------+-------------------------+ + | Mouse Event | Menu Navigation Request | + +==============+=========================+ + | Single Click | :const:`REQ_SCR_DLINE` | + +--------------+-------------------------+ + | Double Click | :const:`REQ_SCR_DPAGE` | + +--------------+-------------------------+ + | Triple Click | :const:`REQ_LAST_ITEM` | + +--------------+-------------------------+ + + If you click an item within the display area of the menu: + + +--------------+------------------------------------------------------+ + | Mouse Event | Action Taken | + +==============+======================================================+ + | Single Click | Menu cursor is positioned on that item. | + +--------------+------------------------------------------------------+ + | Double Click | :const:`REQ_TOGGLE_ITEM` is generated and the driver | + | | returns an error state intended to indicate that an | + | | item specific, custom command should be sent to the | + | | driver. | + +--------------+------------------------------------------------------+ + + +.. method:: Menu.fore() + + Return the foreground attribute. This is the highlight used for selected + menu items. The default is :const:`curses.A_REVERSE`. + + +.. method:: Menu.format() + + Return the maximum-size constraints for the given menu as a 2-tuple of + ``(rows, columns)``. + + +.. method:: Menu.grey() + + Return the grey attribute of menu. This is the highlight used for + un-selectable menu items in menus that permit more than one selection. The + default is :const:`curses.A_UNDERLINE`. + + +.. method:: Menu.item_count() + + Return the number of items associated with the menu. + + +.. method:: Menu.item_init() + + Return the currently set item init hook if one exists, otherwise ``None``. + The item init hook is a Python callable object that takes a menu as its + only argument. It is called at menu post time and immediately after the + selected item changes. + + +.. method:: Menu.item_term() + + Return the currently set item term hook if one exists, otherwise ``None``. + The item term hook is a Python callable object that takes a menu as its + only argument. It is called at menu unpost time and immediately before the + selected item changes. + + +.. method:: Menu.items() + + Return a tuple of the items currently associated with the menu. + + +.. method:: Menu.mark() + + Return the string used to mark a menu item as selected. Defaults to "-". + + +.. method:: Menu.menu_init() + + Return the currently set menu init hook if one exists, otherwise ``None``. + The menu init hook is a Python callable object that takes a menu as its + only argument. It is called at menu post time and immediately after the + top row of the menu changes once it is posted. + + +.. method:: Menu.menu_term() + + Return the currently set menu term hook if one exists, otherwise ``None``. + The menu term hook is a Python callable object that takes a menu as its + only argument. It is called at menu unpost time and immediately before the + top row of the menu changes once it is posted. + + +.. method:: Menu.opts() + + Return the menu's currently set option bits. See below for a description + of menu options. See :ref:`curses-menu-menu-options` for a listing of + available options and their meanings. + + +.. method:: Menu.opts_off(opts) + + Turn off the given, logically-OR'ed option bits, leaving the rest alone. + See :ref:`curses-menu-menu-options` for a listing of available options + and their meanings. + + +.. method:: Menu.opts_on(opts) + + Turn on the given, logically-OR'ed option bits, leaving the rest alone. + See :ref:`curses-menu-menu-options` for a listing of available options + and their meanings. + + +.. method:: Menu.pad() + + Return the currently set pad character. This character appears centered + between an item's name and its description. The default is " ". + + +.. method:: Menu.pattern() + + Return the contents of the menu's pattern buffer. Input events that are + printable characters are appended to this buffer and used to search for + a matching item. + + +.. method:: Menu.pos_cursor() + + Restore the cursor to the position currently associated with the menu's + selected item. Intended to be used after other curses routings have been + called to do screen-painting in response to a menu select. + + +.. method:: Menu.post() + + Write the menu to its associated window buffer without actually painting + the window on the scresn. To display the menu, call :meth:`refresh` or its + equivalent on the menu's associated window object (e.g. the implicit + :func:`doupdate` triggered by a curses input request). :meth:`post` will + also reset the selection status of all menu items. + + +.. method:: Menu.scale() + + Return a 2-tuple ``(rows, columns)`` indicating the minimum size required + for the subwindow of the menu. + + +.. method:: Menu.set_back(attr) + + Sets the background attribute. This is the highlight used for selectable + menu items that are not currently selected. The default is + :const:`curses.A_NORMAL`. + + +.. method:: Menu.set_current_item(item) + + Sets the currently active item (the item on which the menu cursor is + positioned). The `item` argument must currently be associated with the + menu. + + +.. method:: Menu.set_fore(attr) + + Sets the foreground attribute. This is the highlight used for selected + menu items. The default is :const:`curses.A_REVERSE`. + + +.. method:: Menu.set_format(rows, columns) + + Set the maximum display size of the given menu. If this is too small to + display all menu items, the menu will be made scrollable. If this size is + larger than the menu's subwindow and the subwindow is too small to display + all of the menu's items, :meth:`post` will fail. The default is 16 rows + and 1 column. A 0 value for `rows` or `columns` is interpreted as a request + not to change the current value. + + +.. method:: Menu.set_grey(attr) + + Sets the grey attribute of menu. This is the highlight used for + un-selectable menu items in menus that permit more than one selection. The + default is :const:`curses.A_UNDERLINE`. + + +.. method:: Menu.set_item_init(callback) + + Set the item init hook. The item init hook is called at menu post time and + immediately after the selected item changes. `callback` is a callable object + that takes a menu as its only argument. + + +.. method:: Menu.set_item_term(callback) + + Set the item term hook. The item term hook is called at menu unpost time + and immediately before the selected item changes. `callback` is a callable + object that takes a menu as its only argument. + + +.. method:: Menu.set_items(*items) + + Change the list of items associated with the menu. + + +.. method:: Menu.set_mark(mark) + + Set the string used to mark a menu item as selected. This string will + display as a prefix to the item. Changing the length of the menu mark while + the menu is posted can lead to unexpected behavior. + + +.. method:: Menu.set_menu_init(callback) + + Set the menu init hook. The menu init hook is called at menu post time and + immediately after the selected item changes. `callback` is a callable object + that takes a menu as its only argument. + + +.. method:: Menu.set_menu_term(callback) + + Set the menu term hook. The menu term hook is called at menu unpost time + and immediately before the selected item changes. `callback` is a callable + object that takes a menu as its only argument. + + +.. method:: Menu.set_opts(opts) + + Set the menu's options bits by logically-OR'ing options together. + See :ref:`curses-menu-menu-options` for a listing of available options + and their meanings. + + +.. method:: Menu.set_pad(pad) + + Set the menu's pad character. This character is centered between an item's + name and its description. The default is " ". + + +.. method:: Menu.set_pattern(pattern) + + Attempt to set the menu's pattern buffer. If `pattern` matches one or more + items, the pattern buffer will be set. + + +.. method:: Menu.set_spacing(spc_description, spc_rows, spc_columns) + + Set the spacing between menu items. `spc_description` is the number of + spaces between an item name and its description. It must not be larger + than :const:`TABSIZE`. The menu system inserts the value of :meth:`pad` + in the middle of this spacing area and fills the remaining parts with spaces. + `spc_rows` is the number of rows used for each item. It must not be + larger than 3. `spc_columns` is the number of blanks between columns + of items. It must not be larger than :const:`TABSIZE`. Passing 0 for all + arguments results in the spacing being reset to the defaults, which is + 1 for each them. + + +.. method:: Menu.set_sub([win]) + + Set the subwindow used to display the items the are currently visible and + available for selection. Defaults to the ``stdscr``. If no `win` argument + is passed, the menu's subwindow will be reset to the ``stdscr``. + + +.. method:: Menu.set_top_row(row) + + Set the number of the row to be displayed at the top of the menu's window. + This is initially set to 0 and is reset to this value whenever the + :const:`O_ROWMAJOR` option is toggled. The leftmost item on the given row + then becomes the current item. + + +.. method:: Menu.set_userptr(obj) + + Set the menu's user point to `obj`. This is used to associate an arbitrary + piece of data with the menu, and can be any Python object. + + +.. method:: Menu.set_win([win]) + + Set the window used to display the title and/or border of the menu, should + they exist. Defaults to the ``stdscr``. If no `win` argument is passed, the + menu's main window will be reset to the ``stdscr``. + + +.. method:: Menu.sub() + + Return the menu's subwindow, which is used to display the items of the menu + that are currently visible and available for selection. This defaults to the + ``stdscr``. + + +.. method:: Menu.spacing() + + Return a 3-tuple ``(spc_description, spc_rows, spc_columns)`` describing + the spacing within and between menu items. `spc_description` is the + number of spaces between an item name and its description. The menu system + inserts the value of :meth:`pad` in the middle of this spacing area and + fills the remaining parts with spaces. `spc_rows` is the number of rows + used for each item. `spc_columns` is the number of blanks between columns + of items. + + +.. method:: Menu.top_row() + + Return the number of the row that is currently at the top of the menu's + display window. + + +.. method:: Menu.unpost() + + Erase the menu from its associated window. + + +.. method:: Menu.userptr() + + Return the user pointer for the menu. + + +.. method:: Menu.win() + + Return the menu's main window, which is used to display a title and/or + border. This defaults to the ``stdscr``. + + +Constants +--------- + +The :mod:`curses.menu` module defines the following data members: + +.. data:: version + + A string representing the current version of the module. Also available as + :const:`__version__`. + +.. _curses-menu-item-options: + +Constants are available to set options on items. All are on by default: + ++-----------------------+-----------------------------------+ +| Option | Meaning | ++=======================+===================================+ +| :const:`O_SELECTABLE` | Item is selectable from its menu. | ++-----------------------+-----------------------------------+ + +.. _curses-menu-menu-options: + +Constants are available to set options on menus. All are on by default: + ++-----------------------+--------------------------------------------------+ +| Option | Meaning | ++=======================+==================================================+ +| :const:`O_IGNORECASE` | Ignore the case when pattern-matching. | ++-----------------------+--------------------------------------------------+ +| :const:`O_NONCYCLIC` | Don't wrap around to the other side of the menu. | ++-----------------------+--------------------------------------------------+ +| :const:`O_ONEVALUE` | Only one item can be selected at a time. | ++-----------------------+--------------------------------------------------+ +| :const:`O_ROWMAJOR` | Display the menu in row-major order. | ++-----------------------+--------------------------------------------------+ +| :const:`O_SHOWDESC` | Display the item descriptions when the menu is | +| | posted. | ++-----------------------+--------------------------------------------------+ +| :const:`O_SHOWMATCH` | Move the cursor to within the item name while | +| | pattern-matching. | ++-----------------------+--------------------------------------------------+ + +.. _curses-menu-requests: + +Menu driver requests are constants beginning with ``REQ_``: + ++----------------------------+------------------------------------------------+ +| Request | Meaning | ++============================+================================================+ +| :const:`REQ_DOWN_ITEM` | Move down to an item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_LEFT_ITEM` | Move left to an item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_RIGHT_ITEM` | Move right to an item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_UP_ITEM` | Move up to an item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_SCR_DLINE` | Scroll down a line. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_SCR_DPAGE` | Scroll down a page. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_SCR_ULINE` | Scroll up a line. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_SCR_UPAGE` | Scroll up a page. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_FIRST_ITEM` | Move to the first item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_LAST_ITEM` | Move to the last item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_NEXT_ITEM` | Move to the next item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_PREV_ITEM` | Move to the previous item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_TOGGLE_ITEM` | Select/deselect an item. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_CLEAR_PATTERN` | Clear the menu pattern buffer. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_BACK_PATTERN` | Delete the previous character from the pattern | +| | buffer. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_NEXT_MATCH` | Move to the next item matching the pattern | +| | buffer. | ++----------------------------+------------------------------------------------+ +| :const:`REQ_PREV_MATCH` | Move to the previous item matching the pattern | +| | buffer. | ++----------------------------+------------------------------------------------+ + +You can define application-specific commands relative to the menu command +constants: + ++---------------------------+------------------------------------------------+ +| Constant | Meaning | ++===========================+================================================+ +| :const:`MIN_MENU_COMMAND` | The minimum allowable value for pre-defined | +| | requests. | ++---------------------------+------------------------------------------------+ +| :const:`MAX_MENU_COMMAND` | The maximum allowable value for pre-defined | +| | requests. | ++---------------------------+------------------------------------------------+ diff -r d3c7ebdc71bb Doc/library/curses.rst --- a/Doc/library/curses.rst Mon Oct 15 16:57:37 2012 +0200 +++ b/Doc/library/curses.rst Thu Oct 18 00:28:49 2012 -0500 @@ -38,6 +38,9 @@ Module :mod:`curses.panel` A panel stack extension that adds depth to curses windows. + Module :mod:`curses.menu` + A menu extension that adds single- and multi- selection boxes to curses interfaces. + Module :mod:`curses.textpad` Editable text widget for curses supporting :program:`Emacs`\ -like bindings. diff -r d3c7ebdc71bb Include/py_curses.h --- a/Include/py_curses.h Mon Oct 15 16:57:37 2012 +0200 +++ b/Include/py_curses.h Thu Oct 18 00:28:49 2012 -0500 @@ -69,7 +69,7 @@ extern "C" { #endif -#define PyCurses_API_pointers 4 +#define PyCurses_API_pointers 5 /* Type declarations */ @@ -83,10 +83,15 @@ #define PyCurses_CAPSULE_NAME "_curses._C_API" +#define PyCurses_FIND_WO_NUM 4 +#define PyCurses_FIND_WO_RETURN PyCursesWindowObject* +#define PyCurses_FIND_WO_PROTO (WINDOW *win) #ifdef CURSES_MODULE /* This section is used when compiling _cursesmodule.c */ +static PyCurses_FIND_WO_RETURN func_PyCurses_FindWO PyCurses_FIND_WO_PROTO; + #else /* This section is used in modules that use the _cursesmodule API */ @@ -96,6 +101,8 @@ #define PyCursesSetupTermCalled {if (! ((int (*)(void))PyCurses_API[1]) () ) return NULL;} #define PyCursesInitialised {if (! ((int (*)(void))PyCurses_API[2]) () ) return NULL;} #define PyCursesInitialisedColor {if (! ((int (*)(void))PyCurses_API[3]) () ) return NULL;} +#define PyCursesFindWO \ + (*(PyCurses_FIND_WO_RETURN (*) PyCurses_FIND_WO_PROTO) PyCurses_API[PyCurses_FIND_WO_NUM]) #define import_curses() \ PyCurses_API = (void **)PyCapsule_Import(PyCurses_CAPSULE_NAME, 1); diff -r d3c7ebdc71bb Lib/curses/menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/curses/menu.py Thu Oct 18 00:28:49 2012 -0500 @@ -0,0 +1,6 @@ +"""curses.menu + +Module for using menus with curses. +""" + +from _curses_menu import * diff -r d3c7ebdc71bb Lib/test/test_curses.py --- a/Lib/test/test_curses.py Mon Oct 15 16:57:37 2012 +0200 +++ b/Lib/test/test_curses.py Thu Oct 18 00:28:49 2012 -0500 @@ -19,9 +19,10 @@ from test.support import requires, import_module requires('curses') -# If either of these don't exist, skip the tests. +# If any of these don't exist, skip the tests. curses = import_module('curses') curses.panel = import_module('curses.panel') +curses.menu = import_module('curses.menu') # XXX: if newterm was supported we could use it instead of initscr and not exit @@ -311,6 +312,110 @@ else: raise AssertionError("TypeError not raised") +def test_menu(stdscr): + + menu = curses.menu + + def items_from_names(s, with_description=False): + res = [] + for n in s.split(): + if with_description: + res.append(menu.new_item(n, n + " desc")) + else: + res.append(menu.new_item(n)) + return tuple(res) + + item_names_1 = """ + Apple Bear Chalk Dog Elephant Fernsehen Grep HypnoToad Ignatius Jack + """ + + item_names_2 = """ + Action Brittle Chesterfield Dingo Excite Frighten Group Hyphen Innie Jeff + """ + + items_1 = items_from_names(item_names_1) + items_2 = items_from_names(item_names_2) + m = menu.new_menu(*items_1) + + other_win = curses.newwin(20, 40) + other_sub = other_win.derwin(16, 38) + + menu.set_menu_win(other_win) + menu.set_menu_sub(other_sub) + menu.set_menu_mark("*") + + # calling set_{win,sub} without arguments resets them to the stdscr + m.set_win() + m.set_sub() + + m.set_win(other_win) + m.set_sub(other_sub) + + errors = [] + for setter, args, getter in [ + (m.set_back, (curses.A_REVERSE,), m.back), + (m.set_current_item, (items_1[3],), m.current_item), + (m.set_fore, (curses.A_BLINK,), m.fore), + (m.set_format, (4, 2), m.format), + (m.set_grey, (curses.A_UNDERLINE,), m.grey), + (m.set_mark, ("&",), m.mark), + (m.set_opts, (menu.O_SHOWMATCH,), m.opts), + (m.set_pad, (ord("#"),), m.pad), + (m.set_pattern, ("a",), m.pattern), + (m.set_top_row, (1,), m.top_row), + (m.set_spacing, (3, 2, 2), m.spacing), + (m.set_items, items_2, m.items), + ]: + setter(*args) + try: + if len(args) == 1: + assert args[0] == getter(), "{} is the expected return of {}, "\ + "not {}".format(args[0], getter.__name__, getter()) + else: + assert args == getter(), "{} is the expected return of {}, "\ + "not {}".format(args, getter.__name__, getter()) + except AssertionError: + errors.append("{} {} {}".format(getter.__name__, args, getter())) + + if errors: + raise AssertionError("\n".join(errors)) + + + m.opts_on(menu.O_SHOWDESC) + assert m.opts() & menu.O_SHOWDESC + m.opts_off(menu.O_SHOWDESC) + assert not (m.opts() & menu.O_SHOWDESC) + + try: + m.userptr() + raise RuntimeError('userptr should fail since not set') + except menu.error: + pass + + nil = object() + + m.set_userptr(nil) + assert nil is m.userptr() + + def make_menu_hook(scr, row, name): + def hook(menu): + scr.addstr(row, 0, name) + scr.refresh() + return hook + + m.set_item_init(make_menu_hook(stdscr, 1, "item_init")) + m.set_item_term(make_menu_hook(stdscr, 1, "item_term")) + m.set_menu_init(make_menu_hook(stdscr, 1, "menu_init")) + m.set_menu_term(make_menu_hook(stdscr, 1, "menu_term")) + + m.post() + stdscr.refresh() + for action in [menu.REQ_DOWN_ITEM, menu.REQ_UP_ITEM, menu.REQ_LEFT_ITEM, + menu.REQ_RIGHT_ITEM]: + m.driver(action) + stdscr.refresh() + m.unpost() + def main(stdscr): curses.savetty() try: @@ -322,6 +427,7 @@ test_unget_wch(stdscr) test_issue10570() test_encoding(stdscr) + test_menu(stdscr) finally: curses.resetty() diff -r d3c7ebdc71bb Misc/ACKS --- a/Misc/ACKS Mon Oct 15 16:57:37 2012 +0200 +++ b/Misc/ACKS Thu Oct 18 00:28:49 2012 -0500 @@ -1174,6 +1174,7 @@ David Townshend Laurence Tratt Alberto Trevino +Ben Trofatter Matthias Troffaes John Tromp Jason Trowbridge diff -r d3c7ebdc71bb Modules/_curses_menu.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Modules/_curses_menu.c Thu Oct 18 00:28:49 2012 -0500 @@ -0,0 +1,1080 @@ +/* + * Interface to the ncurses menu library + * Original version by Ben Trofatter + */ + +/* Release Number */ + +static char *PyCursesVersion = "1.0"; + +/* Includes */ + +#include "Python.h" + +#include "py_curses.h" + +#include + +typedef struct { + PyObject *PyCursesError; + PyObject *PyCursesMenu_Type; + PyObject *PyCursesItem_Type; + char *default_mark; +} _curses_menustate; + +#define _curses_menustate(o) ((_curses_menustate*)PyModule_GetState(o)) + +static int +_curses_menu_clear(PyObject *m) +{ + Py_CLEAR(_curses_menustate(m)->PyCursesError); + return 0; +} + +static int +_curses_menu_traverse(PyObject *m, visitproc visit, void *arg) +{ + Py_VISIT(_curses_menustate(m)->PyCursesError); + return 0; +} + +static void +_curses_menu_free(void *m) +{ + _curses_menu_clear((PyObject *) m); +} + +static struct PyModuleDef _curses_menumodule; + +#define _curses_menustate_global \ + ((_curses_menustate*)PyModule_GetState(PyState_FindModule(&_curses_menumodule))) + +/* Utility Functions */ + +/* + * Check the return code from a curses function and return None + * or raise an exception as appropriate. + */ + +static PyObject * +PyCursesCheckERR(int code, char *fname) +{ + if (code != ERR) { + Py_INCREF(Py_None); + return Py_None; + } else { + if (!fname) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + catchall_ERR); + } else { + PyErr_Format(_curses_menustate_global->PyCursesError, + "%s() returned ERR", fname); + } + return NULL; + } +} + +/**************************************************************************** + The Item Object +*****************************************************************************/ + +/* Definition of the item object and item type */ + +typedef struct { + PyObject_HEAD + ITEM *item; + char *name; + char *description; +} PyCursesItemObject; + +#define PyCursesItem_Check(ob) \ + ((PyObject*)Py_TYPE(ob) == _curses_menustate_global->PyCursesItem_Type) + +static PyObject * +PyCursesItem_New(ITEM *item, char *name, char *description) +{ + PyCursesItemObject *io; + + io = PyObject_NEW(PyCursesItemObject, + (PyTypeObject *)(_curses_menustate_global)->PyCursesItem_Type); + if (!io) + return NULL; + io->item = item; + io->name = name; + io->description = description; + return (PyObject *)io; +} + +static void +PyCursesItem_Dealloc(PyCursesItemObject *io) +{ + free_item(io->item); + free(io->name); + free(io->description); + PyObject_DEL(io); +} + +#define Item_NoArgOneReturnFunction(X, ERGSTR) \ +static PyObject * \ +PyCursesItem_ ## X (PyCursesItemObject *self) \ +{ \ + return Py_BuildValue(ERGSTR, X(self->item)); \ +} + +#define Item_NoArgTrueFalseFunction(X) \ +static PyObject * \ +PyCursesItem_ ## X (PyCursesItemObject *self) \ +{ \ + if (X(self->item) == FALSE) { \ + Py_INCREF(Py_False); \ + return Py_False; \ + } \ + Py_INCREF(Py_True); \ + return Py_True; \ +} + +#define Item_OneArgNoReturnFunction(X, TYPE, PARSESTR) \ +static PyObject * \ +PyCursesItem_ ## X (PyCursesItemObject *self, PyObject *args) \ +{ \ + TYPE arg; \ + \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg)) \ + return NULL; \ + return PyCursesCheckERR(X(self->item, arg), # X); \ +} + +Item_NoArgOneReturnFunction(item_name, "s") +Item_NoArgOneReturnFunction(item_description, "s") +Item_NoArgOneReturnFunction(item_index, "i") +Item_NoArgTrueFalseFunction(item_value) +Item_OneArgNoReturnFunction(set_item_value, int, "i;True(1) or False(0)") +Item_NoArgTrueFalseFunction(item_visible) +Item_NoArgOneReturnFunction(item_opts, "i") +Item_OneArgNoReturnFunction(set_item_opts, int, "i") +Item_OneArgNoReturnFunction(item_opts_on, int, "i") +Item_OneArgNoReturnFunction(item_opts_off, int, "i") + +static PyObject * +PyCursesItem_set_item_userptr(PyCursesItemObject *self, PyObject *obj) +{ + Py_INCREF(obj); + return PyCursesCheckERR(set_item_userptr(self->item, (void*)obj), + "set_item_userptr"); +} + +static PyObject * +PyCursesItem_item_userptr(PyCursesItemObject *self) +{ + PyObject *obj; + + PyCursesInitialised; + if (!(obj = (PyObject *)item_userptr(self->item))) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + "no userptr set"); + return NULL; + } + Py_INCREF(obj); + return obj; +} + +/* ----------------- Module interface --------------------*/ + +static PyMethodDef PyCursesItem_Methods[] = { + {"description", (PyCFunction)PyCursesItem_item_description, METH_NOARGS}, + {"index", (PyCFunction)PyCursesItem_item_index, METH_NOARGS}, + {"name", (PyCFunction)PyCursesItem_item_name, METH_NOARGS}, + {"opts", (PyCFunction)PyCursesItem_item_opts, METH_NOARGS}, + {"opts_off", (PyCFunction)PyCursesItem_item_opts_off, METH_VARARGS}, + {"opts_on", (PyCFunction)PyCursesItem_item_opts_on, METH_VARARGS}, + {"set_opts", (PyCFunction)PyCursesItem_set_item_opts, METH_VARARGS}, + {"set_userptr", (PyCFunction)PyCursesItem_set_item_userptr, METH_O}, + {"set_value", (PyCFunction)PyCursesItem_set_item_value, METH_VARARGS}, + {"userptr", (PyCFunction)PyCursesItem_item_userptr, METH_NOARGS}, + {"value", (PyCFunction)PyCursesItem_item_value, METH_NOARGS}, + {"visible", (PyCFunction)PyCursesItem_item_visible, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +/* -------------------------------------------------------*/ + +static PyType_Slot PyCursesItem_Type_slots[] = { + {Py_tp_dealloc, PyCursesItem_Dealloc}, + {Py_tp_methods, PyCursesItem_Methods}, + {0, 0}, +}; + +static PyType_Spec PyCursesItem_Type_spec = { + "_curses_menu.curses item", + sizeof(PyCursesItemObject), + 0, + Py_TPFLAGS_DEFAULT, + PyCursesItem_Type_slots +}; + +/**************************************************************************** + The Menu Object +*****************************************************************************/ + +/* Definition of the menu object and menu type */ + +typedef struct { + PyObject_HEAD + MENU *menu; + /* Window bookkeeping */ + PyCursesWindowObject *win; + PyCursesWindowObject *sub; + /* Callback functions */ + PyObject *item_init; + PyObject *item_term; + PyObject *menu_init; + PyObject *menu_term; + /* Item bookkeeping */ + PyCursesItemObject **ios; + ITEM **items; + /* MENU only stores a pointer. The string must continue to exist + * elsewhere. */ + char *mark; +} PyCursesMenuObject; + +#define PyCursesMenu_Check(ob) \ + ((PyObject*)Py_TYPE(ob) == _curses_menustate_global->PyCursesMenu_Type) + +/* To get callbacks to work, we'll need to be able to look up + * PyCursesMenuObjects by their associated MENU *. A list should be + * sufficient unless there are many menus in a given program. + */ + +typedef struct list_of_menus { + PyCursesMenuObject *mo; + struct list_of_menus *next; +} list_of_menus; + +static list_of_menus *lom; + +static int +insert_lom(PyCursesMenuObject *mo) +{ + list_of_menus *new; + + if (!(new = (list_of_menus*)malloc(sizeof(list_of_menus)))) { + PyErr_NoMemory(); + return 0; + } + new->mo = mo; + new->next = lom; + lom = new; + return 1; +} + +static void +remove_lom(PyCursesMenuObject *mo) +{ + list_of_menus *temp, *n; + + temp = lom; + if (temp->mo == mo) { + lom = temp->next; + free(temp); + return; + } + while (temp->next == NULL || temp->next->mo != mo) { + if (!temp->next) { + PyErr_SetString(PyExc_RuntimeError, + "remove_lom: can't find Menu Object"); + return; + } + temp = temp->next; + } + n = temp->next->next; + free(temp->next); + temp->next = n; + return; +} + +PyCursesMenuObject * +find_mo(MENU *menu) +{ + list_of_menus *temp; + + for (temp = lom; temp->mo->menu != menu; temp=temp->next) { + if (!temp->next) + return NULL; + } + return temp->mo; +} + +/* ------------- MENU routines ------------- */ + +/* Allocation and deallocation of Menu Objects */ + +static PyObject * +PyCursesMenu_New(MENU *menu, ITEM **items, PyCursesItemObject **ios) +{ + PyCursesMenuObject *mo; + + if (!(mo = PyObject_NEW(PyCursesMenuObject, + (PyTypeObject*)(_curses_menustate_global)->PyCursesMenu_Type))) + return NULL; + if (insert_lom(mo) < 0) { + Py_DECREF(mo); + return NULL; + } + mo->menu = menu; + mo->items = items; + mo->ios = ios; + mo->item_init = NULL; + mo->item_term = NULL; + mo->menu_init = NULL; + mo->menu_term = NULL; + mo->win = NULL; + mo->sub = NULL; + mo->mark = NULL; + return (PyObject*)mo; +} + +static void +PyCursesMenu_Dealloc(PyCursesMenuObject *mo) +{ + free_menu(mo->menu); + remove_lom(mo); + free(mo->items); + free(mo->ios); + if (mo->item_init != NULL) + Py_DECREF(mo->item_init); + if (mo->item_term != NULL) + Py_DECREF(mo->item_term); + if (mo->menu_init != NULL) + Py_DECREF(mo->menu_init); + if (mo->menu_term != NULL) + Py_DECREF(mo->menu_term); + if (mo->mark != NULL) + free(mo->mark); + if (mo->win != NULL) + Py_DECREF(mo->win); + if (mo->sub != NULL) + Py_DECREF(mo->sub); + PyObject_DEL(mo); +} + +static int +PyCurses_ExtractItems(PyObject *args, ITEM ***items, PyCursesItemObject ***ios) +{ + int i, n; + PyObject *io; + + if (*items != NULL) + free(*items); + if (*ios != NULL) + free(*ios); + n = PyTuple_Size(args); + if (n < 1) + PyErr_SetString(_curses_menustate_global->PyCursesError, + "menus must have at least 1 item"); + if (!(*items = (ITEM**)malloc(sizeof(ITEM*)*n+1))) { + PyErr_NoMemory(); + return 0; + } + if (!(*ios = (PyCursesItemObject**)malloc(sizeof(PyCursesItemObject*)*n+1))) { + free(*items); + PyErr_NoMemory(); + return 0; + } + for (i = 0; i < n; i++) { + io = PyTuple_GetItem(args, i); + if (!PyCursesItem_Check(io)) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + "can only accept items as args"); + goto extract_items_fail; + } + Py_INCREF(io); + (*ios)[i] = (PyCursesItemObject*)io; + (*items)[i] = ((PyCursesItemObject*)io)->item; + } + (*items)[n] = (ITEM*)NULL; + (*ios)[n] = (PyCursesItemObject*)NULL; + return 1; + +extract_items_fail: + free(*items); + free(*ios); + return 0; +} + +#define Menu_NoArgNoReturnFunction(X) \ +static PyObject * \ +PyCursesMenu_ ## X (PyCursesMenuObject *self) \ +{ \ + return PyCursesCheckERR(X(self->menu), #X); \ +} + +#define Menu_OneArgNoReturnFunction(X, TYPE, PARSESTR) \ +static PyObject * \ +PyCursesMenu_ ## X (PyCursesMenuObject *self, PyObject *args) \ +{ \ + TYPE arg; \ + \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg)) \ + return NULL; \ + return PyCursesCheckERR(X(self->menu, arg), #X); \ +} + +#define Menu_TwoArgNoReturnFunction(X, TYPE, PARSESTR) \ +static PyObject * \ +PyCursesMenu_ ## X (PyCursesMenuObject *self, PyObject *args) \ +{ \ + TYPE arg1, arg2; \ + \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg1, &arg2)) \ + return NULL; \ + return PyCursesCheckERR(X(self->menu, arg1, arg2), #X); \ +} + +#define Menu_ThreeArgNoReturnFunction(X, TYPE, PARSESTR) \ +static PyObject * \ +PyCursesMenu_ ## X (PyCursesMenuObject *self, PyObject *args) \ +{ \ + TYPE arg1, arg2, arg3; \ + \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg1, &arg2, &arg3)) \ + return NULL; \ + return PyCursesCheckERR(X(self->menu, arg1, arg2, arg3), #X); \ +} + +#define Menu_NoArgOneReturnFunction(X, ERGSTR) \ +static PyObject * \ +PyCursesMenu_ ## X (PyCursesMenuObject *self) \ +{ \ + return Py_BuildValue(ERGSTR, X(self->menu)); \ +} + +#define Menu_NoArgOneObjectReturnFunction(X, ATTR) \ +static PyObject * PyCursesMenu_ ## X \ +(PyCursesMenuObject *self) \ +{ \ + Py_INCREF(self->ATTR); \ + return (PyObject*)self->ATTR; \ +} + +#define Menu_NoArg2TupleReturnFunction(X, TYPE, ERGSTR) \ +static PyObject * PyCursesMenu_ ## X \ +(PyCursesMenuObject *self) \ +{ \ + TYPE arg1, arg2; \ + \ + X(self->menu, &arg1, &arg2); \ + return Py_BuildValue(ERGSTR, arg1, arg2); \ +} + +#define Menu_NoArg3TupleReturnFunction(X, TYPE, ERGSTR) \ +static PyObject * PyCursesMenu_ ## X \ +(PyCursesMenuObject *self) \ +{ \ + TYPE arg1, arg2, arg3; \ + \ + X(self->menu, &arg1, &arg2, &arg3); \ + return Py_BuildValue(ERGSTR, arg1, arg2, arg3); \ +} + +Menu_NoArgOneReturnFunction(item_count, "i") +Menu_NoArgOneReturnFunction(menu_back, "i") +Menu_OneArgNoReturnFunction(menu_driver, int, "i") +Menu_NoArgOneReturnFunction(menu_fore, "i") +Menu_NoArg2TupleReturnFunction(menu_format, int, "ii") +Menu_NoArgOneReturnFunction(menu_grey, "i") +Menu_NoArgOneReturnFunction(menu_mark, "s") +Menu_NoArgOneReturnFunction(menu_opts, "i") +Menu_OneArgNoReturnFunction(menu_opts_off, int, "i") +Menu_OneArgNoReturnFunction(menu_opts_on, int, "i") +Menu_NoArgOneReturnFunction(menu_pad, "i") +Menu_NoArgOneReturnFunction(menu_pattern, "s") +Menu_NoArg3TupleReturnFunction(menu_spacing, int, "iii") +Menu_NoArgOneReturnFunction(pos_menu_cursor, "i") +Menu_NoArgNoReturnFunction(post_menu) +Menu_NoArg2TupleReturnFunction(scale_menu, int, "ii") +Menu_OneArgNoReturnFunction(set_menu_back, int, "i") +Menu_OneArgNoReturnFunction(set_menu_fore, int, "i") +Menu_TwoArgNoReturnFunction(set_menu_format, int, "ii") +Menu_OneArgNoReturnFunction(set_menu_grey, int, "i") +Menu_OneArgNoReturnFunction(set_menu_opts, int, "i") +Menu_OneArgNoReturnFunction(set_menu_pad, int, "i") +Menu_OneArgNoReturnFunction(set_menu_pattern, char*, "s") +Menu_ThreeArgNoReturnFunction(set_menu_spacing, int, "iii") +Menu_OneArgNoReturnFunction(set_top_row, int, "i") +Menu_NoArgOneReturnFunction(top_row, "i") +Menu_NoArgNoReturnFunction(unpost_menu) + +#define Menu_GetWindowFunction(X) \ +static PyCursesWindowObject * \ +PyCursesMenu_menu_ ##X (PyCursesMenuObject *self) \ +{ \ + WINDOW *win; \ + PyCursesWindowObject *wo; \ + \ + if (self->X) \ + return self->X; \ + win = menu_ ## X(self->menu); \ + wo = PyCursesFindWO(win); \ + return wo; \ +} + +#define Menu_SetWindowFunction(X) \ +static PyObject * \ +PyCursesMenu_set_menu_ ##X (PyCursesMenuObject *self, PyObject *args) \ +{ \ + PyCursesWindowObject *new = NULL; \ + WINDOW *win = NULL; \ + PyObject *res; \ + \ + switch (PyTuple_Size(args)) { \ + case 0: \ + win = NULL; \ + break; \ + case 1: \ + if (!PyArg_ParseTuple(args, "O!;window object", \ + &PyCursesWindow_Type, &new)) \ + return NULL; \ + win = new->win; \ + break; \ + default: \ + PyErr_SetString(PyExc_TypeError, \ + "set_menu_" #X "requires either 0 or 1 argument"); \ + return NULL; \ + } \ + if (!(res = PyCursesCheckERR(set_menu_ ##X (self->menu, win), \ + "set_menu_" #X))) \ + return NULL; \ + if (self->X) \ + Py_DECREF(self->X); \ + \ + if (!new) { \ + self->X = NULL; \ + } else { \ + self->X = new; \ + Py_INCREF(self->X); \ + } \ + return res; \ +} + +Menu_SetWindowFunction(win) +Menu_SetWindowFunction(sub) +Menu_GetWindowFunction(win) +Menu_GetWindowFunction(sub) + +static PyObject * +PyCursesMenu_menu_items(PyCursesMenuObject *self) +{ + int n, i; + PyListObject *l; + PyTupleObject *t; + + n = item_count(self->menu); + if (!(l = (PyListObject*)PyList_New(n))) + return NULL; + for (i = 0; i < n; i++) { + if (PyList_SetItem((PyObject*)l, i, (PyObject*)self->ios[i]) < 0) + return NULL; + } + if (!(t = (PyTupleObject*)PyList_AsTuple((PyObject*)l))) + return NULL; + Py_INCREF(t); + return (PyObject*)t; +} + +static PyObject * +PyCursesMenu_set_menu_items(PyCursesMenuObject *self, PyObject *args) +{ + if (!PyCurses_ExtractItems(args, &(self->items), &(self->ios))) + return NULL; + return PyCursesCheckERR(set_menu_items(self->menu, self->items), + "set_menu_items"); +} + +static PyObject * +PyCursesMenu_current_item(PyCursesMenuObject *self) +{ + ITEM *item; + int i = 0; + PyCursesItemObject *io; + + if (!(item = current_item(self->menu))) + return NULL; + for (io = self->ios[i]; io != NULL; io = self->ios[++i]) { + if (io->item == item) + break; + } + if (!io) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + "couldn't find item"); + return NULL; + } + Py_INCREF(io); + return (PyObject*)io; +} + +static PyObject * +PyCursesMenu_set_current_item(PyCursesMenuObject *self, PyObject *args) +{ + PyCursesItemObject *new; + + if (!PyArg_ParseTuple(args, "O!", + _curses_menustate_global->PyCursesItem_Type, &new)) + return NULL; + return PyCursesCheckERR(set_current_item(self->menu, new->item), + "set_current_item"); +} + +static PyObject * +PyCursesMenu_set_menu_mark(PyCursesMenuObject *self, PyObject *args) +{ + char *new; + + switch (PyTuple_Size(args)) { + case 0: + new = NULL; + break; + case 1: + if (!PyArg_ParseTuple(args, "s", &new)) + return NULL; + break; + } + if (self->mark) + free(self->mark); + if (!new) { + self->mark = NULL; + } else { + if (!(self->mark = malloc(sizeof(char)*strlen(new)+1))) + return PyErr_NoMemory(); + strcpy(self->mark, new); + } + return PyCursesCheckERR(set_menu_mark(self->menu, self->mark), + "set_menu_mark"); +} + +static PyObject * +PyCursesMenu_menu_userptr(PyCursesMenuObject *self) +{ + PyObject *obj; + + PyCursesInitialised; + if (!(obj = (PyObject *)menu_userptr(self->menu))) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + "no userptr set"); + return NULL; + } + Py_INCREF(obj); + return obj; +} + +static PyObject * +PyCursesMenu_set_menu_userptr(PyCursesMenuObject *self, PyObject *obj) +{ + Py_INCREF(obj); + return PyCursesCheckERR(set_menu_userptr(self->menu, (void*)obj), + "set_menu_userptr"); +} + +#define PyCursesMenu_CallbackWrapper(X) \ +void PyCursesMenu_CallbackWrapper_ ## X(MENU *menu) \ +{ \ + PyCursesMenuObject *mo; \ + PyObject *args, *result; \ + char *err; \ + \ + if (!(mo = find_mo(menu))) { \ + err = "couldn't find menu"; \ + goto fail_ ## X; \ + } \ + if (!mo->X) \ + return; \ + if (!(args = Py_BuildValue("(O)", mo))) { \ + err = "couldn't build args"; \ + goto fail_ ## X; \ + } \ + if (!(result = PyObject_CallObject(mo->X, args))) { \ + err = "callback erro"; \ + goto fail_ ## X; \ + } \ + return; \ + \ +fail_ ##X: \ + PyErr_SetString(_curses_menustate_global->PyCursesError, err); \ + return; \ +} + +#define PyCursesMenu_GetCallback(X) \ +static PyObject * \ +PyCursesMenu_ ## X(PyCursesMenuObject *self) \ +{ \ + if (self->X != NULL) { \ + Py_INCREF(self->X); \ + return Py_BuildValue("O", self->X); \ + } \ + Py_INCREF(Py_None); \ + return Py_None; \ +} + +#define PyCursesMenu_SetCallback(X) \ +static PyObject * \ +PyCursesMenu_set_ ## X(PyCursesMenuObject *self, PyObject *args) \ +{ \ + Menu_Hook previous; \ + PyObject *callback; \ + PyObject *result = NULL; \ + \ + if (PyArg_ParseTuple(args, "O", &callback)) { \ + if (!(PyCallable_Check(callback))) { \ + PyErr_SetString(_curses_menustate_global->PyCursesError, \ + "callbacks must be callable"); \ + return NULL; \ + } \ + previous = X(self->menu); \ + if (!(PyCursesCheckERR(set_##X(self->menu, \ + PyCursesMenu_CallbackWrapper_##X), #X))) \ + return PyCursesCheckERR(set_##X(self->menu, previous), #X); \ + if (self->X != NULL) \ + Py_DECREF(self->X); \ + Py_INCREF(callback); \ + self->X = callback; \ + Py_INCREF(Py_None); \ + result = Py_None; \ + } \ + return result; \ +} + +PyCursesMenu_CallbackWrapper(item_init) +PyCursesMenu_GetCallback(item_init) +PyCursesMenu_SetCallback(item_init) +PyCursesMenu_CallbackWrapper(item_term) +PyCursesMenu_GetCallback(item_term) +PyCursesMenu_SetCallback(item_term) +PyCursesMenu_CallbackWrapper(menu_init) +PyCursesMenu_GetCallback(menu_init) +PyCursesMenu_SetCallback(menu_init) +PyCursesMenu_CallbackWrapper(menu_term) +PyCursesMenu_GetCallback(menu_term) +PyCursesMenu_SetCallback(menu_term) + +/* ----------------- Module interface -------------------- */ + +static PyMethodDef PyCursesMenu_Methods[] = { + {"back", (PyCFunction)PyCursesMenu_menu_back, METH_NOARGS}, + {"current_item", (PyCFunction)PyCursesMenu_current_item, METH_NOARGS}, + {"driver", (PyCFunction)PyCursesMenu_menu_driver, METH_VARARGS}, + {"fore", (PyCFunction)PyCursesMenu_menu_fore, METH_NOARGS}, + {"format", (PyCFunction)PyCursesMenu_menu_format, METH_NOARGS}, + {"grey", (PyCFunction)PyCursesMenu_menu_grey, METH_NOARGS}, + {"item_count", (PyCFunction)PyCursesMenu_item_count, METH_NOARGS}, + {"item_init", (PyCFunction)PyCursesMenu_item_init, METH_NOARGS}, + {"items", (PyCFunction)PyCursesMenu_menu_items, METH_NOARGS}, + {"item_term", (PyCFunction)PyCursesMenu_item_term, METH_NOARGS}, + {"mark", (PyCFunction)PyCursesMenu_menu_mark, METH_NOARGS}, + {"menu_init", (PyCFunction)PyCursesMenu_menu_init, METH_NOARGS}, + {"menu_term", (PyCFunction)PyCursesMenu_menu_term, METH_NOARGS}, + {"opts_off", (PyCFunction)PyCursesMenu_menu_opts_off, METH_VARARGS}, + {"opts_on", (PyCFunction)PyCursesMenu_menu_opts_on, METH_VARARGS}, + {"opts", (PyCFunction)PyCursesMenu_menu_opts, METH_NOARGS}, + {"pad", (PyCFunction)PyCursesMenu_menu_pad, METH_NOARGS}, + {"pattern", (PyCFunction)PyCursesMenu_menu_pattern, METH_NOARGS}, + {"pos_cursor", (PyCFunction)PyCursesMenu_pos_menu_cursor, METH_NOARGS}, + {"post", (PyCFunction)PyCursesMenu_post_menu, METH_NOARGS}, + {"scale", (PyCFunction)PyCursesMenu_scale_menu, METH_NOARGS}, + {"set_back", (PyCFunction)PyCursesMenu_set_menu_back, METH_VARARGS}, + {"set_current_item", (PyCFunction)PyCursesMenu_set_current_item, METH_VARARGS}, + {"set_fore", (PyCFunction)PyCursesMenu_set_menu_fore, METH_VARARGS}, + {"set_format", (PyCFunction)PyCursesMenu_set_menu_format, METH_VARARGS}, + {"set_grey", (PyCFunction)PyCursesMenu_set_menu_grey, METH_VARARGS}, + {"set_item_init", (PyCFunction)PyCursesMenu_set_item_init, METH_VARARGS}, + {"set_item_term", (PyCFunction)PyCursesMenu_set_item_term, METH_VARARGS}, + {"set_items", (PyCFunction)PyCursesMenu_set_menu_items, METH_VARARGS}, + {"set_mark", (PyCFunction)PyCursesMenu_set_menu_mark, METH_VARARGS}, + {"set_menu_init", (PyCFunction)PyCursesMenu_set_menu_init, METH_VARARGS}, + {"set_menu_term", (PyCFunction)PyCursesMenu_set_menu_term, METH_VARARGS}, + {"set_opts", (PyCFunction)PyCursesMenu_set_menu_opts, METH_VARARGS}, + {"set_pad", (PyCFunction)PyCursesMenu_set_menu_pad, METH_VARARGS}, + {"set_pattern", (PyCFunction)PyCursesMenu_set_menu_pattern, METH_VARARGS}, + {"set_spacing", (PyCFunction)PyCursesMenu_set_menu_spacing, METH_VARARGS}, + {"set_sub", (PyCFunction)PyCursesMenu_set_menu_sub, METH_VARARGS}, + {"set_top_row", (PyCFunction)PyCursesMenu_set_top_row, METH_VARARGS}, + {"set_userptr", (PyCFunction)PyCursesMenu_set_menu_userptr, METH_O}, + {"set_win", (PyCFunction)PyCursesMenu_set_menu_win, METH_VARARGS}, + {"sub", (PyCFunction)PyCursesMenu_menu_sub, METH_NOARGS}, + {"spacing", (PyCFunction)PyCursesMenu_menu_spacing, METH_NOARGS}, + {"top_row", (PyCFunction)PyCursesMenu_top_row, METH_NOARGS}, + {"unpost", (PyCFunction)PyCursesMenu_unpost_menu, METH_NOARGS}, + {"userptr", (PyCFunction)PyCursesMenu_menu_userptr, METH_NOARGS}, + {"win", (PyCFunction)PyCursesMenu_menu_win, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +/* ------------------------------------------------------- */ + +static PyType_Slot PyCursesMenu_Type_slots[] = { + {Py_tp_dealloc, PyCursesMenu_Dealloc}, + {Py_tp_methods, PyCursesMenu_Methods}, + {0, 0}, +}; + +static PyType_Spec PyCursesMenu_Type_spec = { + "_curses_menu.curses menu", + sizeof(PyCursesMenuObject), + 0, + Py_TPFLAGS_DEFAULT, + PyCursesMenu_Type_slots +}; + +/* Global functions */ + +static PyObject * +PyCurses_new_item(PyObject *self, PyObject *args) +{ + const char *name, *description; + char *iname, *idescription; + ITEM *item; + + switch (PyTuple_Size(args)) { + case 1: + if (!PyArg_ParseTuple(args, "s", &name)) + return NULL; + idescription = NULL; + break; + case 2: + if (!PyArg_ParseTuple(args, "ss", &name, &description)) + return NULL; + if (!(idescription = malloc(sizeof(char)*strlen(description)))) + return PyErr_NoMemory(); + strcpy(idescription, description); + break; + default: + PyErr_SetString(PyExc_TypeError, + "new_item accepts a name and optional description"); + return NULL; + } + if (!(iname = malloc(sizeof(char)*strlen(name)))) + return PyErr_NoMemory(); + strcpy(iname, name); + + if (!(item = new_item(iname, idescription))) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + catchall_NULL); + return NULL; + } + return (PyObject *)PyCursesItem_New(item, iname, idescription); +} + +static PyObject * +PyCurses_new_menu(PyObject *self, PyObject *args) +{ + MENU *menu; + ITEM **items = NULL; + PyCursesItemObject **ios = NULL; + + if (!PyCurses_ExtractItems(args, &items, &ios)) + return NULL; + menu = new_menu(items); + return (PyObject *)PyCursesMenu_New(menu, items, ios); +} + +static PyObject * +PyCurses_set_menu_win(PyObject *self, PyObject *args) +{ + PyCursesWindowObject *wo; + + if (!PyArg_ParseTuple(args, "O!;window object", + &PyCursesWindow_Type, &wo)) + return NULL; + return PyCursesCheckERR(set_menu_win(NULL, wo->win), "set_menu_win()"); +} + +static PyObject * +PyCurses_set_menu_sub(PyObject *self, PyObject *args) +{ + PyCursesWindowObject *wo; + + if (!PyArg_ParseTuple(args, "O!;window object", + &PyCursesWindow_Type, &wo)) + return NULL; + return PyCursesCheckERR(set_menu_sub(NULL, wo->win), "set_menu_sub()"); +} + +static PyObject * +PyCurses_set_menu_mark(PyObject *self, PyObject *args) +{ + char *new; + + switch (PyTuple_Size(args)) { + case 0: + new = NULL; + break; + case 1: + if (!PyArg_ParseTuple(args, "s", &new)) + return NULL; + break; + } + if (!(_curses_menustate_global->default_mark)) + free(_curses_menustate_global->default_mark); + if (!new) { + _curses_menustate_global->default_mark = NULL; + } else { + if (!(_curses_menustate_global->default_mark = malloc(sizeof(char)*strlen(new)+1))) + return PyErr_NoMemory(); + strcpy(_curses_menustate_global->default_mark, new); + } + return PyCursesCheckERR(set_menu_mark(NULL, _curses_menustate_global->default_mark), + "set_menu_mark()"); +} + +static PyObject * +PyCurses_request_by_name(PyObject *self, PyObject *args) +{ + const char *name; + int request; + + if (!PyArg_ParseTuple(args, "s", &name)) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + catchall_ERR); + return NULL; + } + if ((request = menu_request_by_name(name)) == E_NO_MATCH) { + PyErr_Format(_curses_menustate_global->PyCursesError, + "No request found for name %s", name); + return NULL; + } + return Py_BuildValue("i", request); +} + +static PyObject * +PyCurses_request_name(PyObject *self, PyObject *args) +{ + const char* name; + int request; + + if (!PyArg_ParseTuple(args, "i", &request)) { + PyErr_SetString(_curses_menustate_global->PyCursesError, + catchall_ERR); + return NULL; + } + if (!(name = menu_request_name(request))) { + PyErr_Format(_curses_menustate_global->PyCursesError, + "No name found for request %i", request); + return NULL; + } + return Py_BuildValue("s", name); +} + +/* List of functions defined in the module */ + +static PyMethodDef PyCurses_methods[] = { + {"new_item", (PyCFunction)PyCurses_new_item, METH_VARARGS}, + {"new_menu", (PyCFunction)PyCurses_new_menu, METH_VARARGS}, + {"set_menu_win", (PyCFunction)PyCurses_set_menu_win, METH_VARARGS}, + {"set_menu_sub", (PyCFunction)PyCurses_set_menu_sub, METH_VARARGS}, + {"set_menu_mark", (PyCFunction)PyCurses_set_menu_mark, METH_VARARGS}, + {"request_by_name", (PyCFunction)PyCurses_request_by_name, METH_VARARGS}, + {"request_name", (PyCFunction)PyCurses_request_name, METH_VARARGS}, + {NULL, NULL} /* sentinel */ +}; + +/* Initialization function for the module */ + +static struct PyModuleDef _curses_menumodule = { + PyModuleDef_HEAD_INIT, + "_curses_menu", + NULL, + sizeof(_curses_menustate), + PyCurses_methods, + NULL, + _curses_menu_traverse, + _curses_menu_clear, + _curses_menu_free +}; + +PyMODINIT_FUNC +PyInit__curses_menu(void) +{ + PyObject *m, *d, *v; + + /* Create the module and add the functions */ + if (!(m = PyModule_Create(&_curses_menumodule))) + goto fail; + d = PyModule_GetDict(m); + + /* Initialize object type */ + _curses_menustate(m)->PyCursesMenu_Type = \ + PyType_FromSpec(&PyCursesMenu_Type_spec); + if (!(_curses_menustate(m)->PyCursesMenu_Type)) + goto fail; + + _curses_menustate(m)->PyCursesItem_Type = \ + PyType_FromSpec(&PyCursesItem_Type_spec); + if (!(_curses_menustate(m)->PyCursesItem_Type)) + goto fail; + + if (!(_curses_menustate(m)->default_mark = malloc(sizeof(char)*2))) + goto fail; + strcpy(_curses_menustate(m)->default_mark, "-"); + + import_curses(); + + /* For exception _curses_menu.error */ + _curses_menustate(m)->PyCursesError = PyErr_NewException("_curses_menu.error", NULL, NULL); + PyDict_SetItemString(d, "error", _curses_menustate(m)->PyCursesError); + + /* Make the version available */ + v = PyUnicode_FromString(PyCursesVersion); + PyDict_SetItemString(d, "version", v); + PyDict_SetItemString(d, "__version__", v); + Py_DECREF(v); + + /* Set constants */ + +#define SetDictInt(string,ch) \ + do { \ + PyObject *o = PyLong_FromLong((long) (ch)); \ + if (o && PyDict_SetItemString(d, string, o) == 0) { \ + Py_DECREF(o); \ + } \ + } while (0) + + /* Menu options: */ + + SetDictInt("O_ONEVALUE", O_ONEVALUE); + SetDictInt("O_SHOWDESC", O_SHOWDESC); + SetDictInt("O_ROWMAJOR", O_ROWMAJOR); + SetDictInt("O_IGNORECASE", O_IGNORECASE); + SetDictInt("O_SHOWMATCH", O_SHOWMATCH); + SetDictInt("O_NONCYCLIC", O_NONCYCLIC); + + /* Item options: */ + + SetDictInt("O_SELECTABLE", O_SELECTABLE); + + /* Menu driver requests */ + + SetDictInt("REQ_LEFT_ITEM", REQ_LEFT_ITEM); + SetDictInt("REQ_RIGHT_ITEM", REQ_RIGHT_ITEM); + SetDictInt("REQ_UP_ITEM", REQ_UP_ITEM); + SetDictInt("REQ_DOWN_ITEM", REQ_DOWN_ITEM); + SetDictInt("REQ_SCR_ULINE", REQ_SCR_ULINE); + SetDictInt("REQ_SCR_DLINE", REQ_SCR_DLINE); + SetDictInt("REQ_SCR_DPAGE", REQ_SCR_DPAGE); + SetDictInt("REQ_SCR_UPAGE", REQ_SCR_UPAGE); + SetDictInt("REQ_FIRST_ITEM", REQ_FIRST_ITEM); + SetDictInt("REQ_LAST_ITEM", REQ_LAST_ITEM); + SetDictInt("REQ_NEXT_ITEM", REQ_NEXT_ITEM); + SetDictInt("REQ_PREV_ITEM", REQ_PREV_ITEM); + SetDictInt("REQ_TOGGLE_ITEM", REQ_TOGGLE_ITEM); + SetDictInt("REQ_CLEAR_PATTERN", REQ_CLEAR_PATTERN); + SetDictInt("REQ_BACK_PATTERN", REQ_BACK_PATTERN); + SetDictInt("REQ_NEXT_MATCH", REQ_NEXT_MATCH); + SetDictInt("REQ_PREV_MATCH", REQ_PREV_MATCH); + + SetDictInt("MIN_MENU_COMMAND", MIN_MENU_COMMAND); + SetDictInt("MAX_MENU_COMMAND", MAX_MENU_COMMAND); +#undef SetDictInt + return m; + +fail: + Py_XDECREF(m); + return NULL; +} diff -r d3c7ebdc71bb Modules/_cursesmodule.c --- a/Modules/_cursesmodule.c Mon Oct 15 16:57:37 2012 +0200 +++ b/Modules/_cursesmodule.c Thu Oct 18 00:28:49 2012 -0500 @@ -505,6 +505,68 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns") #endif +/* The menu and form extensions require a way to look up PyCursesWindowObjects + * by their WINDOW* pointers. Assuming there aren't too many windows in a + * given application, a simple linked list should work well enough. + */ + +typedef struct _list_of_windows { + PyCursesWindowObject *wo; + struct _list_of_windows *next; +} list_of_windows; + +static list_of_windows *low; + +static int +insert_low(PyCursesWindowObject *wo) +{ + list_of_windows *new; + + if (!(new = (list_of_windows*)malloc(sizeof(list_of_windows)))) { + PyErr_NoMemory(); + return -1; + } + new->wo = wo; + new->next = low; + low = new; + return 0; +} + +static void +remove_low(PyCursesWindowObject *wo) +{ + list_of_windows *temp, *n; + + temp = low; + if (temp->wo == wo) { + low = temp->next; + free(temp); + return; + } + while (temp->next == NULL || temp->next->wo != wo) { + if (temp->next == NULL) { + PyErr_SetString(PyExc_RuntimeError, + "remove_low: can't find Window Object"); + return; + } + temp = temp->next; + } + n = temp->next->next; + free(temp->next); + temp->next = n; + return; +} + +PyCursesWindowObject * +func_PyCurses_FindWO(WINDOW *win) +{ + list_of_windows *temp; + for (temp = low; temp->wo->win != win; temp = temp->next) + if (temp->next == NULL) + return NULL; /* not found!? */ + return temp->wo; +} + /* Allocation and deallocation of Window Objects */ static PyObject * @@ -539,13 +601,20 @@ PyErr_NoMemory(); return NULL; } + if (insert_low(wo) < 0) { + Py_DECREF(wo); + return NULL; + } return (PyObject *)wo; } static void PyCursesWindow_Dealloc(PyCursesWindowObject *wo) { - if (wo->win != stdscr) delwin(wo->win); + if (wo->win != stdscr) { + remove_low(wo); + delwin(wo->win); + } if (wo->encoding != NULL) free(wo->encoding); PyObject_DEL(wo); @@ -3283,6 +3352,7 @@ PyCurses_API[1] = (void *)func_PyCursesSetupTermCalled; PyCurses_API[2] = (void *)func_PyCursesInitialised; PyCurses_API[3] = (void *)func_PyCursesInitialisedColor; + PyCurses_API[4] = (void *)func_PyCurses_FindWO; /* Create the module and add the functions */ m = PyModule_Create(&_cursesmodule); diff -r d3c7ebdc71bb setup.py --- a/setup.py Mon Oct 15 16:57:37 2012 +0200 +++ b/setup.py Thu Oct 18 00:28:49 2012 -0500 @@ -1270,12 +1270,14 @@ curses_defines = [] curses_includes = [] panel_library = 'panel' + menu_library = 'menu' if curses_library == 'ncursesw': curses_defines.append(('HAVE_NCURSESW', '1')) curses_includes.append('/usr/include/ncursesw') # Bug 1464056: If _curses.so links with ncursesw, # _curses_panel.so must link with panelw. panel_library = 'panelw' + menu_library = 'menuw' if host_platform == 'darwin': # On OS X, there is no separate /usr/lib/libncursesw nor # libpanelw. If we are here, we found a locally-supplied @@ -1321,6 +1323,16 @@ else: missing.append('_curses_panel') + # If the curses module is enabled, check for the menu module + if (module_enabled(exts, '_curses') and + self.compiler.find_library_file(lib_dirs, menu_library)): + exts.append( Extension('_curses_menu', ['_curses_menu.c'], + include_dirs=curses_includes, + define_macros=curses_defines, + libraries = [menu_library] + curses_libs) ) + else: + missing.append('_curses_menu') + # Andrew Kuchling's zlib module. Note that some versions of zlib # 1.1.3 have security problems. See CERT Advisory CA-2002-07: # http://www.cert.org/advisories/CA-2002-07.html