123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- import copy, math, colorsys, re, wx, tiddlywiki, tweelexer
- import geometry, metrics, images
- from passageframe import PassageFrame, ImageFrame, StorySettingsFrame
- class PassageWidget(object):
- """
- A PassageWidget is a box standing in for a proxy for a single
- passage in a story. Users can drag them around, double-click
- to open a PassageFrame, and so on.
- This must have a StoryPanel as its parent.
- See the comments on StoryPanel for more information on the
- coordinate systems are used here. In general, you should
- always pass methods logical coordinates, and expect back
- logical coordinates. Use StoryPanel.toPixels() to convert.
- """
- def __init__(self, parent, app, pos = (0, 0), title = '', text = '', tags = (), state = None):
- # inner state
- self.parent = parent
- self.app = app
- self.dimmed = False
- self.brokenEmblem = wx.Bitmap(self.app.iconsPath + 'brokenemblem.png')
- self.externalEmblem = wx.Bitmap(self.app.iconsPath + 'externalemblem.png')
- self.paintBuffer = wx.MemoryDC()
- self.paintBufferBounds = None
- if state:
- self.passage = state['passage']
- self.pos = list(pos) if pos != (0,0) else state['pos']
- self.selected = state['selected']
- else:
- self.passage = tiddlywiki.Tiddler('')
- self.selected = False
- self.pos = list(pos)
- if title: self.passage.title = title
- if text: self.passage.text = text
- if tags: self.passage.tags += tags
- self.bitmap = None
- self.updateBitmap()
- self.passage.update()
- def getSize(self):
- """Returns this instance's logical size."""
- if self.passage.isAnnotation():
- return (PassageWidget.SIZE+self.parent.GRID_SPACING, PassageWidget.SIZE+self.parent.GRID_SPACING)
- return (PassageWidget.SIZE, PassageWidget.SIZE)
- def getCenter(self):
- """Returns this instance's center in logical coordinates."""
- pos = list(self.pos)
- pos[0] += self.getSize()[0] / 2
- pos[1] += self.getSize()[1] / 2
- return pos
- def getLogicalRect(self):
- """Returns this instance's rectangle in logical coordinates."""
- size = self.getSize()
- return wx.Rect(self.pos[0], self.pos[1], size[0], size[1])
- def getPixelRect(self):
- """Returns this instance's rectangle onscreen."""
- origin = self.parent.toPixels(self.pos)
- size = self.parent.toPixels(self.getSize(), scaleOnly = True)
- return wx.Rect(origin[0], origin[1], size[0], size[1])
- def getDirtyPixelRect(self):
- """
- Returns a pixel rectangle of everything that needs to be redrawn for the widget
- in its current position. This includes the widget itself as well as any
- other widgets it links to.
- """
- dirtyRect = self.getPixelRect()
- # first, passages we link to
- for link in self.passage.links:
- widget = self.parent.findWidget(link)
- if widget:
- dirtyRect.Union(widget.getPixelRect())
- # then, those that link to us
- def addLinkingToRect(widget):
- if self.passage.title in widget.passage.links:
- dirtyRect.Union(widget.getPixelRect())
- self.parent.eachWidget(addLinkingToRect)
- return dirtyRect
- def offset(self, x = 0, y = 0):
- """Offsets this widget's position by logical coordinates."""
- self.pos = list(self.pos)
- self.pos[0] += x
- self.pos[1] += y
- def findSpace(self):
- """Moves this widget so it doesn't overlap any others."""
- turns = 0.0
- movecount = 1
- """
- Don't adhere to the grid if snapping isn't enabled.
- Instead, move in 1/5 grid increments.
- """
- griddivision = 1 if self.parent.snapping else 0.2
- while self.intersectsAny() and turns < 99*griddivision:
- """Move in an Ulam spiral pattern: n spaces left, n spaces up, n+1 spaces right, n+1 spaces down"""
- self.pos[int(math.floor((turns*2) % 2))] += self.parent.GRID_SPACING * griddivision * int(math.copysign(1, turns % 2 - 1))
- movecount -= 1
- if movecount <= 0:
- turns += 0.5
- movecount = int(math.ceil(turns)/griddivision)
- def findSpaceQuickly(self):
- """ A quicker findSpace where the position and visibility doesn't really matter """
- while self.intersectsAny():
- self.pos[0] += self.parent.GRID_SPACING
- rightEdge = self.pos[0] + PassageWidget.SIZE
- maxWidth = self.parent.toLogical((self.parent.GetSize().width - self.parent.INSET[0], -1), \
- scaleOnly = True)[0]
- if rightEdge > maxWidth:
- self.pos[0] = 10
- self.pos[1] += self.parent.GRID_SPACING
- def containsRegexp(self, regexp, flags):
- """
- Returns whether this widget's passage contains a regexp.
- """
- return (re.search(regexp, self.passage.title, flags) is not None
- or re.search(regexp, self.passage.text, flags) is not None)
- def replaceRegexp(self, findRegexp, replaceRegexp, flags):
- """
- Performs a regexp replace in this widget's passage title and
- body text. Returns the number of replacements actually made.
- """
- compiledRegexp = re.compile(findRegexp, flags)
- oldTitle = self.passage.title
- newTitle, titleReps = re.subn(compiledRegexp, replaceRegexp, oldTitle)
- self.passage.text, textReps = re.subn(compiledRegexp, replaceRegexp, self.passage.text)
- if titleReps > 0:
- self.parent.changeWidgetTitle(oldTitle, newTitle)
- return titleReps + textReps
- def linksAndDisplays(self):
- return self.passage.linksAndDisplays() + self.getShorthandDisplays()
- def getShorthandDisplays(self):
- """Returns a list of macro tags which match passage names."""
- return filter(self.parent.passageExists, self.passage.macros)
- def getBrokenLinks(self):
- """Returns a list of broken links in this widget."""
- return filter(lambda a: not self.parent.passageExists(a), self.passage.links)
- def getIncludedLinks(self):
- """Returns a list of included passages in this widget."""
- return filter(self.parent.includedPassageExists, self.passage.links)
- def getVariableLinks(self):
- """Returns a list of links which use variables/functions, in this widget."""
- return filter(lambda a: tweelexer.TweeLexer.linkStyle(a) == tweelexer.TweeLexer.PARAM, self.passage.links)
- def setSelected(self, value, exclusive = True):
- """
- Sets whether this widget should be selected. Pass a false value for
- exclusive to prevent other widgets from being deselected.
- """
- if exclusive:
- self.parent.eachWidget(lambda i: i.setSelected(False, False))
- old = self.selected
- self.selected = value
- if self.selected != old:
- self.clearPaintCache()
- # Figure out the dirty rect
- dirtyRect = self.getPixelRect()
- for link in self.linksAndDisplays() + self.passage.images:
- widget = self.parent.findWidget(link)
- if widget:
- dirtyRect.Union(widget.getDirtyPixelRect())
- if self.passage.isStylesheet():
- for t in self.passage.tags:
- if t not in tiddlywiki.TiddlyWiki.INFO_TAGS:
- for widget in self.parent.taggedWidgets(t):
- if widget:
- dirtyRect.Union(widget.getDirtyPixelRect())
- self.parent.Refresh(True, dirtyRect)
- def setDimmed(self, value):
- """Sets whether this widget should be dimmed."""
- old = self.dimmed
- self.dimmed = value
- if self.dimmed != old:
- self.clearPaintCache()
- def clearPaintCache(self):
- """
- Forces the widget to be repainted from scratch.
- """
- self.paintBufferBounds = None
- def openContextMenu(self, event):
- """Opens a contextual menu at the event position given."""
- self.parent.PopupMenu(PassageWidgetContext(self), event.GetPosition())
- def openEditor(self, event = None, fullscreen = False):
- """Opens a PassageFrame to edit this passage."""
- image = self.passage.isImage()
- if not hasattr(self, 'passageFrame'):
- if image:
- self.passageFrame = ImageFrame(None, self, self.app)
- elif self.passage.title == "StorySettings":
- self.passageFrame = StorySettingsFrame(None, self, self.app)
- else:
- self.passageFrame = PassageFrame(None, self, self.app)
- if fullscreen: self.passageFrame.openFullscreen()
- else:
- try:
- self.passageFrame.Iconize(False)
- self.passageFrame.Raise()
- if fullscreen and not image: self.passageFrame.openFullscreen()
- except wx._core.PyDeadObjectError:
- # user closed the frame, so we need to recreate it
- delattr(self, 'passageFrame')
- self.openEditor(event, fullscreen)
- def closeEditor(self, event = None):
- """Closes the PassageFrame associated with this, if it exists."""
- try: self.passageFrame.closeEditor()
- except: pass
- try: self.passageFrame.Destroy()
- except: pass
- def verifyPassage(self, window):
- """
- Check that the passage syntax is well-formed.
- Return -(corrections made) if the check was aborted, +(corrections made) otherwise
- """
- passage = self.passage
- checks = tweelexer.VerifyLexer(self).check()
- problems = 0
- oldtext = passage.text
- newtext = ""
- index = 0
- for warning, replace in checks:
- problems += 1
- if replace:
- start, sub, end = replace
- answer = wx.MessageDialog(window, warning + "\n\nMay I try to fix this for you?",
- 'Problem in ' + self.passage.title,
- wx.ICON_WARNING | wx.YES_NO | wx.CANCEL | wx.YES_DEFAULT
- ).ShowModal()
- if answer == wx.ID_YES:
- newtext += oldtext[index:start] + sub
- index = end
- if hasattr(self, 'passageFrame') and self.passageFrame:
- self.passageFrame.bodyInput.SetText(newtext + oldtext[index:])
- elif answer == wx.ID_CANCEL:
- return -problems
- else:
- answer = wx.MessageDialog(window, warning+"\n\nKeep checking?", 'Problem in '+self.passage.title, wx.ICON_WARNING | wx.YES_NO) \
- .ShowModal()
- if answer == wx.ID_NO:
- return problems
- passage.text = newtext + oldtext[index:]
- return problems
- def intersectsAny(self, dragging = False):
- """Returns whether this widget intersects any other in the same StoryPanel."""
- #Enforce positive coordinates
- if not 'Twine.hide' in self.passage.tags:
- if self.pos[0] < 0 or self.pos[1] < 0:
- return True
- # we do this manually so we don't have to go through all of them
- for widget in self.parent.notDraggingWidgets if dragging else self.parent.widgetDict.itervalues():
- if widget != self and self.intersects(widget):
- return True
- return False
- def intersects(self, other):
- """
- Returns whether this widget intersects another widget or wx.Rect.
- This uses logical coordinates, so you can do this without actually moving the widget onscreen.
- """
- selfRect = self.getLogicalRect()
- if isinstance(other, PassageWidget):
- other = other.getLogicalRect()
- return selfRect.Intersects(other)
- def applyPrefs(self):
- """Passes on the message to any editor windows."""
- self.clearPaintCache()
- try: self.passageFrame.applyPrefs()
- except: pass
- try: self.passageFrame.fullscreen.applyPrefs()
- except: pass
- def updateBitmap(self):
- """If an image passage, updates the bitmap to match the contained base64 data."""
- if self.passage.isImage():
- self.bitmap = images.base64ToBitmap(self.passage.text)
- def getConnectorLine(self, otherWidget, clipped=True):
- """
- Get the line that would be drawn between this widget and another.
- """
- start = self.getCenter()
- end = otherWidget.getCenter()
- #Tweak to make overlapping lines easier to see by shifting the end point
- #Devision by a large constant to so the behavior is not overly noticeable while dragging
- lengthSquared = ((start[0]-end[0])**2+(start[1]-end[1])**2)/1024**2
- end[0] += (0.5 - math.sin(lengthSquared))*PassageWidget.SIZE/8.0
- end[1] += (0.5 - math.cos(lengthSquared))*PassageWidget.SIZE/8.0
- if clipped:
- [start, end] = geometry.clipLineByRects([start, end], otherWidget.getLogicalRect())
- return self.parent.toPixels(start), self.parent.toPixels(end)
- def getConnectedWidgets(self, displayArrows, imageArrows):
- """
- Returns a list of titles of all widgets that will have lines drawn to them.
- """
- ret = []
- for link in self.linksAndDisplays():
- if link in self.passage.links or displayArrows:
- widget = self.parent.findWidget(link)
- if widget:
- ret.append(widget)
- if imageArrows:
- for link in self.passage.images:
- widget = self.parent.findWidget(link)
- if widget:
- ret.append(widget)
- if self.passage.isStylesheet():
- for t in self.passage.tags:
- if t not in tiddlywiki.TiddlyWiki.INFO_TAGS:
- for otherWidget in self.parent.taggedWidgets(t):
- if not otherWidget.dimmed and not otherWidget.passage.isStylesheet():
- ret.append(otherWidget)
- return ret
- def addConnectorLinesToDict(self, displayArrows, imageArrows, flatDesign, lineDictonary, arrowDictonary=None, updateRect=None):
- """
- Appended the connector lines originating from this widget to the list contained in the
- line directory under the appropriate color,width key.
- If an arrow dictionary is also passed it adds the arrows in a similar manner.
- If an update rect is passed it skips any lines, and the associated arrows,
- which lie outside the update rectangle.
- Note: Assumes the list existed in the passed in dictionaries. Either make sure this is the case or
- use a defaultDict.
- """
- colors = PassageWidget.FLAT_COLORS if flatDesign else PassageWidget.COLORS
- # Widths for selected and non selected lines
- widths = 2 * (2 * flatDesign + 1), 1 * (2 * flatDesign + 1)
- widths = max(self.parent.toPixels((widths[0], 0), scaleOnly=True)[0], 2), \
- max(self.parent.toPixels((widths[1], 0), scaleOnly=True)[0], 1)
- widgets = self.getConnectedWidgets(displayArrows, imageArrows)
- if widgets:
- for widget in widgets:
- link = widget.passage.title
- if self.passage.isAnnotation():
- color = colors['connectorAnnotation']
- elif (link in self.passage.displays + self.passage.macros) and link not in self.passage.links:
- color = colors['connectorDisplay']
- elif link in self.passage.images or self.passage.isStylesheet():
- color = colors['connectorResource']
- else:
- color = colors['connector']
- width = widths[not self.selected]
- line, arrow = self.getConnectorTo(widget, not arrowDictonary is None, updateRect)
- lineDictonary[(color, width)].extend(line)
- if arrow:
- arrowDictonary[(color, width)].extend(arrow)
- def getConnectorTo(self, otherWidget, arrowheads=False, updateRect=None):
- # does it actually need to be drawn?
- if otherWidget == self:
- return [], []
- start, end = self.getConnectorLine(otherWidget)
- if updateRect and not geometry.lineRectIntersection([start, end], updateRect):
- return [], []
- line = [[start[0], start[1]], [end[0], end[1]]]
- if not arrowheads:
- return line, []
- else:
- length = max(self.parent.toPixels((PassageWidget.ARROWHEAD_LENGTH, 0), scaleOnly=True)[0], 1)
- arrowheadr = geometry.endPointProjectedFrom((start, end), PassageWidget.ARROWHEAD_ANGLE, length)
- arrowheadl = geometry.endPointProjectedFrom((start, end), 0 - PassageWidget.ARROWHEAD_ANGLE, length)
- return line, [(arrowheadl, end, arrowheadr)]
- def paint(self, dc):
- """
- Handles paint events, either blitting our paint buffer or
- manually redrawing.
- """
- pixPos = self.parent.toPixels(self.pos)
- pixSize = self.parent.toPixels(self.getSize(), scaleOnly = True)
- rect = wx.Rect(pixPos[0], pixPos[1], pixSize[0], pixSize[1])
- if (not self.paintBufferBounds) \
- or (rect.width != self.paintBufferBounds.width \
- or rect.height != self.paintBufferBounds.height):
- self.cachePaint(wx.Size(rect.width, rect.height))
- dc.Blit(rect.x, rect.y, rect.width, rect.height, self.paintBuffer, 0, 0)
- def getTitleColor(self):
- """
- Returns the title bar style that matches this widget's passage.
- """
- flat = self.app.config.ReadBool('flatDesign')
- # First, rely on the header to supply colours
- custom = self.getHeader().passageTitleColor(self.passage)
- if custom:
- return custom[flat]
- # Use default colours
- if self.passage.isAnnotation():
- ind = 'annotation'
- elif self.passage.isImage():
- ind = 'imageTitleBar'
- elif any(t.startswith('Twine.') for t in self.passage.tags):
- ind = 'privateTitleBar'
- elif not self.linksAndDisplays() and not self.getIncludedLinks() and not self.passage.variableLinks:
- ind = 'endTitleBar'
- else:
- ind = 'titleBar'
- colors = PassageWidget.FLAT_COLORS if flat else PassageWidget.COLORS
- return colors[ind]
- def cachePaint(self, size):
- """
- Caches the widget so self.paintBuffer is up-to-date.
- """
- def wordWrap(text, lineWidth, gc):
- """
- Returns a list of lines from a string
- This is somewhat based on the wordwrap function built into wx.lib.
- (For some reason, GraphicsContext.GetPartialTextExtents()
- is returning totally wrong numbers but GetTextExtent() works fine.)
- This assumes that you've already set up the font you want on the GC.
- It gloms multiple spaces together, but for our purposes that's ok.
- """
- words = re.finditer('\S+\s*', text.replace('\r',''))
- lines = ''
- currentLine = ''
- for w in words:
- word = w.group(0)
- wordWidth = gc.GetTextExtent(currentLine + word)[0]
- if wordWidth < lineWidth:
- currentLine += word
- if '\n' in word:
- lines += currentLine
- currentLine = ''
- else:
- lines += currentLine + '\n'
- currentLine = word
- lines += currentLine
- return lines.split('\n')
- # Which set of colors to use
- flat = self.app.config.ReadBool('flatDesign')
- colors = PassageWidget.FLAT_COLORS if flat else PassageWidget.COLORS
- def dim(c, dim, flat=flat):
- """Lowers a color's alpha if dim is true."""
- if isinstance(c, wx.Colour):
- c = list(c.Get(includeAlpha=True))
- elif type(c) is str:
- c = list(ord(a) for a in c[1:].decode('hex'))
- else:
- c = list(c)
- if len(c) < 4:
- c.append(255)
- if dim:
- a = PassageWidget.FLAT_DIMMED_ALPHA if flat else PassageWidget.DIMMED_ALPHA
- if not self.app.config.ReadBool('fastStoryPanel'):
- c[3] *= a
- else:
- c[0] *= a
- c[1] *= a
- c[2] *= a
- return wx.Colour(*c)
- # set up our buffer
- bitmap = wx.EmptyBitmap(size.width, size.height)
- self.paintBuffer.SelectObject(bitmap)
- # switch to a GraphicsContext as necessary
- gc = self.paintBuffer if self.app.config.ReadBool('fastStoryPanel') else wx.GraphicsContext.Create(self.paintBuffer)
- # text font sizes
- # wxWindows works with points, so we need to doublecheck on actual pixels
- titleFontSize = self.parent.toPixels((metrics.size('widgetTitle'), -1), scaleOnly = True)[0]
- titleFontSize = sorted((metrics.size('fontMin'), titleFontSize, metrics.size('fontMax')))[1]
- excerptFontSize = sorted((metrics.size('fontMin'), titleFontSize * 0.9, metrics.size('fontMax')))[1]
- if self.app.config.ReadBool('flatDesign'):
- titleFont = wx.Font(titleFontSize, wx.SWISS, wx.NORMAL, wx.LIGHT, False, 'Arial')
- excerptFont = wx.Font(excerptFontSize, wx.SWISS, wx.NORMAL, wx.LIGHT, False, 'Arial')
- else:
- titleFont = wx.Font(titleFontSize, wx.SWISS, wx.NORMAL, wx.BOLD, False, 'Arial')
- excerptFont = wx.Font(excerptFontSize, wx.SWISS, wx.NORMAL, wx.NORMAL, False, 'Arial')
- titleFontHeight = math.fabs(titleFont.GetPixelSize()[1])
- excerptFontHeight = math.fabs(excerptFont.GetPixelSize()[1])
- tagBarColor = dim(tuple(i*256 for i in colorsys.hsv_to_rgb(0.14 + math.sin(hash("".join(self.passage.tags)))*0.08,
- 0.58 if flat else 0.28,
- 0.88)), self.dimmed)
- tags = set(self.passage.tags) - (tiddlywiki.TiddlyWiki.INFO_TAGS | self.getHeader().invisiblePassageTags())
- # inset for text (we need to know this for layout purposes)
- inset = titleFontHeight / 3
- # frame
- if self.passage.isAnnotation():
- frameColor = colors['frame']
- c = wx.Colour(*colors['annotation'])
- frameInterior = (c,c)
- else:
- frameColor = dim(colors['frame'], self.dimmed)
- frameInterior = (dim(colors['bodyStart'], self.dimmed), dim(colors['bodyEnd'], self.dimmed))
- if not flat:
- gc.SetPen(wx.Pen(frameColor, 1))
- if isinstance(gc, wx.GraphicsContext):
- gc.SetBrush(gc.CreateLinearGradientBrush(0, 0, 0, size.height, \
- frameInterior[0], frameInterior[1]))
- else:
- gc.GradientFillLinear(wx.Rect(0, 0, size.width - 1, size.height - 1), \
- frameInterior[0], frameInterior[1], wx.SOUTH)
- gc.SetBrush(wx.TRANSPARENT_BRUSH)
- gc.DrawRectangle(0, 0, size.width - 1, size.height - 1)
- else:
- gc.SetPen(wx.Pen(frameInterior[0]))
- gc.SetBrush(wx.Brush(frameInterior[0]))
- gc.DrawRectangle(0, 0, size.width, size.height)
- greek = size.width <= PassageWidget.MIN_GREEKING_SIZE * (2 if self.passage.isAnnotation() else 1)
- # title bar
- titleBarHeight = PassageWidget.GREEK_HEIGHT*3 if greek else titleFontHeight + (2 * inset)
- if self.passage.isAnnotation():
- titleBarColor = frameInterior[0]
- else:
- titleBarColor = dim(self.getTitleColor(), self.dimmed)
- gc.SetPen(wx.Pen(titleBarColor, 1))
- gc.SetBrush(wx.Brush(titleBarColor))
- if flat:
- gc.DrawRectangle(0, 0, size.width, titleBarHeight)
- else:
- gc.DrawRectangle(1, 1, size.width - 3, titleBarHeight)
- if not greek:
- # draw title
- # we let clipping prevent writing over the frame
- if isinstance(gc, wx.GraphicsContext):
- gc.ResetClip()
- gc.Clip(inset, inset, size.width - (inset * 2), titleBarHeight - 2)
- else:
- gc.DestroyClippingRegion()
- gc.SetClippingRect(wx.Rect(inset, inset, size.width - (inset * 2), titleBarHeight - 2))
- titleTextColor = dim(colors['titleText'], self.dimmed)
- if isinstance(gc, wx.GraphicsContext):
- gc.SetFont(titleFont, titleTextColor)
- else:
- gc.SetFont(titleFont)
- gc.SetTextForeground(titleTextColor)
- if self.passage.title:
- gc.DrawText(self.passage.title, inset, inset)
- # draw excerpt
- if not self.passage.isImage():
- excerptTop = inset + titleBarHeight
- # we split the excerpt by line, then draw them in turn
- # (we use a library to determine breaks, but have to draw the lines ourselves)
- if isinstance(gc, wx.GraphicsContext):
- gc.ResetClip()
- gc.Clip(inset, inset, size.width - (inset * 2), size.height - (inset * 2)-1)
- else:
- gc.DestroyClippingRegion()
- gc.SetClippingRect(wx.Rect(inset, inset, size.width - (inset * 2), size.height - (inset * 2)-1))
- if self.passage.isAnnotation():
- excerptTextColor = wx.Colour(*colors['annotationText'])
- else:
- excerptTextColor = dim(colors['excerptText'], self.dimmed)
- if isinstance(gc, wx.GraphicsContext):
- gc.SetFont(excerptFont, excerptTextColor)
- else:
- gc.SetFont(excerptFont)
- gc.SetTextForeground(excerptTextColor)
- excerptLines = wordWrap(self.passage.text, size.width - (inset * 2), gc)
- for line in excerptLines:
- gc.DrawText(line, inset, excerptTop)
- excerptTop += excerptFontHeight * PassageWidget.LINE_SPACING \
- * min(1.75,max(1,1.75*size.width/260 if (self.passage.isAnnotation() and line) else 1))
- if excerptTop + excerptFontHeight > size.height - inset: break
- if (self.passage.isStoryText() or self.passage.isStylesheet()) and tags:
- tagBarHeight = excerptFontHeight + (2 * inset)
- gc.SetPen(wx.Pen(tagBarColor, 1))
- gc.SetBrush(wx.Brush(tagBarColor))
- gc.DrawRectangle(0, size.height-tagBarHeight-1, size.width, tagBarHeight+1)
- # draw tags
- tagTextColor = dim(colors['frame'], self.dimmed)
- if isinstance(gc, wx.GraphicsContext):
- gc.SetFont(excerptFont, tagTextColor)
- else:
- gc.SetFont(excerptFont)
- gc.SetTextForeground(tagTextColor)
- text = wordWrap(' '.join(tags), size.width - (inset * 2), gc)[0]
- gc.DrawText(text, inset*2, (size.height-tagBarHeight))
- else:
- # greek title
- gc.SetPen(wx.Pen(colors['titleText'], PassageWidget.GREEK_HEIGHT))
- height = inset
- width = (size.width - inset) / 2
- if isinstance(gc, wx.GraphicsContext):
- gc.StrokeLine(inset, height, width, height)
- else:
- gc.DrawLine(inset, height, width, height)
- height += PassageWidget.GREEK_HEIGHT * 3
- # greek body text
- if not self.passage.isImage():
- gc.SetPen(wx.Pen(colors['annotationText'] \
- if self.passage.isAnnotation() else colors['greek'], PassageWidget.GREEK_HEIGHT))
- chars = len(self.passage.text)
- while height < size.height - inset and chars > 0:
- width = size.height - inset
- if height + (PassageWidget.GREEK_HEIGHT * 2) > size.height - inset:
- width /= 2
- elif chars < 80:
- width = max(4, width * chars / 80)
- if isinstance(gc, wx.GraphicsContext):
- gc.StrokeLine(inset, height, width, height)
- else:
- gc.DrawLine(inset, height, width, height)
- height += PassageWidget.GREEK_HEIGHT * 2
- chars -= 80
- # greek tags
- if (self.passage.isStoryText() or self.passage.isStylesheet()) and tags:
- tagBarHeight = PassageWidget.GREEK_HEIGHT*3
- gc.SetPen(wx.Pen(tagBarColor, 1))
- gc.SetBrush(wx.Brush(tagBarColor))
- height = size.height-tagBarHeight-2
- width = size.width-4
- gc.DrawRectangle(2, height, width, tagBarHeight)
- gc.SetPen(wx.Pen(colors['greek'], PassageWidget.GREEK_HEIGHT))
- height += inset
- width = (width-inset*2)/2
- if isinstance(gc, wx.GraphicsContext):
- gc.StrokeLine(inset, height, width, height)
- else:
- gc.DrawLine(inset, height, width, height)
- if self.passage.isImage():
- if self.bitmap:
- if isinstance(gc, wx.GraphicsContext):
- gc.ResetClip()
- gc.Clip(1, titleBarHeight + 1, size.width - 3, size.height - 3)
- else:
- gc.DestroyClippingRegion()
- gc.SetClippingRect(wx.Rect(1, titleBarHeight + 1, size.width - 3, size.height - 3))
- width = size.width
- height = size.height - titleBarHeight
- # choose smaller of vertical and horizontal scale factor, to preserve aspect ratio
- scale = min(width/float(self.bitmap.GetWidth()), height/float(self.bitmap.GetHeight()))
- img = self.bitmap.ConvertToImage()
- if scale != 1:
- img = img.Scale(scale*self.bitmap.GetWidth(),scale*self.bitmap.GetHeight())
- # offset image horizontally or vertically, to centre after scaling
- offsetWidth = (width - img.GetWidth())/2
- offsetHeight = (height - img.GetHeight())/2
- if isinstance(gc, wx.GraphicsContext):
- gc.DrawBitmap(img.ConvertToBitmap(self.bitmap.GetDepth()),
- 1 + offsetWidth, titleBarHeight + 1 + offsetHeight,
- img.GetWidth(), img.GetHeight())
- else:
- gc.DrawBitmap(img.ConvertToBitmap(self.bitmap.GetDepth()),
- 1 + offsetWidth, titleBarHeight + 1 + offsetHeight)
- if isinstance(gc, wx.GraphicsContext):
- gc.ResetClip()
- else:
- gc.DestroyClippingRegion()
- # draw a broken link emblem in the bottom right if necessary
- # fixme: not sure how to do this with transparency
- def showEmblem(emblem, gc=gc, size=size, inset=inset):
- emblemSize = emblem.GetSize()
- emblemPos = [ size.width - (emblemSize[0] + inset), \
- size.height - (emblemSize[1] + inset) ]
- if isinstance(gc, wx.GraphicsContext):
- gc.DrawBitmap(emblem, emblemPos[0], emblemPos[1], emblemSize[0], emblemSize[1])
- else:
- gc.DrawBitmap(emblem, emblemPos[0], emblemPos[1])
- if len(self.getBrokenLinks()):
- showEmblem(self.brokenEmblem)
- elif len(self.getIncludedLinks()) or len(self.passage.variableLinks):
- showEmblem(self.externalEmblem)
- # finally, draw a selection over ourselves if we're selected
- if self.selected:
- color = dim(titleBarColor if flat else wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT), self.dimmed)
- if self.app.config.ReadBool('fastStoryPanel'):
- gc.SetPen(wx.Pen(color, 2 + flat))
- else:
- gc.SetPen(wx.TRANSPARENT_PEN)
- if isinstance(gc, wx.GraphicsContext):
- r, g, b = color.Get(False)
- color = wx.Colour(r, g, b, 64)
- gc.SetBrush(wx.Brush(color))
- else:
- gc.SetBrush(wx.TRANSPARENT_BRUSH)
- gc.DrawRectangle(0, 0, size.width, size.height)
- self.paintBufferBounds = size
- def serialize(self):
- """Returns a dictionary with state information suitable for pickling."""
- return { 'selected': self.selected, 'pos': self.pos, 'passage': copy.copy(self.passage) }
- @staticmethod
- def posCompare(first, second):
- """
- Sorts PassageWidgets so that the results appear left to right,
- top to bottom. A certain amount of slack is assumed here in
- terms of positioning.
- """
- yDistance = int(first.pos[1] - second.pos[1])
- if abs(yDistance) > 5:
- return yDistance
- xDistance = int(first.pos[0] - second.pos[0])
- if xDistance != 0:
- return xDistance
- return id(first) - id(second) # punt on ties
- def __repr__(self):
- return "<PassageWidget '" + self.passage.title + "'>"
- def getHeader(self):
- """Returns the current selected target header for this Passage Widget."""
- return self.parent.getHeader()
- MIN_PIXEL_SIZE = 10
- MIN_GREEKING_SIZE = 50
- GREEK_HEIGHT = 2
- SIZE = 120
- SHADOW_SIZE = 5
- COLORS = {
- 'frame': (0, 0, 0), \
- 'bodyStart': (255, 255, 255), \
- 'bodyEnd': (212, 212, 212), \
- 'annotation': (85, 87, 83), \
- 'endTitleBar': (16, 51, 96), \
- 'titleBar': (52, 101, 164), \
- 'imageTitleBar': (8, 138, 133), \
- 'privateTitleBar': (130, 130, 130), \
- 'titleText': (255, 255, 255), \
- 'excerptText': (0, 0, 0), \
- 'annotationText': (255,255,255), \
- 'greek': (102, 102, 102),
- 'connector': (186, 189, 182),
- 'connectorDisplay': (132, 164, 189),
- 'connectorResource': (110, 112, 107),
- 'connectorAnnotation': (0, 0, 0),
- }
- FLAT_COLORS = {
- 'frame': (0, 0, 0),
- 'bodyStart': (255, 255, 255),
- 'bodyEnd': (255, 255, 255),
- 'annotation': (212, 212, 212),
- 'endTitleBar': (36, 54, 219),
- 'titleBar': (36, 115, 219),
- 'imageTitleBar': (36, 219, 213),
- 'privateTitleBar': (153, 153, 153),
- 'titleText': (255, 255, 255),
- 'excerptText': (96, 96, 96),
- 'annotationText': (0,0,0),
- 'greek': (192, 192, 192),
- 'connector': (143, 148, 137),
- 'connectorDisplay': (137, 193, 235),
- 'connectorResource': (186, 188, 185),
- 'connectorAnnotation': (255, 255, 255),
- 'selection': (28, 102, 176)
- }
- DIMMED_ALPHA = 0.5
- FLAT_DIMMED_ALPHA = 0.9
- LINE_SPACING = 1.2
- CONNECTOR_WIDTH = 2.0
- CONNECTOR_SELECTED_WIDTH = 5.0
- ARROWHEAD_LENGTH = 10
- MIN_ARROWHEAD_LENGTH = 5
- ARROWHEAD_ANGLE = math.pi / 6
- # contextual menu
- class PassageWidgetContext(wx.Menu):
- def __init__(self, parent):
- wx.Menu.__init__(self)
- self.parent = parent
- title = '"' + parent.passage.title + '"'
- if parent.passage.isStoryPassage():
- test = wx.MenuItem(self, wx.NewId(), 'Test Play From Here')
- self.AppendItem(test)
- self.Bind(wx.EVT_MENU, lambda e: self.parent.parent.parent.testBuild(startAt = parent.passage.title), id = test.GetId())
- edit = wx.MenuItem(self, wx.NewId(), 'Edit ' + title)
- self.AppendItem(edit)
- self.Bind(wx.EVT_MENU, self.parent.openEditor, id = edit.GetId())
- delete = wx.MenuItem(self, wx.NewId(), 'Delete ' + title)
- self.AppendItem(delete)
- self.Bind(wx.EVT_MENU, lambda e: self.parent.parent.removeWidget(self.parent.passage.title), id = delete.GetId())
|