diff --git a/docs/changelog.txt b/docs/changelog.txt index 590734864..49cdc4dbf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -47,6 +47,7 @@ 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 ## Internals diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index e1952158b..30e4d08b6 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4147,7 +4147,9 @@ Base of all the widgets. Inherits from View and has the following attributes: Panel class ----------- -Inherits from Widget, and intended for framing and/or grouping subviews. +Inherits from Widget, and intended for framing and/or grouping subviews. It is +a good class to choose for your "main screen" since it supports window dragging +and frames. Has attributes: @@ -4159,8 +4161,32 @@ Has attributes: Called from ``onRenderBody``. -* ``autoarrange_subviews = bool`` (default: false) -* ``autoarrange_gap = int`` (default: 0) +* ``on_layout = function(frame_body)`` + + Called from ``postComputeFrame``. + +* ``draggable = bool`` (default: ``false``) +* ``drag_anchors = {}`` (default: ``{title=true, frame=false, body=false}``) +* ``drag_bound = 'frame' or 'body'`` (default: ``'frame'``) +* ``on_drag_begin = function()`` (default: ``nil``) +* ``on_drag_end = function(bool)`` (default: ``nil``) + + If ``draggable`` is set to ``true``, then the above attributes come into play + when the panel is dragged around the screen, either with the mouse or the + keyboard. ``drag_anchors`` sets which parts of the panel can be clicked on + with the left mouse button to start dragging. ``drag_bound`` configures + whether the frame of the panel (if any) can be dragged outside the containing + parent's boundary. The body will never be draggable outside of the parent, + but you can allow the frame to cross the boundary by setting ``drag_bound`` to + ``'body'``. The boolean passed to the ``on_drag_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 dragging with the + mouse, hitting :kbd:`Esc` (while dragging with the mouse or keyboard), or by + calling ``Panel:setCursorMoveEnabled(false)`` (while dragging with the + keyboard). + +* ``autoarrange_subviews = bool`` (default: ``false``) +* ``autoarrange_gap = int`` (default: ``0``) If ``autoarrange_subviews`` is set to ``true``, the Panel will automatically handle subview layout. Subviews are laid out vertically @@ -4169,13 +4195,27 @@ Has attributes: height or become visible/hidden and you don't have to worry about recalculating subview positions. -* ``frame_style``, ``frame_title`` (default: nil) +* ``frame_style``, ``frame_title`` (default: ``nil``) + If defined, a frame will be drawn around the panel and subviews will be inset by 1. The attributes are identical to what is defined in the `FramedScreen class`_. When using the predefined frame styles in the ``gui`` module, remember to ``require`` the gui module and prefix the identifier with ``gui.``, e.g. ``gui.GREY_LINE_FRAME``. +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 + 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 + amounts. Hit :kbd:`Enter` to commit the new window position or :kbd:`Esc` to + cancel. If dragging is canceled, then the window is moved back to its original + position. + ResizingPanel class ------------------- diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3779f8954..a46bd14a3 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -3,6 +3,7 @@ local _ENV = mkmodule('gui.widgets') local gui = require('gui') +local guidm = require('gui.dwarfmode') local utils = require('utils') local dscreen = dfhack.screen @@ -70,14 +71,159 @@ Panel.ATTRS { frame_title = DEFAULT_NIL, -- as in gui.FramedScreen on_render = DEFAULT_NIL, on_layout = DEFAULT_NIL, + draggable = false, + drag_anchors = copyall({title=true, frame=false, body=false}), + drag_bound = 'frame', -- or 'body' + on_drag_begin = DEFAULT_NIL, + on_drag_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 + 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:addviews(args.subviews) end +local function Panel_update_frame(self, frame, clear_state) + if clear_state then + self.keyboard_drag = nil + self.saved_frame = nil + self.saved_frame_rect = nil + self.drag_offset = nil + end + if not frame then return end + if self.frame.l == frame.l and self.frame.r == frame.r + and self.frame.t == frame.t and self.frame.b == frame.b + and self.frame.w == frame.w and self.frame.h == frame.h then + return + end + self.frame = frame + self:updateLayout() +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 + local bound_rect = self.drag_bound == 'body' and self.frame_body + or frame_rect + local offset = self.drag_offset + local max_width = parent_rect.width - (bound_rect.x2-frame_rect.x1+1) + local max_height = parent_rect.height - (bound_rect.y2-frame_rect.y1+1) + if frame.t or not frame.b then + local min_pos = frame_rect.y1 - bound_rect.y1 + local requested_pos = mouse_pos.y - parent_rect.y1 - offset.y + frame.t = math.max(min_pos, math.min(max_height, requested_pos)) + end + if frame.b or not frame.t then + local min_pos = bound_rect.y2 - frame_rect.y2 + local requested_pos = parent_rect.y2 - mouse_pos.y + offset.y - + (frame_rect.y2 - frame_rect.y1) + frame.b = math.max(min_pos, math.min(max_height, requested_pos)) + end + if frame.l or not frame.r then + local min_pos = frame_rect.x1 - bound_rect.x1 + local requested_pos = mouse_pos.x - parent_rect.x1 - offset.x + frame.l = math.max(min_pos, math.min(max_width, requested_pos)) + end + if frame.r or not frame.l then + local min_pos = bound_rect.x2 - frame_rect.x2 + local requested_pos = parent_rect.x2 - mouse_pos.x + offset.x - + (frame_rect.x2 - frame_rect.x1) + frame.r = math.max(min_pos, math.min(max_width, requested_pos)) + end + return frame +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) +end + +local function Panel_begin_drag(self, drag_offset) + Panel_update_frame(self, nil, true) + self.drag_offset = drag_offset or {x=0, y=0} + 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 +end + +local function Panel_end_drag(self, frame, 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 +end + +function Panel:onInput(keys) + if self.keyboard_drag then + if keys.SELECT or keys.LEAVESCREEN then + Panel_end_drag(self, keys.LEAVESCREEN and self.saved_frame or nil, + not not keys.SELECT) + return true + end + 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} + Panel_update_frame(self, Panel_make_frame(self, kbd_pos)) + return true + end + end + return + end + if self.drag_offset then + if keys._MOUSE_R_DOWN then + Panel_end_drag(self, self.saved_frame) + elseif keys._MOUSE_L then + Panel_update_frame(self, Panel_make_frame(self)) + end + return true + end + if self:inputToSubviews(keys) then + return true + end + if not keys._MOUSE_L_DOWN then return end + local rect = self.frame_rect + local x,y = self:getMousePos(gui.ViewRect{rect=rect}) + if not x then return end + + local is_dragging = false + if 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}) + return true + end +end + +function Panel:setKeyboardDragEnabled(enabled) + if (enabled and self.keyboard_drag) + or (not enabled and not self.keyboard_drag) then + return + end + if enabled then + Panel_begin_drag(self) + else + Panel_end_drag(self) + end + self.keyboard_drag = enabled +end + function Panel:onRenderBody(dc) if self.on_render then self.on_render(dc) end end @@ -118,6 +264,10 @@ function Panel:onRenderFrame(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 + and df.global.enabler.mouse_lbut == 0 then + Panel_end_drag(self, nil, true) + end end -------------------