#---------------------------------------------------------------------- # Name: wxPython.lib.editor.Editor # Purpose: An intelligent text editor with colorization capabilities. # # Original # Authors: Dirk Holtwic, Robin Dunn # # New # Authors: Adam Feuer, Steve Howell # # History: # This code used to support a fairly complex subclass that did # syntax coloring and outliner collapse mode. Adam and Steve # inherited the code, and added a lot of basic editor # functionality that had not been there before, such as cut-and-paste. # # # Created: 15-Dec-1999 # RCS-ID: $Id$ # Copyright: (c) 1999 by Dirk Holtwick, 1999 # Licence: wxWindows license #---------------------------------------------------------------------- # 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net) # # o 2.5 compatability update. # # 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) # # o wxEditor -> Editor # import os import time import wx import selection import images #---------------------------- def ForceBetween(min, val, max): if val > max: return max if val < min: return min return val def LineTrimmer(lineOfText): if len(lineOfText) == 0: return "" elif lineOfText[-1] == '\r': return lineOfText[:-1] else: return lineOfText def LineSplitter(text): return map (LineTrimmer, text.split('\n')) #---------------------------- class Scroller: def __init__(self, parent): self.parent = parent self.ow = 0 self.oh = 0 self.ox = 0 self.oy = 0 def SetScrollbars(self, fw, fh, w, h, x, y): if (self.ow != w or self.oh != h or self.ox != x or self.oy != y): self.parent.SetScrollbars(fw, fh, w, h, x, y) self.ow = w self.oh = h self.ox = x self.oy = y #---------------------------------------------------------------------- class Editor(wx.ScrolledWindow): def __init__(self, parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): wx.ScrolledWindow.__init__(self, parent, id, pos, size, style|wx.WANTS_CHARS) self.isDrawing = False self.InitCoords() self.InitFonts() self.SetColors() self.MapEvents() self.LoadImages() self.InitDoubleBuffering() self.InitScrolling() self.SelectOff() self.SetFocus() self.SetText([""]) self.SpacesPerTab = 4 ##------------------ Init stuff def InitCoords(self): self.cx = 0 self.cy = 0 self.oldCx = 0 self.oldCy = 0 self.sx = 0 self.sy = 0 self.sw = 0 self.sh = 0 self.sco_x = 0 self.sco_y = 0 def MapEvents(self): self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_SCROLLWIN, self.OnScroll) self.Bind(wx.EVT_CHAR, self.OnChar) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) ##------------------- Platform-specific stuff def NiceFontForPlatform(self): if wx.Platform == "__WXMSW__": return wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL) else: return wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL, False) def UnixKeyHack(self, key): # # this will be obsolete when we get the new wxWindows patch # # 12/14/03 - jmg # # Which patch? I don't know if this is needed, but I don't know # why it's here either. Play it safe; leave it in. # if key <= 26: key += ord('a') - 1 return key ##-------------------- UpdateView/Cursor code def OnSize(self, event): self.AdjustScrollbars() self.SetFocus() def SetCharDimensions(self): # TODO: We need a code review on this. It appears that Linux # improperly reports window dimensions when the scrollbar's there. self.bw, self.bh = self.GetClientSize() if wx.Platform == "__WXMSW__": self.sh = self.bh / self.fh self.sw = (self.bw / self.fw) - 1 else: self.sh = self.bh / self.fh if self.LinesInFile() >= self.sh: self.bw = self.bw - wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) self.sw = (self.bw / self.fw) - 1 self.sw = (self.bw / self.fw) - 1 if self.CalcMaxLineLen() >= self.sw: self.bh = self.bh - wx.SystemSettings_GetMetric(wx.SYS_HSCROLL_Y) self.sh = self.bh / self.fh def UpdateView(self, dc = None): if dc is None: dc = wx.ClientDC(self) if dc.Ok(): self.SetCharDimensions() self.KeepCursorOnScreen() self.DrawSimpleCursor(0,0, dc, True) self.Draw(dc) def OnPaint(self, event): dc = wx.PaintDC(self) if self.isDrawing: return self.isDrawing = True self.UpdateView(dc) wx.CallAfter(self.AdjustScrollbars) self.isDrawing = False def OnEraseBackground(self, evt): pass ##-------------------- Drawing code def InitFonts(self): dc = wx.ClientDC(self) self.font = self.NiceFontForPlatform() dc.SetFont(self.font) self.fw = dc.GetCharWidth() self.fh = dc.GetCharHeight() def SetColors(self): self.fgColor = wx.NamedColour('black') self.bgColor = wx.NamedColour('white') self.selectColor = wx.Colour(238, 220, 120) # r, g, b = emacsOrange def InitDoubleBuffering(self): pass def DrawEditText(self, t, x, y, dc): dc.DrawText(t, (x * self.fw, y * self.fh)) def DrawLine(self, line, dc): if self.IsLine(line): l = line t = self.lines[l] dc.SetTextForeground(self.fgColor) fragments = selection.Selection( self.SelectBegin, self.SelectEnd, self.sx, self.sw, line, t) x = 0 for (data, selected) in fragments: if selected: dc.SetTextBackground(self.selectColor) if x == 0 and len(data) == 0 and len(fragments) == 1: data = ' ' else: dc.SetTextBackground(self.bgColor) self.DrawEditText(data, x, line - self.sy, dc) x += len(data) def Draw(self, odc=None): if not odc: odc = wx.ClientDC(self) bmp = wx.EmptyBitmap(max(1,self.bw), max(1,self.bh)) dc = wx.BufferedDC(odc, bmp) if dc.Ok(): dc.SetFont(self.font) dc.SetBackgroundMode(wx.SOLID) dc.SetTextBackground(self.bgColor) dc.SetTextForeground(self.fgColor) dc.Clear() for line in range(self.sy, self.sy + self.sh): self.DrawLine(line, dc) if len(self.lines) < self.sh + self.sy: self.DrawEofMarker(dc) self.DrawCursor(dc) ##------------------ eofMarker stuff def LoadImages(self): self.eofMarker = images.GetBitmap(images.EofImageData) def DrawEofMarker(self,dc): x = 0 y = (len(self.lines) - self.sy) * self.fh hasTransparency = 1 dc.DrawBitmap(self.eofMarker, (x, y), hasTransparency) ##------------------ cursor-related functions def DrawCursor(self, dc = None): if not dc: dc = wx.ClientDC(self) if (self.LinesInFile())maxlen: maxlen = len(line) return maxlen def KeepCursorOnScreen(self): self.sy = ForceBetween(max(0, self.cy-self.sh), self.sy, self.cy) self.sx = ForceBetween(max(0, self.cx-self.sw), self.sx, self.cx) self.AdjustScrollbars() def HorizBoundaries(self): self.SetCharDimensions() maxLineLen = self.CalcMaxLineLen() self.sx = ForceBetween(0, self.sx, max(self.sw, maxLineLen - self.sw + 1)) self.cx = ForceBetween(self.sx, self.cx, self.sx + self.sw - 1) def VertBoundaries(self): self.SetCharDimensions() self.sy = ForceBetween(0, self.sy, max(self.sh, self.LinesInFile() - self.sh + 1)) self.cy = ForceBetween(self.sy, self.cy, self.sy + self.sh - 1) def cVert(self, num): self.cy = self.cy + num self.cy = ForceBetween(0, self.cy, self.LinesInFile() - 1) self.sy = ForceBetween(self.cy - self.sh + 1, self.sy, self.cy) self.cx = min(self.cx, self.CurrentLineLength()) def cHoriz(self, num): self.cx = self.cx + num self.cx = ForceBetween(0, self.cx, self.CurrentLineLength()) self.sx = ForceBetween(self.cx - self.sw + 1, self.sx, self.cx) def AboveScreen(self, row): return row < self.sy def BelowScreen(self, row): return row >= self.sy + self.sh def LeftOfScreen(self, col): return col < self.sx def RightOfScreen(self, col): return col >= self.sx + self.sw ##----------------- data structure helper functions def GetText(self): return self.lines def SetText(self, lines): self.InitCoords() self.lines = lines self.UnTouchBuffer() self.SelectOff() self.AdjustScrollbars() self.UpdateView(None) def IsLine(self, lineNum): return (0<=lineNum) and (lineNum self.nextScrollTime: self.nextScrollTime = time.time() + self.SCROLLDELAY return True else: return False def SetScrollTimer(self): oneShot = True self.scrollTimer.Start(1000*self.SCROLLDELAY/2, oneShot) self.Bind(wx.EVT_TIMER, self.OnTimer) def OnTimer(self, event): screenX, screenY = wx.GetMousePosition() x, y = self.ScreenToClientXY(screenX, screenY) self.MouseToRow(y) self.MouseToCol(x) self.SelectUpdate() ##-------------------------- Mouse off screen functions def HandleAboveScreen(self, row): self.SetScrollTimer() if self.CanScroll(): row = self.sy - 1 row = max(0, row) self.cy = row def HandleBelowScreen(self, row): self.SetScrollTimer() if self.CanScroll(): row = self.sy + self.sh row = min(row, self.LinesInFile() - 1) self.cy = row def HandleLeftOfScreen(self, col): self.SetScrollTimer() if self.CanScroll(): col = self.sx - 1 col = max(0,col) self.cx = col def HandleRightOfScreen(self, col): self.SetScrollTimer() if self.CanScroll(): col = self.sx + self.sw col = min(col, self.CurrentLineLength()) self.cx = col ##------------------------ mousing functions def MouseToRow(self, mouseY): row = self.sy + (mouseY/ self.fh) if self.AboveScreen(row): self.HandleAboveScreen(row) elif self.BelowScreen(row): self.HandleBelowScreen(row) else: self.cy = min(row, self.LinesInFile() - 1) def MouseToCol(self, mouseX): col = self.sx + (mouseX / self.fw) if self.LeftOfScreen(col): self.HandleLeftOfScreen(col) elif self.RightOfScreen(col): self.HandleRightOfScreen(col) else: self.cx = min(col, self.CurrentLineLength()) def MouseToCursor(self, event): self.MouseToRow(event.GetY()) self.MouseToCol(event.GetX()) def OnMotion(self, event): if event.LeftIsDown() and self.HasCapture(): self.Selecting = True self.MouseToCursor(event) self.SelectUpdate() def OnLeftDown(self, event): self.MouseToCursor(event) self.SelectBegin = (self.cy, self.cx) self.SelectEnd = None self.UpdateView() self.CaptureMouse() def OnLeftUp(self, event): if not self.HasCapture(): return if self.SelectEnd is None: self.OnClick() else: self.Selecting = False self.SelectNotify(False, self.SelectBegin, self.SelectEnd) self.ReleaseMouse() self.scrollTimer.Stop() #------------------------- Scrolling def HorizScroll(self, event, eventType): maxLineLen = self.CalcMaxLineLen() if eventType == wx.EVT_SCROLLWIN_LINEUP: self.sx -= 1 elif eventType == wx.EVT_SCROLLWIN_LINEDOWN: self.sx += 1 elif eventType == wx.EVT_SCROLLWIN_PAGEUP: self.sx -= self.sw elif eventType == wx.EVT_SCROLLWIN_PAGEDOWN: self.sx += self.sw elif eventType == wx.EVT_SCROLLWIN_TOP: self.sx = self.cx = 0 elif eventType == wx.EVT_SCROLLWIN_BOTTOM: self.sx = maxLineLen - self.sw self.cx = maxLineLen else: self.sx = event.GetPosition() self.HorizBoundaries() def VertScroll(self, event, eventType): if eventType == wx.EVT_SCROLLWIN_LINEUP: self.sy -= 1 elif eventType == wx.EVT_SCROLLWIN_LINEDOWN: self.sy += 1 elif eventType == wx.EVT_SCROLLWIN_PAGEUP: self.sy -= self.sh elif eventType == wx.EVT_SCROLLWIN_PAGEDOWN: self.sy += self.sh elif eventType == wx.EVT_SCROLLWIN_TOP: self.sy = self.cy = 0 elif eventType == wx.EVT_SCROLLWIN_BOTTOM: self.sy = self.LinesInFile() - self.sh self.cy = self.LinesInFile() else: self.sy = event.GetPosition() self.VertBoundaries() def OnScroll(self, event): dir = event.GetOrientation() eventType = event.GetEventType() if dir == wx.HORIZONTAL: self.HorizScroll(event, eventType) else: self.VertScroll(event, eventType) self.UpdateView() def AdjustScrollbars(self): for i in range(2): self.SetCharDimensions() self.scroller.SetScrollbars( self.fw, self.fh, self.CalcMaxLineLen()+3, max(self.LinesInFile()+1, self.sh), self.sx, self.sy) #------------ backspace, delete, return def BreakLine(self, event): if self.IsLine(self.cy): t = self.lines[self.cy] self.lines = self.lines[:self.cy] + [t[:self.cx],t[self.cx:]] + self.lines[self.cy+1:] self.cVert(1) self.cx = 0 self.TouchBuffer() def InsertChar(self,char): if self.IsLine(self.cy): t = self.lines[self.cy] t = t[:self.cx] + char + t[self.cx:] self.SetTextLine(self.cy, t) self.cHoriz(1) self.TouchBuffer() def JoinLines(self): t1 = self.lines[self.cy] t2 = self.lines[self.cy+1] self.cx = len(t1) self.lines = self.lines[:self.cy] + [t1 + t2] + self.lines[self.cy+2:] self.TouchBuffer() def DeleteChar(self,x,y,oldtext): newtext = oldtext[:x] + oldtext[x+1:] self.SetTextLine(y, newtext) self.TouchBuffer() def BackSpace(self, event): t = self.GetTextLine(self.cy) if self.cx>0: self.DeleteChar(self.cx-1,self.cy,t) self.cHoriz(-1) self.TouchBuffer() elif self.cx == 0: if self.cy > 0: self.cy -= 1 self.JoinLines() self.TouchBuffer() else: wx.Bell() def Delete(self, event): t = self.GetTextLine(self.cy) if self.cx31) and (key<256): self.InsertChar(chr(key)) else: wx.Bell() return self.UpdateView() self.AdjustScrollbars() def OnChar(self, event): key = event.KeyCode() filters = [self.AltKey, self.MoveSpecialControlKey, self.ControlKey, self.SpecialControlKey, self.MoveSpecialKey, self.ShiftKey, self.NormalChar] for filter in filters: if filter(event,key): break return 0 #----------------------- Eliminate memory leaks def OnDestroy(self, event): self.mdc = None self.odc = None self.bgColor = None self.fgColor = None self.font = None self.selectColor = None self.scrollTimer = None self.eofMarker = None #-------------------- Abstract methods for subclasses def OnClick(self): pass def SelectNotify(self, Selecting, SelectionBegin, SelectionEnd): pass