From 342397538c289e679d5037668ee908479efcd58e Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sat, 3 Sep 2022 16:11:44 +0200 Subject: [PATCH 1/3] Rename scrollBar to scrollBarVertical --- src/trelby.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/trelby.py b/src/trelby.py index 544d5e0c..f7f09d95 100644 --- a/src/trelby.py +++ b/src/trelby.py @@ -167,15 +167,15 @@ def __init__(self, parent, id): hsizer = wx.BoxSizer(wx.HORIZONTAL) - self.scrollBar = wx.ScrollBar(self, -1, style = wx.SB_VERTICAL) + self.scrollBarVertical = wx.ScrollBar(self, -1, style = wx.SB_VERTICAL) self.ctrl = MyCtrl(self, -1) hsizer.Add(self.ctrl, 1, wx.EXPAND) - hsizer.Add(self.scrollBar, 0, wx.EXPAND) + hsizer.Add(self.scrollBarVertical, 0, wx.EXPAND) - self.scrollBar.Bind(wx.EVT_COMMAND_SCROLL, self.ctrl.OnScroll) + self.scrollBarVertical.Bind(wx.EVT_COMMAND_SCROLL, self.ctrl.OnScroll) - self.scrollBar.Bind(wx.EVT_SET_FOCUS, self.OnScrollbarFocus) + self.scrollBarVertical.Bind(wx.EVT_SET_FOCUS, self.OnScrollbarFocus) self.SetSizer(hsizer) @@ -417,8 +417,8 @@ def adjustScrollBar(self): # about draft / layout mode differences. approx = int(((height / self.mm2p) / self.chY) / 1.3) - self.panel.scrollBar.SetScrollbar(self.sp.getTopLine(), approx, - len(self.sp.lines) + approx - 1, approx) + self.panel.scrollBarVertical.SetScrollbar(self.sp.getTopLine(), approx, + len(self.sp.lines) + approx - 1, approx) def clearAutoComp(self): if self.sp.clearAutoComp(): @@ -611,7 +611,7 @@ def OnMouseWheel(self, event): self.updateScreen() def OnScroll(self, event): - pos = self.panel.scrollBar.GetThumbPosition() + pos = self.panel.scrollBarVertical.GetThumbPosition() self.sp.setTopLine(pos) self.sp.clearAutoComp() self.updateScreen() From bee7537c4cf97319403763aaa7f95d31f7778b88 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Wed, 7 Sep 2022 16:15:19 +0200 Subject: [PATCH 2/3] Enable horizontal scrolling By putting the Control into a ScrolledWindow, we get scroll bars whenever the Control's container is smaller than its minimum size. Vertical scrolling of the ScrolledWindow currently is deactivated, as we still have our custom implementation for vertical scrolling. This, however, raises some issues: At first, the vertical scroll bar gets scrolled away along with the Screenplay Control while scrolling horizontally, as it is part of the contents of the ScrolledWindow. Also, the hand-made scrollbar lacks some features, such as auto-hiding or the "precise scrolling mode" on Gtk. Eventually, the custom scrolling implementation is probably going to get in our way when implementing #20 anyway, so we should try to port vertical scrolling to the ScrolledWindow scrolling mechanism. --- src/trelby.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/trelby.py b/src/trelby.py index f7f09d95..ee1dc203 100644 --- a/src/trelby.py +++ b/src/trelby.py @@ -156,14 +156,14 @@ def save(self): def saveScDict(self): util.writeToFile(self.scDictFilename, self.scDict.save(), mainFrame) -class MyPanel(wx.Panel): +class MyPanel(wx.ScrolledWindow): def __init__(self, parent, id): - wx.Panel.__init__( + wx.ScrolledWindow.__init__( self, parent, id, # wxMSW/Windows does not seem to support # wx.NO_BORDER, which sucks - style = wx.WANTS_CHARS | wx.NO_BORDER) + style = wx.WANTS_CHARS | wx.NO_BORDER | wx.HSCROLL) hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -178,6 +178,8 @@ def __init__(self, parent, id): self.scrollBarVertical.Bind(wx.EVT_SET_FOCUS, self.OnScrollbarFocus) self.SetSizer(hsizer) + self.SetScrollRate(int(self.ctrl.chX), int(self.ctrl.chY)) + self.EnableScrolling(True, False) # we never want the scrollbar to get the keyboard focus, pass it on to # the main widget @@ -435,6 +437,8 @@ def isUntouched(self): def updateScreen(self, redraw = True, setCommon = True): self.adjustScrollBar() + self.SetMinSize(wx.Size(int(self.pageW), 10)) # the vertical min size is irrelevant currently, as vertical scrolling is still self-implemented + self.PostSizeEventToParent() if setCommon: self.updateCommon() From 650c7497a2cb2f8931b5a8f3eb29424355659ee1 Mon Sep 17 00:00:00 2001 From: Jano Paetzold Date: Sat, 10 Sep 2022 14:35:25 +0200 Subject: [PATCH 3/3] Let wx.ScrolledView handle vertical scrolling, too This makes our panel render the full document (rather than just the part which is currently visible due to the scroll position), and then let wx.ScrollView scroll through that fully-rendered view. This is still incomplete, as it lacks - porting the remaining scrolling functionality from our panel to wx.ScrolledView (e. g. `makeLineVisible`, which should automatically scroll to the current line) - port all views over to render the full document rather than only the currently visible parts, and provide information about size and line height It also has the draw-back that it always renders the full document, which might lead to performance problems with really large documents and/or older computers (not tested yet and no issues know, but theoretically, it increases the use of resources). --- src/trelby.py | 13 +++-- src/viewmode.py | 131 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 96 insertions(+), 48 deletions(-) diff --git a/src/trelby.py b/src/trelby.py index ee1dc203..e016c919 100644 --- a/src/trelby.py +++ b/src/trelby.py @@ -163,7 +163,7 @@ def __init__(self, parent, id): self, parent, id, # wxMSW/Windows does not seem to support # wx.NO_BORDER, which sucks - style = wx.WANTS_CHARS | wx.NO_BORDER | wx.HSCROLL) + style = wx.WANTS_CHARS | wx.NO_BORDER | wx.HSCROLL | wx.VSCROLL) hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -171,7 +171,6 @@ def __init__(self, parent, id): self.ctrl = MyCtrl(self, -1) hsizer.Add(self.ctrl, 1, wx.EXPAND) - hsizer.Add(self.scrollBarVertical, 0, wx.EXPAND) self.scrollBarVertical.Bind(wx.EVT_COMMAND_SCROLL, self.ctrl.OnScroll) @@ -179,7 +178,7 @@ def __init__(self, parent, id): self.SetSizer(hsizer) self.SetScrollRate(int(self.ctrl.chX), int(self.ctrl.chY)) - self.EnableScrolling(True, False) + self.EnableScrolling(True, True) # we never want the scrollbar to get the keyboard focus, pass it on to # the main widget @@ -188,7 +187,7 @@ def OnScrollbarFocus(self, event): class MyCtrl(wx.Control): - def __init__(self, parent, id): + def __init__(self, parent: MyPanel, id): style = wx.WANTS_CHARS | wx.FULL_REPAINT_ON_RESIZE | wx.NO_BORDER wx.Control.__init__(self, parent, id, style = style) @@ -435,9 +434,13 @@ def isUntouched(self): else: return True + def getVisibleAreaSize(self): + ''' Size of the area visble in the scrolled window''' + return self.panel.GetSize() + def updateScreen(self, redraw = True, setCommon = True): self.adjustScrollBar() - self.SetMinSize(wx.Size(int(self.pageW), 10)) # the vertical min size is irrelevant currently, as vertical scrolling is still self-implemented + self.SetMinSize(wx.Size(int(self.pageW), gd.vm.getDocumentHeight(self))) self.PostSizeEventToParent() if setCommon: diff --git a/src/viewmode.py b/src/viewmode.py index e50028f1..c9166f0a 100644 --- a/src/viewmode.py +++ b/src/viewmode.py @@ -1,8 +1,12 @@ # -*- coding: iso-8859-1 -*- +import math +from typing import Optional import config import mypager import pml +import screenplay +import trelby import util # Number of lines the smooth scroll will try to search. 15-20 is a good @@ -82,6 +86,9 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): def getLineHeight(self, ctrl): raise Exception("getLineHeight not implemented") + def getDocumentHeight(self, ctrl, untilLine: Optional[int] = None) -> int: + raise Exception("getLineHeight not implemented") + # return width of one page in (floating point) pixels def getPageWidth(self, ctrl): raise Exception("getPageWidth not implemented") @@ -147,6 +154,10 @@ def makeLineVisibleGeneric(self, ctrl, line, texts, direction, jumpAhead): # helper function for makeLineVisibleGeneric def _makeLineVisibleHelper(self, ctrl, line, direction, jumpAhead): + currentLine = ctrl.sp.line + linePosition = self.getDocumentHeight(ctrl, currentLine) + + startLine = ctrl.sp.getTopLine() sign = 1 if (direction == config.SCROLL_DOWN) else -1 i = 1 @@ -207,13 +218,13 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): marginLeft = int(ctrl.mm2p * cfg.marginLeft) cox = util.clamp((width - ctrl.pageW) // 2, 0) - fyd = ctrl.sp.cfgGl.fontYdelta + fyd = self.getLineHeight(ctrl) length = len(ls) texts = [] while (y < height) and (i < length): - y += int((ctrl.sp.getSpacingBefore(i) / 10.0) * fyd) + y += self.__getLineHeightWithSpacing(ctrl, i) if y >= height: break @@ -245,6 +256,22 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): def getLineHeight(self, ctrl): return ctrl.sp.cfgGl.fontYdelta + def __getLineHeightWithSpacing(self, ctrl, lineNumber: int): + lineHeightWithoutSpacing = self.getLineHeight(ctrl) + return lineHeightWithoutSpacing + int((ctrl.sp.getSpacingBefore(lineNumber) / 10.0) * lineHeightWithoutSpacing) + + def getDocumentHeight(self, ctrl, untilLine: Optional[int] = None) -> int: + if untilLine is None: + untilLine = len(ctrl.sp.lines) + height = 0 + for i in range(untilLine): + height += self.__getLineHeightWithSpacing(ctrl, i) + + height += ctrl.getVisibleAreaSize().GetHeight() # One should always be able to scroll at least until the last line is on top + + return height + + def getPageWidth(self, ctrl): # this is not really used for much in draft mode, as it has no # concept of page width, but it's safer to return something @@ -264,7 +291,7 @@ def pageCmd(self, ctrl, cs, dir, texts, dpages): # Layout view mode. Pages are shown with the actual layout they would # have. class ViewModeLayout(ViewMode): - + PAGE_GAP = 10 # gap between pages (pixels) def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): cfgGui = ctrl.getCfgGui() textOp = pml.TextOp @@ -274,8 +301,6 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): width, height = ctrl.GetClientSize() - # gap between pages (pixels) - pageGap = 10 pager = mypager.Pager(ctrl.sp.cfg) mm2p = ctrl.mm2p @@ -307,7 +332,7 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): if not topOfPage: y = -int(op.y * mm2p) else: - y = pageGap + y = self.PAGE_GAP break else: @@ -366,7 +391,7 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): cfgGui.fonts[op.flags & 3], op.flags & pml.UNDERLINED)) - y = pageY + ctrl.pageH + pageGap + y = pageY + ctrl.pageH + self.PAGE_GAP pg = None # if user has inserted new text causing the script to overflow @@ -385,6 +410,23 @@ def getLineHeight(self, ctrl): # lines. return int(ctrl.chY * ctrl.mm2p + 1.0) + def getDocumentHeight(self, ctrl, untilLine: Optional[int] = None) -> int: + ''' + :type ctrl: trelby.MyCtrl + ''' + if untilLine is None: + untilPage = len(ctrl.sp.pages) + else: + untilPage = ctrl.sp.line2page(untilLine) - 1 + + height = (untilPage - 1) * (ctrl.pageH + self.PAGE_GAP) + height += 2 * self.PAGE_GAP # gap on top and on the bottom + + if untilLine is not None: + height += ctrl.chY * ctrl.mm2p # in case a specific line is requested, we need to be line exact + + return height + def getPageWidth(self, ctrl): return (ctrl.sp.cfg.paperWidth / ctrl.chX) *\ ctrl.getCfgGui().fonts[pml.NORMAL].fx @@ -402,7 +444,7 @@ def pageCmd(self, ctrl, cs, dir, texts, dpages): # would have, as many pages at a time as fit on the screen, complete pages # only, in a single row. class ViewModeSideBySide(ViewMode): - + PAGE_GAP = 10 # gap between pages (+ screen left edge) def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): cfgGui = ctrl.getCfgGui() textOp = pml.TextOp @@ -414,57 +456,55 @@ def getScreen(self, ctrl, doExtra, partials = False, pageCache = None): mm2p = ctrl.mm2p - # gap between pages (+ screen left edge) - pageGap = 10 - # how many pages fit on screen - pageCnt = max(1, (width - pageGap) // (ctrl.pageW + pageGap)) + pagesPerRow = max(1, (width - self.PAGE_GAP) // (ctrl.pageW + self.PAGE_GAP)) pager = mypager.Pager(ctrl.sp.cfg) - topLine = ctrl.sp.getTopLine() - pageNr = ctrl.sp.line2page(topLine) + pageNr = 1 # for now, we just render the whole document if doExtra and ctrl.sp.cfg.pdfShowSceneNumbers: pager.scene = ctrl.sp.getSceneNumber( ctrl.sp.page2lines(pageNr)[0] - 1) - pagesDone = 0 - - while 1: - if (pagesDone >= pageCnt) or (pageNr >= len(ctrl.sp.pages)): - break + sy = self.PAGE_GAP + while (pageNr < len(ctrl.sp.pages)): + pagesDonePerRow = 0 + sx = self.PAGE_GAP + while 1: + if (pagesDonePerRow >= pagesPerRow) or (pageNr >= len(ctrl.sp.pages)): + break - # we'd have to go back an arbitrary number of pages to get an - # accurate number for this in the worst case, so disable it - # altogether. - pager.sceneContNr = 0 + # we'd have to go back an arbitrary number of pages to get an + # accurate number for this in the worst case, so disable it + # altogether. + pager.sceneContNr = 0 - if pageCache: - pg = pageCache.getPage(pager, pageNr) - else: - pg = ctrl.sp.generatePMLPage(pager, pageNr, False, - doExtra) - if not pg: - break + if pageCache: + pg = pageCache.getPage(pager, pageNr) + else: + pg = ctrl.sp.generatePMLPage(pager, pageNr, False, + doExtra) + if not pg: + break - sx = pageGap + pagesDone * (ctrl.pageW + pageGap) - sy = pageGap + sx += pagesDonePerRow * (ctrl.pageW + self.PAGE_GAP) - dp = DisplayPage(pageNr, sx, sy, sx + ctrl.pageW, - sy + ctrl.pageH) - dpages.append(dp) + dp = DisplayPage(pageNr, sx, sy, sx + ctrl.pageW, + sy + ctrl.pageH) + dpages.append(dp) - for op in pg.ops: - if not isinstance(op, textOp): - continue + for op in pg.ops: + if not isinstance(op, textOp): + continue - texts.append(TextString(op.line, op.text, - int(sx + op.x * mm2p), int(sy + op.y * mm2p), - cfgGui.fonts[op.flags & 3], op.flags & pml.UNDERLINED)) + texts.append(TextString(op.line, op.text, + int(sx + op.x * mm2p), int(sy + op.y * mm2p), + cfgGui.fonts[op.flags & 3], op.flags & pml.UNDERLINED)) - pageNr += 1 - pagesDone += 1 + pageNr += 1 + pagesDonePerRow += 1 + sy += ctrl.pageH + self.PAGE_GAP return (texts, dpages) @@ -472,6 +512,11 @@ def getLineHeight(self, ctrl): # the + 1.0 avoids occasional non-consecutive backgrounds for # lines. return int(ctrl.chY * ctrl.mm2p + 1.0) + def getDocumentHeight(self, ctrl) -> int: + pagesPerRow = max(1, (ctrl.GetClientSize().GetWidth() - self.PAGE_GAP) // (ctrl.pageW + self.PAGE_GAP)) + rows = math.ceil((len(ctrl.sp.pages) - 1) / pagesPerRow) + + return int(rows * (ctrl.pageH + self.PAGE_GAP) + 2 * self.PAGE_GAP) def getPageWidth(self, ctrl): return (ctrl.sp.cfg.paperWidth / ctrl.chX) *\