From 8b98ba5042f7ee33d92ad8f2b284c5e771a6820f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 23 Jan 2023 17:40:16 -0800 Subject: [PATCH] allow windows to be defocused instead of pinned --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 108 +++++++++++++++++++++------------ library/lua/gui.lua | 117 ++++++++++++++++++++++-------------- library/lua/gui/widgets.lua | 13 ++-- plugins/lua/hotkeys.lua | 1 + 5 files changed, 146 insertions(+), 94 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 073837eff..daba5b791 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 63b533d61..a832639e6 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -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() diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 524978baf..af9d68870 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -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 diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 4538c39df..21f79c0ef 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -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} diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 99ba08a05..c377666c2 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -297,6 +297,7 @@ end MenuScreen = defclass(MenuScreen, gui.ZScreen) MenuScreen.ATTRS { focus_path='hotkeys/menu', + initial_pause=false, hotspot=DEFAULT_NIL, }