Merge pull request #2500 from myk002/myk_panel_drag

Support resizing for DFHack Panel widgets
develop
Myk 2022-12-15 14:36:33 -08:00 committed by GitHub
commit 42203b13f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 299 additions and 32 deletions

@ -47,7 +47,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Lua ## Lua
- ``gui.View``: ``visible`` and ``active`` can now be functions that return a boolean - ``gui.View``: ``visible`` and ``active`` can now be functions that return a boolean
- ``widgets.Panel``: new attributes to control window dragging with mouse or keyboard - ``widgets.Panel``: new attributes to control window dragging and resizing with mouse or keyboard
- ``widgets.Window``: Panel subclass with attributes preset for top-level windows
## Internals ## Internals

@ -4182,9 +4182,28 @@ Has attributes:
``true`` if the drag was "successful" (i.e. not canceled) and ``false`` ``true`` if the drag was "successful" (i.e. not canceled) and ``false``
otherwise. Dragging can be canceled by right clicking while dragging with the otherwise. Dragging can be canceled by right clicking while dragging with the
mouse, hitting :kbd:`Esc` (while dragging with the mouse or keyboard), or by mouse, hitting :kbd:`Esc` (while dragging with the mouse or keyboard), or by
calling ``Panel:setCursorMoveEnabled(false)`` (while dragging with the calling ``Panel:setKeyboaredDragEnabled(false)`` (while dragging with the
keyboard). keyboard).
* ``resizable = bool`` (default: ``false``)
* ``resize_anchors = {}`` (default: ``{t=false, l=true, r=true, b=true}``
* ``resize_min = {}`` (default: w and h from the ``frame``, or ``{w=5, h=5}``)
* ``on_resize_begin = function()`` (default: ``nil``)
* ``on_resize_end = function(bool)`` (default: ``nil``)
If ``resizable`` is set to ``true``, then the player can click the mouse on
any edge specified in ``resize_anchors`` and drag the border to resize the
window. If two adjacent edges are enabled as anchors, then the tile where they
meet can be used to resize both edges at the same time. The minimum dimensions
specified in ``resize_min`` (or inherited from ``frame`` are respected when
resizing. The panel is also prevented from resizing beyond the boundaries of
its parent. When the player clicks on a valid anchor, ``on_resize_begin()`` is
called. The boolean passed to the ``on_resize_end`` callback will be ``true``
if the drag was "successful" (i.e. not canceled) and ``false`` otherwise.
Dragging can be canceled by right clicking while resizing with the mouse,
hitting :kbd:`Esc` (while resizing with the mouse or keyboard), or by calling
``Panel:setKeyboardResizeEnabled(false)`` (while resizing with the keyboard).
* ``autoarrange_subviews = bool`` (default: ``false``) * ``autoarrange_subviews = bool`` (default: ``false``)
* ``autoarrange_gap = int`` (default: ``0``) * ``autoarrange_gap = int`` (default: ``0``)
@ -4207,8 +4226,8 @@ Has functions:
* ``panel:setKeyboardDragEnabled(bool)`` * ``panel:setKeyboardDragEnabled(bool)``
If called with something truthy and the panel is not already in keyboard drag If called with ``true`` and the panel is not already in keyboard drag mode,
mode, then any current drag operations are halted where they are (not then any current drag or resize operations are halted where they are (not
canceled), the panel siezes input focus (see `View class`_ above for canceled), the panel siezes input focus (see `View class`_ above for
information on the DFHack focus subsystem), and further keyboard cursor keys information on the DFHack focus subsystem), and further keyboard cursor keys
move the window as if it were being dragged. Shift-cursor keys move by larger move the window as if it were being dragged. Shift-cursor keys move by larger
@ -4216,12 +4235,44 @@ Has functions:
cancel. If dragging is canceled, then the window is moved back to its original cancel. If dragging is canceled, then the window is moved back to its original
position. position.
* ``panel:setKeyboardResizeEnabled(bool)``
If called with ``true`` and the panel is not already in keyboard resize mode,
then any current drag or resize operations are halted where they are (not
canceled), the panel siezes input focus (see `View class`_ above for
information on the DFHack focus subsystem), and further keyboard cursor keys
resize the window as if it were being dragged from the lower right corner. If
neither the bottom or right edge is a valid anchor, an appropriate corner will
be chosen. Shift-cursor keys move by larger amounts. Hit :kbd:`Enter` to
commit the new window size or :kbd:`Esc` to cancel. If resizing is canceled,
then the window size from before the resize operation is restored.
Double clicking:
If the panel is resizable and the user double-clicks on the top edge (the frame
title, if the panel has a frame), then the panel will jump to its maximum size.
If the panel has already been maximized in this fashion, then it will jump to
its minimum size. Both jumps respect the resizable edges defined by the
``resize_anchors`` attribute.
The time duration that a double click can span is defined by the global variable
``DOUBLE_CLICK_MS``. The default value is ``500`` and can be changed by the end
user with a command like::
:lua require('gui.widgets').DOUBLE_CLICK_MS=1000
Window class
------------
Subclass of Panel; sets Panel attributes to useful defaults for a top-level
framed, draggable window.
ResizingPanel class ResizingPanel class
------------------- -------------------
Subclass of Panel; automatically adjusts its own frame height according to Subclass of Panel; automatically adjusts its own frame height and width to the
the size, position, and visibility of its subviews. Pairs nicely with a minimum required to show its subviews. Pairs nicely with a parent Panel that has
parent Panel that has ``autoarrange_subviews`` enabled. ``autoarrange_subviews`` enabled.
Pages class Pages class
----------- -----------

@ -704,8 +704,9 @@ GREY_LINE_FRAME = {
signature_pen = to_pen{ fg = COLOR_DARKGREY, bg = COLOR_BLACK }, signature_pen = to_pen{ fg = COLOR_DARKGREY, bg = COLOR_BLACK },
} }
function paint_frame(x1,y1,x2,y2,style,title) function paint_frame(dc,rect,style,title)
local pen = style.frame_pen local pen = style.frame_pen
local x1,y1,x2,y2 = dc.x1+rect.x1, dc.y1+rect.y1, dc.x1+rect.x2, dc.y1+rect.y2
dscreen.paintTile(style.lt_frame_pen or pen, x1, y1) dscreen.paintTile(style.lt_frame_pen or pen, x1, y1)
dscreen.paintTile(style.rt_frame_pen or pen, x2, y1) dscreen.paintTile(style.rt_frame_pen or pen, x2, y1)
dscreen.paintTile(style.lb_frame_pen or pen, x1, y2) dscreen.paintTile(style.lb_frame_pen or pen, x1, y2)
@ -750,16 +751,13 @@ function FramedScreen:computeFrame(parent_rect)
end end
function FramedScreen:onRenderFrame(dc, rect) function FramedScreen:onRenderFrame(dc, rect)
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
if rect.wgap <= 0 and rect.hgap <= 0 then if rect.wgap <= 0 and rect.hgap <= 0 then
dc:clear() dc:clear()
else else
self:renderParent() self:renderParent()
dc:fill(rect, self.frame_background) dc:fill(rect, self.frame_background)
end end
paint_frame(dc,rect,self.frame_style,self.frame_title)
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
end end
return _ENV return _ENV

@ -64,6 +64,8 @@ end
-- Panel -- -- Panel --
----------- -----------
DOUBLE_CLICK_MS = 500
Panel = defclass(Panel, Widget) Panel = defclass(Panel, Widget)
Panel.ATTRS { Panel.ATTRS {
@ -72,28 +74,47 @@ Panel.ATTRS {
on_render = DEFAULT_NIL, on_render = DEFAULT_NIL,
on_layout = DEFAULT_NIL, on_layout = DEFAULT_NIL,
draggable = false, draggable = false,
drag_anchors = copyall({title=true, frame=false, body=false}), drag_anchors = DEFAULT_NIL,
drag_bound = 'frame', -- or 'body' drag_bound = 'frame', -- or 'body'
on_drag_begin = DEFAULT_NIL, on_drag_begin = DEFAULT_NIL,
on_drag_end = DEFAULT_NIL, on_drag_end = DEFAULT_NIL,
resizable = false,
resize_anchors = DEFAULT_NIL,
resize_min = DEFAULT_NIL,
on_resize_begin = DEFAULT_NIL,
on_resize_end = DEFAULT_NIL,
autoarrange_subviews = false, -- whether to automatically lay out subviews autoarrange_subviews = false, -- whether to automatically lay out subviews
autoarrange_gap = 0, -- how many blank lines to insert between widgets autoarrange_gap = 0, -- how many blank lines to insert between widgets
} }
function Panel:init(args) function Panel:init(args)
self.keyboard_drag = nil -- true when we are in keyboard dragging mode if not self.drag_anchors then
self.drag_anchors = {title=true, frame=false, body=false}
end
if not self.resize_anchors then
self.resize_anchors = {t=false, l=true, r=true, b=true}
end
self.resize_min = self.resize_min or {}
self.resize_min.w = self.resize_min.w or (self.frame or {}).w or 5
self.resize_min.h = self.resize_min.h or (self.frame or {}).h or 5
self.kbd_get_pos = nil -- fn when we are in keyboard dragging mode
self.saved_frame = nil -- copy of frame when dragging started self.saved_frame = nil -- copy of frame when dragging started
self.saved_frame_rect = nil -- copy of frame_rect when dragging started self.saved_frame_rect = nil -- copy of frame_rect when dragging started
self.drag_offset = nil -- relative pos of held panel tile self.drag_offset = nil -- relative pos of held panel tile
self.resize_edge = nil -- which dimension is being resized?
self.last_title_click_ms = 0 -- used to track double-clicking on the title
self:addviews(args.subviews) self:addviews(args.subviews)
end end
local function Panel_update_frame(self, frame, clear_state) local function Panel_update_frame(self, frame, clear_state)
if clear_state then if clear_state then
self.keyboard_drag = nil self.kbd_get_pos = nil
self.saved_frame = nil self.saved_frame = nil
self.saved_frame_rect = nil self.saved_frame_rect = nil
self.drag_offset = nil self.drag_offset = nil
self.resize_edge = nil
end end
if not frame then return end if not frame then return end
if self.frame.l == frame.l and self.frame.r == frame.r if self.frame.l == frame.l and self.frame.r == frame.r
@ -105,6 +126,57 @@ local function Panel_update_frame(self, frame, clear_state)
self:updateLayout() self:updateLayout()
end end
-- dim: the name of the dimension var (i.e. 'h' or 'w')
-- anchor: the name of the anchor var (i.e. 't', 'b', 'l', or 'r')
-- opposite_anchor: the name of the anchor var for the opposite edge
-- max_dim: how big this panel can get from its current pos and fit in parent
-- wanted_dim: how big the player is trying to make the panel
-- max_anchor: max value of the frame anchor for the edge that is being resized
-- wanted_anchor: how small the player is trying to make the anchor value
local function Panel_resize_edge_base(frame, resize_min, dim, anchor,
opposite_anchor, max_dim, wanted_dim,
max_anchor, wanted_anchor)
frame[dim] = math.max(resize_min[dim], math.min(max_dim, wanted_dim))
if frame[anchor] or not frame[opposite_anchor] then
frame[anchor] = math.max(0, math.min(max_anchor, wanted_anchor))
end
end
local function Panel_resize_edge(frame, resize_min, dim, anchor,
opposite_anchor, dim_base, dim_ref, anchor_ref,
dim_far, mouse_ref)
local dim_sign = (anchor == 't' or anchor == 'l') and 1 or -1
local max_dim = dim_base - dim_ref + 1
local wanted_dim = dim_sign * (dim_far - mouse_ref) + 1
local max_anchor = dim_base - resize_min[dim] - dim_ref + 1
local wanted_anchor = dim_sign * (mouse_ref - anchor_ref)
Panel_resize_edge_base(frame, resize_min, dim, anchor, opposite_anchor,
max_dim, wanted_dim, max_anchor, wanted_anchor)
end
local function Panel_resize_frame(self, mouse_pos)
local frame, resize_min = copyall(self.frame), self.resize_min
local parent_rect = self.frame_parent_rect
local ref_rect = self.saved_frame_rect
if self.resize_edge:find('t') then
Panel_resize_edge(frame, resize_min, 'h', 't', 'b', ref_rect.y2,
parent_rect.y1, parent_rect.y1, ref_rect.y2, mouse_pos.y)
end
if self.resize_edge:find('b') then
Panel_resize_edge(frame, resize_min, 'h', 'b', 't', parent_rect.y2,
ref_rect.y1, parent_rect.y2, ref_rect.y1, mouse_pos.y)
end
if self.resize_edge:find('l') then
Panel_resize_edge(frame, resize_min, 'w', 'l', 'r', ref_rect.x2,
parent_rect.x1, parent_rect.x1, ref_rect.x2, mouse_pos.x)
end
if self.resize_edge:find('r') then
Panel_resize_edge(frame, resize_min, 'w', 'r', 'l', parent_rect.x2,
ref_rect.x1, parent_rect.x2, ref_rect.x1, mouse_pos.x)
end
return frame
end
local function Panel_drag_frame(self, mouse_pos) local function Panel_drag_frame(self, mouse_pos)
local frame = copyall(self.frame) local frame = copyall(self.frame)
local parent_rect, frame_rect = self.frame_parent_rect, self.frame_rect local parent_rect, frame_rect = self.frame_parent_rect, self.frame_rect
@ -140,31 +212,69 @@ end
local function Panel_make_frame(self, mouse_pos) local function Panel_make_frame(self, mouse_pos)
mouse_pos = mouse_pos or xy2pos(dfhack.screen.getMousePos()) mouse_pos = mouse_pos or xy2pos(dfhack.screen.getMousePos())
return Panel_drag_frame(self, mouse_pos) return self.resize_edge and Panel_resize_frame(self, mouse_pos)
or Panel_drag_frame(self, mouse_pos)
end end
local function Panel_begin_drag(self, drag_offset) local function Panel_begin_drag(self, drag_offset, resize_edge)
Panel_update_frame(self, nil, true) Panel_update_frame(self, nil, true)
self.drag_offset = drag_offset or {x=0, y=0} self.drag_offset = drag_offset or {x=0, y=0}
self.resize_edge = resize_edge
self.saved_frame = copyall(self.frame) self.saved_frame = copyall(self.frame)
self.saved_frame_rect = copyall(self.frame_rect) self.saved_frame_rect = copyall(self.frame_rect)
self.prev_focus_owner = self.focus_group.cur self.prev_focus_owner = self.focus_group.cur
self:setFocus(true) self:setFocus(true)
if self.on_drag_begin then self.on_drag_begin() end if self.resize_edge then
if self.on_resize_begin then self.on_resize_begin(success) end
else
if self.on_drag_begin then self.on_drag_begin(success) end
end
end end
local function Panel_end_drag(self, frame, success) local function Panel_end_drag(self, frame, success)
success = not not success
if self.prev_focus_owner then if self.prev_focus_owner then
self.prev_focus_owner:setFocus(true) self.prev_focus_owner:setFocus(true)
else else
self:setFocus(false) self:setFocus(false)
end end
Panel_update_frame(self, frame, true) Panel_update_frame(self, frame, true)
if self.on_drag_end then self.on_drag_end(success) end if self.resize_edge then
if self.on_resize_end then self.on_resize_end(success) end
else
if self.on_drag_end then self.on_drag_end(success) end
end
end
local function Panel_on_double_click(self)
local a = self.resize_anchors
local can_vert, can_horiz = a.t or a.b, a.l or a.r
if not can_vert and not can_horiz then return false end
local f, rmin = self.frame, self.resize_min
local maximized = f.t == 0 and f.b == 0 and f.l == 0 and f.r == 0
local frame
if maximized then
frame = {
t=not can_vert and f.t or nil,
l=not can_horiz and f.l or nil,
b=not can_vert and f.b or nil,
r=not can_horiz and f.r or nil,
w=can_vert and rmin.w or f.w,
h=can_horiz and rmin.h or f.h,
}
else
frame = {
t=can_vert and 0 or f.t,
l=can_horiz and 0 or f.l,
b=can_vert and 0 or f.b,
r=can_horiz and 0 or f.r
}
end
Panel_update_frame(self, frame, true)
end end
function Panel:onInput(keys) function Panel:onInput(keys)
if self.keyboard_drag then if self.kbd_get_pos then
if keys.SELECT or keys.LEAVESCREEN then if keys.SELECT or keys.LEAVESCREEN then
Panel_end_drag(self, keys.LEAVESCREEN and self.saved_frame or nil, Panel_end_drag(self, keys.LEAVESCREEN and self.saved_frame or nil,
not not keys.SELECT) not not keys.SELECT)
@ -173,8 +283,10 @@ function Panel:onInput(keys)
for code in pairs(keys) do for code in pairs(keys) do
local dx, dy = guidm.get_movement_delta(code, 1, 10) local dx, dy = guidm.get_movement_delta(code, 1, 10)
if dx then if dx then
local kbd_pos = {x=self.frame_rect.x1+dx, local frame_rect = self.frame_rect
y=self.frame_rect.y1+dy} local kbd_pos = self.kbd_get_pos()
kbd_pos.x = kbd_pos.x + dx
kbd_pos.y = kbd_pos.y + dy
Panel_update_frame(self, Panel_make_frame(self, kbd_pos)) Panel_update_frame(self, Panel_make_frame(self, kbd_pos))
return true return true
end end
@ -197,31 +309,119 @@ function Panel:onInput(keys)
local x,y = self:getMousePos(gui.ViewRect{rect=rect}) local x,y = self:getMousePos(gui.ViewRect{rect=rect})
if not x then return end if not x then return end
if self.resizable and y == 0 then
local now_ms = dfhack.getTickCount()
if now_ms - self.last_title_click_ms <= DOUBLE_CLICK_MS then
self.last_title_click_ms = 0
if Panel_on_double_click(self) then return true end
else
self.last_title_click_ms = now_ms
end
end
local resize_edge = nil
if self.resizable then
local rect = self.frame_rect
if self.resize_anchors.r and self.resize_anchors.b
and x == rect.x2 - rect.x1 and y == rect.y2 - rect.y1 then
resize_edge = 'rb'
elseif self.resize_anchors.l and self.resize_anchors.b
and x == 0 and y == rect.y2 - rect.y1 then
resize_edge = 'lb'
elseif self.resize_anchors.r and self.resize_anchors.t
and x == rect.x2 - rect.x1 and y == 0 then
resize_edge = 'rt'
elseif self.resize_anchors.r and self.resize_anchors.t
and x == 0 and y == 0 then
resize_edge = 'lt'
elseif self.resize_anchors.r and x == rect.x2 - rect.x1 then
resize_edge = 'r'
elseif self.resize_anchors.l and x == 0 then
resize_edge = 'l'
elseif self.resize_anchors.b and y == rect.y2 - rect.y1 then
resize_edge = 'b'
elseif self.resize_anchors.t and y == 0 then
resize_edge = 't'
end
end
local is_dragging = false local is_dragging = false
if self.draggable then if not resize_edge and self.draggable then
local on_body = self:getMousePos() local on_body = self:getMousePos()
is_dragging = (self.drag_anchors.title and self.frame_style and y == 0) is_dragging = (self.drag_anchors.title and self.frame_style and y == 0)
or (self.drag_anchors.frame and not on_body) -- includes inset or (self.drag_anchors.frame and not on_body) -- includes inset
or (self.drag_anchors.body and on_body) or (self.drag_anchors.body and on_body)
end end
if is_dragging then if resize_edge or is_dragging then
Panel_begin_drag(self, {x=x, y=y}) Panel_begin_drag(self, {x=x, y=y}, resize_edge)
return true return true
end end
end end
function Panel:setKeyboardDragEnabled(enabled) function Panel:setKeyboardDragEnabled(enabled)
if (enabled and self.keyboard_drag) if (enabled and self.kbd_get_pos)
or (not enabled and not self.keyboard_drag) then or (not enabled and not self.kbd_get_pos) then
return return
end end
if enabled then if enabled then
Panel_begin_drag(self) local kbd_get_pos = function() return {x=0, y=0} end
Panel_begin_drag(self, kbd_get_pos())
self.kbd_get_pos = kbd_get_pos
else
Panel_end_drag(self)
end
end
local function Panel_get_resize_data(self)
local resize_anchors = self.resize_anchors
if resize_anchors.r and resize_anchors.b then
return 'rb', function()
return {x=self.frame_rect.x2, y=self.frame_rect.y2} end
elseif resize_anchors.l and resize_anchors.b then
return 'lb', function()
return {x=self.frame_rect.x1, y=self.frame_rect.y2} end
elseif resize_anchors.r and resize_anchors.t then
return 'rt', function()
return {x=self.frame_rect.x2, y=self.frame_rect.y1} end
elseif resize_anchors.l and resize_anchors.t then
return 'lt', function()
return {x=self.frame_rect.x1, y=self.frame_rect.y1} end
elseif resize_anchors.b then
return 'b', function()
return {x=(self.frame_rect.x1+self.frame_rect.x2)/2,
y=self.frame_rect.y2} end
elseif resize_anchors.r then
return 'r', function()
return {x=self.frame_rect.x2,
y=(self.frame_rect.y1+self.frame_rect.y2)/2} end
elseif resize_anchors.l then
return 'l', function()
return {x=self.frame_rect.x1,
y=(self.frame_rect.y1+self.frame_rect.y2)/2} end
elseif resize_anchors.t then
return 't', function()
return {x=(self.frame_rect.x1+self.frame_rect.x2)/2,
y=self.frame_rect.y1} end
end
end
function Panel:setKeyboardResizeEnabled(enabled)
if (enabled and self.kbd_get_pos)
or (not enabled and not self.kbd_get_pos) then
return
end
if enabled then
local resize_edge, kbd_get_pos = Panel_get_resize_data(self)
if not resize_edge then
dfhack.printerr('cannot resize window: no anchors are enabled')
else
Panel_begin_drag(self, kbd_get_pos(), resize_edge)
self.kbd_get_pos = kbd_get_pos
end
else else
Panel_end_drag(self) Panel_end_drag(self)
end end
self.keyboard_drag = enabled
end end
function Panel:onRenderBody(dc) function Panel:onRenderBody(dc)
@ -262,14 +462,31 @@ end
function Panel:onRenderFrame(dc, rect) function Panel:onRenderFrame(dc, rect)
Panel.super.onRenderFrame(self, dc, rect) Panel.super.onRenderFrame(self, dc, rect)
if not self.frame_style then return end if not self.frame_style then return end
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2 gui.paint_frame(dc, rect, self.frame_style, self.frame_title)
gui.paint_frame(x1, y1, x2, y2, self.frame_style, self.frame_title) if self.kbd_get_pos then
if self.drag_offset and not self.keyboard_drag local pos = self.kbd_get_pos()
local pen = dfhack.pen.parse{fg=COLOR_GREEN, bg=COLOR_BLACK}
dc:seek(pos.x, pos.y):pen(pen):char(string.char(0xDB))
end
if self.drag_offset and not self.kbd_get_pos
and df.global.enabler.mouse_lbut == 0 then and df.global.enabler.mouse_lbut == 0 then
Panel_end_drag(self, nil, true) Panel_end_drag(self, nil, true)
end end
end end
------------
-- Window --
------------
Window = defclass(Window, Panel)
Window.ATTRS {
frame_style = gui.GREY_LINE_FRAME,
frame_background = gui.CLEAR_PEN,
frame_inset = 1,
draggable = true,
}
------------------- -------------------
-- ResizingPanel -- -- ResizingPanel --
------------------- -------------------