diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 1c8c0b263..64b6dbf0d 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3864,6 +3864,87 @@ The Label widget implements the following methods: Computes the width of the text. +TooltipLabel class +------------------ + +This Label subclass represents text that you want to be able to dynamically +hide, like help text in a tooltip. + +It has the following attributes: + +:tooltip: The string (or a table of strings or a function that returns a string + or a table of strings) to display. The text will be autowrapped to the + width of the widget, though any existing newlines will be kept. +:show_tooltip: Boolean or a callback; if true, the widget is visible. Defaults + to ``true``. +:indent: The number of spaces to indent the tooltip from the left margin. The + default is ``2``. + +The ``text_pen`` attribute of the ``Label`` class is overridden with a default +of COLOR_GREY. + +Note that the text of the tooltip is only refreshed when the widget layout is +updated (i.e. ``updateLayout()`` is called on this widget or a widget that +contains this widget) and the tooltip needs to be rewrapped. + +HotkeyLabel class +----------------- + +This Label subclass is a convenience class for formatting text that responds to +a hotkey. + +It has the following attributes: + +:key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``. +:label: The string (or a function that returns a string) to display after the + hotkey. +:on_activate: If specified, it is the callback that will be called whenever + the hotkey is pressed. + +CycleHotkeyLabel class +---------------------- + +This Label subclass represents a group of related options that the user can +cycle through by pressing a specified hotkey. + +It has the following attributes: + +:key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``. +:label: The string (or a function that returns a string) to display after the + hotkey. +:label_width: The number of spaces to allocate to the ``label`` (for use in + aligning a column of ``CycleHotkeyLabel`` labels). +:options: A list of strings or tables of ``{label=string, value=string}``. + String options use the same string for the label and value. +:initial_option: The value or numeric index of the initial option. +:on_change: The callback to call when the selected option changes. It is called + as ``on_change(new_option_value, old_option_value)``. + +The index of the currently selected option in the ``options`` list is kept in +the ``option_idx`` instance variable. + +The CycleHotkeyLabel widget implements the following methods: + +* ``cyclehotkeylabel:cycle()`` + + Cycles the selected option and triggers the ``on_change`` callback. + +* ``cyclehotkeylabel:getOptionLabel([option_idx])`` + + Retrieves the option label at the given index, or the label of the + currently selected option if no index is given. + +* ``cyclehotkeylabel:getOptionValue([option_idx])`` + + Retrieves the option value at the given index, or the value of the + currently selected option if no index is given. + +ToggleHotkeyLabel +----------------- + +This is a specialized subclass of CycleHotkeyLabel that has two options: +``On`` (with a value of ``true``) and ``Off`` (with a value of ``false``). + List class ---------- diff --git a/docs/changelog.txt b/docs/changelog.txt index 4d9741ef8..26329f0d1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -79,6 +79,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - New string class function: ``string:escape_pattern()`` escapes regex special characters within a string - ``widgets.Panel``: if ``autoarrange_subviews`` is set, ``Panel``\s will now automatically lay out widgets vertically according to their current height. This allows you to have widgets dynamically change height or become visible/hidden and you don't have to worry about recalculating frame layouts - ``widgets.ResizingPanel``: new ``Panel`` subclass that automatically recalculates it's own frame height based on the size, position, and visibility of its subviews +- ``widgets.TooltipLabel``: new ``Label`` subclass that provides tooltip-like behavior +- ``widgets.HotkeyLabel``: new ``Label`` subclass that displays and reacts to hotkeys +- ``widgets.CycleHotkeyLabel``: new ``Label`` subclass that allows users to cycle through a list of options by pressing a hotkey +- ``widgets.ToggleHotkeyLabel``: new ``CycleHotkeyLabel`` subclass that toggles between ``On`` and ``Off`` states - ``safe_index`` now properly handles lua sparse tables that are indexed by numbers - ``widgets``: unset values in ``frame_inset``-table default to ``0`` diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 140882a00..7a83346b1 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -99,10 +99,12 @@ function Panel:postUpdateLayout() local gap = self.autoarrange_gap local y = 0 for _,subview in ipairs(self.subviews) do + if not subview.frame then goto continue end subview.frame.t = y if subview.visible then - y = y + subview.frame.h + gap + y = y + (subview.frame.h or 0) + gap end + ::continue:: end self.frame_rect.height = y @@ -116,11 +118,6 @@ end ResizingPanel = defclass(ResizingPanel, Panel) -function ResizingPanel:init() - -- ensure we have a frame so a containing widget can read our dimensions - if not self.frame then self.frame = {} end -end - -- adjust our frame dimensions according to positions and sizes of our subviews function ResizingPanel:postUpdateLayout(frame_body) local w, h = 0, 0 @@ -132,6 +129,7 @@ function ResizingPanel:postUpdateLayout(frame_body) (subview.frame.h or frame_body.height)) end end + if not self.frame then self.frame = {} end self.frame.w, self.frame.h = w, h end @@ -419,7 +417,9 @@ Label.ATTRS{ function Label:init(args) self.start_line_num = 1 - self:setText(args.text) + -- use existing saved text if no explicit text was specified. this avoids + -- overwriting pre-formatted text that subclasses may have already set + self:setText(args.text or self.text) if not self.text_hpen then self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16 end @@ -493,6 +493,136 @@ function Label:onInput(keys) return check_text_keys(self, keys) end +------------------ +-- TooltipLabel -- +------------------ + +TooltipLabel = defclass(TooltipLabel, Label) + +TooltipLabel.ATTRS{ + tooltip=DEFAULT_NIL, + show_tooltip=true, + indent=2, + text_pen=COLOR_GREY, +} + +function TooltipLabel:getWrappedTooltip() + local tooltip = getval(self.tooltip) + if type(tooltip) == 'table' then + tooltip = table.concat(tooltip, NEWLINE) + end + return tooltip:wrap(self.frame_body.width - self.indent) +end + +function TooltipLabel:preUpdateLayout() + self.visible = getval(self.show_tooltip) +end + +-- we can't set the text in init() since we may not yet have a frame that we +-- can get wrapping bounds from. +function TooltipLabel:postComputeFrame() + local text = {} + for _,line in ipairs(self:getWrappedTooltip():split(NEWLINE)) do + table.insert(text, {gap=self.indent, text=line}) + table.insert(text, NEWLINE) + end + self:setText(text) +end + +----------------- +-- HotkeyLabel -- +----------------- + +HotkeyLabel = defclass(HotkeyLabel, Label) + +HotkeyLabel.ATTRS{ + key=DEFAULT_NIL, + label=DEFAULT_NIL, + on_activate=DEFAULT_NIL, +} + +function HotkeyLabel:init() + self:setText{{key=self.key, key_sep=': ', text=self.label, + on_activate=self.on_activate}} +end + +---------------------- +-- CycleHotkeyLabel -- +---------------------- + +CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label) + +CycleHotkeyLabel.ATTRS{ + key=DEFAULT_NIL, + label=DEFAULT_NIL, + label_width=DEFAULT_NIL, + options=DEFAULT_NIL, + initial_option=1, + on_change=DEFAULT_NIL, +} + +function CycleHotkeyLabel:init() + -- initialize option_idx + for i in ipairs(self.options) do + if self.initial_option == self:getOptionValue(i) then + self.option_idx = i + break + end + end + if not self.option_idx then + error(('cannot find option with value or index: "%s"') + :format(self.initial_option)) + end + + self:setText{ + {key=self.key, key_sep=': ', text=self.label, width=self.label_width, + on_activate=self:callback('cycle')}, + ' ', + {text=self:callback('getOptionLabel')}, + } +end + +function CycleHotkeyLabel:cycle() + local old_option_idx = self.option_idx + if self.option_idx == #self.options then + self.option_idx = 1 + else + self.option_idx = self.option_idx + 1 + end + if self.on_change then + self.on_change(self:getOptionValue(), + self:getOptionValue(old_option_idx)) + end +end + +function CycleHotkeyLabel:getOptionLabel(option_idx) + option_idx = option_idx or self.option_idx + local option = self.options[option_idx] + if type(option) == 'table' then + return option.label + end + return option +end + +function CycleHotkeyLabel:getOptionValue(option_idx) + option_idx = option_idx or self.option_idx + local option = self.options[option_idx] + if type(option) == 'table' then + return option.value + end + return option +end + +----------------------- +-- ToggleHotkeyLabel -- +----------------------- + +ToggleHotkeyLabel = defclass(ToggleHotkeyLabel, CycleHotkeyLabel) +ToggleHotkeyLabel.ATTRS{ + options={{label='On', value=true}, + {label='Off', value=false}}, +} + ---------- -- List -- ----------