def __init__(self, editor): # There are 3 rectangles for marking selection, 1st line, middle lines # and last line. self.editor = editor view = self.editor.canvas self.selRect1 = BGRect(view, "selbg") self.selRect2 = BGRect(view, "selbg") self.selRect3 = BGRect(view, "selbg") self.mainWidth = self.editor.rsubject.db.layoutInfo.mainWidth
def init(self, rsubject): """Set up the editor for a new report. rsubject: A RSubject object for the current report. """ self.rsubject = rsubject if self.edCursor: self.edCursor.stop() # Set the viewing area (x-margin, y-margin, width) self.mainWidth = self.rsubject.db.layoutInfo.mainWidth self.canvas.initArea(20, 10, self.mainWidth) # Special background for overflow text self.ovflRect = BGRect(self.canvas, "ovflbg", under=True) # Render the frames frames = self.rsubject.frames y = 0.0 for f in frames[:-1]: # Separator line HLine(self.canvas, y, self.mainWidth) f.render(self.canvas, self.rsubject, y) y += f.maxHeight # The overflow frame: HLine(self.canvas, y, self.mainWidth) f = frames[-1] yo = 0.0 ol = self.rsubject.getFrameLines(f) if ol: lastline = ol[-1] # vertical space needed by the frame text: yo = lastline.y + lastline.height f.getHeight(yo) f.render(self.canvas, self.rsubject, y) self.setSize(y, f.height) self.edCursor = EdCursor(self) self.selection = Selection(self) self.initCurSel()
class Selection: """This class represents a text selection within an 'editor'. There are two markers, selectionMark and selectionMark2, the first being set by left-cursor-press, the second by left-cursor-move. Their values are index triples (line, word, character), and become invalid when the text is edited - so any editing action must clear them. """ def __init__(self, editor): # There are 3 rectangles for marking selection, 1st line, middle lines # and last line. self.editor = editor view = self.editor.canvas self.selRect1 = BGRect(view, "selbg") self.selRect2 = BGRect(view, "selbg") self.selRect3 = BGRect(view, "selbg") self.mainWidth = self.editor.rsubject.db.layoutInfo.mainWidth def clearSelection(self): # The defining property of a clear selection is that selectiomMark2 # is 'None'. selectionMark need not be 'None' self.selectionMark2 = None self.selRect1.hide() self.selRect2.hide() self.selRect3.hide() def isSelection(self): return (self.selectionMark2 != None) def setMark1(self, newMark): """Called when the left mouse button is pressed. """ self.selectionMark = newMark self.clearSelection() self.editor.edCursor.setPos(*newMark) def setMark2(self, newMark): """Called when the mouse is moved with the left button pressed. """ if (self.selectionMark2 == newMark): return self.selectionMark2 = newMark self.markSelection() def markSelection(self): """Sets the coloured background for the selection. It also resets the (display) cursor position. """ self.editor.edCursor.setPos(*self.selectionMark2) if (self.selectionMark2 == self.selectionMark): self.selRect1.hide() self.selRect2.hide() self.selRect3.hide() return sm1, sm2 = self.order() w, cx = sm1 x1, y1, dy1 = w.getXYH(cx) w, cx = sm2 x2, y2, dy2 = w.getXYH(cx) if (y1 == y2): # single line, 1 rectangle, sm1 to sm2 self.selRect1.setRect(x1, y1, x2-x1, dy1) self.selRect1.show() self.selRect2.hide() self.selRect3.hide() return # selRect1: from sm1 to end of line # selRect2: from start of line to sm2 self.selRect1.setRect(x1, y1, self.mainWidth-x1, dy1) self.selRect1.show() self.selRect2.setRect(0, y2, x2, dy2) self.selRect2.show() y0 = y1+dy1 if (y0 == y2): # adjacent lines, 2 rectangles self.selRect3.hide() return # several lines, all 3 rectangles # selRect3: all lines in between self.selRect3.setRect(0, y0, self.mainWidth, y2-y0) self.selRect3.show() def order(self, mark1=None, mark2=None): """Return the two marks as a tuple so that the first entry is guaranteed not to be after the second. The default marks are the current selection markers. """ if not mark1: mark1 = self.selectionMark if not mark2: mark2 = self.selectionMark2 w1 = mark1[0] w2 = mark2[0] if (w1 == w2): # compare character positions if (mark1[1] > mark2[1]): return (mark2, mark1) return (mark1, mark2) tl1 = w1.tline tl2 = w2.tline if (tl1 != tl2): tls = self.editor.rsubject.tlines if (tls.index(tl1) > tls.index(tl2)): return (mark2, mark1) return (mark1, mark2) if (tl1.twords.index(w1) > tl1.twords.index(w2)): return (mark2, mark1) return (mark1, mark2)
class Editor: """There may be only one instance of this class, because of the slot declarations. Given a canvas widget to draw on, implement a report text editor. It can also be restricted to use in a view-only mode by passing 'False' to the 'editable' argument. Much of the low level stuff is done in the 'graphics view' widget, which is a GUI class. I decided not to make this class inherit from that widget, in order to leave maximum flexibility in how it is created and implemented itself (I hope that makes sense). According to the settings in the layout description the area available for the report can comprise more than one 'frame'. The text can be split over a number of separate boxes - specifically here for the case of longer, principally class teacher, reports, which need more than one page). The editor needs the vertical sizes (in mm) of these boxes, so that the display area can be divided into several blocks which will appear sequentially, and also margin information. To deal with text that is too long for a report box, an additional large 'dummy' box is added after the 'official' box(es). This gets a coloured background, to indicate the overflow. The text being edited is primarily managed by the RSubject object, which stores it as a list of TextLine objects, each containing a list of TWord objects. """ def __init__(self, canvas, editable=True): self.canvas = canvas # the canvas widget self.editable = editable slot = canvas.slot slot("edPress", self.edPress) slot("edMove", self.edMove) slot("edRelease", self.edRelease) slot("keyPress", self.edKey) slot("renderedToEnd", self.renewOvfl) self.edCursor = None self.selection = None self.cursorX = None def init(self, rsubject): """Set up the editor for a new report. rsubject: A RSubject object for the current report. """ self.rsubject = rsubject if self.edCursor: self.edCursor.stop() # Set the viewing area (x-margin, y-margin, width) self.mainWidth = self.rsubject.db.layoutInfo.mainWidth self.canvas.initArea(20, 10, self.mainWidth) # Special background for overflow text self.ovflRect = BGRect(self.canvas, "ovflbg", under=True) # Render the frames frames = self.rsubject.frames y = 0.0 for f in frames[:-1]: # Separator line HLine(self.canvas, y, self.mainWidth) f.render(self.canvas, self.rsubject, y) y += f.maxHeight # The overflow frame: HLine(self.canvas, y, self.mainWidth) f = frames[-1] yo = 0.0 ol = self.rsubject.getFrameLines(f) if ol: lastline = ol[-1] # vertical space needed by the frame text: yo = lastline.y + lastline.height f.getHeight(yo) f.render(self.canvas, self.rsubject, y) self.setSize(y, f.height) self.edCursor = EdCursor(self) self.selection = Selection(self) self.initCurSel() def initCurSel(self): # Convert cursor from index to word format cp, cw, cx = self.rsubject.cursor para = None ipara = -1 for tl in self.rsubject.tlines: if (tl.para != para): ipara += 1 para = tl.para if (ipara == cp): nw = len(tl.twords) if (cw < nw): w = tl.twords[cw] break cw -= nw self.edCursor.setPos(w, cx) # else: # self.edCursor.setPos(self.rsubject.tlines[0].twords[0], 0) self.selection.clearSelection() def setSize(self, y, h): """Set the size of the background rectangle behind the text overflow area and also the displayed canvas area. y: y-coordinate of overflow start h: height of overflow area """ if (h <= 0.0): self.ovflRect.hide() self.canvas.setHeight(y) else: self.ovflRect.setRect(0, y, self.mainWidth, h) self.ovflRect.show() self.canvas.setHeight(y + h) def renewOvfl(self, arg): # This is a 'slot', called when a report has been rendered to # the end. f = self.rsubject.frames[-1] # How much space is used in the overflow area? lastLine = self.rsubject.tlines[-1] if (lastLine.frame != f): y = 0.0 else: y = lastLine.y + lastLine.height self.setSize(f.yF, f.getHeight(y)) def edKey(self, key): """This handles key presses. """ # Any key press except cursor keys (?) causes the selection to be deleted! if isinstance(key, types.IntType): rawkey = (key & (SHIFT - 1)) if (rawkey in (1, 2, 3, 4)): # First check for shift+cursor-key, because these alter the selection if (key & SHIFT): # SHIFT + cursor key # if there is not already a selection, set the first marker before # moving the cursor if not self.selection.selectionMark2: w, cx = self.edCursor.getPos() # Hack to handle empty words which are about to be deleted if (w.string == u"") and (len(w.tline.twords) > 1): # Don't start selection, just move cursor self.cursorKey(rawkey) return self.selection.selectionMark = (w, cx) self.cursorKey(rawkey) self.selection.setMark2(self.edCursor.getPos()) else: # cursor key without SHIFT # clear selection and move cursor. self.selection.clearSelection() self.cursorKey(rawkey) return # All other keys are ignored if this widget is read-only if not self.editable: return self.cursorX = None # If there is a selection this must be deleted if self.delete() and (rawkey in (8, 9)): return # Get cursor position word, cx = self.edCursor.getPos() tline = word.tline if (rawkey == 10): # space if (key & SHIFT): self.insertChar(FixedSpace) return s1 = word.string[:cx] s2 = word.string[cx:] word.setString(s1) # Create a new TWord with the second half of the split: nw = TWord(s2) wx = tline.twords.index(word) tline.insert(nw, wx + 1) nw.setCanvas(self.canvas) # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) self.edCursor.setPos(nw, 0) return if (rawkey == 7): # line break s1 = word.string[:cx] s2 = word.string[cx:] word.setString(s1) # Create a new TWord with the second half of the split: nw = TWord(s2) # And a new Paragraph, copying the properties of the old one: para = Paragraph(tline.para) # And a new TextLine: ntl = TextLine(para, [nw]) lx = self.rsubject.tlines.index(tline) + 1 self.rsubject.tlines.insert(lx, ntl) nw.setCanvas(self.canvas) # Move words following the split: wx = tline.twords.index(word) for w in tline.twords[wx + 1:]: ntl.insert(w) del (tline.twords[wx + 1:]) # Now move subsequent lines to new paragraph while True: lx += 1 if (len(self.rsubject.tlines) <= lx) or \ (self.rsubject.tlines[lx].para != tline.para): break self.rsubject.tlines[lx].para = para # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) # Set cursor to start of new word. self.edCursor.setPos(nw, 0) return if (rawkey == 8) or (rawkey == 9): # delete / backspace if (rawkey == 9): # backspace: take one step back and then do as delete. if (cx == 0): # at start of word para0 = tline.para # if stepping back works ... if not self.edCursor.step(False): return # Get new cursor position word, cx = self.edCursor.getPos() tline = word.tline para = tline.para # needed for deletion test below else: cx -= 1 s = word.string if (len(s) == cx): # at end of word # Join words wx = tline.twords.index(word) + 1 if (wx >= len(tline.twords)): # at end of line # If we arrived at the end of a paragraph with # backspace, and the step backwards didn't skip # to the previous paragraph, do nothing! # That is necessary because of the # automatic deletion of words which become empty # when the cursor leaves them. if (rawkey == 9) and (para == para0): return # If at end of paragraph, join paragraphs nl = self.rsubject.nextLine(tline) if nl: para0 = tline.para para = nl.para if (para != para0): nl2 = nl while True: nl2.setPara(para0) nl2 = self.rsubject.nextLine(nl2) if (not nl2) or (nl2.para != para): break # Next line is (now) in same paragraph. # Move first word of next line to current line: tline.insert(nl.twords[0]) del (nl.twords[0]) if not nl.twords: # Line now empty, delete it self.rsubject.deleteTLine(nl) else: nl.y = None # to ensure re-rendering else: # Nothing to delete return nw = tline.twords[wx] del (tline.twords[wx]) word.setString(s + nw.string) # The removed word must be 'freed' nw.delete() # Re-render from tline: self.rsubject.linify(tline) else: # Not at end of word, the word will be shortened. s = s[:cx] + s[cx + 1:] word.setString(s) # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) # Reset cursor to start of new word/paragraph. self.edCursor.setPos(word, cx) self.deleteCount += 1 if (self.deleteCount >= DELETECOUNT): self.saveText() return # Anything else is ignored return # All other keys are ignored if this widget is read-only if not self.editable: return # character key self.cursorX = None # If there is a selection this must be deleted # This must also reset the cursor appropriately self.delete() self.insertChar(key) def insertChar(self, ch): """Insert the given character into the current word. """ word, cx = self.edCursor.getPos() string = word.string[:cx] + ch + word.string[cx:] word.setString(string) # Re-render from tline: self.rsubject.linify(word.tline) self.edCursor.setPos(word, cx + 1) def insertBlock(self, utext): """Insert the given (unicode) text at the current cursor position. Special style formatting will also be recognized but only after a newline. """ self.saveText() self.selection.clearSelection() word, cx = self.edCursor.getPos() # Get cursor offset from end of word (for new cursor placement) cxn = len(word.string) - cx # Insert the new text at the cursor position ... string = word.string[:cx] + utext + word.string[cx:] textLines = self.rsubject.textToLines(string) tline = word.tline nline0 = textLines[0] word1 = nline0.twords[0] # position to start rendering wx = tline.twords.index(word) # insertion index # delete the word which was under the cursor word.delete() # and set its string to non-empty so that the cursor # repositioning works (!) word.string = u"DUMMY" # and add the rest of the original line to the end of the insertion oline2 = tline.twords[wx + 1:] del (tline.twords[wx:]) nlineL = textLines[-1] word = nlineL.twords[-1] # get new word under cursor for w in oline2: nlineL.insert(w) # Append the first inserted line to tline for w in nline0.twords: tline.insert(w) # Insert the remaining lines into the subjects line list tlx = self.rsubject.tlines.index(tline) + 1 self.rsubject.tlines[tlx:tlx] = textLines[1:] self.rsubject.renderShortened(word1) cx = len(word.string) - cxn self.edCursor.setPos(word, cx) def delete(self): """Delete the text between the two selection marks, if they are set, and place the cursor appropriately afterwards. Return True if something was deleted. """ if not self.selection.isSelection(): return False # Save the current text self.saveText() sm1, sm2 = self.selection.order(self.selection.selectionMark, self.selection.selectionMark2) w1 = sm1[0] w2 = sm2[0] cx = sm1[1] self.edCursor.setPos(w1, cx) # Join words before and after selection w1.setString(w1.string[:cx] + w2.string[sm2[1]:]) # Delete all intervening words, and w2 tl1 = w1.tline wx1 = tl1.twords.index(w1) tl2 = w2.tline wx2 = tl2.twords.index(w2) if (tl1 == tl2): # only delete from 1 line # delete words from wx1+1 to wx2 (incl.) for w in tl1.twords[wx1 + 1:wx2 + 1]: w.delete() del (tl1.twords[wx1 + 1:wx2 + 1]) else: # deletion block covers >1 line # delete words from wx1+1 to end of paragraph for w in tl1.twords[wx1 + 1:]: w.delete() del (tl1.twords[wx1 + 1:]) # delete all the intervening lines while True: tl = self.rsubject.nextLine(tl1) if (tl == tl2): break self.rsubject.deleteTLine(tl) # Move remaining words after w2 in tl2 to end of tl1 for w in tl2.twords[wx2 + 1:]: tl1.insert(w) del (tl2.twords[wx2 + 1:]) # Delete tl2 self.rsubject.deleteTLine(tl2) self.selection.clearSelection() self.rsubject.renderShortened(w1) self.edCursor.setPos(w1, cx) return True def getMarked(self): """Get the text between the two selection marks. This shares much of the same logic as the block delete function, but for the sake of clarity I decided to keep them separate. """ if not self.selection.isSelection(): return u"" sm1, sm2 = self.selection.order(self.selection.selectionMark, self.selection.selectionMark2) w1 = sm1[0] w2 = sm2[0] cx1 = sm1[1] cx2 = sm2[1] if (w1 == w2): return w1.string[cx1:cx2] # Get the word fragments at the beginning and end of the selection snip1 = w1.string[cx1:] snip2 = w2.string[:cx2] tl1 = w1.tline wx1 = tl1.twords.index(w1) tl2 = w2.tline wx2 = tl2.twords.index(w2) # Start the text string with the format of the first line text = tl1.para.getFormat() + snip1 # then get all intervening words if (tl1 == tl2): # only 1 line is involved # get words from wx1+1 to wx2-1 (incl.) for w in tl1.twords[wx1 + 1:wx2]: text += u" " + w.string ch = u" " else: # deletion block covers >1 line # get words from wx1+1 to end of paragraph for w in tl1.twords[wx1 + 1:]: text += u" " + w.string # get all the intervening lines while True: para = tl1.para tl1 = self.rsubject.nextLine(tl1) if (tl1.para == para): text += u" " else: text += u"\n" + tl1.para.getFormat() if (tl1 == tl2): break text += tl1.getText() ch = u"" # Add the remaining words in tl2 up to w2-1 for w in tl2.twords[:wx2]: text += ch + w.string ch = u" " # Add the fragment of the last marked word return text + ch + snip2 def cursorKey(self, key): if (key in (1, 2)): self.cursorX = None if (key == 1): # left self.edCursor.step(False) else: # right self.edCursor.step(True) else: if (self.cursorX == None): w, x = self.edCursor.getPos() self.cursorX = w.getXYH(x)[0] if (key == 3): # up self.edCursor.lineStep(self.cursorX, True) else: # down self.edCursor.lineStep(self.cursorX, False) # These event handlers are passed mm coordinates! def edPress(self, xy): # This also sets the cursor self.selection.setMark1(self.getPos(*xy)) def edMove(self, xy): # This also sets the cursor self.selection.setMark2(self.getPos(*xy)) def edRelease(self, xy): # Not so significant at the moment. # For cursor key up/down only: self.cursorX = None def getPos(self, x, y): """Get the nearest text position to the given (mm) coordinates. Return a (word, charIx) tuple. """ # First find the TextLine tline = self.rsubject.tlines[0] for tl in self.rsubject.tlines: if (y < (tl.y + tl.frame.yF)): break tline = tl return self.getXPos(tline, x) def getXPos(self, tline, x): """Get the nearest text position to the given (mm) x-coordinate within the line 'tline'. Return a (word, charIx) tuple. """ # Find the TWord object words = tline.twords j = 0 # word index imax = len(words) - 1 for w in words: # Find out if the point is in this word - # need to include half the space width after the word, if there # is a following word. x0 = w.getX() x1 = x0 + w.getWidth() if (j == imax): break x2 = words[j + 1].getX() spw = (x2 - x1) / 2 if (x < x1 + spw): break j += 1 word = words[j] # Then the character xvec = word.getOffsets() k = 0 if xvec: xo = x - x0 # xo is x relative to word start p0 = 0.0 for p in xvec: p1 = xvec[k] if (xo < (p0 + p1) / 2): break k += 1 p0 = p1 return (word, k) def saveText(self): """Called by various operations to keep backup versions of the text which can be reverted to using 'undo'. It is a rather primitive mechanism, but it should suffice for the foreseen purpose. """ self.rsubject.saveOnChanged(self.edCursor.getPos()) # Allows saving after a certain number of delete operations: self.deleteCount = 0
class Selection: """This class represents a text selection within an 'editor'. There are two markers, selectionMark and selectionMark2, the first being set by left-cursor-press, the second by left-cursor-move. Their values are index triples (line, word, character), and become invalid when the text is edited - so any editing action must clear them. """ def __init__(self, editor): # There are 3 rectangles for marking selection, 1st line, middle lines # and last line. self.editor = editor view = self.editor.canvas self.selRect1 = BGRect(view, "selbg") self.selRect2 = BGRect(view, "selbg") self.selRect3 = BGRect(view, "selbg") self.mainWidth = self.editor.rsubject.db.layoutInfo.mainWidth def clearSelection(self): # The defining property of a clear selection is that selectiomMark2 # is 'None'. selectionMark need not be 'None' self.selectionMark2 = None self.selRect1.hide() self.selRect2.hide() self.selRect3.hide() def isSelection(self): return (self.selectionMark2 != None) def setMark1(self, newMark): """Called when the left mouse button is pressed. """ self.selectionMark = newMark self.clearSelection() self.editor.edCursor.setPos(*newMark) def setMark2(self, newMark): """Called when the mouse is moved with the left button pressed. """ if (self.selectionMark2 == newMark): return self.selectionMark2 = newMark self.markSelection() def markSelection(self): """Sets the coloured background for the selection. It also resets the (display) cursor position. """ self.editor.edCursor.setPos(*self.selectionMark2) if (self.selectionMark2 == self.selectionMark): self.selRect1.hide() self.selRect2.hide() self.selRect3.hide() return sm1, sm2 = self.order() w, cx = sm1 x1, y1, dy1 = w.getXYH(cx) w, cx = sm2 x2, y2, dy2 = w.getXYH(cx) if (y1 == y2): # single line, 1 rectangle, sm1 to sm2 self.selRect1.setRect(x1, y1, x2 - x1, dy1) self.selRect1.show() self.selRect2.hide() self.selRect3.hide() return # selRect1: from sm1 to end of line # selRect2: from start of line to sm2 self.selRect1.setRect(x1, y1, self.mainWidth - x1, dy1) self.selRect1.show() self.selRect2.setRect(0, y2, x2, dy2) self.selRect2.show() y0 = y1 + dy1 if (y0 == y2): # adjacent lines, 2 rectangles self.selRect3.hide() return # several lines, all 3 rectangles # selRect3: all lines in between self.selRect3.setRect(0, y0, self.mainWidth, y2 - y0) self.selRect3.show() def order(self, mark1=None, mark2=None): """Return the two marks as a tuple so that the first entry is guaranteed not to be after the second. The default marks are the current selection markers. """ if not mark1: mark1 = self.selectionMark if not mark2: mark2 = self.selectionMark2 w1 = mark1[0] w2 = mark2[0] if (w1 == w2): # compare character positions if (mark1[1] > mark2[1]): return (mark2, mark1) return (mark1, mark2) tl1 = w1.tline tl2 = w2.tline if (tl1 != tl2): tls = self.editor.rsubject.tlines if (tls.index(tl1) > tls.index(tl2)): return (mark2, mark1) return (mark1, mark2) if (tl1.twords.index(w1) > tl1.twords.index(w2)): return (mark2, mark1) return (mark1, mark2)
class Editor: """There may be only one instance of this class, because of the slot declarations. Given a canvas widget to draw on, implement a report text editor. It can also be restricted to use in a view-only mode by passing 'False' to the 'editable' argument. Much of the low level stuff is done in the 'graphics view' widget, which is a GUI class. I decided not to make this class inherit from that widget, in order to leave maximum flexibility in how it is created and implemented itself (I hope that makes sense). According to the settings in the layout description the area available for the report can comprise more than one 'frame'. The text can be split over a number of separate boxes - specifically here for the case of longer, principally class teacher, reports, which need more than one page). The editor needs the vertical sizes (in mm) of these boxes, so that the display area can be divided into several blocks which will appear sequentially, and also margin information. To deal with text that is too long for a report box, an additional large 'dummy' box is added after the 'official' box(es). This gets a coloured background, to indicate the overflow. The text being edited is primarily managed by the RSubject object, which stores it as a list of TextLine objects, each containing a list of TWord objects. """ def __init__(self, canvas, editable=True): self.canvas = canvas # the canvas widget self.editable = editable slot = canvas.slot slot("edPress", self.edPress) slot("edMove", self.edMove) slot("edRelease", self.edRelease) slot("keyPress", self.edKey) slot("renderedToEnd", self.renewOvfl) self.edCursor = None self.selection = None self.cursorX = None def init(self, rsubject): """Set up the editor for a new report. rsubject: A RSubject object for the current report. """ self.rsubject = rsubject if self.edCursor: self.edCursor.stop() # Set the viewing area (x-margin, y-margin, width) self.mainWidth = self.rsubject.db.layoutInfo.mainWidth self.canvas.initArea(20, 10, self.mainWidth) # Special background for overflow text self.ovflRect = BGRect(self.canvas, "ovflbg", under=True) # Render the frames frames = self.rsubject.frames y = 0.0 for f in frames[:-1]: # Separator line HLine(self.canvas, y, self.mainWidth) f.render(self.canvas, self.rsubject, y) y += f.maxHeight # The overflow frame: HLine(self.canvas, y, self.mainWidth) f = frames[-1] yo = 0.0 ol = self.rsubject.getFrameLines(f) if ol: lastline = ol[-1] # vertical space needed by the frame text: yo = lastline.y + lastline.height f.getHeight(yo) f.render(self.canvas, self.rsubject, y) self.setSize(y, f.height) self.edCursor = EdCursor(self) self.selection = Selection(self) self.initCurSel() def initCurSel(self): # Convert cursor from index to word format cp, cw, cx = self.rsubject.cursor para = None ipara = -1 for tl in self.rsubject.tlines: if (tl.para != para): ipara += 1 para = tl.para if (ipara == cp): nw = len(tl.twords) if (cw < nw): w = tl.twords[cw] break cw -= nw self.edCursor.setPos(w, cx) # else: # self.edCursor.setPos(self.rsubject.tlines[0].twords[0], 0) self.selection.clearSelection() def setSize(self, y, h): """Set the size of the background rectangle behind the text overflow area and also the displayed canvas area. y: y-coordinate of overflow start h: height of overflow area """ if (h <= 0.0): self.ovflRect.hide() self.canvas.setHeight(y) else: self.ovflRect.setRect(0, y, self.mainWidth, h) self.ovflRect.show() self.canvas.setHeight(y + h) def renewOvfl(self, arg): # This is a 'slot', called when a report has been rendered to # the end. f = self.rsubject.frames[-1] # How much space is used in the overflow area? lastLine = self.rsubject.tlines[-1] if (lastLine.frame != f): y = 0.0 else: y = lastLine.y + lastLine.height self.setSize(f.yF, f.getHeight(y)) def edKey(self, key): """This handles key presses. """ # Any key press except cursor keys (?) causes the selection to be deleted! if isinstance(key, types.IntType): rawkey = (key & (SHIFT-1)) if (rawkey in (1,2,3,4)): # First check for shift+cursor-key, because these alter the selection if (key & SHIFT): # SHIFT + cursor key # if there is not already a selection, set the first marker before # moving the cursor if not self.selection.selectionMark2: w, cx = self.edCursor.getPos() # Hack to handle empty words which are about to be deleted if (w.string == u"") and (len(w.tline.twords) > 1): # Don't start selection, just move cursor self.cursorKey(rawkey) return self.selection.selectionMark = (w, cx) self.cursorKey(rawkey) self.selection.setMark2(self.edCursor.getPos()) else: # cursor key without SHIFT # clear selection and move cursor. self.selection.clearSelection() self.cursorKey(rawkey) return # All other keys are ignored if this widget is read-only if not self.editable: return self.cursorX = None # If there is a selection this must be deleted if self.delete() and (rawkey in (8,9)): return # Get cursor position word, cx = self.edCursor.getPos() tline = word.tline if (rawkey == 10): # space if (key & SHIFT): self.insertChar(FixedSpace) return s1 = word.string[:cx] s2 = word.string[cx:] word.setString(s1) # Create a new TWord with the second half of the split: nw = TWord(s2) wx = tline.twords.index(word) tline.insert(nw, wx+1) nw.setCanvas(self.canvas) # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) self.edCursor.setPos(nw, 0) return if (rawkey == 7): # line break s1 = word.string[:cx] s2 = word.string[cx:] word.setString(s1) # Create a new TWord with the second half of the split: nw = TWord(s2) # And a new Paragraph, copying the properties of the old one: para = Paragraph(tline.para) # And a new TextLine: ntl = TextLine(para, [nw]) lx = self.rsubject.tlines.index(tline) + 1 self.rsubject.tlines.insert(lx, ntl) nw.setCanvas(self.canvas) # Move words following the split: wx = tline.twords.index(word) for w in tline.twords[wx+1:]: ntl.insert(w) del(tline.twords[wx+1:]) # Now move subsequent lines to new paragraph while True: lx += 1 if (len(self.rsubject.tlines) <= lx) or \ (self.rsubject.tlines[lx].para != tline.para): break self.rsubject.tlines[lx].para = para # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) # Set cursor to start of new word. self.edCursor.setPos(nw, 0) return if (rawkey == 8) or (rawkey == 9): # delete / backspace if (rawkey == 9): # backspace: take one step back and then do as delete. if (cx == 0): # at start of word para0 = tline.para # if stepping back works ... if not self.edCursor.step(False): return # Get new cursor position word, cx = self.edCursor.getPos() tline = word.tline para = tline.para # needed for deletion test below else: cx -= 1 s = word.string if (len(s) == cx): # at end of word # Join words wx = tline.twords.index(word) + 1 if (wx >= len(tline.twords)): # at end of line # If we arrived at the end of a paragraph with # backspace, and the step backwards didn't skip # to the previous paragraph, do nothing! # That is necessary because of the # automatic deletion of words which become empty # when the cursor leaves them. if (rawkey == 9) and (para == para0): return # If at end of paragraph, join paragraphs nl = self.rsubject.nextLine(tline) if nl: para0 = tline.para para = nl.para if (para != para0): nl2 = nl while True: nl2.setPara(para0) nl2 = self.rsubject.nextLine(nl2) if (not nl2) or (nl2.para != para): break # Next line is (now) in same paragraph. # Move first word of next line to current line: tline.insert(nl.twords[0]) del(nl.twords[0]) if not nl.twords: # Line now empty, delete it self.rsubject.deleteTLine(nl) else: nl.y = None # to ensure re-rendering else: # Nothing to delete return nw = tline.twords[wx] del(tline.twords[wx]) word.setString(s + nw.string) # The removed word must be 'freed' nw.delete() # Re-render from tline: self.rsubject.linify(tline) else: # Not at end of word, the word will be shortened. s = s[:cx] + s[cx+1:] word.setString(s) # Re-render from this word, noting that it became shorter: self.rsubject.renderShortened(word) # Reset cursor to start of new word/paragraph. self.edCursor.setPos(word, cx) self.deleteCount +=1 if (self.deleteCount >= DELETECOUNT): self.saveText() return # Anything else is ignored return # All other keys are ignored if this widget is read-only if not self.editable: return # character key self.cursorX = None # If there is a selection this must be deleted # This must also reset the cursor appropriately self.delete() self.insertChar(key) def insertChar(self, ch): """Insert the given character into the current word. """ word, cx = self.edCursor.getPos() string = word.string[:cx] + ch + word.string[cx:] word.setString(string) # Re-render from tline: self.rsubject.linify(word.tline) self.edCursor.setPos(word, cx+1) def insertBlock(self, utext): """Insert the given (unicode) text at the current cursor position. Special style formatting will also be recognized but only after a newline. """ self.saveText() self.selection.clearSelection() word, cx = self.edCursor.getPos() # Get cursor offset from end of word (for new cursor placement) cxn = len(word.string) - cx # Insert the new text at the cursor position ... string = word.string[:cx] + utext + word.string[cx:] textLines = self.rsubject.textToLines(string) tline = word.tline nline0 = textLines[0] word1 = nline0.twords[0] # position to start rendering wx = tline.twords.index(word) # insertion index # delete the word which was under the cursor word.delete() # and set its string to non-empty so that the cursor # repositioning works (!) word.string = u"DUMMY" # and add the rest of the original line to the end of the insertion oline2 = tline.twords[wx+1:] del(tline.twords[wx:]) nlineL = textLines[-1] word = nlineL.twords[-1] # get new word under cursor for w in oline2: nlineL.insert(w) # Append the first inserted line to tline for w in nline0.twords: tline.insert(w) # Insert the remaining lines into the subjects line list tlx = self.rsubject.tlines.index(tline) + 1 self.rsubject.tlines[tlx:tlx] = textLines[1:] self.rsubject.renderShortened(word1) cx = len(word.string) - cxn self.edCursor.setPos(word, cx) def delete(self): """Delete the text between the two selection marks, if they are set, and place the cursor appropriately afterwards. Return True if something was deleted. """ if not self.selection.isSelection(): return False # Save the current text self.saveText() sm1, sm2 = self.selection.order(self.selection.selectionMark, self.selection.selectionMark2) w1 = sm1[0] w2 = sm2[0] cx = sm1[1] self.edCursor.setPos(w1, cx) # Join words before and after selection w1.setString(w1.string[:cx] + w2.string[sm2[1]:]) # Delete all intervening words, and w2 tl1 = w1.tline wx1 = tl1.twords.index(w1) tl2 = w2.tline wx2 = tl2.twords.index(w2) if (tl1 == tl2): # only delete from 1 line # delete words from wx1+1 to wx2 (incl.) for w in tl1.twords[wx1+1:wx2+1]: w.delete() del(tl1.twords[wx1+1:wx2+1]) else: # deletion block covers >1 line # delete words from wx1+1 to end of paragraph for w in tl1.twords[wx1+1:]: w.delete() del(tl1.twords[wx1+1:]) # delete all the intervening lines while True: tl = self.rsubject.nextLine(tl1) if (tl == tl2): break self.rsubject.deleteTLine(tl) # Move remaining words after w2 in tl2 to end of tl1 for w in tl2.twords[wx2+1:]: tl1.insert(w) del(tl2.twords[wx2+1:]) # Delete tl2 self.rsubject.deleteTLine(tl2) self.selection.clearSelection() self.rsubject.renderShortened(w1) self.edCursor.setPos(w1, cx) return True def getMarked(self): """Get the text between the two selection marks. This shares much of the same logic as the block delete function, but for the sake of clarity I decided to keep them separate. """ if not self.selection.isSelection(): return u"" sm1, sm2 = self.selection.order(self.selection.selectionMark, self.selection.selectionMark2) w1 = sm1[0] w2 = sm2[0] cx1 = sm1[1] cx2 = sm2[1] if (w1 == w2): return w1.string[cx1:cx2] # Get the word fragments at the beginning and end of the selection snip1 = w1.string[cx1:] snip2 = w2.string[:cx2] tl1 = w1.tline wx1 = tl1.twords.index(w1) tl2 = w2.tline wx2 = tl2.twords.index(w2) # Start the text string with the format of the first line text = tl1.para.getFormat() + snip1 # then get all intervening words if (tl1 == tl2): # only 1 line is involved # get words from wx1+1 to wx2-1 (incl.) for w in tl1.twords[wx1+1:wx2]: text += u" " + w.string ch = u" " else: # deletion block covers >1 line # get words from wx1+1 to end of paragraph for w in tl1.twords[wx1+1:]: text += u" " + w.string # get all the intervening lines while True: para = tl1.para tl1 = self.rsubject.nextLine(tl1) if (tl1.para == para): text += u" " else: text += u"\n" + tl1.para.getFormat() if (tl1 == tl2): break text += tl1.getText() ch = u"" # Add the remaining words in tl2 up to w2-1 for w in tl2.twords[:wx2]: text += ch + w.string ch = u" " # Add the fragment of the last marked word return text + ch + snip2 def cursorKey(self, key): if (key in (1,2)): self.cursorX = None if (key == 1): # left self.edCursor.step(False) else: # right self.edCursor.step(True) else: if (self.cursorX == None): w, x = self.edCursor.getPos() self.cursorX = w.getXYH(x)[0] if (key == 3): # up self.edCursor.lineStep(self.cursorX, True) else: # down self.edCursor.lineStep(self.cursorX, False) # These event handlers are passed mm coordinates! def edPress(self, xy): # This also sets the cursor self.selection.setMark1(self.getPos(*xy)) def edMove(self, xy): # This also sets the cursor self.selection.setMark2(self.getPos(*xy)) def edRelease(self, xy): # Not so significant at the moment. # For cursor key up/down only: self.cursorX = None def getPos(self, x, y): """Get the nearest text position to the given (mm) coordinates. Return a (word, charIx) tuple. """ # First find the TextLine tline = self.rsubject.tlines[0] for tl in self.rsubject.tlines: if (y < (tl.y + tl.frame.yF)): break tline = tl return self.getXPos(tline, x) def getXPos(self, tline, x): """Get the nearest text position to the given (mm) x-coordinate within the line 'tline'. Return a (word, charIx) tuple. """ # Find the TWord object words = tline.twords j = 0 # word index imax = len(words) - 1 for w in words: # Find out if the point is in this word - # need to include half the space width after the word, if there # is a following word. x0 = w.getX() x1 = x0 + w.getWidth() if (j == imax): break x2 = words[j+1].getX() spw = (x2 - x1)/2 if (x < x1 + spw): break j += 1 word = words[j] # Then the character xvec = word.getOffsets() k = 0 if xvec: xo = x - x0 # xo is x relative to word start p0 = 0.0 for p in xvec: p1 = xvec[k] if (xo < (p0 + p1)/2): break k += 1 p0 = p1 return (word, k) def saveText(self): """Called by various operations to keep backup versions of the text which can be reverted to using 'undo'. It is a rather primitive mechanism, but it should suffice for the foreseen purpose. """ self.rsubject.saveOnChanged(self.edCursor.getPos()) # Allows saving after a certain number of delete operations: self.deleteCount = 0