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
- ``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

@ -4182,9 +4182,28 @@ Has attributes:
``true`` if the drag was "successful" (i.e. not canceled) and ``false``
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
calling ``Panel:setCursorMoveEnabled(false)`` (while dragging with the
calling ``Panel:setKeyboaredDragEnabled(false)`` (while dragging with the
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_gap = int`` (default: ``0``)
@ -4207,8 +4226,8 @@ Has functions:
* ``panel:setKeyboardDragEnabled(bool)``
If called with something truthy and the panel is not already in keyboard drag
mode, then any current drag operations are halted where they are (not
If called with ``true`` and the panel is not already in keyboard drag 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
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
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
-------------------
Subclass of Panel; automatically adjusts its own frame height according to
the size, position, and visibility of its subviews. Pairs nicely with a
parent Panel that has ``autoarrange_subviews`` enabled.
Subclass of Panel; automatically adjusts its own frame height and width to the
minimum required to show its subviews. Pairs nicely with a parent Panel that has
``autoarrange_subviews`` enabled.
Pages class
-----------

@ -704,8 +704,9 @@ GREY_LINE_FRAME = {
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 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.rt_frame_pen or pen, x2, y1)
dscreen.paintTile(style.lb_frame_pen or pen, x1, y2)
@ -750,16 +751,13 @@ function FramedScreen:computeFrame(parent_rect)
end
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
dc:clear()
else
self:renderParent()
dc:fill(rect, self.frame_background)
end
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
paint_frame(dc,rect,self.frame_style,self.frame_title)
end
return _ENV

@ -64,6 +64,8 @@ end
-- Panel --
-----------
DOUBLE_CLICK_MS = 500
Panel = defclass(Panel, Widget)
Panel.ATTRS {
@ -72,28 +74,47 @@ Panel.ATTRS {
on_render = DEFAULT_NIL,
on_layout = DEFAULT_NIL,
draggable = false,
drag_anchors = copyall({title=true, frame=false, body=false}),
drag_anchors = DEFAULT_NIL,
drag_bound = 'frame', -- or 'body'
on_drag_begin = 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_gap = 0, -- how many blank lines to insert between widgets
}
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_rect = nil -- copy of frame_rect when dragging started
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)
end
local function Panel_update_frame(self, frame, clear_state)
if clear_state then
self.keyboard_drag = nil
self.kbd_get_pos = nil
self.saved_frame = nil
self.saved_frame_rect = nil
self.drag_offset = nil
self.resize_edge = nil
end
if not frame then return end
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()
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 frame = copyall(self.frame)
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)
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
local function Panel_begin_drag(self, drag_offset)
local function Panel_begin_drag(self, drag_offset, resize_edge)
Panel_update_frame(self, nil, true)
self.drag_offset = drag_offset or {x=0, y=0}
self.resize_edge = resize_edge
self.saved_frame = copyall(self.frame)
self.saved_frame_rect = copyall(self.frame_rect)
self.prev_focus_owner = self.focus_group.cur
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
local function Panel_end_drag(self, frame, success)
success = not not success
if self.prev_focus_owner then
self.prev_focus_owner:setFocus(true)
else
self:setFocus(false)
end
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
function Panel:onInput(keys)
if self.keyboard_drag then
if self.kbd_get_pos then
if keys.SELECT or keys.LEAVESCREEN then
Panel_end_drag(self, keys.LEAVESCREEN and self.saved_frame or nil,
not not keys.SELECT)
@ -173,8 +283,10 @@ function Panel:onInput(keys)
for code in pairs(keys) do
local dx, dy = guidm.get_movement_delta(code, 1, 10)
if dx then
local kbd_pos = {x=self.frame_rect.x1+dx,
y=self.frame_rect.y1+dy}
local frame_rect = self.frame_rect
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))
return true
end
@ -197,31 +309,119 @@ function Panel:onInput(keys)
local x,y = self:getMousePos(gui.ViewRect{rect=rect})
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
if self.draggable then
if not resize_edge and self.draggable then
local on_body = self:getMousePos()
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.body and on_body)
end
if is_dragging then
Panel_begin_drag(self, {x=x, y=y})
if resize_edge or is_dragging then
Panel_begin_drag(self, {x=x, y=y}, resize_edge)
return true
end
end
function Panel:setKeyboardDragEnabled(enabled)
if (enabled and self.keyboard_drag)
or (not enabled and not self.keyboard_drag) then
if (enabled and self.kbd_get_pos)
or (not enabled and not self.kbd_get_pos) then
return
end
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
Panel_end_drag(self)
end
self.keyboard_drag = enabled
end
function Panel:onRenderBody(dc)
@ -262,14 +462,31 @@ end
function Panel:onRenderFrame(dc, rect)
Panel.super.onRenderFrame(self, dc, rect)
if not self.frame_style then return end
local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
gui.paint_frame(x1, y1, x2, y2, self.frame_style, self.frame_title)
if self.drag_offset and not self.keyboard_drag
gui.paint_frame(dc, rect, self.frame_style, self.frame_title)
if self.kbd_get_pos then
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
Panel_end_drag(self, nil, true)
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 --
-------------------