| //===-- IOHandlerCursesGUI.cpp --------------------------------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "lldb/Core/IOHandlerCursesGUI.h" |
| #include "lldb/Host/Config.h" |
| |
| #if LLDB_ENABLE_CURSES |
| #if CURSES_HAVE_NCURSES_CURSES_H |
| #include <ncurses/curses.h> |
| #include <ncurses/panel.h> |
| #else |
| #include <curses.h> |
| #include <panel.h> |
| #endif |
| #endif |
| |
| #if defined(__APPLE__) |
| #include <deque> |
| #endif |
| #include <string> |
| |
| #include "lldb/Core/Debugger.h" |
| #include "lldb/Core/StreamFile.h" |
| #include "lldb/Core/ValueObjectUpdater.h" |
| #include "lldb/Host/File.h" |
| #include "lldb/Utility/Predicate.h" |
| #include "lldb/Utility/Status.h" |
| #include "lldb/Utility/StreamString.h" |
| #include "lldb/Utility/StringList.h" |
| #include "lldb/lldb-forward.h" |
| |
| #include "lldb/Interpreter/CommandCompletions.h" |
| #include "lldb/Interpreter/CommandInterpreter.h" |
| #include "lldb/Interpreter/OptionGroupPlatform.h" |
| |
| #if LLDB_ENABLE_CURSES |
| #include "lldb/Breakpoint/BreakpointLocation.h" |
| #include "lldb/Core/Module.h" |
| #include "lldb/Core/PluginManager.h" |
| #include "lldb/Core/ValueObject.h" |
| #include "lldb/Core/ValueObjectRegister.h" |
| #include "lldb/Symbol/Block.h" |
| #include "lldb/Symbol/CompileUnit.h" |
| #include "lldb/Symbol/Function.h" |
| #include "lldb/Symbol/Symbol.h" |
| #include "lldb/Symbol/VariableList.h" |
| #include "lldb/Target/Process.h" |
| #include "lldb/Target/RegisterContext.h" |
| #include "lldb/Target/StackFrame.h" |
| #include "lldb/Target/StopInfo.h" |
| #include "lldb/Target/Target.h" |
| #include "lldb/Target/Thread.h" |
| #include "lldb/Utility/State.h" |
| #endif |
| |
| #include "llvm/ADT/StringRef.h" |
| |
| #ifdef _WIN32 |
| #include "lldb/Host/windows/windows.h" |
| #endif |
| |
| #include <memory> |
| #include <mutex> |
| |
| #include <cassert> |
| #include <cctype> |
| #include <cerrno> |
| #include <cstdint> |
| #include <cstdio> |
| #include <cstring> |
| #include <functional> |
| #include <type_traits> |
| |
| using namespace lldb; |
| using namespace lldb_private; |
| using llvm::None; |
| using llvm::Optional; |
| using llvm::StringRef; |
| |
| // we may want curses to be disabled for some builds for instance, windows |
| #if LLDB_ENABLE_CURSES |
| |
| #define KEY_CTRL_A 1 |
| #define KEY_CTRL_E 5 |
| #define KEY_CTRL_K 11 |
| #define KEY_RETURN 10 |
| #define KEY_ESCAPE 27 |
| #define KEY_DELETE 127 |
| |
| #define KEY_SHIFT_TAB (KEY_MAX + 1) |
| #define KEY_ALT_ENTER (KEY_MAX + 2) |
| |
| namespace curses { |
| class Menu; |
| class MenuDelegate; |
| class Window; |
| class WindowDelegate; |
| typedef std::shared_ptr<Menu> MenuSP; |
| typedef std::shared_ptr<MenuDelegate> MenuDelegateSP; |
| typedef std::shared_ptr<Window> WindowSP; |
| typedef std::shared_ptr<WindowDelegate> WindowDelegateSP; |
| typedef std::vector<MenuSP> Menus; |
| typedef std::vector<WindowSP> Windows; |
| typedef std::vector<WindowDelegateSP> WindowDelegates; |
| |
| #if 0 |
| type summary add -s "x=${var.x}, y=${var.y}" curses::Point |
| type summary add -s "w=${var.width}, h=${var.height}" curses::Size |
| type summary add -s "${var.origin%S} ${var.size%S}" curses::Rect |
| #endif |
| |
| struct Point { |
| int x; |
| int y; |
| |
| Point(int _x = 0, int _y = 0) : x(_x), y(_y) {} |
| |
| void Clear() { |
| x = 0; |
| y = 0; |
| } |
| |
| Point &operator+=(const Point &rhs) { |
| x += rhs.x; |
| y += rhs.y; |
| return *this; |
| } |
| |
| void Dump() { printf("(x=%i, y=%i)\n", x, y); } |
| }; |
| |
| bool operator==(const Point &lhs, const Point &rhs) { |
| return lhs.x == rhs.x && lhs.y == rhs.y; |
| } |
| |
| bool operator!=(const Point &lhs, const Point &rhs) { |
| return lhs.x != rhs.x || lhs.y != rhs.y; |
| } |
| |
| struct Size { |
| int width; |
| int height; |
| Size(int w = 0, int h = 0) : width(w), height(h) {} |
| |
| void Clear() { |
| width = 0; |
| height = 0; |
| } |
| |
| void Dump() { printf("(w=%i, h=%i)\n", width, height); } |
| }; |
| |
| bool operator==(const Size &lhs, const Size &rhs) { |
| return lhs.width == rhs.width && lhs.height == rhs.height; |
| } |
| |
| bool operator!=(const Size &lhs, const Size &rhs) { |
| return lhs.width != rhs.width || lhs.height != rhs.height; |
| } |
| |
| struct Rect { |
| Point origin; |
| Size size; |
| |
| Rect() : origin(), size() {} |
| |
| Rect(const Point &p, const Size &s) : origin(p), size(s) {} |
| |
| void Clear() { |
| origin.Clear(); |
| size.Clear(); |
| } |
| |
| void Dump() { |
| printf("(x=%i, y=%i), w=%i, h=%i)\n", origin.x, origin.y, size.width, |
| size.height); |
| } |
| |
| void Inset(int w, int h) { |
| if (size.width > w * 2) |
| size.width -= w * 2; |
| origin.x += w; |
| |
| if (size.height > h * 2) |
| size.height -= h * 2; |
| origin.y += h; |
| } |
| |
| // Return a status bar rectangle which is the last line of this rectangle. |
| // This rectangle will be modified to not include the status bar area. |
| Rect MakeStatusBar() { |
| Rect status_bar; |
| if (size.height > 1) { |
| status_bar.origin.x = origin.x; |
| status_bar.origin.y = size.height; |
| status_bar.size.width = size.width; |
| status_bar.size.height = 1; |
| --size.height; |
| } |
| return status_bar; |
| } |
| |
| // Return a menubar rectangle which is the first line of this rectangle. This |
| // rectangle will be modified to not include the menubar area. |
| Rect MakeMenuBar() { |
| Rect menubar; |
| if (size.height > 1) { |
| menubar.origin.x = origin.x; |
| menubar.origin.y = origin.y; |
| menubar.size.width = size.width; |
| menubar.size.height = 1; |
| ++origin.y; |
| --size.height; |
| } |
| return menubar; |
| } |
| |
| void HorizontalSplitPercentage(float top_percentage, Rect &top, |
| Rect &bottom) const { |
| float top_height = top_percentage * size.height; |
| HorizontalSplit(top_height, top, bottom); |
| } |
| |
| void HorizontalSplit(int top_height, Rect &top, Rect &bottom) const { |
| top = *this; |
| if (top_height < size.height) { |
| top.size.height = top_height; |
| bottom.origin.x = origin.x; |
| bottom.origin.y = origin.y + top.size.height; |
| bottom.size.width = size.width; |
| bottom.size.height = size.height - top.size.height; |
| } else { |
| bottom.Clear(); |
| } |
| } |
| |
| void VerticalSplitPercentage(float left_percentage, Rect &left, |
| Rect &right) const { |
| float left_width = left_percentage * size.width; |
| VerticalSplit(left_width, left, right); |
| } |
| |
| void VerticalSplit(int left_width, Rect &left, Rect &right) const { |
| left = *this; |
| if (left_width < size.width) { |
| left.size.width = left_width; |
| right.origin.x = origin.x + left.size.width; |
| right.origin.y = origin.y; |
| right.size.width = size.width - left.size.width; |
| right.size.height = size.height; |
| } else { |
| right.Clear(); |
| } |
| } |
| }; |
| |
| bool operator==(const Rect &lhs, const Rect &rhs) { |
| return lhs.origin == rhs.origin && lhs.size == rhs.size; |
| } |
| |
| bool operator!=(const Rect &lhs, const Rect &rhs) { |
| return lhs.origin != rhs.origin || lhs.size != rhs.size; |
| } |
| |
| enum HandleCharResult { |
| eKeyNotHandled = 0, |
| eKeyHandled = 1, |
| eQuitApplication = 2 |
| }; |
| |
| enum class MenuActionResult { |
| Handled, |
| NotHandled, |
| Quit // Exit all menus and quit |
| }; |
| |
| struct KeyHelp { |
| int ch; |
| const char *description; |
| }; |
| |
| // COLOR_PAIR index names |
| enum { |
| // First 16 colors are 8 black background and 8 blue background colors, |
| // needed by OutputColoredStringTruncated(). |
| BlackOnBlack = 1, |
| RedOnBlack, |
| GreenOnBlack, |
| YellowOnBlack, |
| BlueOnBlack, |
| MagentaOnBlack, |
| CyanOnBlack, |
| WhiteOnBlack, |
| BlackOnBlue, |
| RedOnBlue, |
| GreenOnBlue, |
| YellowOnBlue, |
| BlueOnBlue, |
| MagentaOnBlue, |
| CyanOnBlue, |
| WhiteOnBlue, |
| // Other colors, as needed. |
| BlackOnWhite, |
| MagentaOnWhite, |
| LastColorPairIndex = MagentaOnWhite |
| }; |
| |
| class WindowDelegate { |
| public: |
| virtual ~WindowDelegate() = default; |
| |
| virtual bool WindowDelegateDraw(Window &window, bool force) { |
| return false; // Drawing not handled |
| } |
| |
| virtual HandleCharResult WindowDelegateHandleChar(Window &window, int key) { |
| return eKeyNotHandled; |
| } |
| |
| virtual const char *WindowDelegateGetHelpText() { return nullptr; } |
| |
| virtual KeyHelp *WindowDelegateGetKeyHelp() { return nullptr; } |
| }; |
| |
| class HelpDialogDelegate : public WindowDelegate { |
| public: |
| HelpDialogDelegate(const char *text, KeyHelp *key_help_array); |
| |
| ~HelpDialogDelegate() override; |
| |
| bool WindowDelegateDraw(Window &window, bool force) override; |
| |
| HandleCharResult WindowDelegateHandleChar(Window &window, int key) override; |
| |
| size_t GetNumLines() const { return m_text.GetSize(); } |
| |
| size_t GetMaxLineLength() const { return m_text.GetMaxStringLength(); } |
| |
| protected: |
| StringList m_text; |
| int m_first_visible_line; |
| }; |
| |
| // 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 or a Pad. |
| class Surface { |
| public: |
| enum class Type { Window, Pad }; |
| |
| Surface(Surface::Type type) : m_type(type), m_window(nullptr) {} |
| |
| WINDOW *get() { return m_window; } |
| |
| operator WINDOW *() { return m_window; } |
| |
| Surface SubSurface(Rect bounds) { |
| Surface subSurface(m_type); |
| if (m_type == Type::Pad) |
| subSurface.m_window = |
| ::subpad(m_window, bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x); |
| else |
| subSurface.m_window = |
| ::derwin(m_window, bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x); |
| return subSurface; |
| } |
| |
| // 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); |
| } |
| |
| 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); } |
| |
| int GetMaxX() const { return getmaxx(m_window); } |
| int GetMaxY() const { return getmaxy(m_window); } |
| int GetWidth() const { return GetMaxX(); } |
| int GetHeight() const { return GetMaxY(); } |
| 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) { |
| bytes_left -= right_pad; |
| ::waddnstr(m_window, s, len < 0 ? bytes_left : std::min(bytes_left, len)); |
| } |
| } |
| |
| void Printf(const char *format, ...) __attribute__((format(printf, 2, 3))) { |
| va_list args; |
| va_start(args, format); |
| vw_printw(m_window, format, args); |
| va_end(args); |
| } |
| |
| void PrintfTruncated(int right_pad, const char *format, ...) |
| __attribute__((format(printf, 3, 4))) { |
| va_list args; |
| va_start(args, format); |
| StreamString strm; |
| strm.PrintfVarArg(format, args); |
| va_end(args); |
| PutCStringTruncated(right_pad, strm.GetData()); |
| } |
| |
| 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 |
| // how we get source lines from the Highligher class. Read the line and |
| // convert color escape sequences to curses color attributes. Use |
| // first_skip_count to skip leading visible characters. Returns false if all |
| // visible characters were skipped due to first_skip_count. |
| bool OutputColoredStringTruncated(int right_pad, StringRef string, |
| size_t skip_first_count, |
| bool use_blue_background) { |
| attr_t saved_attr; |
| short saved_pair; |
| bool result = false; |
| wattr_get(m_window, &saved_attr, &saved_pair, nullptr); |
| if (use_blue_background) |
| ::wattron(m_window, COLOR_PAIR(WhiteOnBlue)); |
| while (!string.empty()) { |
| size_t esc_pos = string.find('\x1b'); |
| if (esc_pos == StringRef::npos) { |
| string = string.substr(skip_first_count); |
| if (!string.empty()) { |
| PutCStringTruncated(right_pad, string.data(), string.size()); |
| result = true; |
| } |
| break; |
| } |
| if (esc_pos > 0) { |
| if (skip_first_count > 0) { |
| int skip = std::min(esc_pos, skip_first_count); |
| string = string.substr(skip); |
| skip_first_count -= skip; |
| esc_pos -= skip; |
| } |
| if (esc_pos > 0) { |
| PutCStringTruncated(right_pad, string.data(), esc_pos); |
| result = true; |
| string = string.drop_front(esc_pos); |
| } |
| } |
| bool consumed = string.consume_front("\x1b"); |
| assert(consumed); |
| UNUSED_IF_ASSERT_DISABLED(consumed); |
| // This is written to match our Highlighter classes, which seem to |
| // generate only foreground color escape sequences. If necessary, this |
| // will need to be extended. |
| if (!string.consume_front("[")) { |
| llvm::errs() << "Missing '[' in color escape sequence.\n"; |
| continue; |
| } |
| // Only 8 basic foreground colors and reset, our Highlighter doesn't use |
| // anything else. |
| int value; |
| if (!!string.consumeInteger(10, value) || // Returns false on success. |
| !(value == 0 || (value >= 30 && value <= 37))) { |
| llvm::errs() << "No valid color code in color escape sequence.\n"; |
| continue; |
| } |
| if (!string.consume_front("m")) { |
| llvm::errs() << "Missing 'm' in color escape sequence.\n"; |
| continue; |
| } |
| if (value == 0) { // Reset. |
| wattr_set(m_window, saved_attr, saved_pair, nullptr); |
| if (use_blue_background) |
| ::wattron(m_window, COLOR_PAIR(WhiteOnBlue)); |
| } else { |
| // Mapped directly to first 16 color pairs (black/blue background). |
| ::wattron(m_window, |
| COLOR_PAIR(value - 30 + 1 + (use_blue_background ? 8 : 0))); |
| } |
| } |
| wattr_set(m_window, saved_attr, saved_pair, nullptr); |
| return result; |
| } |
| |
| protected: |
| Type m_type; |
| WINDOW *m_window; |
| }; |
| |
| class Pad : public Surface { |
| public: |
| Pad(Size size) : Surface(Surface::Type::Pad) { |
| m_window = ::newpad(size.height, size.width); |
| } |
| |
| ~Pad() { ::delwin(m_window); } |
| }; |
| |
| class Window : public Surface { |
| public: |
| Window(const char *name) |
| : Surface(Surface::Type::Window), 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) |
| : Surface(Surface::Type::Window), 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) |
| : Surface(Surface::Type::Window), 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()); } |
| |
| Rect GetCenteredRect(int width, int height) { |
| Size size = GetSize(); |
| width = std::min(size.width, width); |
| height = std::min(size.height, height); |
| int x = (size.width - width) / 2; |
| int y = (size.height - height) / 2; |
| return Rect(Point(x, y), Size(width, height)); |
| } |
| |
| 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) |
| m_parent->Touch(); |
| } |
| |
| WindowSP CreateSubWindow(const char *name, const Rect &bounds, |
| bool make_active) { |
| auto get_window = [this, &bounds]() { |
| return m_window |
| ? ::subwin(m_window, bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x) |
| : ::newwin(bounds.size.height, bounds.size.width, |
| bounds.origin.y, bounds.origin.x); |
| }; |
| WindowSP subwindow_sp = std::make_shared<Window>(name, get_window(), true); |
| subwindow_sp->m_is_subwin = subwindow_sp.operator bool(); |
| subwindow_sp->m_parent = this; |
| if (make_active) { |
| m_prev_active_window_idx = m_curr_active_window_idx; |
| m_curr_active_window_idx = m_subwindows.size(); |
| } |
| m_subwindows.push_back(subwindow_sp); |
| ::top_panel(subwindow_sp->m_panel); |
| m_needs_update = true; |
| return subwindow_sp; |
| } |
| |
| bool RemoveSubWindow(Window *window) { |
| Windows::iterator pos, end = m_subwindows.end(); |
| size_t i = 0; |
| for (pos = m_subwindows.begin(); pos != end; ++pos, ++i) { |
| if ((*pos).get() == window) { |
| if (m_prev_active_window_idx == i) |
| m_prev_active_window_idx = UINT32_MAX; |
| else if (m_prev_active_window_idx != UINT32_MAX && |
| m_prev_active_window_idx > i) |
| --m_prev_active_window_idx; |
| |
| if (m_curr_active_window_idx == i) |
| m_curr_active_window_idx = UINT32_MAX; |
| else if (m_curr_active_window_idx != UINT32_MAX && |
| m_curr_active_window_idx > i) |
| --m_curr_active_window_idx; |
| window->Erase(); |
| m_subwindows.erase(pos); |
| m_needs_update = true; |
| if (m_parent) |
| m_parent->Touch(); |
| else |
| ::touchwin(stdscr); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| WindowSP FindSubWindow(const char *name) { |
| Windows::iterator pos, end = m_subwindows.end(); |
| size_t i = 0; |
| for (pos = m_subwindows.begin(); pos != end; ++pos, ++i) { |
| if ((*pos)->m_name == name) |
| return *pos; |
| } |
| 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(); |
| } |
| 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; |
| }; |
| |
| ///////// |
| // 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(Surface &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; } |
| |
| // Returns true if the field has an error, false otherwise. |
| virtual bool FieldDelegateHasError() { return false; } |
| |
| bool FieldDelegateIsVisible() { return m_is_visible; } |
| |
| void FieldDelegateHide() { m_is_visible = false; } |
| |
| void FieldDelegateShow() { m_is_visible = true; } |
| |
| protected: |
| bool m_is_visible = true; |
| }; |
| |
| typedef std::unique_ptr<FieldDelegate> FieldDelegateUP; |
| |
| class TextFieldDelegate : public FieldDelegate { |
| public: |
| TextFieldDelegate(const char *label, const char *content, bool required) |
| : m_label(label), m_required(required), 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 (FieldDelegateHasError()) |
| 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(Surface &surface, bool is_selected) { |
| UpdateScrolling(surface.GetWidth()); |
| |
| surface.MoveCursor(0, 0); |
| const char *text = m_content.c_str() + m_first_visibile_char; |
| surface.PutCString(text, 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(Surface &surface, bool is_selected) { |
| surface.TitledBox(m_label.c_str()); |
| |
| Rect content_bounds = surface.GetFrame(); |
| content_bounds.Inset(1, 1); |
| Surface content_surface = surface.SubSurface(content_bounds); |
| |
| DrawContent(content_surface, is_selected); |
| } |
| |
| void DrawError(Surface &surface) { |
| if (!FieldDelegateHasError()) |
| 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(Surface &surface, bool is_selected) override { |
| Rect frame = surface.GetFrame(); |
| Rect field_bounds, error_bounds; |
| frame.HorizontalSplit(GetFieldHeight(), field_bounds, error_bounds); |
| Surface field_surface = surface.SubSurface(field_bounds); |
| Surface error_surface = surface.SubSurface(error_bounds); |
| |
| DrawField(field_surface, is_selected); |
| DrawError(error_surface); |
| } |
| |
| // Get the position of the last visible character. |
| int GetLastVisibleCharPosition(int width) { |
| int position = m_first_visibile_char + width - 1; |
| return std::min(position, GetContentLength()); |
| } |
| |
| void UpdateScrolling(int width) { |
| if (m_cursor_position < m_first_visibile_char) { |
| m_first_visibile_char = m_cursor_position; |
| return; |
| } |
| |
| if (m_cursor_position > GetLastVisibleCharPosition(width)) |
| m_first_visibile_char = m_cursor_position - (width - 1); |
| } |
| |
| // 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--; |
| } |
| |
| void MoveCursorToStart() { m_cursor_position = 0; } |
| |
| void MoveCursorToEnd() { m_cursor_position = GetContentLength(); } |
| |
| void ScrollLeft() { |
| if (m_first_visibile_char > 0) |
| m_first_visibile_char--; |
| } |
| |
| // Insert a character at the current cursor position and advance the cursor |
| // position. |
| void InsertChar(char character) { |
| m_content.insert(m_cursor_position, 1, character); |
| m_cursor_position++; |
| ClearError(); |
| } |
| |
| // Remove the character before the cursor position, retreat the cursor |
| // position, and scroll left. |
| void RemovePreviousChar() { |
| if (m_cursor_position == 0) |
| return; |
| |
| m_content.erase(m_cursor_position - 1, 1); |
| m_cursor_position--; |
| ScrollLeft(); |
| ClearError(); |
| } |
| |
| // Remove the character after the cursor position. |
| void RemoveNextChar() { |
| if (m_cursor_position == GetContentLength()) |
| return; |
| |
| m_content.erase(m_cursor_position, 1); |
| ClearError(); |
| } |
| |
| // Clear characters from the current cursor position to the end. |
| void ClearToEnd() { |
| m_content.erase(m_cursor_position); |
| ClearError(); |
| } |
| |
| void Clear() { |
| m_content.clear(); |
| m_cursor_position = 0; |
| ClearError(); |
| } |
| |
| // True if the key represents a char that can be inserted in the field |
| // content, false otherwise. |
| virtual bool IsAcceptableChar(int key) { |
| // The behavior of isprint is undefined when the value is not representable |
| // as an unsigned char. So explicitly check for non-ascii key codes. |
| if (key > 127) |
| return false; |
| return isprint(key); |
| } |
| |
| HandleCharResult FieldDelegateHandleChar(int key) override { |
| if (IsAcceptableChar(key)) { |
| ClearError(); |
| InsertChar((char)key); |
| return eKeyHandled; |
| } |
| |
| switch (key) { |
| case KEY_HOME: |
| case KEY_CTRL_A: |
| MoveCursorToStart(); |
| return eKeyHandled; |
| case KEY_END: |
| case KEY_CTRL_E: |
| MoveCursorToEnd(); |
| return eKeyHandled; |
| case KEY_RIGHT: |
| case KEY_SF: |
| MoveCursorRight(); |
| return eKeyHandled; |
| case KEY_LEFT: |
| case KEY_SR: |
| MoveCursorLeft(); |
| return eKeyHandled; |
| case KEY_BACKSPACE: |
| case KEY_DELETE: |
| RemovePreviousChar(); |
| return eKeyHandled; |
| case KEY_DC: |
| RemoveNextChar(); |
| return eKeyHandled; |
| case KEY_EOL: |
| case KEY_CTRL_K: |
| ClearToEnd(); |
| return eKeyHandled; |
| case KEY_DL: |
| case KEY_CLEAR: |
| Clear(); |
| return eKeyHandled; |
| default: |
| break; |
| } |
| return eKeyNotHandled; |
| } |
| |
| bool FieldDelegateHasError() override { return !m_error.empty(); } |
| |
| void FieldDelegateExitCallback() override { |
| if (!IsSpecified() && m_required) |
| SetError("This field is required!"); |
| } |
| |
| bool IsSpecified() { return !m_content.empty(); } |
| |
| void ClearError() { m_error.clear(); } |
| |
| const std::string &GetError() { return m_error; } |
| |
| void SetError(const char *error) { m_error = error; } |
| |
| const std::string &GetText() { return m_content; } |
| |
| void SetText(const char *text) { |
| if (text == nullptr) { |
| m_content.clear(); |
| return; |
| } |
| m_content = text; |
| } |
| |
| protected: |
| std::string m_label; |
| bool m_required; |
| // 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; |
| // 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, bool required) |
| : TextFieldDelegate(label, std::to_string(content).c_str(), required) {} |
| |
| // 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, |
| bool required) |
| : TextFieldDelegate(label, content, required), |
| m_need_to_exist(need_to_exist) {} |
| |
| void FieldDelegateExitCallback() override { |
| TextFieldDelegate::FieldDelegateExitCallback(); |
| if (!IsSpecified()) |
| return; |
| |
| if (!m_need_to_exist) |
| return; |
| |
| FileSpec file = GetResolvedFileSpec(); |
| if (!FileSystem::Instance().Exists(file)) { |
| SetError("File doesn't exist!"); |
| return; |
| } |
| if (FileSystem::Instance().IsDirectory(file)) { |
| SetError("Not a file!"); |
| return; |
| } |
| } |
| |
| FileSpec GetFileSpec() { |
| FileSpec file_spec(GetPath()); |
| return file_spec; |
| } |
| |
| FileSpec GetResolvedFileSpec() { |
| FileSpec file_spec(GetPath()); |
| FileSystem::Instance().Resolve(file_spec); |
| return file_spec; |
| } |
| |
| 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, bool required) |
| : TextFieldDelegate(label, content, required), |
| m_need_to_exist(need_to_exist) {} |
| |
| void FieldDelegateExitCallback() override { |
| TextFieldDelegate::FieldDelegateExitCallback(); |
| if (!IsSpecified()) |
| return; |
| |
| if (!m_need_to_exist) |
| return; |
| |
| FileSpec file = GetResolvedFileSpec(); |
| if (!FileSystem::Instance().Exists(file)) { |
| SetError("Directory doesn't exist!"); |
| return; |
| } |
| if (!FileSystem::Instance().IsDirectory(file)) { |
| SetError("Not a directory!"); |
| return; |
| } |
| } |
| |
| FileSpec GetFileSpec() { |
| FileSpec file_spec(GetPath()); |
| return file_spec; |
| } |
| |
| FileSpec GetResolvedFileSpec() { |
| FileSpec file_spec(GetPath()); |
| FileSystem::Instance().Resolve(file_spec); |
| return file_spec; |
| } |
| |
| const std::string &GetPath() { return m_content; } |
| |
| protected: |
| bool m_need_to_exist; |
| }; |
| |
| class ArchFieldDelegate : public TextFieldDelegate { |
| public: |
| ArchFieldDelegate(const char *label, const char *content, bool required) |
| : TextFieldDelegate(label, content, required) {} |
| |
| void FieldDelegateExitCallback() override { |
| TextFieldDelegate::FieldDelegateExitCallback(); |
| if (!IsSpecified()) |
| return; |
| |
| if (!GetArchSpec().IsValid()) |
| SetError("Not a valid arch!"); |
| } |
| |
| const std::string &GetArchString() { return m_content; } |
| |
| ArchSpec GetArchSpec() { return ArchSpec(GetArchString()); } |
| }; |
| |
| 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(Surface &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(Surface &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(Surface &surface, bool is_selected) override { |
| UpdateScrolling(); |
| |
| surface.TitledBox(m_label.c_str()); |
| |
| Rect content_bounds = surface.GetFrame(); |
| content_bounds.Inset(1, 1); |
| Surface content_surface = surface.SubSurface(content_bounds); |
| |
| DrawContent(content_surface, is_selected); |
| } |
| |
| void SelectPrevious() { |
| if (m_choice > 0) |
| m_choice--; |
| } |
| |
| void SelectNext() { |
| if (m_choice < GetNumberOfChoices() - 1) |
| m_choice++; |
| } |
| |
| void UpdateScrolling() { |
| if (m_choice > GetLastVisibleChoice()) { |
| m_first_visibile_choice = m_choice - (m_number_of_visible_choices - 1); |
| return; |
| } |
| |
| if (m_choice < m_first_visibile_choice) |
| m_first_visibile_choice = m_choice; |
| } |
| |
| HandleCharResult FieldDelegateHandleChar(int key) override { |
| switch (key) { |
| case KEY_UP: |
| SelectPrevious(); |
| return eKeyHandled; |
| case KEY_DOWN: |
| SelectNext(); |
| 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; } |
| |
| void SetChoice(const std::string &choice) { |
| for (int i = 0; i < GetNumberOfChoices(); i++) { |
| if (choice == m_choices[i]) { |
| m_choice = i; |
| return; |
| } |
| } |
| } |
| |
| 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; |
| }; |
| |
| class PlatformPluginFieldDelegate : public ChoicesFieldDelegate { |
| public: |
| PlatformPluginFieldDelegate(Debugger &debugger) |
| : ChoicesFieldDelegate("Platform Plugin", 3, GetPossiblePluginNames()) { |
| PlatformSP platform_sp = debugger.GetPlatformList().GetSelectedPlatform(); |
| if (platform_sp) |
| SetChoice(platform_sp->GetName().AsCString()); |
| } |
| |
| std::vector<std::string> GetPossiblePluginNames() { |
| std::vector<std::string> names; |
| size_t i = 0; |
| for (llvm::StringRef name = |
| PluginManager::GetPlatformPluginNameAtIndex(i++); |
| !name.empty(); name = PluginManager::GetProcessPluginNameAtIndex(i++)) |
| names.push_back(name.str()); |
| return names; |
| } |
| |
| std::string GetPluginName() { |
| std::string plugin_name = GetChoiceContent(); |
| return plugin_name; |
| } |
| }; |
| |
| class ProcessPluginFieldDelegate : public ChoicesFieldDelegate { |
| public: |
| ProcessPluginFieldDelegate() |
| : ChoicesFieldDelegate("Process Plugin", 3, GetPossiblePluginNames()) {} |
| |
| std::vector<std::string> GetPossiblePluginNames() { |
| std::vector<std::string> names; |
| names.push_back("<default>"); |
| |
| size_t i = 0; |
| for (llvm::StringRef name = PluginManager::GetProcessPluginNameAtIndex(i++); |
| !name.empty(); name = PluginManager::GetProcessPluginNameAtIndex(i++)) |
| names.push_back(name.str()); |
| return names; |
| } |
| |
| std::string GetPluginName() { |
| std::string plugin_name = GetChoiceContent(); |
| if (plugin_name == "<default>") |
| return ""; |
| return plugin_name; |
| } |
| }; |
| |
| class LazyBooleanFieldDelegate : public ChoicesFieldDelegate { |
| public: |
| LazyBooleanFieldDelegate(const char *label, const char *calculate_label) |
| : ChoicesFieldDelegate(label, 3, GetPossibleOptions(calculate_label)) {} |
| |
| static constexpr const char *kNo = "No"; |
| static constexpr const char *kYes = "Yes"; |
| |
| std::vector<std::string> GetPossibleOptions(const char *calculate_label) { |
| std::vector<std::string> options; |
| options.push_back(calculate_label); |
| options.push_back(kYes); |
| options.push_back(kNo); |
| return options; |
| } |
| |
| LazyBool GetLazyBoolean() { |
| std::string choice = GetChoiceContent(); |
| if (choice == kNo) |
| return eLazyBoolNo; |
| else if (choice == kYes) |
| return eLazyBoolYes; |
| else |
| return eLazyBoolCalculate; |
| } |
| }; |
| |
| 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(Surface &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(Surface &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); |
| Surface field_surface = surface.SubSurface(field_bounds); |
| Surface remove_button_surface = surface.SubSurface(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(Surface &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(Surface &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); |
| Surface fields_surface = surface.SubSurface(fields_bounds); |
| Surface new_button_surface = surface.SubSurface(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 SelectNext(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; |
| } |
| |
| // If the last element of the field is selected and it didn't handle the key. |
| // Select the next field or new button if the selected field is the last one. |
| HandleCharResult SelectNextInList(int key) { |
| assert(m_selection_type == SelectionType::Field); |
| |
| FieldDelegate &field = m_fields[m_selection_index]; |
| if (field.FieldDelegateHandleChar(key) == eKeyHandled) |
| return eKeyHandled; |
| |
| if (!field.FieldDelegateOnLastOrOnlyElement()) |
| return eKeyNotHandled; |
| |
| field.FieldDelegateExitCallback(); |
| |
| if (m_selection_index == GetNumberOfFields() - 1) { |
| m_selection_type = SelectionType::NewButton; |
| return eKeyHandled; |
| } |
| |
| m_selection_index++; |
| FieldDelegate &next_field = m_fields[m_selection_index]; |
| next_field.FieldDelegateSelectFirstElement(); |
| 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; |
| case SelectionType::Field: |
| return SelectNextInList(key); |
| } |
| break; |
| case '\t': |
| return SelectNext(key); |
| case KEY_SHIFT_TAB: |
| return SelectPrevious(key); |
| 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 ArgumentsFieldDelegate : public ListFieldDelegate<TextFieldDelegate> { |
| public: |
| ArgumentsFieldDelegate() |
| : ListFieldDelegate("Arguments", |
| TextFieldDelegate("Argument", "", false)) {} |
| |
| Args GetArguments() { |
| Args arguments; |
| for (int i = 0; i < GetNumberOfFields(); i++) { |
| arguments.AppendArgument(GetField(i).GetText()); |
| } |
| return arguments; |
| } |
| |
| void AddArguments(const Args &arguments) { |
| for (size_t i = 0; i < arguments.GetArgumentCount(); i++) { |
| AddNewField(); |
| TextFieldDelegate &field = GetField(GetNumberOfFields() - 1); |
| field.SetText(arguments.GetArgumentAtIndex(i)); |
| } |
| } |
| }; |
| |
| template <class KeyFieldDelegateType, class ValueFieldDelegateType> |
| class MappingFieldDelegate : public FieldDelegate { |
| public: |
| MappingFieldDelegate(KeyFieldDelegateType key_field, |
| ValueFieldDelegateType value_field) |
| : m_key_field(key_field), m_value_field(value_field), |
| m_selection_type(SelectionType::Key) {} |
| |
| // Signify which element is selected. The key field or its value field. |
| enum class SelectionType { Key, Value }; |
| |
| // A mapping field is drawn as two text fields with a right arrow in between. |
| // The first field stores the key of the mapping and the second stores the |
| // value if the mapping. |
| // |
| // __[Key]_____________ __[Value]___________ |
| // | | > | | |
| // |__________________| |__________________| |
| // - Error message if it exists. |
| |
| // The mapping field has a height that is equal to the maximum height between |
| // the key and value fields. |
| int FieldDelegateGetHeight() override { |
| return std::max(m_key_field.FieldDelegateGetHeight(), |
| m_value_field.FieldDelegateGetHeight()); |
| } |
| |
| void DrawArrow(Surface &surface) { |
| surface.MoveCursor(0, 1); |
| surface.PutChar(ACS_RARROW); |
| } |
| |
| void FieldDelegateDraw(Surface &surface, bool is_selected) override { |
| Rect bounds = surface.GetFrame(); |
| Rect key_field_bounds, arrow_and_value_field_bounds; |
| bounds.VerticalSplit(bounds.size.width / 2, key_field_bounds, |
| arrow_and_value_field_bounds); |
| Rect arrow_bounds, value_field_bounds; |
| arrow_and_value_field_bounds.VerticalSplit(1, arrow_bounds, |
| value_field_bounds); |
| |
| Surface key_field_surface = surface.SubSurface(key_field_bounds); |
| Surface arrow_surface = surface.SubSurface(arrow_bounds); |
| Surface value_field_surface = surface.SubSurface(value_field_bounds); |
| |
| bool key_is_selected = |
| m_selection_type == SelectionType::Key && is_selected; |
| m_key_field.FieldDelegateDraw(key_field_surface, key_is_selected); |
| DrawArrow(arrow_surface); |
| bool value_is_selected = |
| m_selection_type == SelectionType::Value && is_selected; |
| m_value_field.FieldDelegateDraw(value_field_surface, value_is_selected); |
| } |
| |
| HandleCharResult SelectNext(int key) { |
| if (FieldDelegateOnLastOrOnlyElement()) |
| return eKeyNotHandled; |
| |
| if (!m_key_field.FieldDelegateOnLastOrOnlyElement()) { |
| return m_key_field.FieldDelegateHandleChar(key); |
| } |
| |
| m_key_field.FieldDelegateExitCallback(); |
| m_selection_type = SelectionType::Value; |
| m_value_field.FieldDelegateSelectFirstElement(); |
| return eKeyHandled; |
| } |
| |
| HandleCharResult SelectPrevious(int key) { |
| if (FieldDelegateOnFirstOrOnlyElement()) |
| return eKeyNotHandled; |
| |
| if (!m_value_field.FieldDelegateOnFirstOrOnlyElement()) { |
| return m_value_field.FieldDelegateHandleChar(key); |
| } |
| |
| m_value_field.FieldDelegateExitCallback(); |
| m_selection_type = SelectionType::Key; |
| m_key_field.FieldDelegateSelectLastElement(); |
| return eKeyHandled; |
| } |
| |
| // If the value field is selected, pass the key to it. If the key field is |
| // selected, its last element is selected, and it didn't handle the key, then |
| // select its corresponding value field. |
| HandleCharResult SelectNextField(int key) { |
| if (m_selection_type == SelectionType::Value) { |
| return m_value_field.FieldDelegateHandleChar(key); |
| } |
| |
| if (m_key_field.FieldDelegateHandleChar(key) == eKeyHandled) |
| return eKeyHandled; |
| |
| if (!m_key_field.FieldDelegateOnLastOrOnlyElement()) |
| return eKeyNotHandled; |
| |
| m_key_field.FieldDelegateExitCallback(); |
| m_selection_type = SelectionType::Value; |
| m_value_field.FieldDelegateSelectFirstElement(); |
| return eKeyHandled; |
| } |
| |
| HandleCharResult FieldDelegateHandleChar(int key) override { |
| switch (key) { |
| case KEY_RETURN: |
| return SelectNextField(key); |
| case '\t': |
| return SelectNext(key); |
| case KEY_SHIFT_TAB: |
| return SelectPrevious(key); |
| default: |
| break; |
| } |
| |
| // If the key wasn't handled, pass the key to the selected field. |
| if (m_selection_type == SelectionType::Key) |
| return m_key_field.FieldDelegateHandleChar(key); |
| else |
| return m_value_field.FieldDelegateHandleChar(key); |
| |
| return eKeyNotHandled; |
| } |
| |
| bool FieldDelegateOnFirstOrOnlyElement() override { |
| return m_selection_type == SelectionType::Key; |
| } |
| |
| bool FieldDelegateOnLastOrOnlyElement() override { |
| return m_selection_type == SelectionType::Value; |
| } |
| |
| void FieldDelegateSelectFirstElement() override { |
| m_selection_type = SelectionType::Key; |
| } |
| |
| void FieldDelegateSelectLastElement() override { |
| m_selection_type = SelectionType::Value; |
| } |
| |
| bool FieldDelegateHasError() override { |
| return m_key_field.FieldDelegateHasError() || |
| m_value_field.FieldDelegateHasError(); |
| } |
| |
| KeyFieldDelegateType &GetKeyField() { return m_key_field; } |
| |
| ValueFieldDelegateType &GetValueField() { return m_value_field; } |
| |
| protected: |
| KeyFieldDelegateType m_key_field; |
| ValueFieldDelegateType m_value_field; |
| // See SelectionType class enum. |
| SelectionType m_selection_type; |
| }; |
| |
| class EnvironmentVariableNameFieldDelegate : public TextFieldDelegate { |
| public: |
| EnvironmentVariableNameFieldDelegate(const char *content) |
| : TextFieldDelegate("Name", content, true) {} |
| |
| // Environment variable names can't contain an equal sign. |
| bool IsAcceptableChar(int key) override { |
| return TextFieldDelegate::IsAcceptableChar(key) && key != '='; |
| } |
| |
| const std::string &GetName() { return m_content; } |
| }; |
| |
| class EnvironmentVariableFieldDelegate |
| : public MappingFieldDelegate<EnvironmentVariableNameFieldDelegate, |
| TextFieldDelegate> { |
| public: |
| EnvironmentVariableFieldDelegate() |
| : MappingFieldDelegate( |
| EnvironmentVariableNameFieldDelegate(""), |
| TextFieldDelegate("Value", "", /*required=*/false)) {} |
| |
| const std::string &GetName() { return GetKeyField().GetName(); } |
| |
| const std::string &GetValue() { return GetValueField().GetText(); } |
| |
| void SetName(const char *name) { return GetKeyField().SetText(name); } |
| |
| void SetValue(const char *value) { return GetValueField().SetText(value); } |
| }; |
| |
| class EnvironmentVariableListFieldDelegate |
| : public ListFieldDelegate<EnvironmentVariableFieldDelegate> { |
| public: |
| EnvironmentVariableListFieldDelegate(const char *label) |
| : ListFieldDelegate(label, EnvironmentVariableFieldDelegate()) {} |
| |
| Environment GetEnvironment() { |
| Environment environment; |
| for (int i = 0; i < GetNumberOfFields(); i++) { |
| environment.insert( |
| std::make_pair(GetField(i).GetName(), GetField(i).GetValue())); |
| } |
| return environment; |
| } |
| |
| void AddEnvironmentVariables(const Environment &environment) { |
| for (auto &variable : environment) { |
| AddNewField(); |
| EnvironmentVariableFieldDelegate &field = |
| GetField(GetNumberOfFields() - 1); |
| field.SetName(variable.getKey().str().c_str()); |
| field.SetValue(variable.getValue().c_str()); |
| } |
| } |
| }; |
| |
| 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(Surface &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; |
| |
| virtual std::string GetName() = 0; |
| |
| virtual void UpdateFieldsVisibility() { return; } |
| |
| FieldDelegate *GetField(uint32_t field_index) { |
| if (field_index < m_fields.size()) |
| return m_fields[field_index].get(); |
| return nullptr; |
| } |
| |
| 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; } |
| |
| // If all fields are valid, true is returned. Otherwise, an error message is |
| // set and false is returned. This method is usually called at the start of an |
| // action that requires valid fields. |
| bool CheckFieldsValidity() { |
| for (int i = 0; i < GetNumberOfFields(); i++) { |
| GetField(i)->FieldDelegateExitCallback(); |
| if (GetField(i)->FieldDelegateHasError()) { |
| SetError("Some fields are invalid!"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Factory methods to create and add fields of specific types. |
| |
| TextFieldDelegate *AddTextField(const char *label, const char *content, |
| bool required) { |
| TextFieldDelegate *delegate = |
| new TextFieldDelegate(label, content, required); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| FileFieldDelegate *AddFileField(const char *label, const char *content, |
| bool need_to_exist, bool required) { |
| FileFieldDelegate *delegate = |
| new FileFieldDelegate(label, content, need_to_exist, required); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| DirectoryFieldDelegate *AddDirectoryField(const char *label, |
| const char *content, |
| bool need_to_exist, bool required) { |
| DirectoryFieldDelegate *delegate = |
| new DirectoryFieldDelegate(label, content, need_to_exist, required); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| ArchFieldDelegate *AddArchField(const char *label, const char *content, |
| bool required) { |
| ArchFieldDelegate *delegate = |
| new ArchFieldDelegate(label, content, required); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| IntegerFieldDelegate *AddIntegerField(const char *label, int content, |
| bool required) { |
| IntegerFieldDelegate *delegate = |
| new IntegerFieldDelegate(label, content, required); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| BooleanFieldDelegate *AddBooleanField(const char *label, bool content) { |
| BooleanFieldDelegate *delegate = new BooleanFieldDelegate(label, content); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| LazyBooleanFieldDelegate *AddLazyBooleanField(const char *label, |
| const char *calculate_label) { |
| LazyBooleanFieldDelegate *delegate = |
| new LazyBooleanFieldDelegate(label, calculate_label); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| ChoicesFieldDelegate *AddChoicesField(const char *label, int height, |
| std::vector<std::string> choices) { |
| ChoicesFieldDelegate *delegate = |
| new ChoicesFieldDelegate(label, height, choices); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| PlatformPluginFieldDelegate *AddPlatformPluginField(Debugger &debugger) { |
| PlatformPluginFieldDelegate *delegate = |
| new PlatformPluginFieldDelegate(debugger); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| ProcessPluginFieldDelegate *AddProcessPluginField() { |
| ProcessPluginFieldDelegate *delegate = new ProcessPluginFieldDelegate(); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| template <class T> |
| ListFieldDelegate<T> *AddListField(const char *label, T default_field) { |
| ListFieldDelegate<T> *delegate = |
| new ListFieldDelegate<T>(label, default_field); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| ArgumentsFieldDelegate *AddArgumentsField() { |
| ArgumentsFieldDelegate *delegate = new ArgumentsFieldDelegate(); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| template <class K, class V> |
| MappingFieldDelegate<K, V> *AddMappingField(K key_field, V value_field) { |
| MappingFieldDelegate<K, V> *delegate = |
| new MappingFieldDelegate<K, V>(key_field, value_field); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| EnvironmentVariableNameFieldDelegate * |
| AddEnvironmentVariableNameField(const char *content) { |
| EnvironmentVariableNameFieldDelegate *delegate = |
| new EnvironmentVariableNameFieldDelegate(content); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| EnvironmentVariableFieldDelegate *AddEnvironmentVariableField() { |
| EnvironmentVariableFieldDelegate *delegate = |
| new EnvironmentVariableFieldDelegate(); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| return delegate; |
| } |
| |
| EnvironmentVariableListFieldDelegate * |
| AddEnvironmentVariableListField(const char *label) { |
| EnvironmentVariableListFieldDelegate *delegate = |
| new EnvironmentVariableListFieldDelegate(label); |
| m_fields.push_back(FieldDelegateUP(delegate)); |
| 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<FieldDelegateUP> 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_first_visible_line(0) { |
| assert(m_delegate_sp->GetNumberOfActions() > 0); |
| if (m_delegate_sp->GetNumberOfFields() > 0) |
| m_selection_type = SelectionType::Field; |
| else |
| m_selection_type = SelectionType::Action; |
| } |
| |
| // 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++) { |
| if (!m_delegate_sp->GetField(i)->FieldDelegateIsVisible()) |
| continue; |
| height += m_delegate_sp->GetField(i)->FieldDelegateGetHeight(); |
| } |
| height += GetActionsHeight(); |
| return height; |
| } |
| |
| ScrollContext GetScrollContext() { |
| if (m_selection_type == SelectionType::Action) |
| return ScrollContext(GetContentHeight() - 1); |
| |
| FieldDelegate *field = m_delegate_sp->GetField(m_selection_index); |
| ScrollContext context = field->FieldDelegateGetScrollContext(); |
| |
| int offset = GetErrorHeight(); |
| for (int i = 0; i < m_selection_index; i++) { |
| if (!m_delegate_sp->GetField(i)->FieldDelegateIsVisible()) |
| continue; |
| offset += m_delegate_sp->GetField(i)->FieldDelegateGetHeight(); |
| } |
| context.Offset(offset); |
| |
| // If the context is touching the error, include the error in the context as |
| // well. |
| if (context.start == GetErrorHeight()) |
| context.start = 0; |
| |
| return context; |
| } |
| |
| void UpdateScrolling(Surface &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 (context.start < m_first_visible_line) { |
| m_first_visible_line = context.start; |
| return; |
| } |
| |
| if (context.end > last_visible_line) { |
| m_first_visible_line = context.end - visible_height + 1; |
| } |
| } |
| |
| void DrawError(Surface &surface) { |
| if (!m_delegate_sp->HasError()) |
| return; |
| 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()); |
| } |
| |
| void DrawFields(Surface &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++) { |
| FieldDelegate *field = m_delegate_sp->GetField(i); |
| if (!field->FieldDelegateIsVisible()) |
| continue; |
| bool is_field_selected = a_field_is_selected && m_selection_index == i; |
| int height = field->FieldDelegateGetHeight(); |
| Rect bounds = Rect(Point(0, line), Size(width, height)); |
| Surface field_surface = surface.SubSurface(bounds); |
| field->FieldDelegateDraw(field_surface, is_field_selected); |
| line += height; |
| } |
| } |
| |
| void DrawActions(Surface &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)); |
| Surface action_surface = surface.SubSurface(bounds); |
| action.Draw(action_surface, is_action_selected); |
| x += width; |
| } |
| } |
| |
| void DrawElements(Surface &surface) { |
| Rect frame = surface.GetFrame(); |
| Rect fields_bounds, actions_bounds; |
| frame.HorizontalSplit(surface.GetHeight() - GetActionsHeight(), |
| fields_bounds, actions_bounds); |
| Surface fields_surface = surface.SubSurface(fields_bounds); |
| Surface actions_surface = surface.SubSurface(actions_bounds); |
| |
| DrawFields(fields_surface); |
| DrawActions(actions_surface); |
| } |
| |
| // 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(Surface &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); |
| Surface error_surface = pad.SubSurface(error_bounds); |
| Surface elements_surface = pad.SubSurface(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)); |
| } |
| |
| void DrawSubmitHint(Surface &surface, bool is_active) { |
| surface.MoveCursor(2, surface.GetHeight() - 1); |
| if (is_active) |
| surface.AttributeOn(A_BOLD | COLOR_PAIR(BlackOnWhite)); |
| surface.Printf("[Press Alt+Enter to %s]", |
| m_delegate_sp->GetAction(0).GetLabel().c_str()); |
| if (is_active) |
| surface.AttributeOff(A_BOLD | COLOR_PAIR(BlackOnWhite)); |
| } |
| |
| bool WindowDelegateDraw(Window &window, bool force) override { |
| m_delegate_sp->UpdateFieldsVisibility(); |
| |
| window.Erase(); |
| |
| window.DrawTitleBox(m_delegate_sp->GetName().c_str(), |
| "Press Esc to Cancel"); |
| DrawSubmitHint(window, window.IsActive()); |
| |
| Rect content_bounds = window.GetFrame(); |
| content_bounds.Inset(2, 2); |
| Surface content_surface = window.SubSurface(content_bounds); |
| |
| DrawContent(content_surface); |
| return true; |
| } |
| |
| void SkipNextHiddenFields() { |
| while (true) { |
| if (m_delegate_sp->GetField(m_selection_index)->FieldDelegateIsVisible()) |
| return; |
| |
| if (m_selection_index == m_delegate_sp->GetNumberOfFields() - 1) { |
| m_selection_type = SelectionType::Action; |
| m_selection_index = 0; |
| return; |
| } |
| |
| m_selection_index++; |
| } |
| } |
| |
| HandleCharResult SelectNext(int key) { |
| if (m_selection_type == SelectionType::Action) { |
| if (m_selection_index < m_delegate_sp->GetNumberOfActions() - 1) { |
| m_selection_index++; |
| return eKeyHandled; |
| } |
| |
| m_selection_index = 0; |
| m_selection_type = SelectionType::Field; |
| SkipNextHiddenFields(); |
| if (m_selection_type == SelectionType::Field) { |
| FieldDelegate *next_field = m_delegate_sp->GetField(m_selection_index); |
| next_field->FieldDelegateSelectFirstElement(); |
| } |
| return eKeyHandled; |
| } |
| |
| FieldDelegate *field = m_delegate_sp->GetField(m_selection_index); |
| if (!field->FieldDelegateOnLastOrOnlyElement()) { |
| return field->FieldDelegateHandleChar(key); |
| } |
| |
| field->FieldDelegateExitCallback(); |
| |
| if (m_selection_index == m_delegate_sp->GetNumberOfFields() - 1) { |
| m_selection_type = SelectionType::Action; |
| m_selection_index = 0; |
| return eKeyHandled; |
| } |
| |
| m_selection_index++; |
| SkipNextHiddenFields(); |
| |
| if (m_selection_type == SelectionType::Field) { |
| FieldDelegate *next_field = m_delegate_sp->GetField(m_selection_index); |
| next_field->FieldDelegateSelectFirstElement(); |
| } |
| |
| return eKeyHandled; |
| } |
| |
| void SkipPreviousHiddenFields() { |
| while (true) { |
| if (m_delegate_sp->GetField(m_selection_index)->FieldDelegateIsVisible()) |
| return; |
| |
| if (m_selection_index == 0) { |
| m_selection_type = SelectionType::Action; |
| m_selection_index = 0; |
| return; |
| } |
| |
| m_selection_index--; |
| } |
| } |
| |
| HandleCharResult SelectPrevious(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; |
| SkipPreviousHiddenFields(); |
| if (m_selection_type == SelectionType::Field) { |
| FieldDelegate *previous_field = |
| m_delegate_sp->GetField(m_selection_index); |
| previous_field->FieldDelegateSelectLastElement(); |
| } |
| return eKeyHandled; |
| } |
| |
| FieldDelegate *field = m_delegate_sp->GetField(m_selection_index); |
| if (!field->FieldDelegateOnFirstOrOnlyElement()) { |
| return field->FieldDelegateHandleChar(key); |
| } |
| |
| field->FieldDelegateExitCallback(); |
| |
| if (m_selection_index == 0) { |
| m_selection_type = SelectionType::Action; |
| m_selection_index = m_delegate_sp->GetNumberOfActions() - 1; |
| return eKeyHandled; |
| } |
| |
| m_selection_index--; |
| SkipPreviousHiddenFields(); |
| |
| if (m_selection_type == SelectionType::Field) { |
| FieldDelegate *previous_field = |
| m_delegate_sp->GetField(m_selection_index); |
| previous_field->FieldDelegateSelectLastElement(); |
| } |
| |
| return eKeyHandled; |
| } |
| |
| void ExecuteAction(Window &window, int index) { |
| FormAction &action = m_delegate_sp->GetAction(index); |
| action.Execute(window); |
| if (m_delegate_sp->HasError()) { |
| m_first_visible_line = 0; |
| m_selection_index = 0; |
| m_selection_type = SelectionType::Field; |
| } |
| } |
| |
| // Always return eKeyHandled to absorb all events since forms are always |
| // added as pop-ups that should take full control until canceled or submitted. |
| HandleCharResult WindowDelegateHandleChar(Window &window, int key) override { |
| switch (key) { |
| case '\r': |
| case '\n': |
| case KEY_ENTER: |
| if (m_selection_type == SelectionType::Action) { |
| ExecuteAction(window, m_selection_index); |
| return eKeyHandled; |
| } |
| break; |
| case KEY_ALT_ENTER: |
| ExecuteAction(window, 0); |
| return eKeyHandled; |
| case '\t': |
| SelectNext(key); |
| return eKeyHandled; |
| case KEY_SHIFT_TAB: |
| SelectPrevious(key); |
| return eKeyHandled; |
| case KEY_ESCAPE: |
| window.GetParent()->RemoveSubWindow(&window); |
| 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) { |
| FieldDelegate *field = m_delegate_sp->GetField(m_selection_index); |
| if (field->FieldDelegateHandleChar(key) == eKeyHandled) |
| return eKeyHandled; |
| } |
| |
| // If the key wasn't handled by the possibly selected field, handle some |
| // extra keys for navigation. |
| switch (key) { |
| case KEY_DOWN: |
| SelectNext(key); |
| return eKeyHandled; |
| case KEY_UP: |
| SelectPrevious(key); |
| return eKeyHandled; |
| default: |
| break; |
| } |
| |
| return eKeyHandled; |
| } |
| |
| protected: |
| 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; |
| }; |
| |
| /////////////////////////// |
| // Form Delegate Instances |
| /////////////////////////// |
| |
| class DetachOrKillProcessFormDelegate : public FormDelegate { |
| public: |
| DetachOrKillProcessFormDelegate(Process *process) : m_process(process) { |
| SetError("There is a
|