#include <cstdint>
#include <cstdio>
#include <cstring>
+#include <functional>
#include <type_traits>
using namespace lldb;
#define KEY_RETURN 10
#define KEY_ESCAPE 27
+#define KEY_SHIFT_TAB (KEY_MAX + 1)
+
namespace curses {
class Menu;
class MenuDelegate;
int m_first_visible_line;
};
-class Window {
+// A surface is an abstraction for something than can be drawn on. The surface
+// have a width, a height, a cursor position, and a multitude of drawing
+// operations. This type should be sub-classed to get an actually useful ncurses
+// object, such as a Window, SubWindow, Pad, or a SubPad.
+class Surface {
public:
- Window(const char *name)
- : m_name(name), m_window(nullptr), m_panel(nullptr), m_parent(nullptr),
- m_subwindows(), m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX),
- m_prev_active_window_idx(UINT32_MAX), m_delete(false),
- m_needs_update(true), m_can_activate(true), m_is_subwin(false) {}
+ Surface() : m_window(nullptr) {}
- Window(const char *name, WINDOW *w, bool del = true)
- : m_name(name), m_window(nullptr), m_panel(nullptr), m_parent(nullptr),
- m_subwindows(), m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX),
- m_prev_active_window_idx(UINT32_MAX), m_delete(del),
- m_needs_update(true), m_can_activate(true), m_is_subwin(false) {
- if (w)
- Reset(w);
- }
+ WINDOW *get() { return m_window; }
- Window(const char *name, const Rect &bounds)
- : m_name(name), m_window(nullptr), m_parent(nullptr), m_subwindows(),
- m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX),
- m_prev_active_window_idx(UINT32_MAX), m_delete(true),
- m_needs_update(true), m_can_activate(true), m_is_subwin(false) {
- Reset(::newwin(bounds.size.height, bounds.size.width, bounds.origin.y,
- bounds.origin.y));
- }
+ operator WINDOW *() { return m_window; }
- virtual ~Window() {
- RemoveSubWindows();
- Reset();
+ // Copy a region of the surface to another surface.
+ void CopyToSurface(Surface &target, Point source_origin, Point target_origin,
+ Size size) {
+ ::copywin(m_window, target.get(), source_origin.y, source_origin.x,
+ target_origin.y, target_origin.x,
+ target_origin.y + size.height - 1,
+ target_origin.x + size.width - 1, false);
}
- void Reset(WINDOW *w = nullptr, bool del = true) {
- if (m_window == w)
- return;
-
- if (m_panel) {
- ::del_panel(m_panel);
- m_panel = nullptr;
- }
- if (m_window && m_delete) {
- ::delwin(m_window);
- m_window = nullptr;
- m_delete = false;
- }
- if (w) {
- m_window = w;
- m_panel = ::new_panel(m_window);
- m_delete = del;
- }
- }
+ int GetCursorX() const { return getcurx(m_window); }
+ int GetCursorY() const { return getcury(m_window); }
+ void MoveCursor(int x, int y) { ::wmove(m_window, y, x); }
void AttributeOn(attr_t attr) { ::wattron(m_window, attr); }
void AttributeOff(attr_t attr) { ::wattroff(m_window, attr); }
- void Box(chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) {
- ::box(m_window, v_char, h_char);
- }
- void Clear() { ::wclear(m_window); }
- void Erase() { ::werase(m_window); }
- Rect GetBounds() const {
- return Rect(GetParentOrigin(), GetSize());
- } // Get the rectangle in our parent window
- int GetChar() { return ::wgetch(m_window); }
- int GetCursorX() const { return getcurx(m_window); }
- int GetCursorY() const { return getcury(m_window); }
- Rect GetFrame() const {
- return Rect(Point(), GetSize());
- } // Get our rectangle in our own coordinate system
- Point GetParentOrigin() const { return Point(GetParentX(), GetParentY()); }
- Size GetSize() const { return Size(GetWidth(), GetHeight()); }
- int GetParentX() const { return getparx(m_window); }
- int GetParentY() const { return getpary(m_window); }
+
int GetMaxX() const { return getmaxx(m_window); }
int GetMaxY() const { return getmaxy(m_window); }
int GetWidth() const { return GetMaxX(); }
int GetHeight() const { return GetMaxY(); }
- void MoveCursor(int x, int y) { ::wmove(m_window, y, x); }
- void MoveWindow(int x, int y) { MoveWindow(Point(x, y)); }
- void Resize(int w, int h) { ::wresize(m_window, h, w); }
- void Resize(const Size &size) {
- ::wresize(m_window, size.height, size.width);
- }
- void PutChar(int ch) { ::waddch(m_window, ch); }
- void PutCString(const char *s, int len = -1) { ::waddnstr(m_window, s, len); }
+ Size GetSize() const { return Size(GetWidth(), GetHeight()); }
+ // Get a zero origin rectangle width the surface size.
+ Rect GetFrame() const { return Rect(Point(), GetSize()); }
+
+ void Clear() { ::wclear(m_window); }
+ void Erase() { ::werase(m_window); }
+
void SetBackground(int color_pair_idx) {
::wbkgd(m_window, COLOR_PAIR(color_pair_idx));
}
+ void PutChar(int ch) { ::waddch(m_window, ch); }
+ void PutCString(const char *s, int len = -1) { ::waddnstr(m_window, s, len); }
+
void PutCStringTruncated(int right_pad, const char *s, int len = -1) {
int bytes_left = GetWidth() - GetCursorX();
if (bytes_left > right_pad) {
}
}
- void MoveWindow(const Point &origin) {
- const bool moving_window = origin != GetParentOrigin();
- if (m_is_subwin && moving_window) {
- // Can't move subwindows, must delete and re-create
- Size size = GetSize();
- Reset(::subwin(m_parent->m_window, size.height, size.width, origin.y,
- origin.x),
- true);
- } else {
- ::mvwin(m_window, origin.y, origin.x);
- }
- }
-
- void SetBounds(const Rect &bounds) {
- const bool moving_window = bounds.origin != GetParentOrigin();
- if (m_is_subwin && moving_window) {
- // Can't move subwindows, must delete and re-create
- Reset(::subwin(m_parent->m_window, bounds.size.height, bounds.size.width,
- bounds.origin.y, bounds.origin.x),
- true);
- } else {
- if (moving_window)
- MoveWindow(bounds.origin);
- Resize(bounds.size);
- }
- }
-
void Printf(const char *format, ...) __attribute__((format(printf, 2, 3))) {
va_list args;
va_start(args, format);
PutCStringTruncated(right_pad, strm.GetData());
}
- size_t LimitLengthToRestOfLine(size_t length) const {
- return std::min<size_t>(length, std::max(0, GetWidth() - GetCursorX() - 1));
+ void VerticalLine(int n, chtype v_char = ACS_VLINE) {
+ ::wvline(m_window, v_char, n);
+ }
+ void HorizontalLine(int n, chtype h_char = ACS_HLINE) {
+ ::whline(m_window, h_char, n);
+ }
+ void Box(chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) {
+ ::box(m_window, v_char, h_char);
+ }
+
+ void TitledBox(const char *title, chtype v_char = ACS_VLINE,
+ chtype h_char = ACS_HLINE) {
+ Box(v_char, h_char);
+ int title_offset = 2;
+ MoveCursor(title_offset, 0);
+ PutChar('[');
+ PutCString(title, GetWidth() - title_offset);
+ PutChar(']');
+ }
+
+ void Box(const Rect &bounds, chtype v_char = ACS_VLINE,
+ chtype h_char = ACS_HLINE) {
+ MoveCursor(bounds.origin.x, bounds.origin.y);
+ VerticalLine(bounds.size.height);
+ HorizontalLine(bounds.size.width);
+ PutChar(ACS_ULCORNER);
+
+ MoveCursor(bounds.origin.x + bounds.size.width - 1, bounds.origin.y);
+ VerticalLine(bounds.size.height);
+ PutChar(ACS_URCORNER);
+
+ MoveCursor(bounds.origin.x, bounds.origin.y + bounds.size.height - 1);
+ HorizontalLine(bounds.size.width);
+ PutChar(ACS_LLCORNER);
+
+ MoveCursor(bounds.origin.x + bounds.size.width - 1,
+ bounds.origin.y + bounds.size.height - 1);
+ PutChar(ACS_LRCORNER);
+ }
+
+ void TitledBox(const Rect &bounds, const char *title,
+ chtype v_char = ACS_VLINE, chtype h_char = ACS_HLINE) {
+ Box(bounds, v_char, h_char);
+ int title_offset = 2;
+ MoveCursor(bounds.origin.x + title_offset, bounds.origin.y);
+ PutChar('[');
+ PutCString(title, bounds.size.width - title_offset);
+ PutChar(']');
}
// Curses doesn't allow direct output of color escape sequences, but that's
return result;
}
+protected:
+ WINDOW *m_window;
+};
+
+class Pad : public Surface {
+public:
+ Pad(Size size) { m_window = ::newpad(size.height, size.width); }
+
+ ~Pad() { ::delwin(m_window); }
+};
+
+class SubPad : public Surface {
+public:
+ SubPad(Pad &pad, Rect bounds) {
+ m_window = ::subpad(pad.get(), bounds.size.height, bounds.size.width,
+ bounds.origin.y, bounds.origin.x);
+ }
+ SubPad(SubPad &subpad, Rect bounds) {
+ m_window = ::subpad(subpad.get(), bounds.size.height, bounds.size.width,
+ bounds.origin.y, bounds.origin.x);
+ }
+
+ ~SubPad() { ::delwin(m_window); }
+};
+
+class Window : public Surface {
+public:
+ Window(const char *name)
+ : m_name(name), m_panel(nullptr), m_parent(nullptr), m_subwindows(),
+ m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX),
+ m_prev_active_window_idx(UINT32_MAX), m_delete(false),
+ m_needs_update(true), m_can_activate(true), m_is_subwin(false) {}
+
+ Window(const char *name, WINDOW *w, bool del = true)
+ : m_name(name), m_panel(nullptr), m_parent(nullptr), m_subwindows(),
+ m_delegate_sp(), m_curr_active_window_idx(UINT32_MAX),
+ m_prev_active_window_idx(UINT32_MAX), m_delete(del),
+ m_needs_update(true), m_can_activate(true), m_is_subwin(false) {
+ if (w)
+ Reset(w);
+ }
+
+ Window(const char *name, const Rect &bounds)
+ : m_name(name), m_parent(nullptr), m_subwindows(), m_delegate_sp(),
+ m_curr_active_window_idx(UINT32_MAX),
+ m_prev_active_window_idx(UINT32_MAX), m_delete(true),
+ m_needs_update(true), m_can_activate(true), m_is_subwin(false) {
+ Reset(::newwin(bounds.size.height, bounds.size.width, bounds.origin.y,
+ bounds.origin.y));
+ }
+
+ virtual ~Window() {
+ RemoveSubWindows();
+ Reset();
+ }
+
+ void Reset(WINDOW *w = nullptr, bool del = true) {
+ if (m_window == w)
+ return;
+
+ if (m_panel) {
+ ::del_panel(m_panel);
+ m_panel = nullptr;
+ }
+ if (m_window && m_delete) {
+ ::delwin(m_window);
+ m_window = nullptr;
+ m_delete = false;
+ }
+ if (w) {
+ m_window = w;
+ m_panel = ::new_panel(m_window);
+ m_delete = del;
+ }
+ }
+ //
+ // Get the rectangle in our parent window
+ Rect GetBounds() const { return Rect(GetParentOrigin(), GetSize()); }
+ int GetChar() { return ::wgetch(m_window); }
+ Point GetParentOrigin() const { return Point(GetParentX(), GetParentY()); }
+ int GetParentX() const { return getparx(m_window); }
+ int GetParentY() const { return getpary(m_window); }
+ void MoveWindow(int x, int y) { MoveWindow(Point(x, y)); }
+ void Resize(int w, int h) { ::wresize(m_window, h, w); }
+ void Resize(const Size &size) {
+ ::wresize(m_window, size.height, size.width);
+ }
+ void MoveWindow(const Point &origin) {
+ const bool moving_window = origin != GetParentOrigin();
+ if (m_is_subwin && moving_window) {
+ // Can't move subwindows, must delete and re-create
+ Size size = GetSize();
+ Reset(::subwin(m_parent->m_window, size.height, size.width, origin.y,
+ origin.x),
+ true);
+ } else {
+ ::mvwin(m_window, origin.y, origin.x);
+ }
+ }
+
+ void SetBounds(const Rect &bounds) {
+ const bool moving_window = bounds.origin != GetParentOrigin();
+ if (m_is_subwin && moving_window) {
+ // Can't move subwindows, must delete and re-create
+ Reset(::subwin(m_parent->m_window, bounds.size.height, bounds.size.width,
+ bounds.origin.y, bounds.origin.x),
+ true);
+ } else {
+ if (moving_window)
+ MoveWindow(bounds.origin);
+ Resize(bounds.size);
+ }
+ }
+
void Touch() {
::touchwin(m_window);
if (m_parent)
return WindowSP();
}
- void RemoveSubWindows() {
- m_curr_active_window_idx = UINT32_MAX;
- m_prev_active_window_idx = UINT32_MAX;
- for (Windows::iterator pos = m_subwindows.begin();
- pos != m_subwindows.end(); pos = m_subwindows.erase(pos)) {
- (*pos)->Erase();
+ void RemoveSubWindows() {
+ m_curr_active_window_idx = UINT32_MAX;
+ m_prev_active_window_idx = UINT32_MAX;
+ for (Windows::iterator pos = m_subwindows.begin();
+ pos != m_subwindows.end(); pos = m_subwindows.erase(pos)) {
+ (*pos)->Erase();
+ }
+ if (m_parent)
+ m_parent->Touch();
+ else
+ ::touchwin(stdscr);
+ }
+
+ // Window drawing utilities
+ void DrawTitleBox(const char *title, const char *bottom_message = nullptr) {
+ attr_t attr = 0;
+ if (IsActive())
+ attr = A_BOLD | COLOR_PAIR(BlackOnWhite);
+ else
+ attr = 0;
+ if (attr)
+ AttributeOn(attr);
+
+ Box();
+ MoveCursor(3, 0);
+
+ if (title && title[0]) {
+ PutChar('<');
+ PutCString(title);
+ PutChar('>');
+ }
+
+ if (bottom_message && bottom_message[0]) {
+ int bottom_message_length = strlen(bottom_message);
+ int x = GetWidth() - 3 - (bottom_message_length + 2);
+
+ if (x > 0) {
+ MoveCursor(x, GetHeight() - 1);
+ PutChar('[');
+ PutCString(bottom_message);
+ PutChar(']');
+ } else {
+ MoveCursor(1, GetHeight() - 1);
+ PutChar('[');
+ PutCStringTruncated(1, bottom_message);
+ }
+ }
+ if (attr)
+ AttributeOff(attr);
+ }
+
+ virtual void Draw(bool force) {
+ if (m_delegate_sp && m_delegate_sp->WindowDelegateDraw(*this, force))
+ return;
+
+ for (auto &subwindow_sp : m_subwindows)
+ subwindow_sp->Draw(force);
+ }
+
+ bool CreateHelpSubwindow() {
+ if (m_delegate_sp) {
+ const char *text = m_delegate_sp->WindowDelegateGetHelpText();
+ KeyHelp *key_help = m_delegate_sp->WindowDelegateGetKeyHelp();
+ if ((text && text[0]) || key_help) {
+ std::unique_ptr<HelpDialogDelegate> help_delegate_up(
+ new HelpDialogDelegate(text, key_help));
+ const size_t num_lines = help_delegate_up->GetNumLines();
+ const size_t max_length = help_delegate_up->GetMaxLineLength();
+ Rect bounds = GetBounds();
+ bounds.Inset(1, 1);
+ if (max_length + 4 < static_cast<size_t>(bounds.size.width)) {
+ bounds.origin.x += (bounds.size.width - max_length + 4) / 2;
+ bounds.size.width = max_length + 4;
+ } else {
+ if (bounds.size.width > 100) {
+ const int inset_w = bounds.size.width / 4;
+ bounds.origin.x += inset_w;
+ bounds.size.width -= 2 * inset_w;
+ }
+ }
+
+ if (num_lines + 2 < static_cast<size_t>(bounds.size.height)) {
+ bounds.origin.y += (bounds.size.height - num_lines + 2) / 2;
+ bounds.size.height = num_lines + 2;
+ } else {
+ if (bounds.size.height > 100) {
+ const int inset_h = bounds.size.height / 4;
+ bounds.origin.y += inset_h;
+ bounds.size.height -= 2 * inset_h;
+ }
+ }
+ WindowSP help_window_sp;
+ Window *parent_window = GetParent();
+ if (parent_window)
+ help_window_sp = parent_window->CreateSubWindow("Help", bounds, true);
+ else
+ help_window_sp = CreateSubWindow("Help", bounds, true);
+ help_window_sp->SetDelegate(
+ WindowDelegateSP(help_delegate_up.release()));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ virtual HandleCharResult HandleChar(int key) {
+ // Always check the active window first
+ HandleCharResult result = eKeyNotHandled;
+ WindowSP active_window_sp = GetActiveWindow();
+ if (active_window_sp) {
+ result = active_window_sp->HandleChar(key);
+ if (result != eKeyNotHandled)
+ return result;
+ }
+
+ if (m_delegate_sp) {
+ result = m_delegate_sp->WindowDelegateHandleChar(*this, key);
+ if (result != eKeyNotHandled)
+ return result;
+ }
+
+ // Then check for any windows that want any keys that weren't handled. This
+ // is typically only for a menubar. Make a copy of the subwindows in case
+ // any HandleChar() functions muck with the subwindows. If we don't do
+ // this, we can crash when iterating over the subwindows.
+ Windows subwindows(m_subwindows);
+ for (auto subwindow_sp : subwindows) {
+ if (!subwindow_sp->m_can_activate) {
+ HandleCharResult result = subwindow_sp->HandleChar(key);
+ if (result != eKeyNotHandled)
+ return result;
+ }
+ }
+
+ return eKeyNotHandled;
+ }
+
+ WindowSP GetActiveWindow() {
+ if (!m_subwindows.empty()) {
+ if (m_curr_active_window_idx >= m_subwindows.size()) {
+ if (m_prev_active_window_idx < m_subwindows.size()) {
+ m_curr_active_window_idx = m_prev_active_window_idx;
+ m_prev_active_window_idx = UINT32_MAX;
+ } else if (IsActive()) {
+ m_prev_active_window_idx = UINT32_MAX;
+ m_curr_active_window_idx = UINT32_MAX;
+
+ // Find first window that wants to be active if this window is active
+ const size_t num_subwindows = m_subwindows.size();
+ for (size_t i = 0; i < num_subwindows; ++i) {
+ if (m_subwindows[i]->GetCanBeActive()) {
+ m_curr_active_window_idx = i;
+ break;
+ }
+ }
+ }
+ }
+
+ if (m_curr_active_window_idx < m_subwindows.size())
+ return m_subwindows[m_curr_active_window_idx];
+ }
+ return WindowSP();
+ }
+
+ bool GetCanBeActive() const { return m_can_activate; }
+
+ void SetCanBeActive(bool b) { m_can_activate = b; }
+
+ void SetDelegate(const WindowDelegateSP &delegate_sp) {
+ m_delegate_sp = delegate_sp;
+ }
+
+ Window *GetParent() const { return m_parent; }
+
+ bool IsActive() const {
+ if (m_parent)
+ return m_parent->GetActiveWindow().get() == this;
+ else
+ return true; // Top level window is always active
+ }
+
+ void SelectNextWindowAsActive() {
+ // Move active focus to next window
+ const int num_subwindows = m_subwindows.size();
+ int start_idx = 0;
+ if (m_curr_active_window_idx != UINT32_MAX) {
+ m_prev_active_window_idx = m_curr_active_window_idx;
+ start_idx = m_curr_active_window_idx + 1;
+ }
+ for (int idx = start_idx; idx < num_subwindows; ++idx) {
+ if (m_subwindows[idx]->GetCanBeActive()) {
+ m_curr_active_window_idx = idx;
+ return;
+ }
+ }
+ for (int idx = 0; idx < start_idx; ++idx) {
+ if (m_subwindows[idx]->GetCanBeActive()) {
+ m_curr_active_window_idx = idx;
+ break;
+ }
+ }
+ }
+
+ void SelectPreviousWindowAsActive() {
+ // Move active focus to previous window
+ const int num_subwindows = m_subwindows.size();
+ int start_idx = num_subwindows - 1;
+ if (m_curr_active_window_idx != UINT32_MAX) {
+ m_prev_active_window_idx = m_curr_active_window_idx;
+ start_idx = m_curr_active_window_idx - 1;
+ }
+ for (int idx = start_idx; idx >= 0; --idx) {
+ if (m_subwindows[idx]->GetCanBeActive()) {
+ m_curr_active_window_idx = idx;
+ return;
+ }
+ }
+ for (int idx = num_subwindows - 1; idx > start_idx; --idx) {
+ if (m_subwindows[idx]->GetCanBeActive()) {
+ m_curr_active_window_idx = idx;
+ break;
+ }
+ }
+ }
+
+ const char *GetName() const { return m_name.c_str(); }
+
+protected:
+ std::string m_name;
+ PANEL *m_panel;
+ Window *m_parent;
+ Windows m_subwindows;
+ WindowDelegateSP m_delegate_sp;
+ uint32_t m_curr_active_window_idx;
+ uint32_t m_prev_active_window_idx;
+ bool m_delete;
+ bool m_needs_update;
+ bool m_can_activate;
+ bool m_is_subwin;
+
+private:
+ Window(const Window &) = delete;
+ const Window &operator=(const Window &) = delete;
+};
+
+class DerivedWindow : public Surface {
+public:
+ DerivedWindow(Window &window, Rect bounds) {
+ m_window = ::derwin(window.get(), bounds.size.height, bounds.size.width,
+ bounds.origin.y, bounds.origin.x);
+ }
+ DerivedWindow(DerivedWindow &derived_window, Rect bounds) {
+ m_window = ::derwin(derived_window.get(), bounds.size.height,
+ bounds.size.width, bounds.origin.y, bounds.origin.x);
+ }
+
+ ~DerivedWindow() { ::delwin(m_window); }
+};
+
+/////////
+// Forms
+/////////
+
+// A scroll context defines a vertical region that needs to be visible in a
+// scrolling area. The region is defined by the index of the start and end lines
+// of the region. The start and end lines may be equal, in which case, the
+// region is a single line.
+struct ScrollContext {
+ int start;
+ int end;
+
+ ScrollContext(int line) : start(line), end(line) {}
+ ScrollContext(int _start, int _end) : start(_start), end(_end) {}
+
+ void Offset(int offset) {
+ start += offset;
+ end += offset;
+ }
+};
+
+class FieldDelegate {
+public:
+ virtual ~FieldDelegate() = default;
+
+ // Returns the number of lines needed to draw the field. The draw method will
+ // be given a surface that have exactly this number of lines.
+ virtual int FieldDelegateGetHeight() = 0;
+
+ // Returns the scroll context in the local coordinates of the field. By
+ // default, the scroll context spans the whole field. Bigger fields with
+ // internal navigation should override this method to provide a finer context.
+ // Typical override methods would first get the scroll context of the internal
+ // element then add the offset of the element in the field.
+ virtual ScrollContext FieldDelegateGetScrollContext() {
+ return ScrollContext(0, FieldDelegateGetHeight() - 1);
+ }
+
+ // Draw the field in the given subpad surface. The surface have a height that
+ // is equal to the height returned by FieldDelegateGetHeight(). If the field
+ // is selected in the form window, then is_selected will be true.
+ virtual void FieldDelegateDraw(SubPad &surface, bool is_selected) = 0;
+
+ // Handle the key that wasn't handled by the form window or a container field.
+ virtual HandleCharResult FieldDelegateHandleChar(int key) {
+ return eKeyNotHandled;
+ }
+
+ // This is executed once the user exists the field, that is, once the user
+ // navigates to the next or the previous field. This is particularly useful to
+ // do in-field validation and error setting. Fields with internal navigation
+ // should call this method on their fields.
+ virtual void FieldDelegateExitCallback() { return; }
+
+ // Fields may have internal navigation, for instance, a List Field have
+ // multiple internal elements, which needs to be navigated. To allow for this
+ // mechanism, the window shouldn't handle the navigation keys all the time,
+ // and instead call the key handing method of the selected field. It should
+ // only handle the navigation keys when the field contains a single element or
+ // have the last or first element selected depending on if the user is
+ // navigating forward or backward. Additionally, once a field is selected in
+ // the forward or backward direction, its first or last internal element
+ // should be selected. The following methods implements those mechanisms.
+
+ // Returns true if the first element in the field is selected or if the field
+ // contains a single element.
+ virtual bool FieldDelegateOnFirstOrOnlyElement() { return true; }
+
+ // Returns true if the last element in the field is selected or if the field
+ // contains a single element.
+ virtual bool FieldDelegateOnLastOrOnlyElement() { return true; }
+
+ // Select the first element in the field if multiple elements exists.
+ virtual void FieldDelegateSelectFirstElement() { return; }
+
+ // Select the last element in the field if multiple elements exists.
+ virtual void FieldDelegateSelectLastElement() { return; }
+};
+
+typedef std::shared_ptr<FieldDelegate> FieldDelegateSP;
+
+class TextFieldDelegate : public FieldDelegate {
+public:
+ TextFieldDelegate(const char *label, const char *content)
+ : m_label(label), m_cursor_position(0), m_first_visibile_char(0) {
+ if (content)
+ m_content = content;
+ }
+
+ // Text fields are drawn as titled boxes of a single line, with a possible
+ // error messages at the end.
+ //
+ // __[Label]___________
+ // | |
+ // |__________________|
+ // - Error message if it exists.
+
+ // The text field has a height of 3 lines. 2 lines for borders and 1 line for
+ // the content.
+ int GetFieldHeight() { return 3; }
+
+ // The text field has a full height of 3 or 4 lines. 3 lines for the actual
+ // field and an optional line for an error if it exists.
+ int FieldDelegateGetHeight() override {
+ int height = GetFieldHeight();
+ if (HasError())
+ height++;
+ return height;
+ }
+
+ // Get the cursor X position in the surface coordinate.
+ int GetCursorXPosition() { return m_cursor_position - m_first_visibile_char; }
+
+ int GetContentLength() { return m_content.length(); }
+
+ void DrawContent(SubPad &surface, bool is_selected) {
+ surface.MoveCursor(0, 0);
+ const char *text = m_content.c_str() + m_first_visibile_char;
+ surface.PutCString(text, surface.GetWidth());
+ m_last_drawn_content_width = surface.GetWidth();
+
+ // Highlight the cursor.
+ surface.MoveCursor(GetCursorXPosition(), 0);
+ if (is_selected)
+ surface.AttributeOn(A_REVERSE);
+ if (m_cursor_position == GetContentLength())
+ // Cursor is past the last character. Highlight an empty space.
+ surface.PutChar(' ');
+ else
+ surface.PutChar(m_content[m_cursor_position]);
+ if (is_selected)
+ surface.AttributeOff(A_REVERSE);
+ }
+
+ void DrawField(SubPad &surface, bool is_selected) {
+ surface.TitledBox(m_label.c_str());
+
+ Rect content_bounds = surface.GetFrame();
+ content_bounds.Inset(1, 1);
+ SubPad content_surface = SubPad(surface, content_bounds);
+
+ DrawContent(content_surface, is_selected);
+ }
+
+ void DrawError(SubPad &surface) {
+ if (!HasError())
+ return;
+ surface.MoveCursor(0, 0);
+ surface.AttributeOn(COLOR_PAIR(RedOnBlack));
+ surface.PutChar(ACS_DIAMOND);
+ surface.PutChar(' ');
+ surface.PutCStringTruncated(1, GetError().c_str());
+ surface.AttributeOff(COLOR_PAIR(RedOnBlack));
+ }
+
+ void FieldDelegateDraw(SubPad &surface, bool is_selected) override {
+ Rect frame = surface.GetFrame();
+ Rect field_bounds, error_bounds;
+ frame.HorizontalSplit(GetFieldHeight(), field_bounds, error_bounds);
+ SubPad field_surface = SubPad(surface, field_bounds);
+ SubPad error_surface = SubPad(surface, error_bounds);
+
+ DrawField(field_surface, is_selected);
+ DrawError(error_surface);
+ }
+
+ // The cursor is allowed to move one character past the string.
+ // m_cursor_position is in range [0, GetContentLength()].
+ void MoveCursorRight() {
+ if (m_cursor_position < GetContentLength())
+ m_cursor_position++;
+ }
+
+ void MoveCursorLeft() {
+ if (m_cursor_position > 0)
+ m_cursor_position--;
+ }
+
+ // If the cursor moved past the last visible character, scroll right by one
+ // character.
+ void ScrollRightIfNeeded() {
+ if (m_cursor_position - m_first_visibile_char == m_last_drawn_content_width)
+ m_first_visibile_char++;
+ }
+
+ void ScrollLeft() {
+ if (m_first_visibile_char > 0)
+ m_first_visibile_char--;
+ }
+
+ // If the cursor moved past the first visible character, scroll left by one
+ // character.
+ void ScrollLeftIfNeeded() {
+ if (m_cursor_position < m_first_visibile_char)
+ m_first_visibile_char--;
+ }
+
+ // Insert a character at the current cursor position, advance the cursor
+ // position, and make sure to scroll right if needed.
+ void InsertChar(char character) {
+ m_content.insert(m_cursor_position, 1, character);
+ m_cursor_position++;
+ ScrollRightIfNeeded();
+ }
+
+ // Remove the character before the cursor position, retreat the cursor
+ // position, and make sure to scroll left if needed.
+ void RemoveChar() {
+ if (m_cursor_position == 0)
+ return;
+
+ m_content.erase(m_cursor_position - 1, 1);
+ m_cursor_position--;
+ ScrollLeft();
+ }
+
+ // True if the key represents a char that can be inserted in the field
+ // content, false otherwise.
+ virtual bool IsAcceptableChar(int key) { return isprint(key); }
+
+ HandleCharResult FieldDelegateHandleChar(int key) override {
+ if (IsAcceptableChar(key)) {
+ ClearError();
+ InsertChar((char)key);
+ return eKeyHandled;
+ }
+
+ switch (key) {
+ case KEY_RIGHT:
+ MoveCursorRight();
+ ScrollRightIfNeeded();
+ return eKeyHandled;
+ case KEY_LEFT:
+ MoveCursorLeft();
+ ScrollLeftIfNeeded();
+ return eKeyHandled;
+ case KEY_BACKSPACE:
+ ClearError();
+ RemoveChar();
+ return eKeyHandled;
+ default:
+ break;
+ }
+ return eKeyNotHandled;
+ }
+
+ bool HasError() { return !m_error.empty(); }
+
+ void ClearError() { m_error.clear(); }
+
+ const std::string &GetError() { return m_error; }
+
+ void SetError(const char *error) { m_error = error; }
+
+ // Returns the text content of the field.
+ const std::string &GetText() { return m_content; }
+
+protected:
+ std::string m_label;
+ // The position of the top left corner character of the border.
+ std::string m_content;
+ // The cursor position in the content string itself. Can be in the range
+ // [0, GetContentLength()].
+ int m_cursor_position;
+ // The index of the first visible character in the content.
+ int m_first_visibile_char;
+ // The width of the fields content that was last drawn. Width can change, so
+ // this is used to determine if scrolling is needed dynamically.
+ int m_last_drawn_content_width;
+ // Optional error message. If empty, field is considered to have no error.
+ std::string m_error;
+};
+
+class IntegerFieldDelegate : public TextFieldDelegate {
+public:
+ IntegerFieldDelegate(const char *label, int content)
+ : TextFieldDelegate(label, std::to_string(content).c_str()) {}
+
+ // Only accept digits.
+ bool IsAcceptableChar(int key) override { return isdigit(key); }
+
+ // Returns the integer content of the field.
+ int GetInteger() { return std::stoi(m_content); }
+};
+
+class FileFieldDelegate : public TextFieldDelegate {
+public:
+ FileFieldDelegate(const char *label, const char *content,
+ bool need_to_exist = true)
+ : TextFieldDelegate(label, content), m_need_to_exist(need_to_exist) {}
+
+ // Set appropriate error messages if the file doesn't exists or is, in fact, a
+ // directory.
+ void FieldDelegateExitCallback() override {
+ FileSpec file(GetPath());
+ if (m_need_to_exist && !FileSystem::Instance().Exists(file)) {
+ SetError("File doesn't exist!");
+ return;
+ }
+ if (FileSystem::Instance().IsDirectory(file)) {
+ SetError("Not a file!");
+ return;
+ }
+ }
+
+ // Returns the path of the file.
+ const std::string &GetPath() { return m_content; }
+
+protected:
+ bool m_need_to_exist;
+};
+
+class DirectoryFieldDelegate : public TextFieldDelegate {
+public:
+ DirectoryFieldDelegate(const char *label, const char *content,
+ bool need_to_exist = true)
+ : TextFieldDelegate(label, content), m_need_to_exist(need_to_exist) {}
+
+ // Set appropriate error messages if the directory doesn't exists or is, in
+ // fact, a file.
+ void FieldDelegateExitCallback() override {
+ FileSpec file(GetPath());
+ if (m_need_to_exist && !FileSystem::Instance().Exists(file)) {
+ SetError("Directory doesn't exist!");
+ return;
+ }
+ if (!FileSystem::Instance().IsDirectory(file)) {
+ SetError("Not a directory!");
+ return;
+ }
+ }
+
+ // Returns the path of the file.
+ const std::string &GetPath() { return m_content; }
+
+protected:
+ bool m_need_to_exist;
+};
+
+class BooleanFieldDelegate : public FieldDelegate {
+public:
+ BooleanFieldDelegate(const char *label, bool content)
+ : m_label(label), m_content(content) {}
+
+ // Boolean fields are drawn as checkboxes.
+ //
+ // [X] Label or [ ] Label
+
+ // Boolean fields are have a single line.
+ int FieldDelegateGetHeight() override { return 1; }
+
+ void FieldDelegateDraw(SubPad &surface, bool is_selected) override {
+ surface.MoveCursor(0, 0);
+ surface.PutChar('[');
+ if (is_selected)
+ surface.AttributeOn(A_REVERSE);
+ surface.PutChar(m_content ? ACS_DIAMOND : ' ');
+ if (is_selected)
+ surface.AttributeOff(A_REVERSE);
+ surface.PutChar(']');
+ surface.PutChar(' ');
+ surface.PutCString(m_label.c_str());
+ }
+
+ void ToggleContent() { m_content = !m_content; }
+
+ void SetContentToTrue() { m_content = true; }
+
+ void SetContentToFalse() { m_content = false; }
+
+ HandleCharResult FieldDelegateHandleChar(int key) override {
+ switch (key) {
+ case 't':
+ case '1':
+ SetContentToTrue();
+ return eKeyHandled;
+ case 'f':
+ case '0':
+ SetContentToFalse();
+ return eKeyHandled;
+ case ' ':
+ case '\r':
+ case '\n':
+ case KEY_ENTER:
+ ToggleContent();
+ return eKeyHandled;
+ default:
+ break;
+ }
+ return eKeyNotHandled;
+ }
+
+ // Returns the boolean content of the field.
+ bool GetBoolean() { return m_content; }
+
+protected:
+ std::string m_label;
+ bool m_content;
+};
+
+class ChoicesFieldDelegate : public FieldDelegate {
+public:
+ ChoicesFieldDelegate(const char *label, int number_of_visible_choices,
+ std::vector<std::string> choices)
+ : m_label(label), m_number_of_visible_choices(number_of_visible_choices),
+ m_choices(choices), m_choice(0), m_first_visibile_choice(0) {}
+
+ // Choices fields are drawn as titles boxses of a number of visible choices.
+ // The rest of the choices become visible as the user scroll. The selected
+ // choice is denoted by a diamond as the first character.
+ //
+ // __[Label]___________
+ // |-Choice 1 |
+ // | Choice 2 |
+ // | Choice 3 |
+ // |__________________|
+
+ // Choices field have two border characters plus the number of visible
+ // choices.
+ int FieldDelegateGetHeight() override {
+ return m_number_of_visible_choices + 2;
+ }
+
+ int GetNumberOfChoices() { return m_choices.size(); }
+
+ // Get the index of the last visible choice.
+ int GetLastVisibleChoice() {
+ int index = m_first_visibile_choice + m_number_of_visible_choices;
+ return std::min(index, GetNumberOfChoices()) - 1;
+ }
+
+ void DrawContent(SubPad &surface, bool is_selected) {
+ int choices_to_draw = GetLastVisibleChoice() - m_first_visibile_choice + 1;
+ for (int i = 0; i < choices_to_draw; i++) {
+ surface.MoveCursor(0, i);
+ int current_choice = m_first_visibile_choice + i;
+ const char *text = m_choices[current_choice].c_str();
+ bool highlight = is_selected && current_choice == m_choice;
+ if (highlight)
+ surface.AttributeOn(A_REVERSE);
+ surface.PutChar(current_choice == m_choice ? ACS_DIAMOND : ' ');
+ surface.PutCString(text);
+ if (highlight)
+ surface.AttributeOff(A_REVERSE);
+ }
+ }
+
+ void FieldDelegateDraw(SubPad &surface, bool is_selected) override {
+ surface.TitledBox(m_label.c_str());
+
+ Rect content_bounds = surface.GetFrame();
+ content_bounds.Inset(1, 1);
+ SubPad content_surface = SubPad(surface, content_bounds);
+
+ DrawContent(content_surface, is_selected);
+ }
+
+ void SelectPrevious() {
+ if (m_choice > 0)
+ m_choice--;
+ }
+
+ void SelectNext() {
+ if (m_choice < GetNumberOfChoices() - 1)
+ m_choice++;
+ }
+
+ // If the cursor moved past the first visible choice, scroll up by one
+ // choice.
+ void ScrollUpIfNeeded() {
+ if (m_choice < m_first_visibile_choice)
+ m_first_visibile_choice--;
+ }
+
+ // If the cursor moved past the last visible choice, scroll down by one
+ // choice.
+ void ScrollDownIfNeeded() {
+ if (m_choice > GetLastVisibleChoice())
+ m_first_visibile_choice++;
+ }
+
+ HandleCharResult FieldDelegateHandleChar(int key) override {
+ switch (key) {
+ case KEY_UP:
+ SelectPrevious();
+ ScrollUpIfNeeded();
+ return eKeyHandled;
+ case KEY_DOWN:
+ SelectNext();
+ ScrollDownIfNeeded();
+ return eKeyHandled;
+ default:
+ break;
+ }
+ return eKeyNotHandled;
+ }
+
+ // Returns the content of the choice as a string.
+ std::string GetChoiceContent() { return m_choices[m_choice]; }
+
+ // Returns the index of the choice.
+ int GetChoice() { return m_choice; }
+
+protected:
+ std::string m_label;
+ int m_number_of_visible_choices;
+ std::vector<std::string> m_choices;
+ // The index of the selected choice.
+ int m_choice;
+ // The index of the first visible choice in the field.
+ int m_first_visibile_choice;
+};
+
+template <class T> class ListFieldDelegate : public FieldDelegate {
+public:
+ ListFieldDelegate(const char *label, T default_field)
+ : m_label(label), m_default_field(default_field), m_selection_index(0),
+ m_selection_type(SelectionType::NewButton) {}
+
+ // Signify which element is selected. If a field or a remove button is
+ // selected, then m_selection_index signifies the particular field that
+ // is selected or the field that the remove button belongs to.
+ enum class SelectionType { Field, RemoveButton, NewButton };
+
+ // A List field is drawn as a titled box of a number of other fields of the
+ // same type. Each field has a Remove button next to it that removes the
+ // corresponding field. Finally, the last line contains a New button to add a
+ // new field.
+ //
+ // __[Label]___________
+ // | Field 0 [Remove] |
+ // | Field 1 [Remove] |
+ // | Field 2 [Remove] |
+ // | [New] |
+ // |__________________|
+
+ // List fields have two lines for border characters, 1 line for the New
+ // button, and the total height of the available fields.
+ int FieldDelegateGetHeight() override {
+ // 2 border characters.
+ int height = 2;
+ // Total height of the fields.
+ for (int i = 0; i < GetNumberOfFields(); i++) {
+ height += m_fields[i].FieldDelegateGetHeight();
+ }
+ // A line for the New button.
+ height++;
+ return height;
+ }
+
+ ScrollContext FieldDelegateGetScrollContext() override {
+ int height = FieldDelegateGetHeight();
+ if (m_selection_type == SelectionType::NewButton)
+ return ScrollContext(height - 2, height - 1);
+
+ FieldDelegate &field = m_fields[m_selection_index];
+ ScrollContext context = field.FieldDelegateGetScrollContext();
+
+ // Start at 1 because of the top border.
+ int offset = 1;
+ for (int i = 0; i < m_selection_index; i++) {
+ offset += m_fields[i].FieldDelegateGetHeight();
+ }
+ context.Offset(offset);
+
+ // If the scroll context is touching the top border, include it in the
+ // context to show the label.
+ if (context.start == 1)
+ context.start--;
+
+ // If the scroll context is touching the new button, include it as well as
+ // the bottom border in the context.
+ if (context.end == height - 3)
+ context.end += 2;
+
+ return context;
+ }
+
+ void DrawRemoveButton(SubPad &surface, int highlight) {
+ surface.MoveCursor(1, surface.GetHeight() / 2);
+ if (highlight)
+ surface.AttributeOn(A_REVERSE);
+ surface.PutCString("[Remove]");
+ if (highlight)
+ surface.AttributeOff(A_REVERSE);
+ }
+
+ void DrawFields(SubPad &surface, bool is_selected) {
+ int line = 0;
+ int width = surface.GetWidth();
+ for (int i = 0; i < GetNumberOfFields(); i++) {
+ int height = m_fields[i].FieldDelegateGetHeight();
+ Rect bounds = Rect(Point(0, line), Size(width, height));
+ Rect field_bounds, remove_button_bounds;
+ bounds.VerticalSplit(bounds.size.width - sizeof(" [Remove]"),
+ field_bounds, remove_button_bounds);
+ SubPad field_surface = SubPad(surface, field_bounds);
+ SubPad remove_button_surface = SubPad(surface, remove_button_bounds);
+
+ bool is_element_selected = m_selection_index == i && is_selected;
+ bool is_field_selected =
+ is_element_selected && m_selection_type == SelectionType::Field;
+ bool is_remove_button_selected =
+ is_element_selected &&
+ m_selection_type == SelectionType::RemoveButton;
+ m_fields[i].FieldDelegateDraw(field_surface, is_field_selected);
+ DrawRemoveButton(remove_button_surface, is_remove_button_selected);
+
+ line += height;
+ }
+ }
+
+ void DrawNewButton(SubPad &surface, bool is_selected) {
+ const char *button_text = "[New]";
+ int x = (surface.GetWidth() - sizeof(button_text) - 1) / 2;
+ surface.MoveCursor(x, 0);
+ bool highlight =
+ is_selected && m_selection_type == SelectionType::NewButton;
+ if (highlight)
+ surface.AttributeOn(A_REVERSE);
+ surface.PutCString(button_text);
+ if (highlight)
+ surface.AttributeOff(A_REVERSE);
+ }
+
+ void FieldDelegateDraw(SubPad &surface, bool is_selected) override {
+ surface.TitledBox(m_label.c_str());
+
+ Rect content_bounds = surface.GetFrame();
+ content_bounds.Inset(1, 1);
+ Rect fields_bounds, new_button_bounds;
+ content_bounds.HorizontalSplit(content_bounds.size.height - 1,
+ fields_bounds, new_button_bounds);
+ SubPad fields_surface = SubPad(surface, fields_bounds);
+ SubPad new_button_surface = SubPad(surface, new_button_bounds);
+
+ DrawFields(fields_surface, is_selected);
+ DrawNewButton(new_button_surface, is_selected);
+ }
+
+ void AddNewField() {
+ m_fields.push_back(m_default_field);
+ m_selection_index = GetNumberOfFields() - 1;
+ m_selection_type = SelectionType::Field;
+ FieldDelegate &field = m_fields[m_selection_index];
+ field.FieldDelegateSelectFirstElement();
+ }
+
+ void RemoveField() {
+ m_fields.erase(m_fields.begin() + m_selection_index);
+ if (m_selection_index != 0)
+ m_selection_index--;
+
+ if (GetNumberOfFields() > 0) {
+ m_selection_type = SelectionType::Field;
+ FieldDelegate &field = m_fields[m_selection_index];
+ field.FieldDelegateSelectFirstElement();
+ } else
+ m_selection_type = SelectionType::NewButton;
+ }
+
+ HandleCharResult SelecteNext(int key) {
+ if (m_selection_type == SelectionType::NewButton)
+ return eKeyNotHandled;
+
+ if (m_selection_type == SelectionType::RemoveButton) {
+ if (m_selection_index == GetNumberOfFields() - 1) {
+ m_selection_type = SelectionType::NewButton;
+ return eKeyHandled;
+ }
+ m_selection_index++;
+ m_selection_type = SelectionType::Field;
+ FieldDelegate &next_field = m_fields[m_selection_index];
+ next_field.FieldDelegateSelectFirstElement();
+ return eKeyHandled;
+ }
+
+ FieldDelegate &field = m_fields[m_selection_index];
+ if (!field.FieldDelegateOnLastOrOnlyElement()) {
+ return field.FieldDelegateHandleChar(key);
+ }
+
+ field.FieldDelegateExitCallback();
+
+ m_selection_type = SelectionType::RemoveButton;
+ return eKeyHandled;
+ }
+
+ HandleCharResult SelectPrevious(int key) {
+ if (FieldDelegateOnFirstOrOnlyElement())
+ return eKeyNotHandled;
+
+ if (m_selection_type == SelectionType::RemoveButton) {
+ m_selection_type = SelectionType::Field;
+ FieldDelegate &field = m_fields[m_selection_index];
+ field.FieldDelegateSelectLastElement();
+ return eKeyHandled;
+ }
+
+ if (m_selection_type == SelectionType::NewButton) {
+ m_selection_type = SelectionType::RemoveButton;
+ m_selection_index = GetNumberOfFields() - 1;
+ return eKeyHandled;
+ }
+
+ FieldDelegate &field = m_fields[m_selection_index];
+ if (!field.FieldDelegateOnFirstOrOnlyElement()) {
+ return field.FieldDelegateHandleChar(key);
+ }
+
+ field.FieldDelegateExitCallback();
+
+ m_selection_type = SelectionType::RemoveButton;
+ m_selection_index--;
+ return eKeyHandled;
+ }
+
+ HandleCharResult FieldDelegateHandleChar(int key) override {
+ switch (key) {
+ case '\r':
+ case '\n':
+ case KEY_ENTER:
+ switch (m_selection_type) {
+ case SelectionType::NewButton:
+ AddNewField();
+ return eKeyHandled;
+ case SelectionType::RemoveButton:
+ RemoveField();
+ return eKeyHandled;
+ default:
+ break;
+ }
+ break;
+ case '\t':
+ SelecteNext(key);
+ return eKeyHandled;
+ case KEY_SHIFT_TAB:
+ SelectPrevious(key);
+ return eKeyHandled;
+ default:
+ break;
+ }
+
+ // If the key wasn't handled and one of the fields is selected, pass the key
+ // to that field.
+ if (m_selection_type == SelectionType::Field) {
+ return m_fields[m_selection_index].FieldDelegateHandleChar(key);
+ }
+
+ return eKeyNotHandled;
+ }
+
+ bool FieldDelegateOnLastOrOnlyElement() override {
+ if (m_selection_type == SelectionType::NewButton) {
+ return true;
+ }
+ return false;
+ }
+
+ bool FieldDelegateOnFirstOrOnlyElement() override {
+ if (m_selection_type == SelectionType::NewButton &&
+ GetNumberOfFields() == 0)
+ return true;
+
+ if (m_selection_type == SelectionType::Field && m_selection_index == 0) {
+ FieldDelegate &field = m_fields[m_selection_index];
+ return field.FieldDelegateOnFirstOrOnlyElement();
+ }
+
+ return false;
+ }
+
+ void FieldDelegateSelectFirstElement() override {
+ if (GetNumberOfFields() == 0) {
+ m_selection_type = SelectionType::NewButton;
+ return;
+ }
+
+ m_selection_type = SelectionType::Field;
+ m_selection_index = 0;
+ }
+
+ void FieldDelegateSelectLastElement() override {
+ m_selection_type = SelectionType::NewButton;
+ return;
+ }
+
+ int GetNumberOfFields() { return m_fields.size(); }
+
+ // Returns the form delegate at the current index.
+ T &GetField(int index) { return m_fields[index]; }
+
+protected:
+ std::string m_label;
+ // The default field delegate instance from which new field delegates will be
+ // created though a copy.
+ T m_default_field;
+ std::vector<T> m_fields;
+ int m_selection_index;
+ // See SelectionType class enum.
+ SelectionType m_selection_type;
+};
+
+class FormAction {
+public:
+ FormAction(const char *label, std::function<void(Window &)> action)
+ : m_action(action) {
+ if (label)
+ m_label = label;
+ }
+
+ // Draw a centered [Label].
+ void Draw(SubPad &surface, bool is_selected) {
+ int x = (surface.GetWidth() - m_label.length()) / 2;
+ surface.MoveCursor(x, 0);
+ if (is_selected)
+ surface.AttributeOn(A_REVERSE);
+ surface.PutChar('[');
+ surface.PutCString(m_label.c_str());
+ surface.PutChar(']');
+ if (is_selected)
+ surface.AttributeOff(A_REVERSE);
+ }
+
+ void Execute(Window &window) { m_action(window); }
+
+ const std::string &GetLabel() { return m_label; }
+
+protected:
+ std::string m_label;
+ std::function<void(Window &)> m_action;
+};
+
+class FormDelegate {
+public:
+ FormDelegate() {}
+
+ virtual ~FormDelegate() = default;
+
+ FieldDelegateSP &GetField(int field_index) { return m_fields[field_index]; }
+
+ FormAction &GetAction(int action_index) { return m_actions[action_index]; }
+
+ int GetNumberOfFields() { return m_fields.size(); }
+
+ int GetNumberOfActions() { return m_actions.size(); }
+
+ bool HasError() { return !m_error.empty(); }
+
+ void ClearError() { m_error.clear(); }
+
+ const std::string &GetError() { return m_error; }
+
+ void SetError(const char *error) { m_error = error; }
+
+ // Factory methods to create and add fields of specific types.
+
+ TextFieldDelegate *AddTextField(const char *label, const char *content) {
+ TextFieldDelegate *delegate = new TextFieldDelegate(label, content);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ FileFieldDelegate *AddFileField(const char *label, const char *content,
+ bool need_to_exist = true) {
+ FileFieldDelegate *delegate =
+ new FileFieldDelegate(label, content, need_to_exist);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ DirectoryFieldDelegate *AddDirectoryField(const char *label,
+ const char *content,
+ bool need_to_exist = true) {
+ DirectoryFieldDelegate *delegate =
+ new DirectoryFieldDelegate(label, content, need_to_exist);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ IntegerFieldDelegate *AddIntegerField(const char *label, int content) {
+ IntegerFieldDelegate *delegate = new IntegerFieldDelegate(label, content);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ BooleanFieldDelegate *AddBooleanField(const char *label, bool content) {
+ BooleanFieldDelegate *delegate = new BooleanFieldDelegate(label, content);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ ChoicesFieldDelegate *AddChoicesField(const char *label, int height,
+ std::vector<std::string> choices) {
+ ChoicesFieldDelegate *delegate =
+ new ChoicesFieldDelegate(label, height, choices);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ template <class T>
+ ListFieldDelegate<T> *AddListField(const char *label, T default_field) {
+ ListFieldDelegate<T> *delegate =
+ new ListFieldDelegate<T>(label, default_field);
+ FieldDelegateSP delegate_sp = FieldDelegateSP(delegate);
+ m_fields.push_back(delegate_sp);
+ return delegate;
+ }
+
+ // Factory methods for adding actions.
+
+ void AddAction(const char *label, std::function<void(Window &)> action) {
+ m_actions.push_back(FormAction(label, action));
+ }
+
+protected:
+ std::vector<FieldDelegateSP> m_fields;
+ std::vector<FormAction> m_actions;
+ // Optional error message. If empty, form is considered to have no error.
+ std::string m_error;
+};
+
+typedef std::shared_ptr<FormDelegate> FormDelegateSP;
+
+class FormWindowDelegate : public WindowDelegate {
+public:
+ FormWindowDelegate(FormDelegateSP &delegate_sp)
+ : m_delegate_sp(delegate_sp), m_selection_index(0),
+ m_selection_type(SelectionType::Field), m_first_visible_line(0) {}
+
+ // Signify which element is selected. If a field or an action is selected,
+ // then m_selection_index signifies the particular field or action that is
+ // selected.
+ enum class SelectionType { Field, Action };
+
+ // A form window is padded by one character from all sides. First, if an error
+ // message exists, it is drawn followed by a separator. Then one or more
+ // fields are drawn. Finally, all available actions are drawn on a single
+ // line.
+ //
+ // ___<Form Name>_________________________________________________
+ // | |
+ // | - Error message if it exists. |
+ // |-------------------------------------------------------------|
+ // | Form elements here. |
+ // | Form actions here. |
+ // | |
+ // |______________________________________[Press Esc to cancel]__|
+ //
+
+ // One line for the error and another for the horizontal line.
+ int GetErrorHeight() {
+ if (m_delegate_sp->HasError())
+ return 2;
+ return 0;
+ }
+
+ // Actions span a single line.
+ int GetActionsHeight() {
+ if (m_delegate_sp->GetNumberOfActions() > 0)
+ return 1;
+ return 0;
+ }
+
+ // Get the total number of needed lines to draw the contents.
+ int GetContentHeight() {
+ int height = 0;
+ height += GetErrorHeight();
+ for (int i = 0; i < m_delegate_sp->GetNumberOfFields(); i++) {
+ height += m_delegate_sp->GetField(i)->FieldDelegateGetHeight();
}
- if (m_parent)
- m_parent->Touch();
- else
- ::touchwin(stdscr);
+ height += GetActionsHeight();
+ return height;
}
- WINDOW *get() { return m_window; }
+ ScrollContext GetScrollContext() {
+ if (m_selection_type == SelectionType::Action)
+ return ScrollContext(GetContentHeight() - 1);
- operator WINDOW *() { return m_window; }
+ FieldDelegateSP &field = m_delegate_sp->GetField(m_selection_index);
+ ScrollContext context = field->FieldDelegateGetScrollContext();
- // Window drawing utilities
- void DrawTitleBox(const char *title, const char *bottom_message = nullptr) {
- attr_t attr = 0;
- if (IsActive())
- attr = A_BOLD | COLOR_PAIR(BlackOnWhite);
- else
- attr = 0;
- if (attr)
- AttributeOn(attr);
+ int offset = GetErrorHeight();
+ for (int i = 0; i < m_selection_index; i++) {
+ offset += m_delegate_sp->GetField(i)->FieldDelegateGetHeight();
+ }
+ context.Offset(offset);
- Box();
- MoveCursor(3, 0);
+ // If the context is touching the error, include the error in the context as
+ // well.
+ if (context.start == GetErrorHeight())
+ context.start = 0;
- if (title && title[0]) {
- PutChar('<');
- PutCString(title);
- PutChar('>');
+ return context;
+ }
+
+ void UpdateScrolling(DerivedWindow &surface) {
+ ScrollContext context = GetScrollContext();
+ int content_height = GetContentHeight();
+ int surface_height = surface.GetHeight();
+ int visible_height = std::min(content_height, surface_height);
+ int last_visible_line = m_first_visible_line + visible_height - 1;
+
+ // If the last visible line is bigger than the content, then it is invalid
+ // and needs to be set to the last line in the content. This can happen when
+ // a field has shrunk in height.
+ if (last_visible_line > content_height - 1) {
+ m_first_visible_line = content_height - visible_height;
}
- if (bottom_message && bottom_message[0]) {
- int bottom_message_length = strlen(bottom_message);
- int x = GetWidth() - 3 - (bottom_message_length + 2);
+ if (context.start < m_first_visible_line) {
+ m_first_visible_line = context.start;
+ return;
+ }
- if (x > 0) {
- MoveCursor(x, GetHeight() - 1);
- PutChar('[');
- PutCString(bottom_message);
- PutChar(']');
- } else {
- MoveCursor(1, GetHeight() - 1);
- PutChar('[');
- PutCStringTruncated(1, bottom_message);
- }
+ if (context.end > last_visible_line) {
+ m_first_visible_line = context.end - visible_height + 1;
}
- if (attr)
- AttributeOff(attr);
}
- virtual void Draw(bool force) {
- if (m_delegate_sp && m_delegate_sp->WindowDelegateDraw(*this, force))
+ void DrawError(SubPad &surface) {
+ if (!m_delegate_sp->HasError())
return;
-
- for (auto &subwindow_sp : m_subwindows)
- subwindow_sp->Draw(force);
+ surface.MoveCursor(0, 0);
+ surface.AttributeOn(COLOR_PAIR(RedOnBlack));
+ surface.PutChar(ACS_DIAMOND);
+ surface.PutChar(' ');
+ surface.PutCStringTruncated(1, m_delegate_sp->GetError().c_str());
+ surface.AttributeOff(COLOR_PAIR(RedOnBlack));
+
+ surface.MoveCursor(0, 1);
+ surface.HorizontalLine(surface.GetWidth());
}
- bool CreateHelpSubwindow() {
- if (m_delegate_sp) {
- const char *text = m_delegate_sp->WindowDelegateGetHelpText();
- KeyHelp *key_help = m_delegate_sp->WindowDelegateGetKeyHelp();
- if ((text && text[0]) || key_help) {
- std::unique_ptr<HelpDialogDelegate> help_delegate_up(
- new HelpDialogDelegate(text, key_help));
- const size_t num_lines = help_delegate_up->GetNumLines();
- const size_t max_length = help_delegate_up->GetMaxLineLength();
- Rect bounds = GetBounds();
- bounds.Inset(1, 1);
- if (max_length + 4 < static_cast<size_t>(bounds.size.width)) {
- bounds.origin.x += (bounds.size.width - max_length + 4) / 2;
- bounds.size.width = max_length + 4;
- } else {
- if (bounds.size.width > 100) {
- const int inset_w = bounds.size.width / 4;
- bounds.origin.x += inset_w;
- bounds.size.width -= 2 * inset_w;
- }
- }
-
- if (num_lines + 2 < static_cast<size_t>(bounds.size.height)) {
- bounds.origin.y += (bounds.size.height - num_lines + 2) / 2;
- bounds.size.height = num_lines + 2;
- } else {
- if (bounds.size.height > 100) {
- const int inset_h = bounds.size.height / 4;
- bounds.origin.y += inset_h;
- bounds.size.height -= 2 * inset_h;
- }
- }
- WindowSP help_window_sp;
- Window *parent_window = GetParent();
- if (parent_window)
- help_window_sp = parent_window->CreateSubWindow("Help", bounds, true);
- else
- help_window_sp = CreateSubWindow("Help", bounds, true);
- help_window_sp->SetDelegate(
- WindowDelegateSP(help_delegate_up.release()));
- return true;
- }
+ void DrawFields(SubPad &surface) {
+ int line = 0;
+ int width = surface.GetWidth();
+ bool a_field_is_selected = m_selection_type == SelectionType::Field;
+ for (int i = 0; i < m_delegate_sp->GetNumberOfFields(); i++) {
+ bool is_field_selected = a_field_is_selected && m_selection_index == i;
+ FieldDelegateSP &field = m_delegate_sp->GetField(i);
+ int height = field->FieldDelegateGetHeight();
+ Rect bounds = Rect(Point(0, line), Size(width, height));
+ SubPad field_surface = SubPad(surface, bounds);
+ field->FieldDelegateDraw(field_surface, is_field_selected);
+ line += height;
}
- return false;
}
- virtual HandleCharResult HandleChar(int key) {
- // Always check the active window first
- HandleCharResult result = eKeyNotHandled;
- WindowSP active_window_sp = GetActiveWindow();
- if (active_window_sp) {
- result = active_window_sp->HandleChar(key);
- if (result != eKeyNotHandled)
- return result;
+ void DrawActions(SubPad &surface) {
+ int number_of_actions = m_delegate_sp->GetNumberOfActions();
+ int width = surface.GetWidth() / number_of_actions;
+ bool an_action_is_selected = m_selection_type == SelectionType::Action;
+ int x = 0;
+ for (int i = 0; i < number_of_actions; i++) {
+ bool is_action_selected = an_action_is_selected && m_selection_index == i;
+ FormAction &action = m_delegate_sp->GetAction(i);
+ Rect bounds = Rect(Point(x, 0), Size(width, 1));
+ SubPad action_surface = SubPad(surface, bounds);
+ action.Draw(action_surface, is_action_selected);
+ x += width;
}
+ }
- if (m_delegate_sp) {
- result = m_delegate_sp->WindowDelegateHandleChar(*this, key);
- if (result != eKeyNotHandled)
- return result;
- }
+ void DrawElements(SubPad &surface) {
+ Rect frame = surface.GetFrame();
+ Rect fields_bounds, actions_bounds;
+ frame.HorizontalSplit(surface.GetHeight() - GetActionsHeight(),
+ fields_bounds, actions_bounds);
+ SubPad fields_surface = SubPad(surface, fields_bounds);
+ SubPad actions_surface = SubPad(surface, actions_bounds);
- // Then check for any windows that want any keys that weren't handled. This
- // is typically only for a menubar. Make a copy of the subwindows in case
- // any HandleChar() functions muck with the subwindows. If we don't do
- // this, we can crash when iterating over the subwindows.
- Windows subwindows(m_subwindows);
- for (auto subwindow_sp : subwindows) {
- if (!subwindow_sp->m_can_activate) {
- HandleCharResult result = subwindow_sp->HandleChar(key);
- if (result != eKeyNotHandled)
- return result;
- }
- }
+ DrawFields(fields_surface);
+ DrawActions(actions_surface);
+ }
- return eKeyNotHandled;
+ // Contents are first drawn on a pad. Then a subset of that pad is copied to
+ // the derived window starting at the first visible line. This essentially
+ // provides scrolling functionality.
+ void DrawContent(DerivedWindow &surface) {
+ UpdateScrolling(surface);
+
+ int width = surface.GetWidth();
+ int height = GetContentHeight();
+ Pad pad = Pad(Size(width, height));
+
+ Rect frame = pad.GetFrame();
+ Rect error_bounds, elements_bounds;
+ frame.HorizontalSplit(GetErrorHeight(), error_bounds, elements_bounds);
+ SubPad error_surface = SubPad(pad, error_bounds);
+ SubPad elements_surface = SubPad(pad, elements_bounds);
+
+ DrawError(error_surface);
+ DrawElements(elements_surface);
+
+ int copy_height = std::min(surface.GetHeight(), pad.GetHeight());
+ pad.CopyToSurface(surface, Point(0, m_first_visible_line), Point(),
+ Size(width, copy_height));
}
- WindowSP GetActiveWindow() {
- if (!m_subwindows.empty()) {
- if (m_curr_active_window_idx >= m_subwindows.size()) {
- if (m_prev_active_window_idx < m_subwindows.size()) {
- m_curr_active_window_idx = m_prev_active_window_idx;
- m_prev_active_window_idx = UINT32_MAX;
- } else if (IsActive()) {
- m_prev_active_window_idx = UINT32_MAX;
- m_curr_active_window_idx = UINT32_MAX;
+ bool WindowDelegateDraw(Window &window, bool force) override {
- // Find first window that wants to be active if this window is active
- const size_t num_subwindows = m_subwindows.size();
- for (size_t i = 0; i < num_subwindows; ++i) {
- if (m_subwindows[i]->GetCanBeActive()) {
- m_curr_active_window_idx = i;
- break;
- }
- }
- }
+ window.Erase();
+
+ window.DrawTitleBox(window.GetName(), "Press Esc to cancel");
+
+ Rect content_bounds = window.GetFrame();
+ content_bounds.Inset(2, 2);
+ DerivedWindow content_surface = DerivedWindow(window, content_bounds);
+
+ DrawContent(content_surface);
+ return true;
+ }
+
+ HandleCharResult SelecteNext(int key) {
+ if (m_selection_type == SelectionType::Action) {
+ if (m_selection_index < m_delegate_sp->GetNumberOfActions() - 1) {
+ m_selection_index++;
+ return eKeyHandled;
}
- if (m_curr_active_window_idx < m_subwindows.size())
- return m_subwindows[m_curr_active_window_idx];
+ m_selection_index = 0;
+ m_selection_type = SelectionType::Field;
+ FieldDelegateSP &next_field = m_delegate_sp->GetField(m_selection_index);
+ next_field->FieldDelegateSelectFirstElement();
+ return eKeyHandled;
}
- return WindowSP();
- }
- bool GetCanBeActive() const { return m_can_activate; }
+ FieldDelegateSP &field = m_delegate_sp->GetField(m_selection_index);
+ if (!field->FieldDelegateOnLastOrOnlyElement()) {
+ return field->FieldDelegateHandleChar(key);
+ }
- void SetCanBeActive(bool b) { m_can_activate = b; }
+ field->FieldDelegateExitCallback();
- void SetDelegate(const WindowDelegateSP &delegate_sp) {
- m_delegate_sp = delegate_sp;
- }
+ if (m_selection_index == m_delegate_sp->GetNumberOfFields() - 1) {
+ m_selection_type = SelectionType::Action;
+ m_selection_index = 0;
+ return eKeyHandled;
+ }
- Window *GetParent() const { return m_parent; }
+ m_selection_index++;
- bool IsActive() const {
- if (m_parent)
- return m_parent->GetActiveWindow().get() == this;
- else
- return true; // Top level window is always active
+ FieldDelegateSP &next_field = m_delegate_sp->GetField(m_selection_index);
+ next_field->FieldDelegateSelectFirstElement();
+
+ return eKeyHandled;
}
- void SelectNextWindowAsActive() {
- // Move active focus to next window
- const int num_subwindows = m_subwindows.size();
- int start_idx = 0;
- if (m_curr_active_window_idx != UINT32_MAX) {
- m_prev_active_window_idx = m_curr_active_window_idx;
- start_idx = m_curr_active_window_idx + 1;
- }
- for (int idx = start_idx; idx < num_subwindows; ++idx) {
- if (m_subwindows[idx]->GetCanBeActive()) {
- m_curr_active_window_idx = idx;
- return;
+ HandleCharResult SelectePrevious(int key) {
+ if (m_selection_type == SelectionType::Action) {
+ if (m_selection_index > 0) {
+ m_selection_index--;
+ return eKeyHandled;
}
+ m_selection_index = m_delegate_sp->GetNumberOfFields() - 1;
+ m_selection_type = SelectionType::Field;
+ FieldDelegateSP &previous_field =
+ m_delegate_sp->GetField(m_selection_index);
+ previous_field->FieldDelegateSelectLastElement();
+ return eKeyHandled;
}
- for (int idx = 0; idx < start_idx; ++idx) {
- if (m_subwindows[idx]->GetCanBeActive()) {
- m_curr_active_window_idx = idx;
- break;
- }
+
+ FieldDelegateSP &field = m_delegate_sp->GetField(m_selection_index);
+ if (!field->FieldDelegateOnFirstOrOnlyElement()) {
+ return field->FieldDelegateHandleChar(key);
}
- }
- void SelectPreviousWindowAsActive() {
- // Move active focus to previous window
- const int num_subwindows = m_subwindows.size();
- int start_idx = num_subwindows - 1;
- if (m_curr_active_window_idx != UINT32_MAX) {
- m_prev_active_window_idx = m_curr_active_window_idx;
- start_idx = m_curr_active_window_idx - 1;
+ field->FieldDelegateExitCallback();
+
+ if (m_selection_index == 0) {
+ m_selection_type = SelectionType::Action;
+ m_selection_index = m_delegate_sp->GetNumberOfActions() - 1;
+ return eKeyHandled;
}
- for (int idx = start_idx; idx >= 0; --idx) {
- if (m_subwindows[idx]->GetCanBeActive()) {
- m_curr_active_window_idx = idx;
- return;
+
+ m_selection_index--;
+
+ FieldDelegateSP &previous_field =
+ m_delegate_sp->GetField(m_selection_index);
+ previous_field->FieldDelegateSelectLastElement();
+
+ return eKeyHandled;
+ }
+
+ void ExecuteAction(Window &window) {
+ FormAction &action = m_delegate_sp->GetAction(m_selection_index);
+ action.Execute(window);
+ m_first_visible_line = 0;
+ m_selection_index = 0;
+ m_selection_type = SelectionType::Field;
+ }
+
+ HandleCharResult WindowDelegateHandleChar(Window &window, int key) override {
+ switch (key) {
+ case '\r':
+ case '\n':
+ case KEY_ENTER:
+ if (m_selection_type == SelectionType::Action) {
+ ExecuteAction(window);
+ return eKeyHandled;
}
+ break;
+ case '\t':
+ return SelecteNext(key);
+ case KEY_SHIFT_TAB:
+ return SelectePrevious(key);
+ case KEY_ESCAPE:
+ window.GetParent()->RemoveSubWindow(&window);
+ return eKeyHandled;
+ default:
+ break;
}
- for (int idx = num_subwindows - 1; idx > start_idx; --idx) {
- if (m_subwindows[idx]->GetCanBeActive()) {
- m_curr_active_window_idx = idx;
- break;
- }
+
+ // If the key wasn't handled and one of the fields is selected, pass the key
+ // to that field.
+ if (m_selection_type == SelectionType::Field) {
+ FieldDelegateSP &field = m_delegate_sp->GetField(m_selection_index);
+ return field->FieldDelegateHandleChar(key);
}
- }
- const char *GetName() const { return m_name.c_str(); }
+ return eKeyNotHandled;
+ }
protected:
- std::string m_name;
- WINDOW *m_window;
- PANEL *m_panel;
- Window *m_parent;
- Windows m_subwindows;
- WindowDelegateSP m_delegate_sp;
- uint32_t m_curr_active_window_idx;
- uint32_t m_prev_active_window_idx;
- bool m_delete;
- bool m_needs_update;
- bool m_can_activate;
- bool m_is_subwin;
-
-private:
- Window(const Window &) = delete;
- const Window &operator=(const Window &) = delete;
+ FormDelegateSP m_delegate_sp;
+ // The index of the currently selected SelectionType.
+ int m_selection_index;
+ // See SelectionType class enum.
+ SelectionType m_selection_type;
+ // The first visible line from the pad.
+ int m_first_visible_line;
};
class MenuDelegate {
window.SelectNextWindowAsActive();
return eKeyHandled;
- case KEY_BTAB:
+ case KEY_SHIFT_TAB:
window.SelectPreviousWindowAsActive();
return eKeyHandled;
init_pair(17, COLOR_BLACK, COLOR_WHITE);
init_pair(18, COLOR_MAGENTA, COLOR_WHITE);
static_assert(LastColorPairIndex == 18, "Color indexes do not match.");
+
+ define_key("\033[Z", KEY_SHIFT_TAB);
}
}