Merge pull request #2558 from myk002/myk_z

refine and document ZScreen
develop
Myk 2023-01-06 19:09:14 -08:00 committed by GitHub
commit e24d81cb4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 176 additions and 20 deletions

@ -56,6 +56,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``widgets.Window``: Panel subclass with attributes preset for top-level windows
- `overlay`: ``OverlayWidget`` now inherits from ``Panel`` instead of ``Widget`` to get all the frame and mouse integration goodies
- ``dfhack.gui.getDFViewscreen()``: returns the topmost underlying DF viewscreen
- ``gui.ZScreen``: Screen subclass that implements window raising, multi-viewscreen input handling, and viewscreen event pass-through so the underlying map can be interacted with and dragged around while DFHack screens are visible
- ``gui.View``: new function: ``view:getMouseFramePos()`` for detecting whether the mouse is within (or over) the exterior frame of a view
## Internals

@ -3921,8 +3921,13 @@ The class has the following methods:
* ``view:getMousePos([view_rect])``
Returns the mouse *x,y* in coordinates local to the given ViewRect (or
``frame_body`` if no ViewRect is passed) if it is within its clip area,
or nothing otherwise.
``frame_body`` if no ViewRect is passed) if it is within its clip area, or
nothing otherwise.
* ``view:getMouseFramePos()``
Returns the mouse *x,y* in coordinates local to ``frame_rect`` if it is
within its clip area, or nothing otherwise.
* ``view:updateLayout([parent_rect])``
@ -4005,7 +4010,7 @@ The class has the following methods:
Screen class
------------
This is a View subclass intended for use as a stand-alone dialog or screen.
This is a View subclass intended for use as a stand-alone modal dialog or screen.
It adds the following methods:
* ``screen:isShown()``
@ -4073,6 +4078,105 @@ It adds the following methods:
Defined as callbacks for native code.
ZScreen class
-------------
A screen subclass that allows the underlying viewscreens to be interacted with.
For example, a DFHack GUI tool implemented as a ZScreen can allow the player to
interact with the underlying map. That is, even when the DFHack tool window is
visible, players will be able to use vanilla designation tools, select units, or
scan/drag the map around.
If multiple ZScreens are on the stack and the player clicks on a visible element
of a non-top ZScreen, that ZScreen will be raised to the top of the viewscreen
stack. This allows multiple DFHack gui tools to be usable at the same time.
Clicks that are not over any visible ZScreen element, of course, are passed
through to the underlying viewscreen.
If :kbd:`Esc` or the right mouse button is pressed, and the ZScreen widgets
don't otherwise handle them, then the top ZScreen is dismissed. If the ZScreen
is "locked", then the screen is not dismissed and the input is passed on to the
underlying DF viewscreen. :kbd:`Alt`:kbd:`L` toggles the locked status if the
ZScreen widgets don't otherwise handle that key sequence. If you have a
``Panel`` with the ``lockable`` attribute set and a frame that has pens defined
for the lock icon (like ``Window`` widgets have by default), then a lock icon
will appear in the upper right corner of the frame. Clicking on this icon will
toggle the ZScreen ``locked`` status just as if :kbd:`Alt`:kbd:`L` had been
pressed.
Keyboard input goes to the top ZScreen, as usual. If the subviews of the top
ZScreen don't handle the input (i.e. they all return something falsey), the
input is passed directly to the first underlying non-ZScreen.
All this behavior is implemented in ``ZScreen:onInput()``, which subclasses
should *not* override. Instead, ZScreen subclasses should delegate all input
processing to subviews. Consider using a `Window class`_ widget as your top
level input processor.
When rendering, the parent viewscreen is automatically rendered first, so
subclasses do not have to call ``self:renderParent()``. Calls to ``logic()``
(a world "tick" when playing the game) are also passed through, so the game
progresses normally and can be paused/unpaused as normal by the player.
ZScreens that handle the :kbd:`Space` key may want to provide an alternate way
to pause. Note that passing ``logic()`` calls through to the underlying map is
required for allowing the player to drag the map with the mouse.
ZScreen provides the following functions:
* ``zscreen:raise()``
Raises the ZScreen to the top of the viewscreen stack and returns a reference
to ``self``. A common pattern is to check if a tool dialog is already active
when the tool command is run and raise the existing dialog if it exists or
show a new dialog if it doesn't. See the sample code below for an example.
* ``zscreen:toggleLocked()``
Toggles whether the window closes on :kbd:`ESC` or r-click (unlocked) or not
(locked).
* ``zscreen:isMouseOver()``
The default implementation iterates over the direct subviews of the ZScreen
subclass and sees if ``getMouseFramePos()`` returns a position for any of
them. Subclasses can override this function if that logic is not appropriate.
Here is an example skeleton for a ZScreen tool dialog::
local gui = require('gui')
local widgets = require('gui.widgets')
MyWindow = defclass(MyWindow, widgets.Window)
MyWindow.ATTRS {
frame_title='My Window',
frame={w=50, h=45},
resizable=true, -- if resizing makes sense for your dialog
}
function MyWindow:init()
self:addviews{
-- add subviews here
}
end
function MyWindow:onInput(keys)
-- if required
end
MyScreen = defclass(MyScreen, gui.ZScreen)
MyScreen.ATTRS {
focus_path='myscreen',
}
function MyScreen:init()
self:addviews{MyWindow{}}
end
function MyScreen:onDismiss()
view = nil
end
view = view and view:raise() or MyScreen{}:show()
FramedScreen class
------------------
@ -4221,6 +4325,11 @@ Has attributes:
hitting :kbd:`Esc` (while resizing with the mouse or keyboard), or by calling
``Panel:setKeyboardResizeEnabled(false)`` (while resizing with the keyboard).
* ``lockable = bool`` (default: ``false``)
Determines whether the panel will draw a lock icon in its frame. See
`ZScreen class`_ for details.
* ``autoarrange_subviews = bool`` (default: ``false``)
* ``autoarrange_gap = int`` (default: ``0``)
@ -4282,7 +4391,7 @@ Window class
------------
Subclass of Panel; sets Panel attributes to useful defaults for a top-level
framed, draggable window.
framed, lockable, draggable window.
ResizingPanel class
-------------------

@ -702,12 +702,16 @@ function ZScreen:render(dc)
ZScreen.super.render(self, dc)
end
local function zscreen_is_top(self)
function ZScreen:isOnTop()
return dfhack.gui.getCurViewscreen(true) == self._native
end
function ZScreen:toggleLocked()
self.locked = not self.locked
end
function ZScreen:onInput(keys)
if not zscreen_is_top(self) then
if not self:isOnTop() then
if keys._MOUSE_L_DOWN and self:isMouseOver() then
self:raise()
else
@ -719,8 +723,17 @@ function ZScreen:onInput(keys)
if ZScreen.super.onInput(self, keys) then
return
end
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.CUSTOM_ALT_L then
self:toggleLocked()
return
end
if not self.locked and (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) then
self:dismiss()
-- ensure underlying DF screens don't also react to the click
df.global.enabler.mouse_rbut_down = 0
df.global.enabler.mouse_rbut = 0
return
end
@ -729,18 +742,18 @@ function ZScreen:onInput(keys)
end
end
-- move this viewscreen to the top of the stack (if it's not there already)
function ZScreen:raise()
if self:isDismissed() or zscreen_is_top(self) then
return
if self:isDismissed() or self:isOnTop() then
return self
end
dscreen.raise(self)
return self
end
-- subclasses should override this and return whether the mouse is over an
-- owned screen element
function ZScreen:isMouseOver()
return false
for _,sv in ipairs(self.subviews) do
if sv:getMouseFramePos() then return true end
end
end
--------------------------
@ -772,10 +785,13 @@ GREY_LINE_FRAME = {
rt_frame_pen = to_pen{ tile=903, ch=187, fg=COLOR_GREY, bg=COLOR_BLACK },
rb_frame_pen = to_pen{ tile=917, ch=188, fg=COLOR_GREY, bg=COLOR_BLACK },
title_pen = to_pen{ fg=COLOR_BLACK, bg=COLOR_GREY },
inactive_title_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
signature_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
locked_pen = to_pen{tile=779, ch=216, fg=COLOR_GREY, bg=COLOR_GREEN},
unlocked_pen = to_pen{tile=782, ch=216, fg=COLOR_GREY, bg=COLOR_BLACK},
}
function paint_frame(dc,rect,style,title)
function paint_frame(dc,rect,style,title,show_lock,locked,inactive)
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)
@ -796,7 +812,16 @@ function paint_frame(dc,rect,style,title)
if #tstr > x2-x1-1 then
tstr = string.sub(tstr,1,x2-x1-1)
end
dscreen.paintString(style.title_pen or pen, x, y1, tstr)
dscreen.paintString(inactive and style.inactive_title_pen or style.title_pen or pen,
x, y1, tstr)
end
if show_lock then
if locked and style.locked_pen then
dscreen.paintTile(style.locked_pen, x2-1, y1)
elseif not locked and style.unlocked_pen then
dscreen.paintTile(style.unlocked_pen, x2-1, y1)
end
end
end

@ -59,11 +59,11 @@ function MessageBox:onDestroy()
end
function MessageBox:onInput(keys)
if keys.SELECT or keys.LEAVESCREEN then
if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self:dismiss()
if keys.SELECT and self.on_accept then
self.on_accept()
elseif keys.LEAVESCREEN and self.on_cancel then
elseif (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) and self.on_cancel then
self.on_cancel()
end
return true
@ -130,7 +130,7 @@ function InputBox:onInput(keys)
self.on_input(self.subviews.edit.text)
end
return true
elseif keys.LEAVESCREEN then
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self:dismiss()
if self.on_cancel then
self.on_cancel()
@ -231,7 +231,7 @@ function ListBox:getWantedFrameSize()
end
function ListBox:onInput(keys)
if keys.LEAVESCREEN then
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self:dismiss()
if self.on_cancel then
self.on_cancel()

@ -81,6 +81,7 @@ Panel.ATTRS {
resize_min = DEFAULT_NIL,
on_resize_begin = DEFAULT_NIL,
on_resize_end = DEFAULT_NIL,
lockable = false,
autoarrange_subviews = false, -- whether to automatically lay out subviews
autoarrange_gap = 0, -- how many blank lines to insert between widgets
}
@ -464,7 +465,14 @@ end
function Panel:onRenderFrame(dc, rect)
Panel.super.onRenderFrame(self, dc, rect)
if not self.frame_style then return end
gui.paint_frame(dc, rect, self.frame_style, self.frame_title)
local locked = nil
if self.lockable then
locked = self.parent_view and self.parent_view.locked
end
local inactive = self.parent_view and self.parent_view.isOnTop
and not self.parent_view:isOnTop()
gui.paint_frame(dc, rect, self.frame_style, self.frame_title,
self.lockable, locked, inactive)
if self.kbd_get_pos then
local pos = self.kbd_get_pos()
local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK}
@ -487,8 +495,20 @@ Window.ATTRS {
frame_background = gui.CLEAR_PEN,
frame_inset = 1,
draggable = true,
lockable = true,
}
function Window:onInput(keys)
if keys._MOUSE_L_DOWN and self.parent_view and self.parent_view.toggleLocked then
local x,y = dscreen.getMousePos()
local frame_rect = self.frame_rect
if x == frame_rect.x2-1 and y == frame_rect.y1 then
self.parent_view:toggleLocked()
end
end
return Window.super.onInput(self, keys)
end
-------------------
-- ResizingPanel --
-------------------