[PATCH 20/24] Allow TUI windows in Python

Tom Tromey
2020-01-04T18:34:43Z

This patch adds support for writing new TUI windows in Python.

2020-01-04 Tom Tromey <tom@tromey.com>

* NEWS: Add entry for gdb.register_window_type.
* tui/tui-layout.h (window_factory): New typedef.
(tui_register_window): Declare.
* tui/tui-layout.c (saved_tui_windows): New global.
(tui_apply_current_layout): Use it.
(tui_register_window): New function.
* python/python.c (do_start_initialization): Call
gdbpy_initialize_tui.
(python_GdbMethods): Add "register_window_type" function.
* python/python-internal.h (gdbpy_register_tui_window)
(gdbpy_initialize_tui): Declare.
* python/py-tui.c: New file.
* Makefile.in (SUBDIR_PYTHON_SRCS): Add py-tui.c.

gdb/doc/ChangeLog
2020-01-04 Tom Tromey <tom@tromey.com>

* python.texi (Python API): Add menu item.
(TUI Windows In Python): New node.

gdb/testsuite/ChangeLog
2020-01-04 Tom Tromey <tom@tromey.com>

* gdb.python/tui-window.exp: New file.
* gdb.python/tui-window.py: New file.

Change-Id: I85fbfb923a1840450a00a7dce113a05d7f048baa
---
gdb/ChangeLog | 16 +
gdb/Makefile.in | 1 +
gdb/NEWS | 5 +
gdb/doc/ChangeLog | 5 +
gdb/doc/python.texi | 101 +++++
gdb/python/py-tui.c | 510 ++++++++++++++++++++++++
gdb/python/python-internal.h | 4 +
gdb/python/python.c | 10 +-
gdb/testsuite/ChangeLog | 5 +
gdb/testsuite/gdb.python/tui-window.exp | 51 +++
gdb/testsuite/gdb.python/tui-window.py | 37 ++
gdb/tui/tui-layout.c | 28 +-
gdb/tui/tui-layout.h | 10 +
13 files changed, 779 insertions(+), 4 deletions(-)
create mode 100644 gdb/python/py-tui.c
create mode 100644 gdb/testsuite/gdb.python/tui-window.exp
create mode 100644 gdb/testsuite/gdb.python/tui-window.py

diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index 448a495bb3b..adfe607dc87 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -401,6 +401,7 @@ SUBDIR_PYTHON_SRCS = \
python/py-symbol.c \
python/py-symtab.c \
python/py-threadevent.c \
+ python/py-tui.c \
python/py-type.c \
python/py-unwind.c \
python/py-utils.c \
diff --git a/gdb/NEWS b/gdb/NEWS
index a936620c0a8..f15c5f5c15f 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -15,6 +15,11 @@ tui new-layout NAME WINDOW WEIGHT [WINDOW WEIGHT]...
Define a new TUI layout, specifying its name and the windows that
will be displayed.

+* Python API
+
+ ** gdb.register_window_type can be used to implement new TUI windows
+ in Python.
+
*** Changes in GDB 9

* 'thread-exited' event is now available in the annotations interface.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 8124077ab33..d0ce9ea35e7 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -163,6 +163,7 @@ optional arguments while skipping others. Example:
using Python.
* Lazy Strings In Python:: Python representation of lazy strings.
* Architectures In Python:: Python representation of architectures.
+* TUI Windows In Python:: Implementing new TUI windows.
@end menu

@node Basic Python
@@ -5673,6 +5674,106 @@ instruction in bytes.
@end table
@end defun

+@node TUI Windows In Python
+@subsubsection Implementing new TUI windows
+@cindex Python TUI Windows
+
+New TUI (@pxref{TUI}) windows can be implemented in Python.
+
+@findex gdb.register_window_type
+@defun gdb.register_window_type (@var{name}, @var{factory})
+Because TUI windows are created and destroyed depending on the layout
+the user chooses, new window types are implemented by registering a
+factory function with @value{GDBN}.
+
+@var{name} is the name of the new window. It's an error to try to
+replace one of the built-in windows, but other window types can be
+replaced.
+
+@var{function} is a factory function that is called to create the TUI
+window. This is called with a single argument of type
+@code{gdb.TuiWindow}, described below. It should return an object
+that implements the TUI window protocol, also described below.
+@end defun
+
+As mentioned above, when a factory function is called, it is passed a
+an object of type @code{gdb.TuiWindow}. This object has these
+methods and attributes:
+
+@defun TuiWindow.is_valid ()
+This method returns @code{True} when this window is valid. When the
+user changes the TUI layout, windows no longer visible in the new
+layout will be destroyed. At this point, the @code{gdb.TuiWindow}
+will no longer be valid, and methods (and attributes) other than
+@code{is_valid} will throw an exception.
+@end defun
+
+@defvar TuiWindow.width
+This attribute holds the width of the window. It is not writable.
+@end defvar
+
+@defvar TuiWindow.height
+This attribute holds the height of the window. It is not writable.
+@end defvar
+
+@defvar TuiWindow.title
+This attribute holds the window's title, a string. This is normally
+displayed above the window. This attribute can be modified.
+@end defvar
+
+@defun TuiWindow.erase ()
+Remove all the contents of the window.
+@end defun
+
+@defun TuiWindow.write (@var{string})
+Write @var{string} to the window. @var{string} can contain ANSI
+terminal escape styling sequences; @value{GDBN} will convert translate
+these as appropriate for the terminal.
+@end defun
+
+The factory function that you supply should return an object
+conforming to the TUI window protocol. These are the method that can
+be called on this object, which is referred to below as the ``window
+object''. The methods documented below are optional; if the object
+does not implement one of these methods, @value{GDBN} will not attempt
+to call it. Additional new methods may be added to the window
+protocol in the future. @value{GDBN} guarantees that they will begin
+with a lower-case letter, so you can start implementation methods with
+upper-case letters or underscore to avoid any future conflicts.
+
+@defun Window.close ()
+When the TUI window is closed, the @code{gdb.TuiWindow} object will be
+put into an invalid state. At this time, @value{GDBN} will call
+@code{close} method on the window object.
+
+After this method is called, @value{GDBN} will discard any references
+it holds on this window object, and will no longer call methods on
+this object.
+@end defun
+
+@defun Window.render ()
+In some situations, a TUI window can change size. For example, this
+can happen if the user resizes the terminal, or changes the layout.
+When this happens, @value{GDBN} will call the @code{render} method on
+the window object.
+
+If your window is intended to update in response to changes in the
+inferior, you will probably also want to register event listeners and
+send output to the @code{gdb.TuiWindow}.
+@end defun
+
+@defun Window.hscroll (@var{num})
+This is a request to scroll the window horizontally. @var{num} is the
+amount by which to scroll, with negative numbers meaning to scroll
+right.
+@end defun
+
+@defun Window.vscroll (@var{num})
+This is a request to scroll the window vertically. @var{num} is the
+amount by which to scroll, with negative numbers meaning to scroll
+backward.
+@end defun
+
@node Python Auto-loading
@subsection Python Auto-loading
@cindex Python auto-loading
diff --git a/gdb/python/py-tui.c b/gdb/python/py-tui.c
new file mode 100644
index 00000000000..4cb86ae75da
--- /dev/null
+++ b/gdb/python/py-tui.c
@@ -0,0 +1,510 @@
+/* TUI windows implemented in Python
+
+ Copyright (C) 2020 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ 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 3 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, see <http://www.gnu.org/licenses/>. */
+
+
+#include "defs.h"
+#include "arch-utils.h"
+#include "python-internal.h"
+#include "gdb_curses.h"
+
+#ifdef TUI
+
+#include "tui/tui-data.h"
+#include "tui/tui-io.h"
+#include "tui/tui-layout.h"
+#include "tui/tui-wingeneral.h"
+#include "tui/tui-winsource.h"
+
+class tui_py_window;
+
+/* A PyObject representing a TUI window. */
+
+struct gdbpy_tui_window
+{
+ PyObject_HEAD
+
+ /* The TUI window, or nullptr if the window has been deleted. */
+ tui_py_window *window;
+};
+
+extern PyTypeObject gdbpy_tui_window_object_type
+ CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
+
+/* A TUI window written in Python. */
+
+class tui_py_window : public tui_win_info
+{
+public:
+
+ tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper)
+ : m_name (name),
+ m_wrapper (std::move (wrapper))
+ {
+ m_wrapper->window = this;
+ }
+
+ ~tui_py_window ();
+
+ DISABLE_COPY_AND_ASSIGN (tui_py_window);
+
+ /* Set the "user window" to the indicated reference. The user
+ window is the object returned the by user-defined window
+ constructor. */
+ void set_user_window (gdbpy_ref<> &&user_window)
+ {
+ m_window = std::move (user_window);
+ }
+
+ const char *name () const override
+ {
+ return m_name.c_str ();
+ }
+
+ void rerender () override;
+ void do_scroll_vertical (int num_to_scroll) override;
+ void do_scroll_horizontal (int num_to_scroll) override;
+
+ /* Erase and re-box the window. */
+ void erase ()
+ {
+ if (is_visible ())
+ {
+ werase (handle.get ());
+ check_and_display_highlight_if_needed ();
+ cursor_x = 0;
+ cursor_y = 0;
+ }
+ }
+
+ /* Write STR to the window. */
+ void output (const char *str);
+
+ /* A helper function to compute the viewport width. */
+ int viewport_width () const
+ {
+ return std::max (0, width - 2);
+ }
+
+ /* A helper function to compute the viewport height. */
+ int viewport_height () const
+ {
+ return std::max (0, height - 2);
+ }
+
+private:
+
+ /* Location of the cursor. */
+ int cursor_x = 0;
+ int cursor_y = 0;
+
+ /* The name of this window. */
+ std::string m_name;
+
+ /* The underlying Python window object. */
+ gdbpy_ref<> m_window;
+
+ /* The Python wrapper for this object. */
+ gdbpy_ref<gdbpy_tui_window> m_wrapper;
+};
+
+tui_py_window::~tui_py_window ()
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+
+ if (PyObject_HasAttrString (m_window.get (), "close"))
+ {
+ gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "close",
+ nullptr));
+ if (result == nullptr)
+ gdbpy_print_stack ();
+ }
+
+ /* Unlink. */
+ m_wrapper->window = nullptr;
+ /* Explicitly free the Python references. We have to do this
+ manually because we need to hold the GIL while doing so. */
+ m_wrapper.reset (nullptr);
+ m_window.reset (nullptr);
+}
+
+void
+tui_py_window::rerender ()
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+
+ if (PyObject_HasAttrString (m_window.get (), "render"))
+ {
+ gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "render",
+ nullptr));
+ if (result == nullptr)
+ gdbpy_print_stack ();
+ }
+}
+
+void
+tui_py_window::do_scroll_horizontal (int num_to_scroll)
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+
+ if (PyObject_HasAttrString (m_window.get (), "hscroll"))
+ {
+ gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll",
+ "i", num_to_scroll, nullptr));
+ if (result == nullptr)
+ gdbpy_print_stack ();
+ }
+}
+
+void
+tui_py_window::do_scroll_vertical (int num_to_scroll)
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+
+ if (PyObject_HasAttrString (m_window.get (), "vscroll"))
+ {
+ gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll",
+ "i", num_to_scroll, nullptr));
+ if (result == nullptr)
+ gdbpy_print_stack ();
+ }
+}
+
+void
+tui_py_window::output (const char *text)
+{
+ int vwidth = viewport_width ();
+
+ while (cursor_y < viewport_height () && *text != '\0')
+ {
+ wmove (handle.get (), cursor_y + 1, cursor_x + 1);
+
+ std::string line = tui_copy_source_line (&text, 0, 0,
+ vwidth - cursor_x, 0);
+ tui_puts (line.c_str (), handle.get ());
+
+ if (*text == '\n')
+ {
+ ++text;
+ ++cursor_y;
+ cursor_x = 0;
+ }
+ else
+ cursor_x = getcurx (handle.get ()) - 1;
+ }
+
+ wrefresh (handle.get ());
+}
+
+\f
+
+/* A callable that is used to create a TUI window. It wraps the
+ user-supplied window constructor. */
+
+class gdbpy_tui_window_maker
+{
+public:
+
+ explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr)
+ : m_constr (std::move (constr))
+ {
+ }
+
+ ~gdbpy_tui_window_maker ();
+
+ gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other)
+ : m_constr (std::move (other.m_constr))
+ {
+ }
+
+ gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other)
+ {
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+ m_constr = other.m_constr;
+ }
+
+ gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other)
+ {
+ m_constr = std::move (other.m_constr);
+ return *this;
+ }
+
+ gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other)
+ {
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+ m_constr = other.m_constr;
+ return *this;
+ }
+
+ tui_win_info *operator() (const char *name);
+
+private:
+
+ /* A constructor that is called to make a TUI window. */
+ gdbpy_ref<> m_constr;
+};
+
+gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+ m_constr.reset (nullptr);
+}
+
+tui_win_info *
+gdbpy_tui_window_maker::operator() (const char *win_name)
+{
+ gdbpy_enter enter_py (get_current_arch (), current_language);
+
+ gdbpy_ref<gdbpy_tui_window> wrapper
+ (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type));
+ if (wrapper == nullptr)
+ {
+ gdbpy_print_stack ();
+ return nullptr;
+ }
+
+ std::unique_ptr<tui_py_window> window
+ (new tui_py_window (win_name, wrapper));
+
+ gdbpy_ref<> user_window
+ (PyObject_CallFunctionObjArgs (m_constr.get (),
+ (PyObject *) wrapper.get (),
+ nullptr));
+ if (user_window == nullptr)
+ {
+ gdbpy_print_stack ();
+ return nullptr;
+ }
+
+ window->set_user_window (std::move (user_window));
+ /* Window is now owned by the TUI. */
+ return window.release ();
+}
+
+/* Implement "gdb.register_window_type". */
+
+PyObject *
+gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw)
+{
+ static const char *keywords[] = { "name", "constructor", nullptr };
+
+ const char *name;
+ PyObject *cons_obj;
+
+ if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords,
+ &name, &cons_obj))
+ return nullptr;
+
+ try
+ {
+ gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj));
+ tui_register_window (name, constr);
+ }
+ catch (const gdb_exception &except)
+ {
+ gdbpy_convert_exception (except);
+ return nullptr;
+ }
+
+ Py_RETURN_NONE;
+}
+
+\f
+
+/* Require that "Window" be a valid window. */
+
+#define REQUIRE_WINDOW(Window) \
+ do { \
+ if ((Window)->window == nullptr) \
+ return PyErr_Format (PyExc_RuntimeError, \
+ _("TUI window is invalid.")); \
+ } while (0)
+
+/* Python function which checks the validity of a TUI window
+ object. */
+static PyObject *
+gdbpy_tui_is_valid (PyObject *self, PyObject *args)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+ if (win->window != nullptr)
+ Py_RETURN_TRUE;
+ Py_RETURN_FALSE;
+}
+
+/* Python function that erases the TUI window. */
+static PyObject *
+gdbpy_tui_erase (PyObject *self, PyObject *args)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+ REQUIRE_WINDOW (win);
+
+ win->window->erase ();
+
+ Py_RETURN_NONE;
+}
+
+/* Python function that writes some text to a TUI window. */
+static PyObject *
+gdbpy_tui_write (PyObject *self, PyObject *args)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+ const char *text;
+
+ if (!PyArg_ParseTuple (args, "s", &text))
+ return nullptr;
+
+ REQUIRE_WINDOW (win);
+
+ win->window->output (text);
+
+ Py_RETURN_NONE;
+}
+
+/* Return the width of the TUI window. */
+static PyObject *
+gdbpy_tui_width (PyObject *self, void *closure)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+ REQUIRE_WINDOW (win);
+ return PyLong_FromLong (win->window->viewport_width ());
+}
+
+/* Return the height of the TUI window. */
+static PyObject *
+gdbpy_tui_height (PyObject *self, void *closure)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+ REQUIRE_WINDOW (win);
+ return PyLong_FromLong (win->window->viewport_height ());
+}
+
+/* Return the title of the TUI window. */
+static PyObject *
+gdbpy_tui_title (PyObject *self, void *closure)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+ REQUIRE_WINDOW (win);
+ return host_string_to_python_string (win->window->title.c_str ()).release ();
+}
+
+/* Set the title of the TUI window. */
+static int
+gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure)
+{
+ gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+ if (win->window == nullptr)
+ {
+ PyErr_Format (PyExc_RuntimeError, _("TUI window is invalid."));
+ return -1;
+ }
+
+ if (win->window == nullptr)
+ {
+ PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute."));
+ return -1;
+ }
+
+ gdb::unique_xmalloc_ptr<char> value
+ = python_string_to_host_string (newvalue);
+ if (value == nullptr)
+ return -1;
+
+ win->window->title = value.get ();
+ return 0;
+}
+
+static gdb_PyGetSetDef tui_object_getset[] =
+{
+ { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL },
+ { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL },
+ { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.",
+ NULL },
+ { NULL } /* Sentinel */
+};
+
+static PyMethodDef tui_object_methods[] =
+{
+ { "is_valid", gdbpy_tui_is_valid, METH_NOARGS,
+ "is_valid () -> Boolean\n\
+Return true if this TUI window is valid, false if not." },
+ { "erase", gdbpy_tui_erase, METH_NOARGS,
+ "Erase the TUI window." },
+ { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS,
+ "Append a string to the TUI window." },
+ { NULL } /* Sentinel. */
+};
+
+PyTypeObject gdbpy_tui_window_object_type =
+{
+ PyVarObject_HEAD_INIT (NULL, 0)
+ "gdb.TuiWindow", /*tp_name*/
+ sizeof (gdbpy_tui_window), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ 0, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash */
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro */
+ 0, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ "GDB TUI window object", /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ tui_object_methods, /* tp_methods */
+ 0, /* tp_members */
+ tui_object_getset, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ 0, /* tp_init */
+ 0, /* tp_alloc */
+};
+
+#endif /* TUI */
+
+/* Initialize this module. */
+
+int
+gdbpy_initialize_tui ()
+{
+#ifdef TUI
+ gdbpy_tui_window_object_type.tp_new = PyType_GenericNew;
+ if (PyType_Ready (&gdbpy_tui_window_object_type) < 0)
+ return -1;
+#endif /* TUI */
+
+ return 0;
+}
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index e2464548a7e..bbb66bd0f5c 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -447,6 +447,8 @@ PyObject *gdbpy_parameter_value (enum var_types type, void *var);
char *gdbpy_parse_command_name (const char *name,
struct cmd_list_element ***base_list,
struct cmd_list_element **start_list);
+PyObject *gdbpy_register_tui_window (PyObject *self, PyObject *args,
+ PyObject *kw);

PyObject *symtab_and_line_to_sal_object (struct symtab_and_line sal);
PyObject *symtab_to_symtab_object (struct symtab *symtab);
@@ -543,6 +545,8 @@ int gdbpy_initialize_xmethods (void)
CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
int gdbpy_initialize_unwind (void)
CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
+int gdbpy_initialize_tui ()
+ CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;

/* A wrapper for PyErr_Fetch that handles reference counting for the
caller. */
diff --git a/gdb/python/python.c b/gdb/python/python.c
index bf214fae6e2..55103de2bd1 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -1686,7 +1686,8 @@ do_start_initialization ()
|| gdbpy_initialize_event () < 0
|| gdbpy_initialize_arch () < 0
|| gdbpy_initialize_xmethods () < 0
- || gdbpy_initialize_unwind () < 0)
+ || gdbpy_initialize_unwind () < 0
+ || gdbpy_initialize_tui () < 0)
return false;

#define GDB_PY_DEFINE_EVENT_TYPE(name, py_name, doc, base) \
@@ -2038,6 +2039,13 @@ or None if not set." },
"convenience_variable (NAME, VALUE) -> None.\n\
Set the value of the convenience variable $NAME." },

+#ifdef TUI
+ { "register_window_type", (PyCFunction) gdbpy_register_tui_window,
+ METH_VARARGS | METH_KEYWORDS,
+ "register_window_type (NAME, CONSTRUCSTOR) -> None\n\
+Register a TUI window constructor." },
+#endif /* TUI */
+
{NULL, NULL, 0, NULL}
};

diff --git a/gdb/testsuite/gdb.python/tui-window.exp b/gdb/testsuite/gdb.python/tui-window.exp
new file mode 100644
index 00000000000..5c8909d4079
--- /dev/null
+++ b/gdb/testsuite/gdb.python/tui-window.exp
@@ -0,0 +1,51 @@
+# Copyright (C) 2020 Free Software Foundation, Inc.
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+# Test a TUI window implemented in Python.
+
+load_lib gdb-python.exp
+load_lib tuiterm.exp
+
+# This test doesn't care about the inferior.
+standard_testfile py-arch.c
+
+if {[build_executable "failed to prepare" ${testfile} ${srcfile}] == -1} {
+ return -1
+}
+
+Term::clean_restart 24 80 $testfile
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+set remote_python_file [gdb_remote_download host \
+ ${srcdir}/${subdir}/${testfile}.py]
+gdb_test_no_output "source ${remote_python_file}" \
+ "source ${testfile}.py"
+
+gdb_test_no_output "tui new-layout test test 1 locator 0 cmd 1"
+
+if {![Term::enter_tui]} {
+ unsupported "TUI not supported"
+}
+
+Term::command "layout test"
+Term::check_contents "test title" \
+ "This Is The Title"
+Term::check_contents "Window display" "Test: 0"
+
+Term::resize 51 51
+# Remember that a resize request actually does two resizes...
+Term::check_contents "Window was updated" "Test: 2"
diff --git a/gdb/testsuite/gdb.python/tui-window.py b/gdb/testsuite/gdb.python/tui-window.py
new file mode 100644
index 00000000000..4deb585f138
--- /dev/null
+++ b/gdb/testsuite/gdb.python/tui-window.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2020 Free Software Foundation, Inc.
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+# A TUI window implemented in Python.
+
+import gdb
+
+the_window = None
+
+class TestWindow:
+ def __init__(self, win):
+ global the_window
+ the_window = win
+ self.count = 0
+ self.win = win
+ win.title = "This Is The Title"
+
+ def render(self):
+ self.win.erase()
+ w = self.win.width
+ h = self.win.height
+ self.win.write("Test: " + str(self.count) + " " + str(w) + "x" + str(h))
+ self.count = self.count + 1
+
+gdb.register_window_type("test", TestWindow)
diff --git a/gdb/tui/tui-layout.c b/gdb/tui/tui-layout.c
index 797acc6e8b7..d55660f89f6 100644
--- a/gdb/tui/tui-layout.c
+++ b/gdb/tui/tui-layout.c
@@ -65,6 +65,11 @@ static tui_layout_split *asm_regs_layout;
/* See tui-data.h. */
std::vector<tui_win_info *> tui_windows;

+/* When applying a layout, this is the list of all windows that were
+ in the previous layout. This is used to re-use windows when
+ changing a layout. */
+static std::vector<tui_win_info *> saved_tui_windows;
+
/* See tui-layout.h. */

void
@@ -75,10 +80,10 @@ tui_apply_current_layout ()

extract_display_start_addr (&gdbarch, &addr);

- std::vector<tui_win_info *> saved_windows = std::move (tui_windows);
+ saved_tui_windows = std::move (tui_windows);
tui_windows.clear ();

- for (tui_win_info *win_info : saved_windows)
+ for (tui_win_info *win_info : saved_tui_windows)
win_info->make_visible (false);

applied_layout->apply (0, 0, tui_term_width (), tui_term_height ());
@@ -94,7 +99,7 @@ tui_apply_current_layout ()

/* Now delete any window that was not re-applied. */
tui_win_info *focus = tui_win_with_focus ();
- for (tui_win_info *win_info : saved_windows)
+ for (tui_win_info *win_info : saved_tui_windows)
{
if (!win_info->is_visible ())
{
@@ -107,6 +112,8 @@ tui_apply_current_layout ()
if (gdbarch == nullptr && TUI_DISASM_WIN != nullptr)
tui_get_begin_asm_address (&gdbarch, &addr);
tui_update_source_windows_with_addr (gdbarch, addr);
+
+ saved_tui_windows.clear ();
}

/* See tui-layout. */
@@ -395,6 +402,21 @@ initialize_known_windows ()

/* See tui-layout.h. */

+void
+tui_register_window (const char *name, window_factory &&factory)
+{
+ std::string name_copy = name;
+
+ if (name_copy == "src" || name_copy == "cmd" || name_copy == "regs"
+ || name_copy == "asm" || name_copy == "locator")
+ error (_("Window type \"%s\" is built-in"), name);
+
+ known_window_types->emplace (std::move (name_copy),
+ std::move (factory));
+}
+
+/* See tui-layout.h. */
+
std::unique_ptr<tui_layout_base>
tui_layout_window::clone () const
{
diff --git a/gdb/tui/tui-layout.h b/gdb/tui/tui-layout.h
index 6607e8d40d8..90618377e17 100644
--- a/gdb/tui/tui-layout.h
+++ b/gdb/tui/tui-layout.h
@@ -249,4 +249,14 @@ extern void tui_apply_current_layout ();
extern void tui_adjust_window_height (struct tui_win_info *win,
int new_height);

+/* The type of a function that is used to create a TUI window. */
+
+typedef std::function<tui_gen_win_info * (const char *name)> window_factory;
+
+/* Register a new TUI window type. NAME is the name of the window
+ type. FACTORY is a function that can be called to instantiate the
+ window. */
+
+extern void tui_register_window (const char *name, window_factory &&factory);
+
#endif /* TUI_TUI_LAYOUT_H */
--
2.17.2