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 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 8962d795a..a3b91af18 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,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 ------------------- diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 27484541f..661351d31 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -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 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() diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index e95410fb2..9d8e30d18 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,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 -- -------------------