From 26a603d3b2cbbc21416d769dd5888e1547f9825e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 18 Jul 2024 21:43:39 +0200 Subject: [PATCH 01/25] Add simple Table of Contents to journal --- gui/journal.lua | 56 +++++++++++++++++++++++++++++--- internal/journal/text_editor.lua | 4 +++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 32c74dabb..6ea1b01b8 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -79,13 +79,33 @@ function JournalScreen:init(options) subviews={ text_editor.TextEditor{ view_id='journal_editor', - frame={l=1, t=1, b=1, r=0}, + frame={l=1, t=1, b=1, r=30}, text=content, - on_change=function(text) self:saveContextContent(text) end - } + on_change=function(text) self:onTextChange(text) end + }, + + widgets.List{ + view_id='table_of_contents', + frame={r=0, t=2, b=1, w=30}, + choices={}, + icon_width=2, + on_submit=self:callback('onTableOfContentsSubmit') + -- on_submit=self:callback('onSubmit'), + -- on_submit2=self:callback('onSubmit2'), + }, + -- widgets.Panel{ + -- view_id='table_of_contents', + -- frame={l=1,t=1, b=1, w=30} + -- } } - } + }, } + + self:reloadTableOfContents(content) +end + +function JournalScreen:onTableOfContentsSubmit(ind, choice) + self.subviews.journal_editor:setCursor(choice.line_cursor) end function JournalScreen:loadContextContent() @@ -95,6 +115,34 @@ function JournalScreen:loadContextContent() return site_data.text ~= nil and site_data.text[1] or '' end +function JournalScreen:onTextChange(text) + self:saveContextContent(text) + self:reloadTableOfContents(text) +end + +function JournalScreen:reloadTableOfContents(text) + local sections = {} + + local line_cursor = 1 + for line in text:gmatch("[^\n]*") do + local header, section = line:match("^(#+)%s(.+)") + if header ~= nil then + table.insert(sections, { + -- line_cur, + -- #header, + line_cursor=line_cursor, + text=string.rep(" ", #header - 1) .. section, + -- cat=cat, + -- icon=icon, + }) + end + + line_cursor = line_cursor + #line + 1 + end + + self.subviews.table_of_contents:setChoices(sections) +end + function JournalScreen:saveContextContent(text) if self.save_on_change and dfhack.isWorldLoaded() then dfhack.persistent.saveSiteData(JOURNAL_PERSIST_KEY, {text={text}}) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 913734c66..3bbce9e66 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -59,6 +59,10 @@ function TextEditor:init() self:setFocus(true) end +function TextEditor:setCursor(cursor_offset) + return self.subviews.text_area:setCursor(cursor_offset) +end + function TextEditor:getPreferredFocusState() return true end From 04dc395f69ccd1f691fef6a68333dab1cd4161fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 12:03:53 +0200 Subject: [PATCH 02/25] Move journal table of contents to right and add resize control --- gui/journal.lua | 105 +++++++++++++++++++++---------- internal/journal/text_editor.lua | 9 ++- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 6ea1b01b8..4eb7460f3 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -18,12 +18,47 @@ JournalWindow.ATTRS { frame_title='DF Journal', resizable=true, resize_min=RESIZE_MIN, - frame_inset=0 + frame_inset=0, + content='' } function JournalWindow:init() local config_frame = copyall(journal_config.data.frame or {}) self.frame = self:sanitizeFrame(config_frame) + + self:addviews({ + widgets.Panel{ + view_id='table_of_contents_panel', + frame_title='Table of contents', + frame_style = gui.FRAME_INTERIOR, + + resizable=true, + + frame_background = gui.CLEAR_PEN, + + resize_min={w=20}, + resize_anchors={r=true}, + frame={l=0, t=1, b=1, w=30}, + on_resize_begin=self:callback('onPanelResizeBegin'), + on_resize_end=self:callback('onPanelResizeEnd'), + subviews={ + widgets.List{ + frame={l=1,t=1}, + view_id='table_of_contents', + choices={}, + on_submit=self:callback('onTableOfContentsSubmit') + }, + } + }, + text_editor.TextEditor{ + view_id='journal_editor', + frame={t=2, b=1, l=31, r=0}, + resize_min={w=30, h=10}, + frame_inset={r=1}, + text=self.content, + on_change=function(text) self:onTextChange(text) end + }, + }) end function JournalWindow:sanitizeFrame(frame) @@ -59,6 +94,40 @@ function JournalWindow:saveConfig() journal_config:write() end +function JournalWindow:onPanelResizeBegin() + self.resizing_panels = true +end + +function JournalWindow:onPanelResizeEnd() + self.resizing_panels = false +end + +function JournalWindow:esnurePanelsRelSize() + self.subviews.table_of_contents_panel.frame.w = math.min( + self.subviews.table_of_contents_panel.frame.w, + self.frame.w - self.subviews.journal_editor.resize_min.w + ) + self.subviews.journal_editor.frame.l = self.subviews.table_of_contents_panel.frame.w + 1 +end + +function JournalWindow:preUpdateLayout() + self:esnurePanelsRelSize() +end + +function JournalWindow:onRenderBody(painter) + if self.resizing_panels then + self:esnurePanelsRelSize() + self:updateLayout() + end + + return JournalWindow.super.onRenderBody(self, painter) +end + +function JournalWindow:onTableOfContentsSubmit(ind, choice) + self.subviews.journal_editor:setCursor(choice.line_cursor) + self.subviews.journal_editor:scrollToCursor(choice.line_cursor) +end + JournalScreen = defclass(JournalScreen, gui.ZScreen) JournalScreen.ATTRS { focus_path='journal', @@ -71,43 +140,17 @@ function JournalScreen:init(options) self:addviews{ JournalWindow{ view_id='journal_window', - frame_title='DF Journal', frame={w=65, h=45}, resizable=true, - resize_min={w=32, h=10}, + resize_min={w=50, h=20}, frame_inset=0, - subviews={ - text_editor.TextEditor{ - view_id='journal_editor', - frame={l=1, t=1, b=1, r=30}, - text=content, - on_change=function(text) self:onTextChange(text) end - }, - - widgets.List{ - view_id='table_of_contents', - frame={r=0, t=2, b=1, w=30}, - choices={}, - icon_width=2, - on_submit=self:callback('onTableOfContentsSubmit') - -- on_submit=self:callback('onSubmit'), - -- on_submit2=self:callback('onSubmit2'), - }, - -- widgets.Panel{ - -- view_id='table_of_contents', - -- frame={l=1,t=1, b=1, w=30} - -- } - } + content=content }, } self:reloadTableOfContents(content) end -function JournalScreen:onTableOfContentsSubmit(ind, choice) - self.subviews.journal_editor:setCursor(choice.line_cursor) -end - function JournalScreen:loadContextContent() local site_data = dfhack.persistent.getSiteData(JOURNAL_PERSIST_KEY) or { text = {''} @@ -128,12 +171,8 @@ function JournalScreen:reloadTableOfContents(text) local header, section = line:match("^(#+)%s(.+)") if header ~= nil then table.insert(sections, { - -- line_cur, - -- #header, line_cursor=line_cursor, text=string.rep(" ", #header - 1) .. section, - -- cat=cat, - -- icon=icon, }) end diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 3bbce9e66..6f8d70a2f 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -7,7 +7,7 @@ local wrapped_text = reqscript('internal/journal/wrapped_text') local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} -TextEditor = defclass(TextEditor, widgets.Widget) +TextEditor = defclass(TextEditor, widgets.Panel) TextEditor.ATTRS{ text = '', @@ -59,6 +59,13 @@ function TextEditor:init() self:setFocus(true) end +function TextEditor:scrollToCursor(cursor_offset) + local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( + cursor_offset + ) + self:setRenderStartLineY(cursor_liny_y) +end + function TextEditor:setCursor(cursor_offset) return self.subviews.text_area:setCursor(cursor_offset) end From 1f265fbcb7de516a73c3ffcfba1ce867a7ec65c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 13:11:25 +0200 Subject: [PATCH 03/25] Add button to toggle journal table of contest visiblity --- gui/journal.lua | 102 ++++++++++++++++++------------- internal/journal/text_editor.lua | 10 +-- test/gui/journal.lua | 11 ++-- 3 files changed, 72 insertions(+), 51 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 4eb7460f3..91dbb6d0e 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -19,7 +19,8 @@ JournalWindow.ATTRS { resizable=true, resize_min=RESIZE_MIN, frame_inset=0, - content='' + content='', + on_text_change=DEFAULT_NIL } function JournalWindow:init() @@ -27,6 +28,18 @@ function JournalWindow:init() self.frame = self:sanitizeFrame(config_frame) self:addviews({ + widgets.Panel{ + frame={t=1, r=0,h=1}, + subviews={ + widgets.TextButton{ + frame={l=0,w=13}, + label='ToC', + key='CUSTOM_CTRL_T', + on_activate=self:callback('toggleToCVisibililty'), + enabled=true, + }, + } + }, widgets.Panel{ view_id='table_of_contents_panel', frame_title='Table of contents', @@ -38,7 +51,8 @@ function JournalWindow:init() resize_min={w=20}, resize_anchors={r=true}, - frame={l=0, t=1, b=1, w=30}, + frame={l=0, t=3, b=0, w=30}, + visible=false, on_resize_begin=self:callback('onPanelResizeBegin'), on_resize_end=self:callback('onPanelResizeEnd'), subviews={ @@ -52,13 +66,20 @@ function JournalWindow:init() }, text_editor.TextEditor{ view_id='journal_editor', - frame={t=2, b=1, l=31, r=0}, + frame={t=3, b=0, l=31, r=0}, resize_min={w=30, h=10}, frame_inset={r=1}, text=self.content, on_change=function(text) self:onTextChange(text) end }, }) + + self:reloadTableOfContents(self.content) +end + +function JournalWindow:toggleToCVisibililty() + self.subviews.table_of_contents_panel.visible = not self.subviews.table_of_contents_panel.visible + self:updateLayout() end function JournalWindow:sanitizeFrame(frame) @@ -83,10 +104,6 @@ function JournalWindow:sanitizeFrame(frame) return frame end -function JournalWindow:postUpdateLayout() - self:saveConfig() -end - function JournalWindow:saveConfig() utils.assign(journal_config.data, { frame = self.frame @@ -103,24 +120,48 @@ function JournalWindow:onPanelResizeEnd() end function JournalWindow:esnurePanelsRelSize() - self.subviews.table_of_contents_panel.frame.w = math.min( - self.subviews.table_of_contents_panel.frame.w, - self.frame.w - self.subviews.journal_editor.resize_min.w - ) - self.subviews.journal_editor.frame.l = self.subviews.table_of_contents_panel.frame.w + 1 + local toc = self.subviews.table_of_contents_panel + local editor = self.subviews.journal_editor + + toc.frame.w = toc.visible and math.min( + math.max(toc.frame.w, toc.resize_min.w), + self.frame.w - editor.resize_min.w + ) or 0 + editor.frame.l = toc.frame.w + 1 end function JournalWindow:preUpdateLayout() self:esnurePanelsRelSize() end -function JournalWindow:onRenderBody(painter) - if self.resizing_panels then - self:esnurePanelsRelSize() - self:updateLayout() +function JournalWindow:postUpdateLayout() + self:saveConfig() +end + +function JournalWindow:onTextChange(text) + self:reloadTableOfContents(text) + if self.on_text_change ~= nil then + self.on_text_change(text) end +end - return JournalWindow.super.onRenderBody(self, painter) +function JournalWindow:reloadTableOfContents(text) + local sections = {} + + local line_cursor = 1 + for line in text:gmatch("[^\n]*") do + local header, section = line:match("^(#+)%s(.+)") + if header ~= nil then + table.insert(sections, { + line_cursor=line_cursor, + text=string.rep(" ", #header - 1) .. section, + }) + end + + line_cursor = line_cursor + #line + 1 + end + + self.subviews.table_of_contents:setChoices(sections) end function JournalWindow:onTableOfContentsSubmit(ind, choice) @@ -141,14 +182,13 @@ function JournalScreen:init(options) JournalWindow{ view_id='journal_window', frame={w=65, h=45}, - resizable=true, resize_min={w=50, h=20}, + resizable=true, frame_inset=0, - content=content + content=content, + on_text_change=self:callback('onTextChange') }, } - - self:reloadTableOfContents(content) end function JournalScreen:loadContextContent() @@ -160,26 +200,6 @@ end function JournalScreen:onTextChange(text) self:saveContextContent(text) - self:reloadTableOfContents(text) -end - -function JournalScreen:reloadTableOfContents(text) - local sections = {} - - local line_cursor = 1 - for line in text:gmatch("[^\n]*") do - local header, section = line:match("^(#+)%s(.+)") - if header ~= nil then - table.insert(sections, { - line_cursor=line_cursor, - text=string.rep(" ", #header - 1) .. section, - }) - end - - line_cursor = line_cursor + #line + 1 - end - - self.subviews.table_of_contents:setChoices(sections) end function JournalScreen:saveContextContent(text) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 6f8d70a2f..a54edad4d 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -60,10 +60,12 @@ function TextEditor:init() end function TextEditor:scrollToCursor(cursor_offset) - local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( - cursor_offset - ) - self:setRenderStartLineY(cursor_liny_y) + if self.scrollbar.visible then + local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( + cursor_offset + ) + self:setRenderStartLineY(cursor_liny_y) + end end function TextEditor:setCursor(cursor_offset) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 278602f09..929b3a594 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -80,16 +80,15 @@ local function arrange_empty_journal(options) local journal_window = journal.subviews.journal_window if not options.allow_size_restore then - journal_window.frame.w = 50 - journal_window.frame.h = 50 + journal_window.frame= {w = 50, h = 50} end if options.w then - journal_window.frame.w = options.w + 6 + journal_window.frame.w = options.w + 7 end if options.h then - journal_window.frame.h = options.h + 4 + journal_window.frame.h = options.h + 5 end journal:updateLayout() @@ -101,7 +100,7 @@ local function arrange_empty_journal(options) journal:onRender() - return journal, text_area + return journal, text_area, journal_window end local function read_rendered_text(text_area) @@ -165,7 +164,7 @@ function test.load() end function test.load_input_multiline_text() - local journal, text_area = arrange_empty_journal({w=80}) + local journal, text_area, journal_window = arrange_empty_journal({w=80}) local text = table.concat({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', From 77c0bebfccc0b2169de3aedbadb26c941db81ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 13:15:01 +0200 Subject: [PATCH 04/25] Add journal README for Table of contest --- docs/gui/journal.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index b8ef5b1c5..aa8f14cb1 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -46,6 +46,7 @@ Supported Features - replace selected text, if available - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in +- Table of contests (:kbd:`Ctrl` + :kbd:`T`), with headers line prefixed by `#`, e.g. `# Fort history`, `## Year 1` Usage ----- From dc008c8873760646c3bb15e92e0a758ee7fc6296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 17:22:55 +0200 Subject: [PATCH 05/25] Improve journal table of contents logic, add tests --- gui/journal.lua | 13 ++- internal/journal/text_editor.lua | 117 ++++++++++--------- test/gui/journal.lua | 193 ++++++++++++++++++++++++++++++- 3 files changed, 264 insertions(+), 59 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 91dbb6d0e..e8f580a5b 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -19,7 +19,7 @@ JournalWindow.ATTRS { resizable=true, resize_min=RESIZE_MIN, frame_inset=0, - content='', + init_text='', on_text_change=DEFAULT_NIL } @@ -69,7 +69,7 @@ function JournalWindow:init() frame={t=3, b=0, l=31, r=0}, resize_min={w=30, h=10}, frame_inset={r=1}, - text=self.content, + init_text=self.init_text, on_change=function(text) self:onTextChange(text) end }, }) @@ -78,7 +78,10 @@ function JournalWindow:init() end function JournalWindow:toggleToCVisibililty() - self.subviews.table_of_contents_panel.visible = not self.subviews.table_of_contents_panel.visible + self.subviews.table_of_contents_panel.visible = + not self.subviews.table_of_contents_panel.visible + + self:reloadTableOfContents(self.subviews.journal_editor:text()) self:updateLayout() end @@ -146,6 +149,10 @@ function JournalWindow:onTextChange(text) end function JournalWindow:reloadTableOfContents(text) + if not self.subviews.table_of_contents_panel.visible then + return + end + local sections = {} local line_cursor = 1 diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index a54edad4d..f10f5669b 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -10,7 +10,7 @@ local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} TextEditor = defclass(TextEditor, widgets.Panel) TextEditor.ATTRS{ - text = '', + init_text = '', text_pen = COLOR_LIGHTCYAN, ignore_keys = {'STRING_A096'}, select_pen = COLOR_CYAN, @@ -20,51 +20,58 @@ TextEditor.ATTRS{ function TextEditor:init() self.render_start_line_y = 1 - self.scrollbar = widgets.Scrollbar{ - view_id='text_area_scrollbar', - frame={r=0,t=1}, - on_scroll=self:callback('onScrollbar') - } - self.editor = TextEditorView{ - view_id='text_area', - frame={l=0,r=2,t=0}, - text = self.text, - text_pen = self.text_pen, - ignore_keys = self.ignore_keys, - select_pen = self.select_pen, - debug = self.debug, - - on_change = function (val) - self:updateLayout() - if self.on_change then - self.on_change(val) - end - end, - - on_cursor_change = function () - local x, y = self.editor.wrapped_text:indexToCoords(self.editor.cursor) - if (y >= self.render_start_line_y + self.editor.frame_body.height) then - self:setRenderStartLineY(y - self.editor.frame_body.height + 1) - elseif (y < self.render_start_line_y) then - self:setRenderStartLineY(y) - end - end - } self:addviews{ - self.editor, - self.scrollbar, + TextEditorView{ + view_id='text_area', + frame={l=0,r=2,t=0}, + text = self.init_text, + text_pen = self.text_pen, + ignore_keys = self.ignore_keys, + select_pen = self.select_pen, + debug = self.debug, + + on_change = function (val) + self:updateLayout() + if self.on_change then + self.on_change(val) + end + end, + on_cursor_change = self:callback('onCursorChanged') + }, + widgets.Scrollbar{ + view_id='scrollbar', + frame={r=0,t=1}, + on_scroll=self:callback('onScrollbar') + }, widgets.HelpButton{command="gui/journal", frame={r=0,t=0}} } self:setFocus(true) end +function TextEditor:text() + return self.subviews.text_area.text +end + +function TextEditor:onCursorChanged(text) + local x, y = self.subviews.text_area.wrapped_text:indexToCoords( + self.subviews.text_area.cursor + ) + if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then + self:updateScrollbar( + y - self.subviews.text_area.frame_body.height + 1 + ) + elseif (y < self.render_start_line_y) then + self:updateScrollbar(y) + end +end + function TextEditor:scrollToCursor(cursor_offset) - if self.scrollbar.visible then + if self.subviews.scrollbar.visible then local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( cursor_offset ) - self:setRenderStartLineY(cursor_liny_y) + self:updateScrollbar(cursor_liny_y) end end @@ -77,11 +84,11 @@ function TextEditor:getPreferredFocusState() end function TextEditor:postUpdateLayout() - self:updateScrollbar() + self:updateScrollbar(self.render_start_line_y) end function TextEditor:onScrollbar(scroll_spec) - local height = self.editor.frame_body.height + local height = self.subviews.text_area.frame_body.height local render_start_line = self.render_start_line_y if scroll_spec == 'down_large' then @@ -96,47 +103,47 @@ function TextEditor:onScrollbar(scroll_spec) render_start_line = tonumber(scroll_spec) end - self:setRenderStartLineY(math.min( - #self.editor.wrapped_text.lines - height + 1, - math.max(1, render_start_line) - )) - self:updateScrollbar() + self:updateScrollbar(render_start_line) end -function TextEditor:updateScrollbar() - local lines_count = #self.editor.wrapped_text.lines +function TextEditor:updateScrollbar(scrollbar_current_y) + local lines_count = #self.subviews.text_area.wrapped_text.lines + -- local render_start_line_y = scrollbar_current_y + + local render_start_line_y = (math.min( + #self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1, + math.max(1, scrollbar_current_y) + )) - self.scrollbar:update( - self.render_start_line_y, + self.subviews.scrollbar:update( + render_start_line_y, self.frame_body.height, lines_count ) if (self.frame_body.height >= lines_count) then - self:setRenderStartLineY(1) + render_start_line_y = 1 end + + self.render_start_line_y = render_start_line_y + self.subviews.text_area:setRenderStartLineY(self.render_start_line_y) end function TextEditor:renderSubviews(dc) - self.editor.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) + self.subviews.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) TextEditor.super.renderSubviews(self, dc) end function TextEditor:onInput(keys) - if (self.scrollbar.is_dragging) then - return self.scrollbar:onInput(keys) + if (self.subviews.scrollbar.is_dragging) then + return self.subviews.scrollbar:onInput(keys) end return TextEditor.super.onInput(self, keys) end -function TextEditor:setRenderStartLineY(render_start_line_y) - self.render_start_line_y = render_start_line_y - self.editor:setRenderStartLineY(render_start_line_y) -end - TextEditorView = defclass(TextEditorView, widgets.Widget) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 929b3a594..4fbed891a 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -2372,7 +2372,7 @@ end function test.scroll_long_text() local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.text_area_scrollbar + local scrollbar = journal.subviews.scrollbar local text = table.concat({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -2582,3 +2582,194 @@ function test.scroll_follows_cursor() journal:dismiss() end + +function test.generate_table_of_contents() + local journal, text_area = arrange_empty_journal({w=100, h=10}) + + local text = table.concat({ + '# Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + '# Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n') + + simulate_input_text(text) + + expect.eq(journal.subviews.table_of_contents_panel.visible, false) + + simulate_input_keys('CUSTOM_CTRL_T') + + expect.eq(journal.subviews.table_of_contents_panel.visible, true) + + local toc_items = journal.subviews.table_of_contents.choices + + expect.eq(#toc_items, 6) + + local expectChoiceToMatch = function (a, b) + expect.eq(a.line_cursor, b.line_cursor) + expect.eq(a.text, b.text) + end + + expectChoiceToMatch(toc_items[1], {line_cursor=1, text='Header 1'}) + expectChoiceToMatch(toc_items[2], {line_cursor=114, text='Header 2'}) + expectChoiceToMatch(toc_items[3], {line_cursor=204, text=' Subheader 1'}) + expectChoiceToMatch(toc_items[4], {line_cursor=338, text=' Subheader 2'}) + expectChoiceToMatch(toc_items[5], {line_cursor=485, text=' Subsubheader 1'}) + expectChoiceToMatch(toc_items[6], {line_cursor=504, text='Header 3'}) + + journal:dismiss() +end + +function test.jump_to_table_of_contents_sections() + local journal, text_area = arrange_empty_journal({w=100, h=10}) + local scrollbar = journal.subviews.text_area_scrollbar + + local text = table.concat({ + '# Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + '# Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_T') + + local toc = journal.subviews.table_of_contents + + toc:setSelected(1) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + '_ Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + '# Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + }, '\n')) + + toc:setSelected(2) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + '_ Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + }, '\n')) + + toc:setSelected(3) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + '_# Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + }, '\n')) + + toc:setSelected(4) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '_# Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n')) + + toc:setSelected(5) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '_## Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n')) + + toc:setSelected(6) + toc:submit() + + gui_journal.view:onRender() + + expect.eq(read_rendered_text(text_area), table.concat({ + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '_ Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n')) + + journal:dismiss() +end From 48c4b501a157f3cc44013d3e22ae5a3023dab73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 18:01:50 +0200 Subject: [PATCH 06/25] Polishing journal table of contents resize logic --- gui/journal.lua | 16 +++++++++++++-- internal/journal/text_editor.lua | 3 +-- test/gui/journal.lua | 35 +++++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index e8f580a5b..624f0fd4e 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -81,7 +81,7 @@ function JournalWindow:toggleToCVisibililty() self.subviews.table_of_contents_panel.visible = not self.subviews.table_of_contents_panel.visible - self:reloadTableOfContents(self.subviews.journal_editor:text()) + self:reloadTableOfContents(self.subviews.journal_editor:getText()) self:updateLayout() end @@ -120,6 +120,18 @@ end function JournalWindow:onPanelResizeEnd() self.resizing_panels = false + + self:esnurePanelsRelSize() + self:updateLayout() +end + +function JournalWindow:onRenderBody(painter) + if self.resizing_panels then + self:esnurePanelsRelSize() + self:updateLayout() + end + + return JournalWindow.super.onRenderBody(painter) end function JournalWindow:esnurePanelsRelSize() @@ -192,7 +204,7 @@ function JournalScreen:init(options) resize_min={w=50, h=20}, resizable=true, frame_inset=0, - content=content, + init_text=content, on_text_change=self:callback('onTextChange') }, } diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index f10f5669b..189b3ab3a 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -20,7 +20,6 @@ TextEditor.ATTRS{ function TextEditor:init() self.render_start_line_y = 1 - self:addviews{ TextEditorView{ view_id='text_area', @@ -49,7 +48,7 @@ function TextEditor:init() self:setFocus(true) end -function TextEditor:text() +function TextEditor:getText() return self.subviews.text_area.text end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 4fbed891a..1003d976a 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -49,14 +49,13 @@ local function simulate_mouse_click(element, x, y) gui_journal.view:onRender() end -local function simulate_mouse_drag(text_area, x_from, y_from, x_to, y_to) - local g_x_from, g_y_from = text_area.frame_body:globalXY(x_from, y_from) - local g_x_to, g_y_to = text_area.frame_body:globalXY(x_to, y_to) +local function simulate_mouse_drag(element, x_from, y_from, x_to, y_to) + local g_x_from, g_y_from = element.frame_body:globalXY(x_from, y_from) + local g_x_to, g_y_to = element.frame_body:globalXY(x_to, y_to) df.global.gps.mouse_x = g_x_from df.global.gps.mouse_y = g_y_from - gui.simulateInput(dfhack.gui.getCurViewscreen(true), { _MOUSE_L=true, _MOUSE_L_DOWN=true, @@ -2635,7 +2634,6 @@ end function test.jump_to_table_of_contents_sections() local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.text_area_scrollbar local text = table.concat({ '# Header 1', @@ -2773,3 +2771,30 @@ function test.jump_to_table_of_contents_sections() journal:dismiss() end + + +function test.resize_table_of_contents_together() + local journal, text_area = arrange_empty_journal({w=100, h=10}) + + local text = table.concat({ + '# Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + }, '\n') + + simulate_input_text(text) + + expect.eq(text_area.frame_body.width, 101) + + simulate_input_keys('CUSTOM_CTRL_T') + + expect.eq(text_area.frame_body.width, 81) + + local toc_panel = journal.subviews.table_of_contents_panel + -- simulate mouse drag resize of toc panel + simulate_mouse_drag(toc_panel, toc_panel.frame_body.width, 1, toc_panel.frame_body.width + 10, 1) + + expect.eq(text_area.frame_body.width, 71) + + journal:dismiss() +end From 49b8f37bf5b758bf0b16f4dbff97049c00058cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 20 Jul 2024 21:41:09 +0200 Subject: [PATCH 07/25] Add journal feat of saving entire layout (including table of contents) --- gui/journal.lua | 63 +++++++++++++++++++++++++++++--------------- test/gui/journal.lua | 32 +++++++++++++++++----- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 624f0fd4e..451ff57ac 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -20,13 +20,25 @@ JournalWindow.ATTRS { resize_min=RESIZE_MIN, frame_inset=0, init_text='', - on_text_change=DEFAULT_NIL + on_text_change=DEFAULT_NIL, + on_layout_change=DEFAULT_NIL } -function JournalWindow:init() - local config_frame = copyall(journal_config.data.frame or {}) - self.frame = self:sanitizeFrame(config_frame) +function JournalWindow:loadConfig() + local window_frame = copyall(journal_config.data.frame or {}) + local table_of_contents = copyall(journal_config.data.toc or { + width=20, + visible=false + }) + + self.frame = self:sanitizeFrame(window_frame) + local toc_panel = self.subviews.table_of_contents_panel + toc_panel.frame.w = table_of_contents.width + toc_panel.visible = table_of_contents.visible +end + +function JournalWindow:init() self:addviews({ widgets.Panel{ frame={t=1, r=0,h=1}, @@ -51,7 +63,7 @@ function JournalWindow:init() resize_min={w=20}, resize_anchors={r=true}, - frame={l=0, t=3, b=0, w=30}, + frame={l=0, t=3, b=0, w=20}, visible=false, on_resize_begin=self:callback('onPanelResizeBegin'), on_resize_end=self:callback('onPanelResizeEnd'), @@ -74,7 +86,8 @@ function JournalWindow:init() }, }) - self:reloadTableOfContents(self.content) + self:loadConfig() + self:reloadTableOfContents(self.init_text) end function JournalWindow:toggleToCVisibililty() @@ -108,8 +121,14 @@ function JournalWindow:sanitizeFrame(frame) end function JournalWindow:saveConfig() + local toc_panel = self.subviews.table_of_contents_panel + utils.assign(journal_config.data, { - frame = self.frame + frame = self.frame, + toc = { + width = toc_panel.frame.w, + visible = toc_panel.visible + } }) journal_config:write() end @@ -120,8 +139,8 @@ end function JournalWindow:onPanelResizeEnd() self.resizing_panels = false - self:esnurePanelsRelSize() + self:updateLayout() end @@ -138,11 +157,11 @@ function JournalWindow:esnurePanelsRelSize() local toc = self.subviews.table_of_contents_panel local editor = self.subviews.journal_editor - toc.frame.w = toc.visible and math.min( + toc.frame.w = math.min( math.max(toc.frame.w, toc.resize_min.w), self.frame.w - editor.resize_min.w - ) or 0 - editor.frame.l = toc.frame.w + 1 + ) + editor.frame.l = toc.visible and (toc.frame.w + 1) or 1 end function JournalWindow:preUpdateLayout() @@ -195,7 +214,7 @@ JournalScreen.ATTRS { } function JournalScreen:init(options) - local content = self:loadContextContent() + local context = self:loadContext() self:addviews{ JournalWindow{ @@ -204,25 +223,27 @@ function JournalScreen:init(options) resize_min={w=50, h=20}, resizable=true, frame_inset=0, - init_text=content, - on_text_change=self:callback('onTextChange') + init_text=context.text[1], + + on_text_change=self:callback('onTextChange'), }, } end -function JournalScreen:loadContextContent() - local site_data = dfhack.persistent.getSiteData(JOURNAL_PERSIST_KEY) or { - text = {''} - } - return site_data.text ~= nil and site_data.text[1] or '' +function JournalScreen:loadContext() + local site_data = dfhack.persistent.getSiteData(JOURNAL_PERSIST_KEY) or {} + site_data.text = site_data.text or {''} + + return site_data end function JournalScreen:onTextChange(text) - self:saveContextContent(text) + self:saveContext(text) end -function JournalScreen:saveContextContent(text) +function JournalScreen:saveContext(text) if self.save_on_change and dfhack.isWorldLoaded() then + local toc_panel = self.subviews.table_of_contents_panel dfhack.persistent.saveSiteData(JOURNAL_PERSIST_KEY, {text={text}}) end end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 1003d976a..80403892d 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -78,7 +78,7 @@ local function arrange_empty_journal(options) local journal_window = journal.subviews.journal_window - if not options.allow_size_restore then + if not options.allow_layout_restore then journal_window.frame= {w = 50, h = 50} end @@ -90,13 +90,19 @@ local function arrange_empty_journal(options) journal_window.frame.h = options.h + 5 end - journal:updateLayout() local text_area = journal_window.subviews.text_area text_area.enable_cursor_blink = false text_area:setText('') + if not options.allow_layout_restore then + local toc_panel = journal_window.subviews.table_of_contents_panel + toc_panel.visible = false + toc_panel.frame.w = 20 + end + + journal:updateLayout() journal:onRender() return journal, text_area, journal_window @@ -2348,24 +2354,29 @@ function test.cut_and_paste_selected_text() journal:dismiss() end -function test.restore_size_and_position() +function test.restore_layout() local journal, _ = arrange_empty_journal() + journal.subviews.journal_window.frame = { l = 13, t = 13, w = 80, h = 23 } + journal.subviews.table_of_contents_panel.frame.w = 37 + journal:updateLayout() journal:dismiss() - journal, _ = arrange_empty_journal({allow_size_restore=true}) + journal, _ = arrange_empty_journal({allow_layout_restore=true}) expect.eq(journal.subviews.journal_window.frame.l, 13) expect.eq(journal.subviews.journal_window.frame.t, 13) expect.eq(journal.subviews.journal_window.frame.w, 80) expect.eq(journal.subviews.journal_window.frame.h, 23) + expect.eq(journal.subviews.table_of_contents_panel.frame.w, 37) + journal:dismiss() end @@ -2633,7 +2644,11 @@ function test.generate_table_of_contents() end function test.jump_to_table_of_contents_sections() - local journal, text_area = arrange_empty_journal({w=100, h=10}) + local journal, text_area = arrange_empty_journal({ + w=100, + h=10, + allow_layout_restore=false + }) local text = table.concat({ '# Header 1', @@ -2772,9 +2787,12 @@ function test.jump_to_table_of_contents_sections() journal:dismiss() end - function test.resize_table_of_contents_together() - local journal, text_area = arrange_empty_journal({w=100, h=10}) + local journal, text_area = arrange_empty_journal({ + w=100, + h=10, + allow_layout_restore=false + }) local text = table.concat({ '# Header 1', From 12042709a65757ee166466fe162a0010ebd3bff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 21 Jul 2024 09:38:11 +0200 Subject: [PATCH 08/25] Fix journal issue with numpad keys not interpreted as text --- internal/journal/text_editor.lua | 4 +-- test/gui/journal.lua | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 189b3ab3a..95402574b 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -513,10 +513,10 @@ function TextEditorView:onInput(keys) if self:onMouseInput(keys) then return true - elseif self:onCursorInput(keys) then - return true elseif self:onTextManipulationInput(keys) then return true + elseif self:onCursorInput(keys) then + return true elseif keys.CUSTOM_CTRL_C then self:copy() return true diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 80403892d..e2bf78131 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -183,6 +183,51 @@ function test.load_input_multiline_text() journal:dismiss() end +function test.handle_numpad_numbers_as_text() + local journal, text_area, journal_window = arrange_empty_journal({w=80}) + + local text = table.concat({ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + simulate_input_text(text) + + simulate_input_keys({ + STANDARDSCROLL_LEFT = true, + KEYBOARD_CURSOR_LEFT = true, + _STRING = 52, + STRING_A052 = true, + }) + + expect.eq(read_rendered_text(text_area), text .. '4_') + + simulate_input_keys({ + STRING_A054 = true, + STANDARDSCROLL_RIGHT = true, + KEYBOARD_CURSOR_RIGHT = true, + _STRING = 54, + }) + expect.eq(read_rendered_text(text_area), text .. '46_') + + simulate_input_keys({ + KEYBOARD_CURSOR_DOWN = true, + STRING_A050 = true, + _STRING = 50, + STANDARDSCROLL_DOWN = true, + }) + + expect.eq(read_rendered_text(text_area), text .. '462_') + + simulate_input_keys({ + KEYBOARD_CURSOR_UP = true, + STRING_A056 = true, + STANDARDSCROLL_UP = true, + _STRING = 56, + }) + + expect.eq(read_rendered_text(text_area), text .. '4628_') + journal:dismiss() +end + function test.wrap_text_to_available_width() local journal, text_area = arrange_empty_journal({w=55}) From 0f6e0569de3ec8a73038a5f64e3c9089d93b1e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 21 Jul 2024 14:54:10 +0200 Subject: [PATCH 09/25] Add undo/redo feature to journal --- docs/gui/journal.rst | 1 + internal/journal/text_editor.lua | 142 ++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index aa8f14cb1..c7b225a17 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -35,6 +35,7 @@ Supported Features - Jump to Beginning/End: Quickly move the cursor to the beginning or end of the text using :kbd:`Shift` + :kbd:`Up` and :kbd:`Shift` + :kbd:`Down`. - Select Word/Line: Use double click to select current word, or triple click to select current line - Select All: Select entire text by :kbd:`Ctrl` + :kbd:`A` +- Undo/Redo: Undo/Redo changes by :kbd:`Ctrl` + :kbd:`Z` / :kbd:`Ctrl` + :kbd:`Y` - Clipboard Operations: Perform OS clipboard cut, copy, and paste operations on selected text, allowing you to paste the copied content into other applications. - Copy Text: Use :kbd:`Ctrl` + :kbd:`C` to copy selected text. - copy selected text, if available diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 95402574b..db6119428 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -6,6 +6,83 @@ local widgets = require 'gui.widgets' local wrapped_text = reqscript('internal/journal/wrapped_text') local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} +local HISTORY_ENTRY = { + TEXT_BLOCK = 1, + WHITESPACE_BLOCK = 2, + BACKSPACE = 2, + DELETE = 3, + OTHER = 4 +} + +TextEditorHistory = defclass(TextEditorHistory) + +TextEditorHistory.ATTRS{ + history_size = 25, +} + +function TextEditorHistory:init() + self.past = {} + self.future = {} +end + +function TextEditorHistory:store(history_entry_type, text, cursor) + local last_entry = self.past[#self.past] + + if not last_entry or history_entry_type == HISTORY_ENTRY.OTHER or + last_entry.entry_type ~= history_entry_type then + table.insert(self.past, { + entry_type=history_entry_type, + text=text, + cursor=cursor + }) + end + + self.future = {} + + if #self.past > self.history_size then + table.remove(self.past, 1) + end +end + +function TextEditorHistory:undo(curr_text, curr_cursor) + if #self.past == 0 then + return nil + end + + local history_entry = table.remove(self.past, #self.past) + + table.insert(self.future, { + entry_type=OTHER, + text=curr_text, + cursor=curr_cursor + }) + + if #self.future > self.history_size then + table.remove(self.future, 1) + end + + return history_entry +end + +function TextEditorHistory:redo(curr_text, curr_cursor) + if #self.future == 0 then + return true + end + + local history_entry = table.remove(self.future, #self.future) + + table.insert(self.past, { + entry_type=OTHER, + text=curr_text, + cursor=curr_cursor + }) + + if #self.past > self.history_size then + table.remove(self.past, 1) + end + + return history_entry +end TextEditor = defclass(TextEditor, widgets.Panel) @@ -154,7 +231,8 @@ TextEditorView.ATTRS{ on_change = DEFAULT_NIL, on_cursor_change = DEFAULT_NIL, enable_cursor_blink = true, - debug = false + debug = false, + history_size = 10, } function TextEditorView:init() @@ -179,6 +257,8 @@ function TextEditorView:init() text=self.text, wrap_width=256 } + + self.history = TextEditorHistory{history_size=self.history_size} end function TextEditorView:setRenderStartLineY(render_start_line_y) @@ -401,7 +481,7 @@ function TextEditorView:onRenderBody(dc) local cursor_char = self:charAtCursor() local x, y = self.wrapped_text:indexToCoords(self.cursor) local debug_msg = string.format( - 'x: %s y: %s ind: %s #line: %s char: %s', + 'x: %s y: %s ind: %s #line: %s char: %s hist-: %s hist+: %s', x, y, self.cursor, @@ -409,7 +489,9 @@ function TextEditorView:onRenderBody(dc) (cursor_char == NEWLINE and 'NEWLINE') or (cursor_char == ' ' and 'SPACE') or (cursor_char == '' and 'nil') or - cursor_char + cursor_char, + #self.history.past, + #self.history.future ) local sel_debug_msg = self.sel_end and string.format( 'sel_end: %s', @@ -513,6 +595,8 @@ function TextEditorView:onInput(keys) if self:onMouseInput(keys) then return true + elseif self:onHistoryInput(keys) then + return true elseif self:onTextManipulationInput(keys) then return true elseif self:onCursorInput(keys) then @@ -522,9 +606,33 @@ function TextEditorView:onInput(keys) return true elseif keys.CUSTOM_CTRL_X then self:cut() + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) return true elseif keys.CUSTOM_CTRL_V then self:paste() + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + return true + end +end + +function TextEditorView:onHistoryInput(keys) + if keys.CUSTOM_CTRL_Z then + local history_entry = self.history:undo(self.text, self.cursor) + + if history_entry then + self:setText(history_entry.text) + self:setCursor(history_entry.cursor) + end + + return true + elseif keys.CUSTOM_CTRL_Y then + local history_entry = self.history:redo(self.text, self.cursor) + + if history_entry then + self:setText(history_entry.text) + self:setCursor(history_entry.cursor) + end + return true end end @@ -652,12 +760,20 @@ end function TextEditorView:onTextManipulationInput(keys) if keys.SELECT then -- handle enter + self.history:store( + HISTORY_ENTRY.WHITESPACE_BLOCK, + self.text, + self.cursor + ) self:insert(NEWLINE) + return true elseif keys._STRING then if keys._STRING == 0 then -- handle backspace + self.history:store(HISTORY_ENTRY.BACKSPACE, self.text, self.cursor) + if (self:hasSelection()) then self:eraseSelection() else @@ -671,12 +787,19 @@ function TextEditorView:onTextManipulationInput(keys) ) self:eraseSelection() end + else + local cv = string.char(keys._STRING) + if (self:hasSelection()) then + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) self:eraseSelection() + else + local entry_type = cv == ' ' and HISTORY_ENTRY.WHITESPACE_BLOCK + or HISTORY_ENTRY.TEXT_BLOCK + self.history:store(entry_type, self.text, self.cursor) end - local cv = string.char(keys._STRING) self:insert(cv) end @@ -687,6 +810,8 @@ function TextEditorView:onTextManipulationInput(keys) return true elseif keys.CUSTOM_CTRL_U then -- delete current line + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + if (self:hasSelection()) then -- delete all lines that has selection self:setSelection( @@ -701,18 +826,24 @@ function TextEditorView:onTextManipulationInput(keys) ) self:eraseSelection() end + return true elseif keys.CUSTOM_CTRL_K then -- delete from cursor to end of current line + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + local line_end = self:lineEndOffset(self.sel_end or self.cursor) - 1 self:setSelection( self.cursor, math.max(line_end, self.cursor) ) self:eraseSelection() + return true elseif keys.CUSTOM_CTRL_D then -- delete char, there is no support for `Delete` key + self.history:store(HISTORY_ENTRY.DELETE, self.text, self.cursor) + if (self:hasSelection()) then self:eraseSelection() else @@ -725,6 +856,8 @@ function TextEditorView:onTextManipulationInput(keys) return true elseif keys.CUSTOM_CTRL_W then -- delete one word backward + self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) + if not self:hasSelection() and self.cursor ~= 1 then self:setSelection( self:wordStartOffset(), @@ -732,6 +865,7 @@ function TextEditorView:onTextManipulationInput(keys) ) end self:eraseSelection() + return true end end From 7f9976cfc987c1c396674e70cfb061be6577aac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 21 Jul 2024 15:35:21 +0200 Subject: [PATCH 10/25] Restore journal last cursor position --- gui/journal.lua | 42 ++++++++++++++++++++++-------- internal/journal/text_editor.lua | 44 ++++++++++++++++++++++---------- test/gui/journal.lua | 42 +++++++++++++++++++++++++++--- 3 files changed, 102 insertions(+), 26 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 451ff57ac..32ad9e080 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -19,8 +19,10 @@ JournalWindow.ATTRS { resizable=true, resize_min=RESIZE_MIN, frame_inset=0, - init_text='', + init_text=DEFAULT_NIL, + init_cursor=1, on_text_change=DEFAULT_NIL, + on_cursor_change=DEFAULT_NIL, on_layout_change=DEFAULT_NIL } @@ -82,7 +84,13 @@ function JournalWindow:init() resize_min={w=30, h=10}, frame_inset={r=1}, init_text=self.init_text, - on_change=function(text) self:onTextChange(text) end + init_cursor=self.init_cursor, + on_text_change=function(text) self:onTextChange(text) end, + on_cursor_change=function(cursor) + if self.on_cursor_change ~= nil then + self.on_cursor_change(cursor) + end + end }, }) @@ -210,7 +218,8 @@ end JournalScreen = defclass(JournalScreen, gui.ZScreen) JournalScreen.ATTRS { focus_path='journal', - save_on_change=true + save_on_change=true, + save_prefix='' } function JournalScreen:init(options) @@ -223,16 +232,22 @@ function JournalScreen:init(options) resize_min={w=50, h=20}, resizable=true, frame_inset=0, + init_text=context.text[1], + init_cursor=context.cursor[1], - on_text_change=self:callback('onTextChange'), + on_text_change=self:callback('saveContext'), + on_cursor_change=self:callback('saveContext') }, } end function JournalScreen:loadContext() - local site_data = dfhack.persistent.getSiteData(JOURNAL_PERSIST_KEY) or {} + local site_data = dfhack.persistent.getSiteData( + self.save_prefix .. JOURNAL_PERSIST_KEY + ) or {} site_data.text = site_data.text or {''} + site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} return site_data end @@ -241,10 +256,15 @@ function JournalScreen:onTextChange(text) self:saveContext(text) end -function JournalScreen:saveContext(text) +function JournalScreen:saveContext() if self.save_on_change and dfhack.isWorldLoaded() then - local toc_panel = self.subviews.table_of_contents_panel - dfhack.persistent.saveSiteData(JOURNAL_PERSIST_KEY, {text={text}}) + local text = self.subviews.journal_editor:getText() + local cursor = self.subviews.journal_editor:getCursor() + + dfhack.persistent.saveSiteData( + self.save_prefix .. JOURNAL_PERSIST_KEY, + {text={text}, cursor={cursor}} + ) end end @@ -252,12 +272,14 @@ function JournalScreen:onDismiss() view = nil end -function main() +function main(options) if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then qerror('journal requires a fortress map to be loaded') end - view = view and view:raise() or JournalScreen{}:show() + view = view and view:raise() or JournalScreen{ + save_prefix=options and options.save_prefix or '' + }:show() end if not dfhack_flags.module then diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index db6119428..fd450f4e4 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -88,32 +88,36 @@ TextEditor = defclass(TextEditor, widgets.Panel) TextEditor.ATTRS{ init_text = '', + init_cursor = DEFAULT_NIL, text_pen = COLOR_LIGHTCYAN, ignore_keys = {'STRING_A096'}, select_pen = COLOR_CYAN, - on_change = DEFAULT_NIL, + on_text_change = DEFAULT_NIL, + on_cursor_change = DEFAULT_NIL, debug = false } function TextEditor:init() self.render_start_line_y = 1 + self:addviews{ TextEditorView{ view_id='text_area', frame={l=0,r=2,t=0}, text = self.init_text, + text_pen = self.text_pen, ignore_keys = self.ignore_keys, select_pen = self.select_pen, debug = self.debug, - on_change = function (val) + on_text_change = function (val) self:updateLayout() - if self.on_change then - self.on_change(val) + if self.on_text_change then + self.on_text_change(val) end end, - on_cursor_change = self:callback('onCursorChanged') + on_cursor_change = self:callback('onCursorChange') }, widgets.Scrollbar{ view_id='scrollbar', @@ -129,10 +133,15 @@ function TextEditor:getText() return self.subviews.text_area.text end -function TextEditor:onCursorChanged(text) +function TextEditor:getCursor() + return self.subviews.text_area.cursor +end + +function TextEditor:onCursorChange(cursor) local x, y = self.subviews.text_area.wrapped_text:indexToCoords( self.subviews.text_area.cursor ) + if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then self:updateScrollbar( y - self.subviews.text_area.frame_body.height + 1 @@ -140,6 +149,10 @@ function TextEditor:onCursorChanged(text) elseif (y < self.render_start_line_y) then self:updateScrollbar(y) end + + if self.on_cursor_change then + self.on_cursor_change(cursor) + end end function TextEditor:scrollToCursor(cursor_offset) @@ -161,6 +174,12 @@ end function TextEditor:postUpdateLayout() self:updateScrollbar(self.render_start_line_y) + + if self.subviews.text_area.cursor == nil then + local cursor = self.init_cursor or #self.text + 1 + self.subviews.text_area:setCursor(cursor) + self:scrollToCursor(cursor) + end end function TextEditor:onScrollbar(scroll_spec) @@ -184,7 +203,6 @@ end function TextEditor:updateScrollbar(scrollbar_current_y) local lines_count = #self.subviews.text_area.wrapped_text.lines - -- local render_start_line_y = scrollbar_current_y local render_start_line_y = (math.min( #self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1, @@ -220,7 +238,6 @@ function TextEditor:onInput(keys) end - TextEditorView = defclass(TextEditorView, widgets.Widget) TextEditorView.ATTRS{ @@ -228,7 +245,7 @@ TextEditorView.ATTRS{ text_pen = COLOR_LIGHTCYAN, ignore_keys = {'STRING_A096'}, pen_selection = COLOR_CYAN, - on_change = DEFAULT_NIL, + on_text_change = DEFAULT_NIL, on_cursor_change = DEFAULT_NIL, enable_cursor_blink = true, debug = false, @@ -240,7 +257,8 @@ function TextEditorView:init() self.clipboard = nil self.clipboard_mode = CLIPBOARD_MODE.LOCAL self.render_start_line_y = 1 - self.cursor = #self.text + 1 + + self.cursor = nil self.main_pen = dfhack.pen.parse({ fg=self.text_pen, @@ -295,7 +313,7 @@ function TextEditorView:setCursor(cursor_offset) self.last_cursor_x = nil if self.on_cursor_change then - self.on_cursor_change() + self.on_cursor_change(self.cursor) end end @@ -394,8 +412,8 @@ function TextEditorView:setText(text) self:recomputeLines() - if changed and self.on_change then - self.on_change(text) + if changed and self.on_text_change then + self.on_text_change(text) end end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index e2bf78131..357ea9cad 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -72,7 +72,8 @@ end local function arrange_empty_journal(options) options = options or {} - gui_journal.main() + gui_journal.main({save_prefix='test:'}) + local journal = gui_journal.view journal.save_on_change = options.save_on_change or false @@ -94,7 +95,9 @@ local function arrange_empty_journal(options) local text_area = journal_window.subviews.text_area text_area.enable_cursor_blink = false - text_area:setText('') + if not options.save_on_change then + text_area:setText('') + end if not options.allow_layout_restore then local toc_panel = journal_window.subviews.table_of_contents_panel @@ -2411,6 +2414,7 @@ function test.restore_layout() journal.subviews.table_of_contents_panel.frame.w = 37 journal:updateLayout() + journal:dismiss() journal, _ = arrange_empty_journal({allow_layout_restore=true}) @@ -2420,7 +2424,39 @@ function test.restore_layout() expect.eq(journal.subviews.journal_window.frame.w, 80) expect.eq(journal.subviews.journal_window.frame.h, 23) - expect.eq(journal.subviews.table_of_contents_panel.frame.w, 37) + journal:dismiss() +end + +function test.restore_text_between_sessions() + local journal, text_area = arrange_empty_journal({w=80,save_on_change=true}) + + simulate_input_keys('CUSTOM_CTRL_A') + simulate_input_keys('CUSTOM_CTRL_D') + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas,', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n') + + simulate_input_text(text) + simulate_mouse_click(text_area, 10, 1) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed c_nsectetur, urna sit amet aliquet egestas,', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); + + journal:dismiss() + + journal, text_area = arrange_empty_journal({w=80, save_on_change=true}) + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed c_nsectetur, urna sit amet aliquet egestas,', + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, '\n')); journal:dismiss() end From 0e23cfe8fb1a30250f9acd24a2f994f2637360a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 22 Jul 2024 18:43:58 +0200 Subject: [PATCH 11/25] Move journal scrollbar 1 pixel to the right to save space --- gui/journal.lua | 2 +- test/gui/journal.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 32ad9e080..87cea5add 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -82,7 +82,7 @@ function JournalWindow:init() view_id='journal_editor', frame={t=3, b=0, l=31, r=0}, resize_min={w=30, h=10}, - frame_inset={r=1}, + frame_inset={r=0}, init_text=self.init_text, init_cursor=self.init_cursor, on_text_change=function(text) self:onTextChange(text) end, diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 357ea9cad..67a92c18c 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -84,7 +84,7 @@ local function arrange_empty_journal(options) end if options.w then - journal_window.frame.w = options.w + 7 + journal_window.frame.w = options.w + 6 end if options.h then From 0c36c14fda1e198aafdae71c178c9ee3234a526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 14:30:41 +0200 Subject: [PATCH 12/25] Implement new gui for journal table of contents collapsing mechanism --- gui/journal.lua | 149 ++++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 55 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 87cea5add..4999fe067 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -13,6 +13,50 @@ JOURNAL_PERSIST_KEY = 'journal' journal_config = journal_config or json.open('dfhack-config/journal.json') +local TO_THE_RIGHT = string.char(16) +local TO_THE_LEFT = string.char(17) + +function get_shifter_text(state) + local ch = state and TO_THE_RIGHT or TO_THE_LEFT + return { + ' ', NEWLINE, + ch, NEWLINE, + ch, NEWLINE, + ' ', NEWLINE, + } +end + +Shifter = defclass(Shifter, widgets.Widget) +Shifter.ATTRS { + frame={l=0, w=1, t=0, b=0}, + collapsed=false, + on_changed=DEFAULT_NIL, +} + +function Shifter:init() + self:addviews{ + widgets.Label{ + view_id='shifter_label', + frame={l=0, r=0, t=0, b=0}, + text=get_shifter_text(self.collapsed), + on_click=function () + self:toggle(not self.collapsed) + end + } + } +end + +function Shifter:toggle(state) + self.collapsed = state + self.subviews.shifter_label:setText(get_shifter_text(state)) + + self:updateLayout() + + if self.on_changed then + self.on_changed(self.collapsed) + end +end + JournalWindow = defclass(JournalWindow, widgets.Window) JournalWindow.ATTRS { frame_title='DF Journal', @@ -26,63 +70,57 @@ JournalWindow.ATTRS { on_layout_change=DEFAULT_NIL } -function JournalWindow:loadConfig() - local window_frame = copyall(journal_config.data.frame or {}) - local table_of_contents = copyall(journal_config.data.toc or { - width=20, - visible=false - }) - - self.frame = self:sanitizeFrame(window_frame) +function JournalWindow:init() + local frame, toc_visible, toc_width = self.loadConfig() - local toc_panel = self.subviews.table_of_contents_panel - toc_panel.frame.w = table_of_contents.width - toc_panel.visible = table_of_contents.visible -end + self.frame = self:sanitizeFrame(frame) -function JournalWindow:init() self:addviews({ - widgets.Panel{ - frame={t=1, r=0,h=1}, - subviews={ - widgets.TextButton{ - frame={l=0,w=13}, - label='ToC', - key='CUSTOM_CTRL_T', - on_activate=self:callback('toggleToCVisibililty'), - enabled=true, - }, - } - }, widgets.Panel{ view_id='table_of_contents_panel', - frame_title='Table of contents', - frame_style = gui.FRAME_INTERIOR, + frame={l=0, w=toc_width, t=1, b=0}, + visible=toc_visible, + resize_min={w=25}, resizable=true, + resize_anchors={l=false, t=false, b=true, r=true}, + frame_style=gui.FRAME_INTERIOR, + + frame_title='Table of contents', frame_background = gui.CLEAR_PEN, - resize_min={w=20}, - resize_anchors={r=true}, - frame={l=0, t=3, b=0, w=20}, - visible=false, on_resize_begin=self:callback('onPanelResizeBegin'), on_resize_end=self:callback('onPanelResizeEnd'), + subviews={ widgets.List{ - frame={l=1,t=1}, + frame={l=1, t=0, r=1, b=0}, view_id='table_of_contents', choices={}, on_submit=self:callback('onTableOfContentsSubmit') }, } }, + Shifter{ + view_id='shifter', + frame={l=0, w=1, t=1, b=0}, + collapsed=not toc_visible, + on_changed = function (collapsed) + self.subviews.table_of_contents_panel.visible = not collapsed + if not colllapsed then + self:reloadTableOfContents(self.init_text) + end + + self:ensurePanelsRelSize() + self:updateLayout() + end, + }, text_editor.TextEditor{ view_id='journal_editor', - frame={t=3, b=0, l=31, r=0}, + frame={t=1, b=0, l=25, r=0}, resize_min={w=30, h=10}, - frame_inset={r=0}, + frame_inset={l=1,r=0}, init_text=self.init_text, init_cursor=self.init_cursor, on_text_change=function(text) self:onTextChange(text) end, @@ -94,18 +132,9 @@ function JournalWindow:init() }, }) - self:loadConfig() self:reloadTableOfContents(self.init_text) end -function JournalWindow:toggleToCVisibililty() - self.subviews.table_of_contents_panel.visible = - not self.subviews.table_of_contents_panel.visible - - self:reloadTableOfContents(self.subviews.journal_editor:getText()) - self:updateLayout() -end - function JournalWindow:sanitizeFrame(frame) local w, h = dfhack.screen.getWindowSize() local min = RESIZE_MIN @@ -129,51 +158,61 @@ function JournalWindow:sanitizeFrame(frame) end function JournalWindow:saveConfig() - local toc_panel = self.subviews.table_of_contents_panel + local toc = self.subviews.table_of_contents_panel utils.assign(journal_config.data, { frame = self.frame, toc = { - width = toc_panel.frame.w, - visible = toc_panel.visible + width = toc.frame.w, + visible = toc.visible } }) journal_config:write() end +function JournalWindow:loadConfig() + local window_frame = copyall(journal_config.data.frame or {}) + local table_of_contents = copyall(journal_config.data.toc or { + width=20, + visible=false + }) + + return window_frame, table_of_contents.visible or false, table_of_contents.width or 25 +end + function JournalWindow:onPanelResizeBegin() self.resizing_panels = true end function JournalWindow:onPanelResizeEnd() self.resizing_panels = false - self:esnurePanelsRelSize() + self:ensurePanelsRelSize() self:updateLayout() end function JournalWindow:onRenderBody(painter) if self.resizing_panels then - self:esnurePanelsRelSize() + self:ensurePanelsRelSize() self:updateLayout() end - return JournalWindow.super.onRenderBody(painter) + return JournalWindow.super.onRenderBody(self, painter) end -function JournalWindow:esnurePanelsRelSize() - local toc = self.subviews.table_of_contents_panel +function JournalWindow:ensurePanelsRelSize() + local toc_panel = self.subviews.table_of_contents_panel local editor = self.subviews.journal_editor - toc.frame.w = math.min( - math.max(toc.frame.w, toc.resize_min.w), + toc_panel.frame.w = math.min( + math.max(toc_panel.frame.w, toc_panel.resize_min.w), self.frame.w - editor.resize_min.w ) - editor.frame.l = toc.visible and (toc.frame.w + 1) or 1 + editor.frame.l = toc_panel.visible and toc_panel.frame.w or 1 end function JournalWindow:preUpdateLayout() - self:esnurePanelsRelSize() + self:ensurePanelsRelSize() end function JournalWindow:postUpdateLayout() From f3de0179bfa85a6825181d51c9f9e306d2eae08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 15:02:35 +0200 Subject: [PATCH 13/25] Adjust journal tests to new collapsing table of contents gui --- gui/journal.lua | 50 +++++++++++++++++++++++++++++++++++++------- test/gui/journal.lua | 26 ++++++++++++----------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 4999fe067..811bfadef 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -47,8 +47,15 @@ function Shifter:init() end function Shifter:toggle(state) - self.collapsed = state - self.subviews.shifter_label:setText(get_shifter_text(state)) + if state == nil then + self.collapsed = not self.collapsed + else + self.collapsed = state + end + + self.subviews.shifter_label:setText( + get_shifter_text(self.collapsed) + ) self:updateLayout() @@ -65,15 +72,17 @@ JournalWindow.ATTRS { frame_inset=0, init_text=DEFAULT_NIL, init_cursor=1, + save_layout=true, + on_text_change=DEFAULT_NIL, on_cursor_change=DEFAULT_NIL, on_layout_change=DEFAULT_NIL } function JournalWindow:init() - local frame, toc_visible, toc_width = self.loadConfig() + local frame, toc_visible, toc_width = self:loadConfig() - self.frame = self:sanitizeFrame(frame) + self.frame = frame and self:sanitizeFrame(frame) or self.frame self:addviews({ widgets.Panel{ @@ -109,7 +118,9 @@ function JournalWindow:init() on_changed = function (collapsed) self.subviews.table_of_contents_panel.visible = not collapsed if not colllapsed then - self:reloadTableOfContents(self.init_text) + self:reloadTableOfContents( + self.subviews.journal_editor:getText() + ) end self:ensurePanelsRelSize() @@ -135,6 +146,15 @@ function JournalWindow:init() self:reloadTableOfContents(self.init_text) end +function JournalWindow:onInput(keys) + if keys.CUSTOM_CTRL_O then + self.subviews.shifter:toggle() + return true + end + + return JournalWindow.super.onInput(self, keys) +end + function JournalWindow:sanitizeFrame(frame) local w, h = dfhack.screen.getWindowSize() local min = RESIZE_MIN @@ -158,6 +178,10 @@ function JournalWindow:sanitizeFrame(frame) end function JournalWindow:saveConfig() + if not self.save_layout then + return + end + local toc = self.subviews.table_of_contents_panel utils.assign(journal_config.data, { @@ -171,6 +195,10 @@ function JournalWindow:saveConfig() end function JournalWindow:loadConfig() + if not self.save_layout then + return nil, false, 25 + end + local window_frame = copyall(journal_config.data.frame or {}) local table_of_contents = copyall(journal_config.data.toc or { width=20, @@ -258,10 +286,11 @@ JournalScreen = defclass(JournalScreen, gui.ZScreen) JournalScreen.ATTRS { focus_path='journal', save_on_change=true, + save_layout=true, save_prefix='' } -function JournalScreen:init(options) +function JournalScreen:init() local context = self:loadContext() self:addviews{ @@ -272,6 +301,8 @@ function JournalScreen:init(options) resizable=true, frame_inset=0, + save_layout=self.save_layout, + init_text=context.text[1], init_cursor=context.cursor[1], @@ -316,8 +347,13 @@ function main(options) qerror('journal requires a fortress map to be loaded') end + local save_layout = options and options.save_layout + local save_on_change = options and options.save_on_change + view = view and view:raise() or JournalScreen{ - save_prefix=options and options.save_prefix or '' + save_prefix=options and options.save_prefix or '', + save_layout=save_layout == nil and true or save_layout, + save_on_change=save_on_change == nil and true or save_on_change, }:show() end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 67a92c18c..b8153ac07 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -72,11 +72,13 @@ end local function arrange_empty_journal(options) options = options or {} - gui_journal.main({save_prefix='test:'}) + gui_journal.main({ + save_prefix='test:', + save_on_change=options.save_on_change or false, + save_layout=options.allow_layout_restore or false + }) local journal = gui_journal.view - journal.save_on_change = options.save_on_change or false - local journal_window = journal.subviews.journal_window if not options.allow_layout_restore then @@ -84,11 +86,11 @@ local function arrange_empty_journal(options) end if options.w then - journal_window.frame.w = options.w + 6 + journal_window.frame.w = options.w + 7 end if options.h then - journal_window.frame.h = options.h + 5 + journal_window.frame.h = options.h + 3 end @@ -102,7 +104,7 @@ local function arrange_empty_journal(options) if not options.allow_layout_restore then local toc_panel = journal_window.subviews.table_of_contents_panel toc_panel.visible = false - toc_panel.frame.w = 20 + toc_panel.frame.w = 25 end journal:updateLayout() @@ -2403,7 +2405,7 @@ function test.cut_and_paste_selected_text() end function test.restore_layout() - local journal, _ = arrange_empty_journal() + local journal, _ = arrange_empty_journal({allow_layout_restore=true}) journal.subviews.journal_window.frame = { l = 13, @@ -2701,7 +2703,7 @@ function test.generate_table_of_contents() expect.eq(journal.subviews.table_of_contents_panel.visible, false) - simulate_input_keys('CUSTOM_CTRL_T') + simulate_input_keys('CUSTOM_CTRL_O') expect.eq(journal.subviews.table_of_contents_panel.visible, true) @@ -2753,7 +2755,7 @@ function test.jump_to_table_of_contents_sections() simulate_input_text(text) - simulate_input_keys('CUSTOM_CTRL_T') + simulate_input_keys('CUSTOM_CTRL_O') local toc = journal.subviews.table_of_contents @@ -2885,15 +2887,15 @@ function test.resize_table_of_contents_together() expect.eq(text_area.frame_body.width, 101) - simulate_input_keys('CUSTOM_CTRL_T') + simulate_input_keys('CUSTOM_CTRL_O') - expect.eq(text_area.frame_body.width, 81) + expect.eq(text_area.frame_body.width, 101 - 24) local toc_panel = journal.subviews.table_of_contents_panel -- simulate mouse drag resize of toc panel simulate_mouse_drag(toc_panel, toc_panel.frame_body.width, 1, toc_panel.frame_body.width + 10, 1) - expect.eq(text_area.frame_body.width, 71) + expect.eq(text_area.frame_body.width, 101 - 24 - 10) journal:dismiss() end From af181ab2b4e067dee9e69a51c0b0fbab92fe249b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 15:13:00 +0200 Subject: [PATCH 14/25] Fix journal docs table of contents shortcut --- docs/gui/journal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index c7b225a17..5c1c48a77 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -47,7 +47,7 @@ Supported Features - replace selected text, if available - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in -- Table of contests (:kbd:`Ctrl` + :kbd:`T`), with headers line prefixed by `#`, e.g. `# Fort history`, `## Year 1` +- Table of contests (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by `#`, e.g. `# Fort history`, `## Year 1` Usage ----- From 961c74bd5432ab39218242bd948e8d5fc8858216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 15:32:47 +0200 Subject: [PATCH 15/25] Restore gui/journal fix for ASCII mode layout --- internal/journal/text_editor.lua | 2 +- test/gui/journal.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index fd450f4e4..d010a659f 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -103,7 +103,7 @@ function TextEditor:init() self:addviews{ TextEditorView{ view_id='text_area', - frame={l=0,r=2,t=0}, + frame={l=0,r=3,t=0}, text = self.init_text, text_pen = self.text_pen, diff --git a/test/gui/journal.lua b/test/gui/journal.lua index b8153ac07..940972321 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -86,7 +86,7 @@ local function arrange_empty_journal(options) end if options.w then - journal_window.frame.w = options.w + 7 + journal_window.frame.w = options.w + 8 end if options.h then From 49053dd4584eb3078aa97d2033fb420a1f688aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 17:05:54 +0200 Subject: [PATCH 16/25] Fix typo in gui/journal docs --- docs/gui/journal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index 5c1c48a77..d9ab1d366 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -47,7 +47,7 @@ Supported Features - replace selected text, if available - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in -- Table of contests (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by `#`, e.g. `# Fort history`, `## Year 1` +- Table of contests (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by '#', e.g. '# Fort history', '## Year 1' Usage ----- From fff7aac45d520acd1a4a228a20507cdd94b48496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 28 Jul 2024 23:01:11 +0200 Subject: [PATCH 17/25] Improve journal resize gui, add bottom buttons bar --- gui/journal.lua | 112 ++++++++++++++--------------------- internal/journal/shifter.lua | 55 +++++++++++++++++ test/gui/journal.lua | 2 +- 3 files changed, 99 insertions(+), 70 deletions(-) create mode 100644 internal/journal/shifter.lua diff --git a/gui/journal.lua b/gui/journal.lua index 811bfadef..717440b0a 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -6,70 +6,25 @@ local widgets = require 'gui.widgets' local utils = require 'utils' local json = require 'json' local text_editor = reqscript('internal/journal/text_editor') +local shifter = reqscript('internal/journal/shifter') local RESIZE_MIN = {w=32, h=10} JOURNAL_PERSIST_KEY = 'journal' -journal_config = journal_config or json.open('dfhack-config/journal.json') - -local TO_THE_RIGHT = string.char(16) -local TO_THE_LEFT = string.char(17) - -function get_shifter_text(state) - local ch = state and TO_THE_RIGHT or TO_THE_LEFT - return { - ' ', NEWLINE, - ch, NEWLINE, - ch, NEWLINE, - ' ', NEWLINE, - } -end - -Shifter = defclass(Shifter, widgets.Widget) -Shifter.ATTRS { - frame={l=0, w=1, t=0, b=0}, - collapsed=false, - on_changed=DEFAULT_NIL, +local INVISIBLE_FRAME = { + frame_pen=gui.CLEAR_PEN, + signature_pen=false, } -function Shifter:init() - self:addviews{ - widgets.Label{ - view_id='shifter_label', - frame={l=0, r=0, t=0, b=0}, - text=get_shifter_text(self.collapsed), - on_click=function () - self:toggle(not self.collapsed) - end - } - } -end - -function Shifter:toggle(state) - if state == nil then - self.collapsed = not self.collapsed - else - self.collapsed = state - end - - self.subviews.shifter_label:setText( - get_shifter_text(self.collapsed) - ) - - self:updateLayout() - - if self.on_changed then - self.on_changed(self.collapsed) - end -end +journal_config = journal_config or json.open('dfhack-config/journal.json') JournalWindow = defclass(JournalWindow, widgets.Window) JournalWindow.ATTRS { frame_title='DF Journal', resizable=true, resize_min=RESIZE_MIN, - frame_inset=0, + frame_inset={l=0,r=0,t=0,b=0}, init_text=DEFAULT_NIL, init_cursor=1, save_layout=true, @@ -87,15 +42,13 @@ function JournalWindow:init() self:addviews({ widgets.Panel{ view_id='table_of_contents_panel', - frame={l=0, w=toc_width, t=1, b=0}, + frame={l=0, w=toc_width, t=1, b=1}, visible=toc_visible, - resize_min={w=25}, + resize_min={w=20}, resizable=true, resize_anchors={l=false, t=false, b=true, r=true}, - frame_style=gui.FRAME_INTERIOR, - - frame_title='Table of contents', + frame_style=INVISIBLE_FRAME, frame_background = gui.CLEAR_PEN, @@ -111,12 +64,14 @@ function JournalWindow:init() }, } }, - Shifter{ + shifter.Shifter{ view_id='shifter', - frame={l=0, w=1, t=1, b=0}, + frame={l=0, w=1, t=1, b=2}, collapsed=not toc_visible, on_changed = function (collapsed) self.subviews.table_of_contents_panel.visible = not collapsed + self.subviews.table_of_contents_divider.visible = not collapsed + if not colllapsed then self:reloadTableOfContents( self.subviews.journal_editor:getText() @@ -127,9 +82,25 @@ function JournalWindow:init() self:updateLayout() end, }, + widgets.Divider{ + frame={l=0,r=0,b=2,h=1}, + frame_style_l=false, + frame_style_r=false, + interior_l=true, + }, + widgets.Divider{ + view_id='table_of_contents_divider', + + frame={l=30,t=0,b=2,w=1}, + visible=toc_visible, + + interior_b=true, + frame_style_t=false, + frmae_style=INVISIBLE_FRAME, + }, text_editor.TextEditor{ view_id='journal_editor', - frame={t=1, b=0, l=25, r=0}, + frame={t=1, b=3, l=25, r=0}, resize_min={w=30, h=10}, frame_inset={l=1,r=0}, init_text=self.init_text, @@ -141,20 +112,22 @@ function JournalWindow:init() end end }, + widgets.Panel{ + frame={l=0,r=0,b=1,h=1}, + frame_inset={l=1,r=1,t=0, w=100}, + subviews={ + widgets.HotkeyLabel{ + key='CUSTOM_CTRL_O', + label='Table of Contents', + on_activate=function() self.subviews.shifter:toggle() end + } + } + } }) self:reloadTableOfContents(self.init_text) end -function JournalWindow:onInput(keys) - if keys.CUSTOM_CTRL_O then - self.subviews.shifter:toggle() - return true - end - - return JournalWindow.super.onInput(self, keys) -end - function JournalWindow:sanitizeFrame(frame) local w, h = dfhack.screen.getWindowSize() local min = RESIZE_MIN @@ -231,12 +204,14 @@ end function JournalWindow:ensurePanelsRelSize() local toc_panel = self.subviews.table_of_contents_panel local editor = self.subviews.journal_editor + local divider = self.subviews.table_of_contents_divider toc_panel.frame.w = math.min( math.max(toc_panel.frame.w, toc_panel.resize_min.w), self.frame.w - editor.resize_min.w ) editor.frame.l = toc_panel.visible and toc_panel.frame.w or 1 + divider.frame.l = editor.frame.l - 1 end function JournalWindow:preUpdateLayout() @@ -299,7 +274,6 @@ function JournalScreen:init() frame={w=65, h=45}, resize_min={w=50, h=20}, resizable=true, - frame_inset=0, save_layout=self.save_layout, diff --git a/internal/journal/shifter.lua b/internal/journal/shifter.lua new file mode 100644 index 000000000..1c4aefea4 --- /dev/null +++ b/internal/journal/shifter.lua @@ -0,0 +1,55 @@ +-- >> / << toggle button +--@ module = true + +local widgets = require 'gui.widgets' + +local TO_THE_RIGHT = string.char(16) +local TO_THE_LEFT = string.char(17) + +function get_shifter_text(state) + local ch = state and TO_THE_RIGHT or TO_THE_LEFT + return { + ' ', NEWLINE, + ch, NEWLINE, + ch, NEWLINE, + ' ', NEWLINE, + } +end + +Shifter = defclass(Shifter, widgets.Widget) +Shifter.ATTRS { + frame={l=0, w=1, t=0, b=0}, + collapsed=false, + on_changed=DEFAULT_NIL, +} + +function Shifter:init() + self:addviews{ + widgets.Label{ + view_id='shifter_label', + frame={l=0, r=0, t=0, b=0}, + text=get_shifter_text(self.collapsed), + on_click=function () + self:toggle(not self.collapsed) + end + } + } +end + +function Shifter:toggle(state) + if state == nil then + self.collapsed = not self.collapsed + else + self.collapsed = state + end + + self.subviews.shifter_label:setText( + get_shifter_text(self.collapsed) + ) + + self:updateLayout() + + if self.on_changed then + self.on_changed(self.collapsed) + end +end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 940972321..563804644 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -90,7 +90,7 @@ local function arrange_empty_journal(options) end if options.h then - journal_window.frame.h = options.h + 3 + journal_window.frame.h = options.h + 6 end From a9c517e1996c3ce92e3e1522dfe358896eec11ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 1 Aug 2024 21:50:53 +0200 Subject: [PATCH 18/25] Change journal words jump shortcuts to ctrl+left/ctrl+right It works only in adventure beta 51.01+ --- docs/gui/journal.rst | 4 ++-- gui/journal.lua | 2 +- internal/journal/text_editor.lua | 4 ++-- test/gui/journal.lua | 30 +++++++++++++++--------------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index d9ab1d366..81d77d130 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -20,7 +20,7 @@ Supported Features ------------------ - Cursor Control: Navigate through text using arrow keys (left, right, up, down) for precise cursor placement. -- Fast Rewind: Use :kbd:`Shift` + :kbd:`Left` / :kbd:`Ctrl` + :kbd:`B` and :kbd:`Shift` + :kbd:`Right` / :kbd:`Ctrl` + :kbd:`F` to move the cursor one word back or forward. +- Fast Rewind: Use :kbd:`Ctrl` + :kbd:`Left` / :kbd:`Ctrl` + :kbd:`B` and :kbd:`Ctrl` + :kbd:`Right` / :kbd:`Ctrl` + :kbd:`F` to move the cursor one word back or forward. - Longest X Position Memory: The cursor remembers the longest x position when moving up or down, making vertical navigation more intuitive. - Mouse Control: Use the mouse to position the cursor within the text, providing an alternative to keyboard navigation. - New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting multiline text input. @@ -47,7 +47,7 @@ Supported Features - replace selected text, if available - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in -- Table of contests (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by '#', e.g. '# Fort history', '## Year 1' +- Table of contents (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by '#', e.g. '# Fort history', '## Year 1' Usage ----- diff --git a/gui/journal.lua b/gui/journal.lua index 717440b0a..03e8b109a 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -42,7 +42,7 @@ function JournalWindow:init() self:addviews({ widgets.Panel{ view_id='table_of_contents_panel', - frame={l=0, w=toc_width, t=1, b=1}, + frame={l=0, w=toc_width, t=0, b=1}, visible=toc_visible, resize_min={w=20}, diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index d010a659f..044f86b35 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -750,12 +750,12 @@ function TextEditorView:onCursorInput(keys) -- go to text end self:setCursor(#self.text + 1) return true - elseif keys.CUSTOM_CTRL_B or keys.KEYBOARD_CURSOR_LEFT_FAST then + elseif keys.CUSTOM_CTRL_B or keys.A_MOVE_W_DOWN then -- back one word local word_start = self:wordStartOffset() self:setCursor(word_start) return true - elseif keys.CUSTOM_CTRL_F or keys.KEYBOARD_CURSOR_RIGHT_FAST then + elseif keys.CUSTOM_CTRL_F or keys.A_MOVE_E_DOWN then -- forward one word local word_end = self:wordEndOffset() self:setCursor(word_end) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 563804644..fe5b3a35b 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -699,7 +699,7 @@ function test.fast_rewind_words_right() text_area:setCursor(1) journal:onRender() - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -709,7 +709,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', @@ -720,7 +720,7 @@ function test.fast_rewind_words_right() }, '\n')); for i=1,6 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -731,7 +731,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -741,7 +741,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -752,7 +752,7 @@ function test.fast_rewind_words_right() }, '\n')); for i=1,17 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -763,7 +763,7 @@ function test.fast_rewind_words_right() 'libero._', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -786,7 +786,7 @@ function test.fast_rewind_words_left() simulate_input_text(text) - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -796,7 +796,7 @@ function test.fast_rewind_words_left() '_ibero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -807,7 +807,7 @@ function test.fast_rewind_words_left() }, '\n')); for i=1,8 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -818,7 +818,7 @@ function test.fast_rewind_words_left() 'libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -829,7 +829,7 @@ function test.fast_rewind_words_left() }, '\n')); for i=1,16 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -840,7 +840,7 @@ function test.fast_rewind_words_left() 'libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -1482,12 +1482,12 @@ function test.fast_rewind_reset_selection() 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_LEFT_FAST') + simulate_input_keys('A_MOVE_W_DOWN') expect.eq(read_selected_text(text_area), '') simulate_input_keys('CUSTOM_CTRL_A') - simulate_input_keys('KEYBOARD_CURSOR_RIGHT_FAST') + simulate_input_keys('A_MOVE_E_DOWN') expect.eq(read_selected_text(text_area), '') journal:dismiss() From 59c54ffa9e5a654bd98e1210ddcf1447d246cee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 1 Aug 2024 22:47:26 +0200 Subject: [PATCH 19/25] Make journal selected section follows cursor --- gui/journal.lua | 75 +++++++++++--------------- internal/journal/table_of_contents.lua | 62 +++++++++++++++++++++ internal/journal/text_editor.lua | 1 - test/gui/journal.lua | 69 ++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 internal/journal/table_of_contents.lua diff --git a/gui/journal.lua b/gui/journal.lua index 03e8b109a..58827d487 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -7,6 +7,7 @@ local utils = require 'utils' local json = require 'json' local text_editor = reqscript('internal/journal/text_editor') local shifter = reqscript('internal/journal/shifter') +local table_of_contents = reqscript('internal/journal/table_of_contents') local RESIZE_MIN = {w=32, h=10} @@ -40,29 +41,20 @@ function JournalWindow:init() self.frame = frame and self:sanitizeFrame(frame) or self.frame self:addviews({ - widgets.Panel{ + table_of_contents.TableOfContents{ view_id='table_of_contents_panel', frame={l=0, w=toc_width, t=0, b=1}, visible=toc_visible, + frame_inset={l=0, t=1, b=1, r=1}, resize_min={w=20}, resizable=true, resize_anchors={l=false, t=false, b=true, r=true}, - frame_style=INVISIBLE_FRAME, - - frame_background = gui.CLEAR_PEN, on_resize_begin=self:callback('onPanelResizeBegin'), on_resize_end=self:callback('onPanelResizeEnd'), - subviews={ - widgets.List{ - frame={l=1, t=0, r=1, b=0}, - view_id='table_of_contents', - choices={}, - on_submit=self:callback('onTableOfContentsSubmit') - }, - } + on_submit=self:callback('onTableOfContentsSubmit') }, shifter.Shifter{ view_id='shifter', @@ -73,7 +65,7 @@ function JournalWindow:init() self.subviews.table_of_contents_divider.visible = not collapsed if not colllapsed then - self:reloadTableOfContents( + self.subviews.table_of_contents_panel:reload( self.subviews.journal_editor:getText() ) end @@ -105,12 +97,8 @@ function JournalWindow:init() frame_inset={l=1,r=0}, init_text=self.init_text, init_cursor=self.init_cursor, - on_text_change=function(text) self:onTextChange(text) end, - on_cursor_change=function(cursor) - if self.on_cursor_change ~= nil then - self.on_cursor_change(cursor) - end - end + on_text_change=self:callback('onTextChange'), + on_cursor_change=self:callback('onCursorChange'), }, widgets.Panel{ frame={l=0,r=0,b=1,h=1}, @@ -125,7 +113,19 @@ function JournalWindow:init() } }) - self:reloadTableOfContents(self.init_text) + self.subviews.table_of_contents_panel:reload(self.init_text) +end + +function JournalWindow:onInput(keys) + if (keys.A_MOVE_N_DOWN) then + print('works N') + return false + end + if (keys.A_MOVE_S_DOWN) then + print('works S') + end + + return JournalWindow.super.onInput(self, keys) end function JournalWindow:sanitizeFrame(frame) @@ -222,34 +222,23 @@ function JournalWindow:postUpdateLayout() self:saveConfig() end -function JournalWindow:onTextChange(text) - self:reloadTableOfContents(text) - if self.on_text_change ~= nil then - self.on_text_change(text) - end -end +function JournalWindow:onCursorChange(cursor) + local section_index, cursor_section = self.subviews.table_of_contents_panel:cursorSection( + cursor + ) + self.subviews.table_of_contents_panel:setSelectedSection(section_index) -function JournalWindow:reloadTableOfContents(text) - if not self.subviews.table_of_contents_panel.visible then - return + if self.on_cursor_change ~= nil then + self.on_cursor_change(cursor) end +end - local sections = {} - - local line_cursor = 1 - for line in text:gmatch("[^\n]*") do - local header, section = line:match("^(#+)%s(.+)") - if header ~= nil then - table.insert(sections, { - line_cursor=line_cursor, - text=string.rep(" ", #header - 1) .. section, - }) - end +function JournalWindow:onTextChange(text) + self.subviews.table_of_contents_panel:reload(text) - line_cursor = line_cursor + #line + 1 + if self.on_text_change ~= nil then + self.on_text_change(text) end - - self.subviews.table_of_contents:setChoices(sections) end function JournalWindow:onTableOfContentsSubmit(ind, choice) diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua new file mode 100644 index 000000000..eac816b3e --- /dev/null +++ b/internal/journal/table_of_contents.lua @@ -0,0 +1,62 @@ +--@ module = true + +local gui = require 'gui' +local widgets = require 'gui.widgets' + +TableOfContents = defclass(TableOfContents, widgets.Panel) +TableOfContents.ATTRS { + frame_style=INVISIBLE_FRAME, + frame_background = gui.CLEAR_PEN, + on_submit=DEFAULT_NIL +} + +function TableOfContents:init() + self:addviews({ + widgets.List{ + frame={l=1, t=0, r=1, b=0}, + view_id='table_of_contents', + choices={}, + on_submit=self.on_submit + }, + }) +end + +function TableOfContents:cursorSection(cursor) + local section_ind = nil + + for ind, choice in ipairs(self.subviews.table_of_contents.choices) do + if choice.line_cursor > cursor then + break + end + section_ind = ind + end + + return section_ind, self.subviews.table_of_contents.choices[section_ind] +end + +function TableOfContents:setSelectedSection(section_index) + self.subviews.table_of_contents:setSelected(section_index) +end + +function TableOfContents:reload(text) + if not self.visible then + return + end + + local sections = {} + + local line_cursor = 1 + for line in text:gmatch("[^\n]*") do + local header, section = line:match("^(#+)%s(.+)") + if header ~= nil then + table.insert(sections, { + line_cursor=line_cursor, + text=string.rep(" ", #header - 1) .. section, + }) + end + + line_cursor = line_cursor + #line + 1 + end + + self.subviews.table_of_contents:setChoices(sections) +end diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 044f86b35..8bdd0aeb7 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -237,7 +237,6 @@ function TextEditor:onInput(keys) return TextEditor.super.onInput(self, keys) end - TextEditorView = defclass(TextEditorView, widgets.Widget) TextEditorView.ATTRS{ diff --git a/test/gui/journal.lua b/test/gui/journal.lua index fe5b3a35b..f3584fcc1 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -2899,3 +2899,72 @@ function test.resize_table_of_contents_together() journal:dismiss() end + +function test.table_of_contents_selection_follows_cursor() + local journal, text_area = arrange_empty_journal({ + w=100, + h=50, + allow_layout_restore=false + }) + + local text = table.concat({ + '# Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + '# Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_O') + + local toc = journal.subviews.table_of_contents + + text_area:setCursor(1) + gui_journal.view:onRender() + + expect.eq(toc:getSelected(), 1) + + + text_area:setCursor(8) + gui_journal.view:onRender() + + expect.eq(toc:getSelected(), 1) + + + text_area:setCursor(140) + gui_journal.view:onRender() + + expect.eq(toc:getSelected(), 2) + + + text_area:setCursor(300) + gui_journal.view:onRender() + + print(read_rendered_text(text_area)) + + expect.eq(toc:getSelected(), 3) + + + text_area:setCursor(646) + gui_journal.view:onRender() + + print(read_rendered_text(text_area)) + + expect.eq(toc:getSelected(), 6) + + journal:dismiss() +end From e7001f4b40c5443874092e6fc6bd13799b44c5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 1 Aug 2024 23:10:26 +0200 Subject: [PATCH 20/25] Add keyboard navigation to journal table of contents Jump to previous/next section by ctrl+up/ctrl+down, working only in beta 51.01+ --- docs/gui/journal.rst | 1 + gui/journal.lua | 36 ++++++++++--- internal/journal/table_of_contents.lua | 9 +++- test/gui/journal.lua | 72 ++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index 81d77d130..ba3619d2e 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -48,6 +48,7 @@ Supported Features - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in - Table of contents (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by '#', e.g. '# Fort history', '## Year 1' +- Table of contents navigation: jump to previous/next section by :kbd:`Ctrl` + :kbd:`Up` / :kbd:`Ctrl` + :kbd:`Down` Usage ----- diff --git a/gui/journal.lua b/gui/journal.lua index 58827d487..30d5072ca 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -118,11 +118,31 @@ end function JournalWindow:onInput(keys) if (keys.A_MOVE_N_DOWN) then - print('works N') + local curr_cursor = self.subviews.journal_editor:getCursor() + local section_index, section = self.subviews.table_of_contents_panel:cursorSection( + curr_cursor + ) + + if section.line_cursor == curr_cursor then + self.subviews.table_of_contents_panel:setSelectedSection( + section_index - 1 + ) + self.subviews.table_of_contents_panel:submit() + else + self:onTableOfContentsSubmit(section_index, section) + end + + return false + elseif (keys.A_MOVE_S_DOWN) then + local section_index = self.subviews.table_of_contents_panel:cursorSection( + self.subviews.journal_editor:getCursor() + ) + self.subviews.table_of_contents_panel:setSelectedSection( + section_index + 1 + ) + self.subviews.table_of_contents_panel:submit() + return false - end - if (keys.A_MOVE_S_DOWN) then - print('works S') end return JournalWindow.super.onInput(self, keys) @@ -223,7 +243,7 @@ function JournalWindow:postUpdateLayout() end function JournalWindow:onCursorChange(cursor) - local section_index, cursor_section = self.subviews.table_of_contents_panel:cursorSection( + local section_index = self.subviews.table_of_contents_panel:cursorSection( cursor ) self.subviews.table_of_contents_panel:setSelectedSection(section_index) @@ -241,9 +261,9 @@ function JournalWindow:onTextChange(text) end end -function JournalWindow:onTableOfContentsSubmit(ind, choice) - self.subviews.journal_editor:setCursor(choice.line_cursor) - self.subviews.journal_editor:scrollToCursor(choice.line_cursor) +function JournalWindow:onTableOfContentsSubmit(ind, section) + self.subviews.journal_editor:setCursor(section.line_cursor) + self.subviews.journal_editor:scrollToCursor(section.line_cursor) end JournalScreen = defclass(JournalScreen, gui.ZScreen) diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua index eac816b3e..c325d3ca7 100644 --- a/internal/journal/table_of_contents.lua +++ b/internal/journal/table_of_contents.lua @@ -35,7 +35,14 @@ function TableOfContents:cursorSection(cursor) end function TableOfContents:setSelectedSection(section_index) - self.subviews.table_of_contents:setSelected(section_index) + local curr_sel = self.subviews.table_of_contents:getSelected() + if curr_sel ~= section_index then + self.subviews.table_of_contents:setSelected(section_index) + end +end + +function TableOfContents:submit() + return self.subviews.table_of_contents:submit() end function TableOfContents:reload(text) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index f3584fcc1..6e94ef7e4 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -2954,17 +2954,83 @@ function test.table_of_contents_selection_follows_cursor() text_area:setCursor(300) gui_journal.view:onRender() - print(read_rendered_text(text_area)) - expect.eq(toc:getSelected(), 3) text_area:setCursor(646) gui_journal.view:onRender() - print(read_rendered_text(text_area)) + expect.eq(toc:getSelected(), 6) + + journal:dismiss() +end + +function test.table_of_contents_keyboard_navigation() + local journal, text_area = arrange_empty_journal({ + w=100, + h=50, + allow_layout_restore=false + }) + + local text = table.concat({ + '# Header 1', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Nulla ut lacus ut tortor semper consectetur.', + '# Header 2', + 'Ut eu orci non nibh hendrerit posuere.', + 'Sed euismod odio eu fringilla bibendum.', + '## Subheader 1', + 'Etiam dignissim diam nec aliquet facilisis.', + 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', + '## Subheader 2', + 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', + '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', + '### Subsubheader 1', + '# Header 3', + 'Donec quis lectus ac erat placerat eleifend.', + 'Aenean non orci id erat malesuada pharetra.', + 'Nunc in lectus et metus finibus venenatis.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_O') + + local toc = journal.subviews.table_of_contents + + text_area:setCursor(5) + gui_journal.view:onRender() + + simulate_input_keys('A_MOVE_N_DOWN') + + expect.eq(toc:getSelected(), 1) + + simulate_input_keys('A_MOVE_N_DOWN') expect.eq(toc:getSelected(), 6) + simulate_input_keys('A_MOVE_N_DOWN') + simulate_input_keys('A_MOVE_N_DOWN') + + expect.eq(toc:getSelected(), 4) + + simulate_input_keys('A_MOVE_S_DOWN') + + expect.eq(toc:getSelected(), 5) + + simulate_input_keys('A_MOVE_S_DOWN') + simulate_input_keys('A_MOVE_S_DOWN') + simulate_input_keys('A_MOVE_S_DOWN') + + expect.eq(toc:getSelected(), 2) + + + text_area:setCursor(250) + gui_journal.view:onRender() + + simulate_input_keys('A_MOVE_N_DOWN') + + expect.eq(toc:getSelected(), 3) + journal:dismiss() end From 17e3706720a0d6f14a0ec3bd30c6b61ff444b914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 1 Aug 2024 23:26:31 +0200 Subject: [PATCH 21/25] Fix journal issue with resize frame --- gui/journal.lua | 6 ------ internal/journal/table_of_contents.lua | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 30d5072ca..7a9c153a9 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -13,11 +13,6 @@ local RESIZE_MIN = {w=32, h=10} JOURNAL_PERSIST_KEY = 'journal' -local INVISIBLE_FRAME = { - frame_pen=gui.CLEAR_PEN, - signature_pen=false, -} - journal_config = journal_config or json.open('dfhack-config/journal.json') JournalWindow = defclass(JournalWindow, widgets.Window) @@ -88,7 +83,6 @@ function JournalWindow:init() interior_b=true, frame_style_t=false, - frmae_style=INVISIBLE_FRAME, }, text_editor.TextEditor{ view_id='journal_editor', diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua index c325d3ca7..7639cd591 100644 --- a/internal/journal/table_of_contents.lua +++ b/internal/journal/table_of_contents.lua @@ -3,6 +3,11 @@ local gui = require 'gui' local widgets = require 'gui.widgets' +local INVISIBLE_FRAME = { + frame_pen=gui.CLEAR_PEN, + signature_pen=false, +} + TableOfContents = defclass(TableOfContents, widgets.Panel) TableOfContents.ATTRS { frame_style=INVISIBLE_FRAME, From b42b847041bd8e45a51105f70985e899c68956e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 1 Aug 2024 23:43:47 +0200 Subject: [PATCH 22/25] Fix journal save layout defaults --- gui/journal.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 7a9c153a9..01a91e337 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -187,10 +187,12 @@ function JournalWindow:loadConfig() end local window_frame = copyall(journal_config.data.frame or {}) - local table_of_contents = copyall(journal_config.data.toc or { - width=20, - visible=false - }) + window_frame.w = window_frame.w or 80 + window_frame.h = window_frame.h or 50 + + local table_of_contents = copyall(journal_config.data.toc or {}) + table_of_contents.width = table_of_contents.width or 20 + table_of_contents.visible = table_of_contents.visible or false return window_frame, table_of_contents.visible or false, table_of_contents.width or 25 end From 3cfeb81a83b7865747977aa461cc17a597c3bab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 2 Aug 2024 00:48:01 +0200 Subject: [PATCH 23/25] Temporary disable journal test basing on 51+ DF version --- test/gui/journal.lua | 419 ++++++++++++++++++++++--------------------- 1 file changed, 216 insertions(+), 203 deletions(-) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 6e94ef7e4..7a2674a20 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -6,6 +6,8 @@ config = { mode = 'fortress' } +local df_major_version = tonumber(dfhack.getCompiledDFVersion():match('%d+')) + local function simulate_input_keys(...) local keys = {...} for _,key in ipairs(keys) do @@ -687,172 +689,6 @@ function test.keyboard_arrow_right_navigation() journal:dismiss() end -function test.fast_rewind_words_right() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,6 do - simulate_input_keys('A_MOVE_E_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112:_Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,17 do - simulate_input_keys('A_MOVE_E_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - journal:dismiss() -end - -function test.fast_rewind_words_left() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - '_ibero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus _ec ', - 'libero.', - }, '\n')); - - for i=1,8 do - simulate_input_keys('A_MOVE_W_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - '_nte nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet _gestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,16 do - simulate_input_keys('A_MOVE_W_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - journal:dismiss() -end - function test.handle_backspace() local journal, text_area = arrange_empty_journal({w=55}) @@ -1458,41 +1294,6 @@ function test.arrows_reset_selection() journal:dismiss() end -function test.fast_rewind_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('A_MOVE_E_DOWN') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - function test.click_reset_selection() local journal, text_area = arrange_empty_journal({w=65}) @@ -2873,7 +2674,7 @@ end function test.resize_table_of_contents_together() local journal, text_area = arrange_empty_journal({ w=100, - h=10, + h=20, allow_layout_restore=false }) @@ -2893,7 +2694,13 @@ function test.resize_table_of_contents_together() local toc_panel = journal.subviews.table_of_contents_panel -- simulate mouse drag resize of toc panel - simulate_mouse_drag(toc_panel, toc_panel.frame_body.width, 1, toc_panel.frame_body.width + 10, 1) + simulate_mouse_drag( + toc_panel, + toc_panel.frame_body.width + 1, + 1, + toc_panel.frame_body.width + 1 + 10, + 1 + ) expect.eq(text_area.frame_body.width, 101 - 24 - 10) @@ -2965,6 +2772,11 @@ function test.table_of_contents_selection_follows_cursor() journal:dismiss() end +if df_major_version < 51 then + -- temporary ignore test features that base on newest API of the DF game + return +end + function test.table_of_contents_keyboard_navigation() local journal, text_area = arrange_empty_journal({ w=100, @@ -3034,3 +2846,204 @@ function test.table_of_contents_keyboard_navigation() journal:dismiss() end + +function test.fast_rewind_words_right() + local journal, text_area = arrange_empty_journal({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + text_area:setCursor(1) + journal:onRender() + + simulate_input_keys('A_MOVE_E_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_E_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,6 do + simulate_input_keys('A_MOVE_E_DOWN') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_E_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit._', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_E_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112:_Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,17 do + simulate_input_keys('A_MOVE_E_DOWN') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + simulate_input_keys('A_MOVE_E_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero._', + }, '\n')); + + journal:dismiss() +end + +function test.fast_rewind_words_left() + local journal, text_area = arrange_empty_journal({w=55}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('A_MOVE_W_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + '_ibero.', + }, '\n')); + + simulate_input_keys('A_MOVE_W_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus _ec ', + 'libero.', + }, '\n')); + + for i=1,8 do + simulate_input_keys('A_MOVE_W_DOWN') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + '_nte nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_W_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet _gestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + for i=1,16 do + simulate_input_keys('A_MOVE_W_DOWN') + end + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_W_DOWN') + + expect.eq(read_rendered_text(text_area), table.concat({ + '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', + 'elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ', + 'ante nibh porttitor mi, vitae rutrum eros metus nec ', + 'libero.', + }, '\n')); + + journal:dismiss() +end + +function test.fast_rewind_reset_selection() + local journal, text_area = arrange_empty_journal({w=65}) + + local text = table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n') + + simulate_input_text(text) + + simulate_input_keys('CUSTOM_CTRL_A') + + expect.eq(read_rendered_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + expect.eq(read_selected_text(text_area), table.concat({ + '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', + 'porttitor mi, vitae rutrum eros metus nec libero.', + }, '\n')); + + simulate_input_keys('A_MOVE_W_DOWN') + expect.eq(read_selected_text(text_area), '') + + simulate_input_keys('CUSTOM_CTRL_A') + + simulate_input_keys('A_MOVE_E_DOWN') + expect.eq(read_selected_text(text_area), '') + + journal:dismiss() +end From 184ceb653726803c521dc79c832b86ce37679a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 2 Aug 2024 09:07:25 +0200 Subject: [PATCH 24/25] Add shortcuts to Journal previous/next feature --- gui/journal.lua | 52 ++++++--------------- internal/journal/table_of_contents.lua | 63 ++++++++++++++++++++------ 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 01a91e337..9f43e16f7 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -40,7 +40,7 @@ function JournalWindow:init() view_id='table_of_contents_panel', frame={l=0, w=toc_width, t=0, b=1}, visible=toc_visible, - frame_inset={l=0, t=1, b=1, r=1}, + frame_inset={l=1, t=0, b=1, r=1}, resize_min={w=20}, resizable=true, @@ -61,7 +61,8 @@ function JournalWindow:init() if not colllapsed then self.subviews.table_of_contents_panel:reload( - self.subviews.journal_editor:getText() + self.subviews.journal_editor:getText(), + self.subviews.journal_editor:getCursor() ) end @@ -107,39 +108,10 @@ function JournalWindow:init() } }) - self.subviews.table_of_contents_panel:reload(self.init_text) -end - -function JournalWindow:onInput(keys) - if (keys.A_MOVE_N_DOWN) then - local curr_cursor = self.subviews.journal_editor:getCursor() - local section_index, section = self.subviews.table_of_contents_panel:cursorSection( - curr_cursor - ) - - if section.line_cursor == curr_cursor then - self.subviews.table_of_contents_panel:setSelectedSection( - section_index - 1 - ) - self.subviews.table_of_contents_panel:submit() - else - self:onTableOfContentsSubmit(section_index, section) - end - - return false - elseif (keys.A_MOVE_S_DOWN) then - local section_index = self.subviews.table_of_contents_panel:cursorSection( - self.subviews.journal_editor:getCursor() - ) - self.subviews.table_of_contents_panel:setSelectedSection( - section_index + 1 - ) - self.subviews.table_of_contents_panel:submit() - - return false - end - - return JournalWindow.super.onInput(self, keys) + self.subviews.table_of_contents_panel:reload( + self.init_text, + self.subviews.journal_editor:getCursor() or self.init_cursor + ) end function JournalWindow:sanitizeFrame(frame) @@ -239,9 +211,8 @@ function JournalWindow:postUpdateLayout() end function JournalWindow:onCursorChange(cursor) - local section_index = self.subviews.table_of_contents_panel:cursorSection( - cursor - ) + self.subviews.table_of_contents_panel:setCursor(cursor) + local section_index = self.subviews.table_of_contents_panel:currentSection() self.subviews.table_of_contents_panel:setSelectedSection(section_index) if self.on_cursor_change ~= nil then @@ -250,7 +221,10 @@ function JournalWindow:onCursorChange(cursor) end function JournalWindow:onTextChange(text) - self.subviews.table_of_contents_panel:reload(text) + self.subviews.table_of_contents_panel:reload( + text, + self.subviews.journal_editor:getCursor() + ) if self.on_text_change ~= nil then self.on_text_change(text) diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua index 7639cd591..1e9a66cc3 100644 --- a/internal/journal/table_of_contents.lua +++ b/internal/journal/table_of_contents.lua @@ -12,45 +12,81 @@ TableOfContents = defclass(TableOfContents, widgets.Panel) TableOfContents.ATTRS { frame_style=INVISIBLE_FRAME, frame_background = gui.CLEAR_PEN, - on_submit=DEFAULT_NIL + on_submit=DEFAULT_NIL, + text_cursor=DEFAULT_NIL } function TableOfContents:init() self:addviews({ widgets.List{ - frame={l=1, t=0, r=1, b=0}, + frame={l=0, t=0, r=0, b=3}, view_id='table_of_contents', choices={}, on_submit=self.on_submit }, + widgets.HotkeyLabel{ + frame={b=0}, + key='A_MOVE_N_DOWN', + label='Previous Section', + on_activate=self:callback('previousSection'), + }, + widgets.HotkeyLabel{ + frame={b=1}, + key='A_MOVE_S_DOWN', + label='Next Section', + on_activate=self:callback('nextSection'), + } }) end -function TableOfContents:cursorSection(cursor) - local section_ind = nil +function TableOfContents:previousSection() + local section_cursor, section = self:currentSection() - for ind, choice in ipairs(self.subviews.table_of_contents.choices) do - if choice.line_cursor > cursor then - break - end - section_ind = ind + if section.line_cursor == self.text_cursor then + self.subviews.table_of_contents:setSelected(section_cursor - 1) end - return section_ind, self.subviews.table_of_contents.choices[section_ind] + self.subviews.table_of_contents:submit() +end + +function TableOfContents:nextSection() + local curr_sel = self.subviews.table_of_contents:getSelected() + + local target_sel = self.text_cursor and + self:currentSection() + 1 or curr_sel + 1 + + if curr_sel ~= target_sel then + self.subviews.table_of_contents:setSelected(target_sel) + self.subviews.table_of_contents:submit() + end end function TableOfContents:setSelectedSection(section_index) local curr_sel = self.subviews.table_of_contents:getSelected() + if curr_sel ~= section_index then self.subviews.table_of_contents:setSelected(section_index) end end -function TableOfContents:submit() - return self.subviews.table_of_contents:submit() +function TableOfContents:currentSection() + local section_ind = nil + + for ind, choice in ipairs(self.subviews.table_of_contents.choices) do + if choice.line_cursor > self.text_cursor then + break + end + section_ind = ind + end + + return section_ind, self.subviews.table_of_contents.choices[section_ind] +end + +function TableOfContents:setCursor(cursor) + self.text_cursor = cursor end -function TableOfContents:reload(text) +function TableOfContents:reload(text, cursor) if not self.visible then return end @@ -70,5 +106,6 @@ function TableOfContents:reload(text) line_cursor = line_cursor + #line + 1 end + self.text_cursor = cursor self.subviews.table_of_contents:setChoices(sections) end From 61ec67de3641a0f10388c75027b56a4464f0b846 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 2 Aug 2024 17:07:03 -0700 Subject: [PATCH 25/25] guard DF51-only features --- internal/journal/table_of_contents.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua index 1e9a66cc3..8365564a6 100644 --- a/internal/journal/table_of_contents.lua +++ b/internal/journal/table_of_contents.lua @@ -3,6 +3,8 @@ local gui = require 'gui' local widgets = require 'gui.widgets' +local df_major_version = tonumber(dfhack.getCompiledDFVersion():match('%d+')) + local INVISIBLE_FRAME = { frame_pen=gui.CLEAR_PEN, signature_pen=false, @@ -17,13 +19,23 @@ TableOfContents.ATTRS { } function TableOfContents:init() - self:addviews({ + self:addviews{ widgets.List{ frame={l=0, t=0, r=0, b=3}, view_id='table_of_contents', choices={}, on_submit=self.on_submit }, + } + + if df_major_version < 51 then + -- widgets below this line require DF 51 + -- TODO: remove this check once DF 51 is stable and DFHack is no longer + -- releasing new versions for DF 50 + return + end + + self:addviews{ widgets.HotkeyLabel{ frame={b=0}, key='A_MOVE_N_DOWN', @@ -36,7 +48,7 @@ function TableOfContents:init() label='Next Section', on_activate=self:callback('nextSection'), } - }) + } end function TableOfContents:previousSection()