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

@ -3921,8 +3921,13 @@ The class has the following methods:
* ``view:getMousePos([view_rect])`` * ``view:getMousePos([view_rect])``
Returns the mouse *x,y* in coordinates local to the given ViewRect (or 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, ``frame_body`` if no ViewRect is passed) if it is within its clip area, or
or nothing otherwise. 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])`` * ``view:updateLayout([parent_rect])``
@ -4005,7 +4010,7 @@ The class has the following methods:
Screen class 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: It adds the following methods:
* ``screen:isShown()`` * ``screen:isShown()``
@ -4073,6 +4078,105 @@ It adds the following methods:
Defined as callbacks for native code. 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 FramedScreen class
------------------ ------------------
@ -4221,6 +4325,11 @@ Has attributes:
hitting :kbd:`Esc` (while resizing with the mouse or keyboard), or by calling hitting :kbd:`Esc` (while resizing with the mouse or keyboard), or by calling
``Panel:setKeyboardResizeEnabled(false)`` (while resizing with the keyboard). ``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_subviews = bool`` (default: ``false``)
* ``autoarrange_gap = int`` (default: ``0``) * ``autoarrange_gap = int`` (default: ``0``)
@ -4282,7 +4391,7 @@ Window class
------------ ------------
Subclass of Panel; sets Panel attributes to useful defaults for a top-level Subclass of Panel; sets Panel attributes to useful defaults for a top-level
framed, draggable window. framed, lockable, draggable window.
ResizingPanel class ResizingPanel class
------------------- -------------------

@ -702,12 +702,16 @@ function ZScreen:render(dc)
ZScreen.super.render(self, dc) ZScreen.super.render(self, dc)
end end
local function zscreen_is_top(self) function ZScreen:isOnTop()
return dfhack.gui.getCurViewscreen(true) == self._native return dfhack.gui.getCurViewscreen(true) == self._native
end end
function ZScreen:toggleLocked()
self.locked = not self.locked
end
function ZScreen:onInput(keys) 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 if keys._MOUSE_L_DOWN and self:isMouseOver() then
self:raise() self:raise()
else else
@ -719,8 +723,17 @@ function ZScreen:onInput(keys)
if ZScreen.super.onInput(self, keys) then if ZScreen.super.onInput(self, keys) then
return return
end 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() 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 return
end end
@ -729,18 +742,18 @@ function ZScreen:onInput(keys)
end end
end end
-- move this viewscreen to the top of the stack (if it's not there already)
function ZScreen:raise() function ZScreen:raise()
if self:isDismissed() or zscreen_is_top(self) then if self:isDismissed() or self:isOnTop() then
return return self
end end
dscreen.raise(self) dscreen.raise(self)
return self
end end
-- subclasses should override this and return whether the mouse is over an
-- owned screen element
function ZScreen:isMouseOver() function ZScreen:isMouseOver()
return false for _,sv in ipairs(self.subviews) do
if sv:getMouseFramePos() then return true end
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 }, 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 }, 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 }, 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 }, 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 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 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)
@ -796,7 +812,16 @@ function paint_frame(dc,rect,style,title)
if #tstr > x2-x1-1 then if #tstr > x2-x1-1 then
tstr = string.sub(tstr,1,x2-x1-1) tstr = string.sub(tstr,1,x2-x1-1)
end 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
end end

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

@ -81,6 +81,7 @@ Panel.ATTRS {
resize_min = DEFAULT_NIL, resize_min = DEFAULT_NIL,
on_resize_begin = DEFAULT_NIL, on_resize_begin = DEFAULT_NIL,
on_resize_end = DEFAULT_NIL, on_resize_end = DEFAULT_NIL,
lockable = false,
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
} }
@ -464,7 +465,14 @@ 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
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 if self.kbd_get_pos then
local pos = self.kbd_get_pos() local pos = self.kbd_get_pos()
local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK} local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK}
@ -487,8 +495,20 @@ Window.ATTRS {
frame_background = gui.CLEAR_PEN, frame_background = gui.CLEAR_PEN,
frame_inset = 1, frame_inset = 1,
draggable = true, 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 -- -- ResizingPanel --
------------------- -------------------