Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Implement zooming #85

Closed
wants to merge 14 commits into from
9 changes: 9 additions & 0 deletions doc/manual.xml
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,15 @@

</sect1>

<sect1>
<title>View/Zoom in,Zoom out</title>

<para>Makes the script appear bigger or smaller in steps of
25&nbsp;%. You can also zoom using CTRL + Plus or CTRL + Minus on
your keyboard.</para>

</sect1>

<sect1>
<title>View/Show formatting</title>

Expand Down
4 changes: 4 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,10 @@ def __init__(self):

Command("Watermark", "Generate watermarked PDFs.",
isMenu = True),
Command("ZoomIn", "Make script appear bigger", [util.Key(ord('+'), ctrl = True).toInt()], isMenu = True,
isFixed = True),
Command("ZoomOut", "Make script appear smaller.", [util.Key(ord('-'), ctrl = True).toInt()], isMenu = True,
isFixed = True),
]

self.recalc()
Expand Down
183 changes: 127 additions & 56 deletions src/trelby.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: iso-8859-1 -*-
from typing import Optional

from error import TrelbyError
import autocompletiondlg
Expand Down Expand Up @@ -165,17 +166,34 @@ def __init__(self, parent, id):
# wx.NO_BORDER, which sucks
style = wx.WANTS_CHARS | wx.NO_BORDER)

hsizer = wx.BoxSizer(wx.HORIZONTAL)
# Vertical scrolling is achieved using a scroll bar that is connected to the Screenplay object. It changes
# the sp.line property when scrolled, which causes the rendering code to render starting from a different line.
# The reasons for this architectural decision are unknown as of 2023, but it has the advantage that only the
# part of the screenplay that gets displayed given the current scrolling position will be rendered.
self.scrollBarVertical = wx.ScrollBar(self, -1, style = wx.SB_VERTICAL)

# Horizontal scrolling is achieved by putting the MyControl instance into a ScrolledWindow. The MyControl
# content will always be rendered at full width, but if the available size is less than the rendered size, a
# scroll bar will automatically appear an allow scrolling.
scrolledWindowForCtrl = wx.ScrolledWindow(self, id, style = wx.HSCROLL)

self.ctrl = MyCtrl(scrolledWindowForCtrl, -1, self.scrollBarVertical)

self.scrollBar = wx.ScrollBar(self, -1, style = wx.SB_VERTICAL)
self.ctrl = MyCtrl(self, -1)
scrolledWindowForCtrl.SetScrollRate(int(self.ctrl.chX), int(self.ctrl.chY))
scrolledWindowForCtrl.EnableScrolling(True, False)

hsizer.Add(self.ctrl, 1, wx.EXPAND)
hsizer.Add(self.scrollBar, 0, wx.EXPAND)
sizer = wx.BoxSizer(wx.VERTICAL) # we need this sizer only to make the MyCtrl expand to the size of the ScrolledWindow
sizer.Add(self.ctrl, 1, wx.EXPAND)
scrolledWindowForCtrl.SetSizer(sizer)

self.scrollBar.Bind(wx.EVT_COMMAND_SCROLL, self.ctrl.OnScroll)
# the vertical scroll bar will be placed next to the (horizontally scrollable) ScrolledWindow that contains the
# MyCtrl instance
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(scrolledWindowForCtrl, 1, wx.EXPAND)
hsizer.Add(self.scrollBarVertical, 0, wx.EXPAND)

self.scrollBar.Bind(wx.EVT_SET_FOCUS, self.OnScrollbarFocus)
self.scrollBarVertical.Bind(wx.EVT_COMMAND_SCROLL, self.ctrl.OnScroll)
self.scrollBarVertical.Bind(wx.EVT_SET_FOCUS, self.OnScrollbarFocus)

self.SetSizer(hsizer)

Expand All @@ -186,11 +204,12 @@ def OnScrollbarFocus(self, event):

class MyCtrl(wx.Control):

def __init__(self, parent, id):
def __init__(self, parent, id, scrollBarVertical: wx.ScrollBar):
style = wx.WANTS_CHARS | wx.FULL_REPAINT_ON_RESIZE | wx.NO_BORDER
wx.Control.__init__(self, parent, id, style = style)

self.panel = parent
self.scrollBarVertical = scrollBarVertical
self.panel = parent.GetParent()

self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_PAINT, self.OnPaint)
Expand Down Expand Up @@ -417,8 +436,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.scrollBarVertical.SetScrollbar(self.sp.getTopLine(), approx,
len(self.sp.lines) + approx - 1, approx)

def clearAutoComp(self):
if self.sp.clearAutoComp():
Expand All @@ -435,6 +454,8 @@ def isUntouched(self):

def updateScreen(self, redraw = True, setCommon = True):
self.adjustScrollBar()
self.SetMinSize(wx.Size(int(self.pageW * self._zoomFactor), 10)) # the vertical min size is irrelevant currently, as vertical scrolling is still self-implemented
self.PostSizeEventToParent()

if setCommon:
self.updateCommon()
Expand Down Expand Up @@ -563,7 +584,7 @@ def OnLeftDown(self, event, mark = False):
self.sp.clearMark()
self.updateScreen()

pos = event.GetPosition()
pos = self.__getScaledGCDC().DeviceToLogical(event.GetPosition())
line, col = gd.vm.pos2linecol(self, pos.x, pos.y)

self.mouseSelectActive = True
Expand All @@ -587,7 +608,7 @@ def OnMotion(self, event):
self.OnLeftDown(event, mark = True)

def OnRightDown(self, event):
pos = event.GetPosition()
pos = self.__getScaledGCDC().DeviceToLogical(event.GetPosition())
line, col = gd.vm.pos2linecol(self, pos.x, pos.y)

if self.sp.mark:
Expand All @@ -611,7 +632,7 @@ def OnMouseWheel(self, event):
self.updateScreen()

def OnScroll(self, event):
pos = self.panel.scrollBar.GetThumbPosition()
pos = self.scrollBarVertical.GetThumbPosition()
self.sp.setTopLine(pos)
self.sp.clearAutoComp()
self.updateScreen()
Expand Down Expand Up @@ -1385,6 +1406,34 @@ def OnKeyChar(self, ev):

self.updateScreen()

_zoomFactor: float = 1.0

def zoom(self, difference: float) -> None:
if self._zoomFactor + difference <= 0:
return

self._zoomFactor += difference
self.updateScreen()

def GetLogicalClientSize(self, context: Optional[wx.GCDC] = None) -> wx.Size:
"""
When calculating layout based on the available size, use this rather than GetClientSize.

GetClientSize returns the real size in absolute px, no zoom taken into account. When calculating the layout
however, we need to do that independently of zoom. That's why we use a unit called "logical px" for creating
the layout, that later gets translated to "real" px.
"""
if context is None:
context = self.__getScaledGCDC()

return context.DeviceToLogicalRel(self.GetClientSize())

def __getScaledGCDC(self):
context = wx.GCDC()
context.SetUserScale(self._zoomFactor, self._zoomFactor)

return context

def OnPaint(self, event):
#ldkjfldsj = util.TimerDev("paint")

Expand Down Expand Up @@ -1414,39 +1463,48 @@ def OnPaint(self, event):

strings, dpages = gd.vm.getScreen(self, True, True)

# draw background

dc.SetBrush(cfgGui.workspaceBrush)
dc.SetPen(cfgGui.workspacePen)
dc.DrawRectangle(0, 0, size.width, size.height)

dc.SetPen(cfgGui.tabBorderPen)
dc.DrawLine(0,0,0,size.height)

# draw pages

zoomedDrawingContext = wx.GCDC(dc)
zoomedDrawingContext.SetUserScale(self._zoomFactor, self._zoomFactor)

size = self.GetLogicalClientSize(zoomedDrawingContext)

if not dpages:
# draft mode; draw an infinite page
lx = util.clamp((size.width - self.pageW) // 2, 0)
rx = lx + self.pageW

dc.SetBrush(cfgGui.textBgBrush)
dc.SetPen(cfgGui.textBgPen)
dc.DrawRectangle(lx, 5, self.pageW, size.height - 5)
zoomedDrawingContext.SetBrush(cfgGui.textBgBrush)
zoomedDrawingContext.SetPen(cfgGui.textBgPen)
zoomedDrawingContext.DrawRectangle(lx, 5, self.pageW, size.height - 5)

dc.SetPen(cfgGui.pageBorderPen)
dc.DrawLine(lx, 5, lx, size.height)
dc.DrawLine(rx, 5, rx, size.height)
zoomedDrawingContext.SetPen(cfgGui.pageBorderPen)
zoomedDrawingContext.DrawLine(lx, 5, lx, size.height)
zoomedDrawingContext.DrawLine(rx, 5, rx, size.height)

else:
dc.SetBrush(cfgGui.textBgBrush)
dc.SetPen(cfgGui.pageBorderPen)
zoomedDrawingContext.SetBrush(cfgGui.textBgBrush)
zoomedDrawingContext.SetPen(cfgGui.pageBorderPen)
for dp in dpages:
dc.DrawRectangle(dp.x1, dp.y1, dp.x2 - dp.x1 + 1,
zoomedDrawingContext.DrawRectangle(dp.x1, dp.y1, dp.x2 - dp.x1 + 1,
dp.y2 - dp.y1 + 1)

dc.SetPen(cfgGui.pageShadowPen)
zoomedDrawingContext.SetPen(cfgGui.pageShadowPen)
for dp in dpages:
# + 2 because DrawLine doesn't draw to end point but stops
# one pixel short...
dc.DrawLine(dp.x1 + 1, dp.y2 + 1, dp.x2 + 1, dp.y2 + 1)
dc.DrawLine(dp.x2 + 1, dp.y1 + 1, dp.x2 + 1, dp.y2 + 2)
zoomedDrawingContext.DrawLine(dp.x1 + 1, dp.y2 + 1, dp.x2 + 1, dp.y2 + 1)
zoomedDrawingContext.DrawLine(dp.x2 + 1, dp.y1 + 1, dp.x2 + 1, dp.y2 + 2)

for t in strings:
i = t.line
Expand All @@ -1458,72 +1516,72 @@ def OnPaint(self, event):
l = ls[i]

if l.lt == screenplay.NOTE:
dc.SetPen(cfgGui.notePen)
dc.SetBrush(cfgGui.noteBrush)
zoomedDrawingContext.SetPen(cfgGui.notePen)
zoomedDrawingContext.SetBrush(cfgGui.noteBrush)

nx = t.x - 5
nw = self.sp.cfg.getType(l.lt).width * fx + 10

dc.DrawRectangle(nx, y, nw, lineh)
zoomedDrawingContext.DrawRectangle(nx, y, nw, lineh)

dc.SetPen(cfgGui.textPen)
util.drawLine(dc, nx - 1, y, 0, lineh)
util.drawLine(dc, nx + nw, y, 0, lineh)
zoomedDrawingContext.SetPen(cfgGui.textPen)
util.drawLine(zoomedDrawingContext, nx - 1, y, 0, lineh)
util.drawLine(zoomedDrawingContext, nx + nw, y, 0, lineh)

if self.sp.isFirstLineOfElem(i):
util.drawLine(dc, nx - 1, y - 1, nw + 2, 0)
util.drawLine(zoomedDrawingContext, nx - 1, y - 1, nw + 2, 0)

if self.sp.isLastLineOfElem(i):
util.drawLine(dc, nx - 1, y + lineh,
util.drawLine(zoomedDrawingContext, nx - 1, y + lineh,
nw + 2, 0)

if marked and self.sp.isLineMarked(i, marked):
c1, c2 = self.sp.getMarkedColumns(i, marked)

dc.SetPen(cfgGui.selectedPen)
dc.SetBrush(cfgGui.selectedBrush)
zoomedDrawingContext.SetPen(cfgGui.selectedPen)
zoomedDrawingContext.SetBrush(cfgGui.selectedBrush)

dc.DrawRectangle(t.x + c1 * fx, y, (c2 - c1 + 1) * fx,
zoomedDrawingContext.DrawRectangle(t.x + c1 * fx, y, (c2 - c1 + 1) * fx,
lineh)

if mainFrame.showFormatting:
dc.SetPen(cfgGui.bluePen)
util.drawLine(dc, t.x, y, 0, lineh)
zoomedDrawingContext.SetPen(cfgGui.bluePen)
util.drawLine(zoomedDrawingContext, t.x, y, 0, lineh)

extraIndent = 1 if self.sp.needsExtraParenIndent(i) else 0

util.drawLine(dc,
util.drawLine(zoomedDrawingContext,
t.x + (self.sp.cfg.getType(l.lt).width - extraIndent) * fx,
y, 0, lineh)

dc.SetTextForeground(cfgGui.redColor)
dc.SetFont(cfgGui.fonts[pml.NORMAL].font)
dc.DrawText(config.lb2char(l.lb), t.x - 10, y)
zoomedDrawingContext.SetTextForeground(cfgGui.redColor)
zoomedDrawingContext.SetFont(cfgGui.fonts[pml.NORMAL].font)
zoomedDrawingContext.DrawText(config.lb2char(l.lb), t.x - 10, y)

if not dpages:
if cfgGl.pbi == config.PBI_REAL_AND_UNADJ:
if self.sp.line2pageNoAdjust(i) != \
self.sp.line2pageNoAdjust(i + 1):
dc.SetPen(cfgGui.pagebreakNoAdjustPen)
util.drawLine(dc, 0, y + lineh - 1,
zoomedDrawingContext.SetPen(cfgGui.pagebreakNoAdjustPen)
util.drawLine(zoomedDrawingContext, 0, y + lineh - 1,
size.width, 0)

if cfgGl.pbi in (config.PBI_REAL,
config.PBI_REAL_AND_UNADJ):
thisPage = self.sp.line2page(i)

if thisPage != self.sp.line2page(i + 1):
dc.SetPen(cfgGui.pagebreakPen)
util.drawLine(dc, 0, y + lineh - 1,
zoomedDrawingContext.SetPen(cfgGui.pagebreakPen)
util.drawLine(zoomedDrawingContext, 0, y + lineh - 1,
size.width, 0)

if i == self.sp.line:
posX = t.x
cursorY = y
acFi = fi
dc.SetPen(cfgGui.cursorPen)
dc.SetBrush(cfgGui.cursorBrush)
dc.DrawRectangle(t.x + self.sp.column * fx, y, fx, fi.fy)
zoomedDrawingContext.SetPen(cfgGui.cursorPen)
zoomedDrawingContext.SetBrush(cfgGui.cursorBrush)
zoomedDrawingContext.DrawRectangle(t.x + self.sp.column * fx, y, fx, fi.fy)

if len(t.text) != 0:
#tl = texts.get(fi.font)
Expand Down Expand Up @@ -1552,22 +1610,22 @@ def OnPaint(self, event):
len(t.text) * fx - 1))

if ulines:
dc.SetPen(cfgGui.textPen)
zoomedDrawingContext.SetPen(cfgGui.textPen)

for ul in ulines:
util.drawLine(dc, ul[0], ul[1], ul[2], 0)
util.drawLine(zoomedDrawingContext, ul[0], ul[1], ul[2], 0)

if ulinesHdr:
dc.SetPen(cfgGui.textHdrPen)
zoomedDrawingContext.SetPen(cfgGui.textHdrPen)

for ul in ulinesHdr:
util.drawLine(dc, ul[0], ul[1], ul[2], 0)
util.drawLine(zoomedDrawingContext, ul[0], ul[1], ul[2], 0)

for tl in texts:
gd.vm.drawTexts(self, dc, tl)
gd.vm.drawTexts(self, zoomedDrawingContext, tl)

if self.sp.acItems and (cursorY > 0):
self.drawAutoComp(dc, posX, cursorY, acFi)
self.drawAutoComp(zoomedDrawingContext, posX, cursorY, acFi)

def drawAutoComp(self, dc, posX, cursorY, fi):
ac = self.sp.acItems
Expand Down Expand Up @@ -1740,6 +1798,9 @@ def __init__(self, parent, id, title):
else:
viewMenu.Check(ID_VIEW_STYLE_SIDE_BY_SIDE, True)

viewMenu.AppendSeparator()
viewMenu.Append(ID_VIEW_ZOOM_IN, 'Zoom &in\tCTRL-+')
viewMenu.Append(ID_VIEW_ZOOM_OUT, 'Zoom &out\tCTRL--')
viewMenu.AppendSeparator()
viewMenu.AppendCheckItem(ID_VIEW_SHOW_FORMATTING, "&Show formatting")
viewMenu.Append(ID_VIEW_FULL_SCREEN, "&Fullscreen\tF11")
Expand Down Expand Up @@ -1923,6 +1984,8 @@ def addTB(id, iconFilename, toolTip):
self.Bind(wx.EVT_MENU, self.OnViewModeChange, id=ID_VIEW_STYLE_DRAFT)
self.Bind(wx.EVT_MENU, self.OnViewModeChange, id=ID_VIEW_STYLE_LAYOUT)
self.Bind(wx.EVT_MENU, self.OnViewModeChange, id=ID_VIEW_STYLE_SIDE_BY_SIDE)
self.Bind(wx.EVT_MENU, self.OnZoomIn, id=ID_VIEW_ZOOM_IN)
self.Bind(wx.EVT_MENU, self.OnZoomOut, id=ID_VIEW_ZOOM_OUT)
self.Bind(wx.EVT_MENU, self.OnShowFormatting, id=ID_VIEW_SHOW_FORMATTING)
self.Bind(wx.EVT_MENU, self.ToggleFullscreen, id=ID_VIEW_FULL_SCREEN)
self.Bind(wx.EVT_MENU, self.OnFindNextError, id=ID_SCRIPT_FIND_ERROR)
Expand Down Expand Up @@ -2043,6 +2106,8 @@ def allocIds(self):
"ID_VIEW_STYLE_DRAFT",
"ID_VIEW_STYLE_LAYOUT",
"ID_VIEW_STYLE_SIDE_BY_SIDE",
"ID_VIEW_ZOOM_IN",
"ID_VIEW_ZOOM_OUT",
"ID_TOOLBAR_SETTINGS",
"ID_TOOLBAR_SCRIPTSETTINGS",
"ID_TOOLBAR_REPORTS",
Expand Down Expand Up @@ -2557,6 +2622,12 @@ def OnSize(self, event):
gd.width, gd.height = self.GetSize()
event.Skip()

def OnZoomIn(self, event=None):
self.panel.ctrl.zoom(0.25)

def OnZoomOut(self, event=None):
self.panel.ctrl.zoom(-0.25)

class MyApp(wx.App):

def OnInit(self):
Expand Down
Loading
Loading