add some more handy widgets to the library

TooltipLabel
HotkeyLabel
CycleHotkeyLabel
ToggleHotkeyLabel
develop
myk002 2022-04-17 20:07:47 -07:00 committed by Myk
parent b6703b2b05
commit 07761e1d5d
3 changed files with 222 additions and 7 deletions

@ -3864,6 +3864,87 @@ The Label widget implements the following methods:
Computes the width of the text. 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 List class
---------- ----------

@ -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 - 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.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.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 - ``safe_index`` now properly handles lua sparse tables that are indexed by numbers
- ``widgets``: unset values in ``frame_inset``-table default to ``0`` - ``widgets``: unset values in ``frame_inset``-table default to ``0``

@ -99,10 +99,12 @@ function Panel:postUpdateLayout()
local gap = self.autoarrange_gap local gap = self.autoarrange_gap
local y = 0 local y = 0
for _,subview in ipairs(self.subviews) do for _,subview in ipairs(self.subviews) do
if not subview.frame then goto continue end
subview.frame.t = y subview.frame.t = y
if subview.visible then if subview.visible then
y = y + subview.frame.h + gap y = y + (subview.frame.h or 0) + gap
end end
::continue::
end end
self.frame_rect.height = y self.frame_rect.height = y
@ -116,11 +118,6 @@ end
ResizingPanel = defclass(ResizingPanel, Panel) 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 -- adjust our frame dimensions according to positions and sizes of our subviews
function ResizingPanel:postUpdateLayout(frame_body) function ResizingPanel:postUpdateLayout(frame_body)
local w, h = 0, 0 local w, h = 0, 0
@ -132,6 +129,7 @@ function ResizingPanel:postUpdateLayout(frame_body)
(subview.frame.h or frame_body.height)) (subview.frame.h or frame_body.height))
end end
end end
if not self.frame then self.frame = {} end
self.frame.w, self.frame.h = w, h self.frame.w, self.frame.h = w, h
end end
@ -419,7 +417,9 @@ Label.ATTRS{
function Label:init(args) function Label:init(args)
self.start_line_num = 1 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 if not self.text_hpen then
self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16 self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16
end end
@ -493,6 +493,136 @@ function Label:onInput(keys)
return check_text_keys(self, keys) return check_text_keys(self, keys)
end 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 -- -- List --
---------- ----------