From 574be3fe73223d75114dd94bcc99395da293daf6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Jan 2023 21:50:43 -0800 Subject: [PATCH 1/8] provide a useful default impl of isMouseOver --- library/lua/gui.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 27484541f..a7f68dd25 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -737,10 +737,10 @@ function ZScreen:raise() dscreen.raise(self) end --- subclasses should override this and return whether the mouse is over an --- owned screen element +-- subclasses should either annotate their viewable panel with view_id='main' +-- or override this and return whether the mouse is over an owned screen element function ZScreen:isMouseOver() - return false + return self.subviews.main and self.subviews.main:getMouseFramePos() or false end -------------------------- From fbf895fe0cd7bdb0596f9a0e95f722c3c2b1aac2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Jan 2023 23:20:45 -0800 Subject: [PATCH 2/8] document ZScreen (and view:getMouseFramePos()) --- docs/dev/Lua API.rst | 62 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 8962d795a..ca3a6c9ed 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -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,57 @@ 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. + +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. Note that this is + handled automatically for common cases (e.g. player clicks on a Window + belonging to a ZScreen that is not the top viewscreen). + +* ``zscreen:isMouseOver()`` + + If the ZScreen subclass has a subview with a ``view_id`` equal to "main", + then the mouse will be considered to be over the visible viewscreen elements + when ``self.subviews.main:getMouseFramePos()`` returns a position. Subclasses + can override this function if that logic is not appropriate, for example if + there are multiple independent windows being shown and this function should + return true if the mouse is over any of them. FramedScreen class ------------------ From 96f19621c9ed5bb6492ce68b55e6fb8e9ad3656a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Jan 2023 23:24:44 -0800 Subject: [PATCH 3/8] update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index bfc01b322..cd4191c81 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 From f43358002dfe4cf69e24776ca1c5a51db6ceecaf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Jan 2023 10:35:22 -0800 Subject: [PATCH 4/8] Allow dialogs to close on r-click --- library/lua/gui/dialogs.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index e5448986f..e7228db1e 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -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() From fccefd1155499cc5ab437a91d78a10822706204e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Jan 2023 10:42:07 -0800 Subject: [PATCH 5/8] don't pass through handled r-clicks --- library/lua/gui.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index a7f68dd25..044d5a0fd 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -721,6 +721,9 @@ function ZScreen:onInput(keys) end if 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 From 1f5ae4165f6eec58cf01962279c2df73782923af Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Jan 2023 11:11:11 -0800 Subject: [PATCH 6/8] return self from raise, update docs --- docs/dev/Lua API.rst | 44 +++++++++++++++++++++++++++++++++++++++++--- library/lua/gui.lua | 9 +++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ca3a6c9ed..8c9655b69 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4117,9 +4117,10 @@ ZScreen provides the following functions: * ``zscreen:raise()`` - Raises the ZScreen to the top of the viewscreen stack. Note that this is - handled automatically for common cases (e.g. player clicks on a Window - belonging to a ZScreen that is not the top viewscreen). + 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:isMouseOver()`` @@ -4130,6 +4131,43 @@ ZScreen provides the following functions: there are multiple independent windows being shown and this function should return true if the mouse is over any of them. +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{view_id='main'}} + end + + function MyScreen:onDismiss() + view = nil + end + + view = view and view:raise() or MyScreen{}:show() + FramedScreen class ------------------ diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 044d5a0fd..4777f8186 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -702,12 +702,12 @@ 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: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 @@ -734,10 +734,11 @@ 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 either annotate their viewable panel with view_id='main' From 810430f1a2f217288ed40d9328b0b727622f46eb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Jan 2023 18:48:53 -0800 Subject: [PATCH 7/8] make windows lockable (can ignore r-click and esc) --- docs/dev/Lua API.rst | 33 ++++++++++++++++++++++++--------- library/lua/gui.lua | 31 +++++++++++++++++++++++++------ library/lua/gui/widgets.lua | 19 ++++++++++++++++++- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 8c9655b69..a3b91af18 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4094,7 +4094,15 @@ 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. +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 @@ -4122,14 +4130,16 @@ ZScreen provides the following functions: 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()`` - If the ZScreen subclass has a subview with a ``view_id`` equal to "main", - then the mouse will be considered to be over the visible viewscreen elements - when ``self.subviews.main:getMouseFramePos()`` returns a position. Subclasses - can override this function if that logic is not appropriate, for example if - there are multiple independent windows being shown and this function should - return true if the mouse is over any of them. + 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:: @@ -4159,7 +4169,7 @@ Here is an example skeleton for a ZScreen tool dialog:: } function MyScreen:init() - self:addviews{MyWindow{view_id='main'}} + self:addviews{MyWindow{}} end function MyScreen:onDismiss() @@ -4315,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``) @@ -4376,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 ------------------- diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 4777f8186..426458159 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -706,6 +706,10 @@ 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 self:isOnTop() then if keys._MOUSE_L_DOWN and self:isMouseOver() then @@ -719,7 +723,13 @@ 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 @@ -732,7 +742,6 @@ 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 self:isOnTop() then return self @@ -741,10 +750,10 @@ function ZScreen:raise() return self end --- subclasses should either annotate their viewable panel with view_id='main' --- or override this and return whether the mouse is over an owned screen element function ZScreen:isMouseOver() - return self.subviews.main and self.subviews.main:getMouseFramePos() or false + for _,sv in ipairs(self.subviews) do + if sv:getMouseFramePos() then return true end + end end -------------------------- @@ -777,9 +786,11 @@ GREY_LINE_FRAME = { 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 }, 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) 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) @@ -802,6 +813,14 @@ function paint_frame(dc,rect,style,title) end dscreen.paintString(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 FramedScreen = defclass(FramedScreen, Screen) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index e95410fb2..4df6d07ff 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -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,11 @@ 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 + gui.paint_frame(dc, rect, self.frame_style, self.frame_title, self.lockable, locked) if self.kbd_get_pos then local pos = self.kbd_get_pos() local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK} @@ -487,8 +492,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 -- ------------------- From 093eac3eb2fc3ff05467684655b22472839c8058 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Jan 2023 18:58:08 -0800 Subject: [PATCH 8/8] use a black background for non-top ZScreen titles --- library/lua/gui.lua | 6 ++++-- library/lua/gui/widgets.lua | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 426458159..661351d31 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -785,12 +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,show_lock,locked) +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) @@ -811,7 +812,8 @@ function paint_frame(dc,rect,style,title,show_lock,locked) 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 diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 4df6d07ff..9d8e30d18 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -469,7 +469,10 @@ function Panel:onRenderFrame(dc, rect) if self.lockable then locked = self.parent_view and self.parent_view.locked end - gui.paint_frame(dc, rect, self.frame_style, self.frame_title, self.lockable, locked) + 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}