allow windows to be defocused instead of pinned

develop
Myk Taylor 2023-01-23 17:40:16 -08:00
parent 5ad6ce16e8
commit 8b98ba5042
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
5 changed files with 146 additions and 94 deletions

@ -42,6 +42,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `hotkeys`: clicking on the DFHack logo no longer closes the popup menu
- `gui/launcher`: sped up initialization time for faster load of the UI
- `orders`: orders plugin functionality is now offered via an overlay widget when the manager orders screen is open
- Many DFHack windows can now be unfocused by clicking somewhere not over the tool window. This has the same effect as pinning previously did, but without the extra clicking.
## Documentation

@ -4117,32 +4117,28 @@ It adds the following methods:
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 "pinned", then the screen is not dismissed and the input is passed on to the
underlying DF viewscreen. :kbd:`Alt`:kbd:`L` toggles the pinned status if the
ZScreen widgets don't otherwise handle that key sequence. If you have a
``Panel`` with the ``pinnable`` attribute set and a frame that has pens defined
for the pin icon (like ``Window`` widgets have by default), then a pin icon
will appear in the upper right corner of the frame. Clicking on this icon will
toggle the ZScreen ``pinned`` 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.
A screen subclass that allows multi-layer interactivity. For example, a DFHack
GUI tool implemented as a ZScreen can allow the player to interact with the
underlying map, or even other DFHack ZScreen windows! That is, even when the
DFHack tool window is visible, players will be able to use vanilla designation
tools, select units, and scan/drag the map around.
At most one ZScreen can have keyboard focus at a time. That ZScreen's widgets
will have a chance to handle the input before anything else. If unhandled, the
input skips all unfocused ZScreens under that ZScreen and is passed directly to
the first non-ZScreen viewscreen. There are class attributes that can be set to
control what kind of unhandled input is passed to the lower layers.
If multiple ZScreens are visible and the player left or right clicks on a
visible element of a non-focused ZScreen, that ZScreen will be given focus. This
allows multiple DFHack GUI tools to be usable at the same time. If the mouse is
clicked away from the ZScreen widgets, that ZScreen loses focus. If no ZScreen
has focus, all input is passed directly through to the first underlying
non-ZScreen viewscreen.
For a ZScreen with keyboard focus, if :kbd:`Esc` or the right mouse button is
pressed, and the ZScreen widgets don't otherwise handle them, then the ZScreen
is dismissed.
All this behavior is implemented in ``ZScreen:onInput()``, which subclasses
**must not override**. Instead, ZScreen subclasses should delegate all input
@ -4152,24 +4148,23 @@ 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.
progresses normally and can be paused/unpaused as normal by the player. Note
that passing ``logic()`` calls through to the underlying map is required for
allowing the player to drag the map with the mouse. ZScreen subclasses can set
attributes that control whether the game is paused when the ZScreen is shown and
whether the game is forced to continue being paused while the ZScreen is shown.
If pausing is forced, child ``Window`` widgets will show a force-pause icon to
indicate which tool is forcing the pausing.
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:togglePinned()``
Toggles whether the window closes on :kbd:`ESC` or r-click (unpinned) or not
(pinned).
Raises the ZScreen to the top of the viewscreen stack, gives it keyboard
focus, 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:isMouseOver()``
@ -4177,6 +4172,37 @@ ZScreen provides the following functions:
subclass and sees if ``getMouseFramePos()`` returns a position for any of
them. Subclasses can override this function if that logic is not appropriate.
* ``zscreen:hasFocus()``
Whether the ZScreen has keyboard focus. Subclasses will generally not need to
check this because they can assume if they are getting input, then they have
focus.
ZScreen subclasses can set the following attributes:
* ``initial_pause`` (default: ``true``)
Whether to pause the game when the ZScreen is shown.
* ``force_pause`` (default: ``false``)
Whether to ensure the game *stays* paused while the ZScreen is shown.
* ``pass_pause`` (default: ``true``)
Whether to pass the pause key to the lower viewscreens if it is not handled
by this ZScreen.
* ``pass_movement_keys`` (default: ``false``)
Whether to pass the map movement keys to the lower viewscreens if they ar not
handled by this ZScreen.
* ``pass_mouse_clicks`` (default: ``true``)
Whether to pass mouse clicks to the lower viewscreens if they are not handled
by this ZScreen.
Here is an example skeleton for a ZScreen tool dialog::
local gui = require('gui')
@ -4187,11 +4213,12 @@ Here is an example skeleton for a ZScreen tool dialog::
frame_title='My Window',
frame={w=50, h=45},
resizable=true, -- if resizing makes sense for your dialog
resize_min={w=50, h=20}, -- try to allow users to shrink your windows
}
function MyWindow:init()
self:addviews{
-- add subviews here
-- add subview widgets here
}
end
@ -4202,6 +4229,7 @@ Here is an example skeleton for a ZScreen tool dialog::
MyScreen = defclass(MyScreen, gui.ZScreen)
MyScreen.ATTRS {
focus_path='myscreen',
-- set pause and passthrough attributes as appropriate
}
function MyScreen:init()

@ -13,16 +13,18 @@ CLEAR_PEN = to_pen{tile=909, ch=32, fg=0, bg=0, write_to_lower=true}
TRANSPARENT_PEN = to_pen{tile=0, ch=0}
KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true}
local FAKE_INPUT_KEYS = {
local MOUSE_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_MOUSE_M = true,
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = true,
_MOUSE_M_DOWN = true,
_STRING = true,
}
local FAKE_INPUT_KEYS = copyall(MOUSE_KEYS)
FAKE_INPUT_KEYS._STRING = true
function simulateInput(screen,...)
local keys = {}
local function push_key(arg)
@ -692,8 +694,34 @@ end
-----------------------------
ZScreen = defclass(ZScreen, Screen)
ZScreen.ATTRS{
initial_pause=true,
force_pause=false,
pass_pause=true,
pass_movement_keys=false,
pass_mouse_clicks=true,
}
function ZScreen:init()
self.saved_pause_state = df.global.pause_state
if self.initial_pause then
df.global.pause_state = true
end
self.defocused = false
end
function ZScreen:onDestroy()
if self.force_pause or self.initial_pause then
-- never go from unpaused to paused, just from paused to unpaused
df.global.pause_state = df.global.pause_state or self.saved_pause_state
end
end
-- this is necessary for middle-click map scrolling to function
function ZScreen:onIdle()
if self.force_pause then
df.global.pause_state = true
end
if self._native and self._native.parent then
self._native.parent:logic()
end
@ -704,17 +732,15 @@ function ZScreen:render(dc)
ZScreen.super.render(self, dc)
end
function ZScreen:isOnTop()
return dfhack.gui.getCurViewscreen(true) == self._native
end
function ZScreen:togglePinned()
self.pinned = not self.pinned
function ZScreen:hasFocus()
return not self.defocused
and dfhack.gui.getCurViewscreen(true) == self._native
end
function ZScreen:onInput(keys)
if not self:isOnTop() then
if keys._MOUSE_L_DOWN and self:isMouseOver() then
local has_mouse = self:isMouseOver()
if not self:hasFocus() then
if (keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN) and has_mouse then
self:raise()
else
self:sendInputToParent(keys)
@ -734,30 +760,42 @@ function ZScreen:onInput(keys)
return
end
if keys.CUSTOM_ALT_L then
self:togglePinned()
if keys._MOUSE_L_DOWN and not has_mouse then
self.defocused = true
self:sendInputToParent(keys)
return
end
if (self:isMouseOver() or not self.pinned)
and (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) then
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self:dismiss()
-- ensure underlying DF screens don't also react to the click
-- ensure underlying DF screens don't also react to the rclick
df.global.enabler.mouse_rbut_down = 0
df.global.enabler.mouse_rbut = 0
return
end
if not keys._MOUSE_L or not self:isMouseOver() then
self:sendInputToParent(keys)
else
local passit = self.pass_pause and keys.D_PAUSE
if not passit and self.pass_mouse_clicks then
for key in pairs(MOUSE_KEYS) do
if keys[key] then
passit = true
break
end
end
end
if not passit and self.pass_movement_keys then
passit = require('gui.dwarfmode').getMapKey(keys)
end
if passit then
self:sendInputToParent(keys)
end
return
end
end
function ZScreen:raise()
if self:isDismissed() or self:isOnTop() then
if self:isDismissed() or self:hasFocus() then
return self
end
dscreen.raise(self)
self.defocused = false
return self
end
@ -809,8 +847,7 @@ local BASE_FRAME = {
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 },
pinned_pen = to_pen{tile=779, ch=216, fg=COLOR_GREY, bg=COLOR_GREEN},
unpinned_pen = to_pen{tile=782, ch=216, fg=COLOR_GREY, bg=COLOR_BLACK},
paused_pen = to_pen{tile=782, ch=216, fg=COLOR_GREY, bg=COLOR_BLACK},
}
local function make_frame(name, double_line)
@ -840,7 +877,7 @@ THIN_FRAME = make_frame('Thin', false)
-- for compatibility with pre-steam code
GREY_LINE_FRAME = WINDOW_FRAME
function paint_frame(dc,rect,style,title,show_pin,pinned,inactive)
function paint_frame(dc,rect,style,title,inactive, pause_forced)
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)
@ -865,27 +902,15 @@ function paint_frame(dc,rect,style,title,show_pin,pinned,inactive)
x, y1, tstr)
end
if show_pin then
if pinned and style.pinned_pen then
local pin_texpos = dfhack.textures.getGreenPinTexposStart()
if pin_texpos == -1 then
dscreen.paintTile(style.pinned_pen, x2-1, y1)
else
dscreen.paintTile(style.pinned_pen, x2-2, y1-1, nil, pin_texpos+0)
dscreen.paintTile(style.pinned_pen, x2-1, y1-1, nil, pin_texpos+1)
dscreen.paintTile(style.pinned_pen, x2-2, y1, nil, pin_texpos+2)
dscreen.paintTile(style.pinned_pen, x2-1, y1, nil, pin_texpos+3)
end
elseif not pinned and style.unpinned_pen then
local pin_texpos = dfhack.textures.getRedPinTexposStart()
if pin_texpos == -1 then
dscreen.paintTile(style.unpinned_pen, x2-1, y1)
else
dscreen.paintTile(style.unpinned_pen, x2-2, y1-1, nil, pin_texpos+0)
dscreen.paintTile(style.unpinned_pen, x2-1, y1-1, nil, pin_texpos+1)
dscreen.paintTile(style.unpinned_pen, x2-2, y1, nil, pin_texpos+2)
dscreen.paintTile(style.unpinned_pen, x2-1, y1, nil, pin_texpos+3)
end
if pause_forced then
local pause_texpos = dfhack.textures.getRedPinTexposStart()
if pause_texpos == -1 then
dscreen.paintTile(style.paused_pen, x2-1, y1)
else
dscreen.paintTile(style.paused_pen, x2-2, y1-1, nil, pause_texpos+0)
dscreen.paintTile(style.paused_pen, x2-1, y1-1, nil, pause_texpos+1)
dscreen.paintTile(style.paused_pen, x2-2, y1, nil, pause_texpos+2)
dscreen.paintTile(style.paused_pen, x2-1, y1, nil, pause_texpos+3)
end
end
end

@ -505,14 +505,11 @@ end
function Panel:onRenderFrame(dc, rect)
Panel.super.onRenderFrame(self, dc, rect)
if not self.frame_style then return end
local pinned = nil
if self.pinnable then
pinned = self.parent_view and self.parent_view.pinned
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.pinnable, pinned, inactive)
local inactive = self.parent_view and self.parent_view.hasFocus
and not self.parent_view:hasFocus()
local pause_forced = self.parent_view and self.parent_view.force_pause
gui.paint_frame(dc, rect, self.frame_style, self.frame_title, inactive,
pause_forced)
if self.kbd_get_pos then
local pos = self.kbd_get_pos()
local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK}

@ -297,6 +297,7 @@ end
MenuScreen = defclass(MenuScreen, gui.ZScreen)
MenuScreen.ATTRS {
focus_path='hotkeys/menu',
initial_pause=false,
hotspot=DEFAULT_NIL,
}