443 lines
15 KiB
Python
443 lines
15 KiB
Python
# Based on iwidgets2.2.0/scrolledtext.itk code.
|
|
|
|
import Tkinter
|
|
import Pmw
|
|
|
|
class ScrolledText(Pmw.MegaWidget):
|
|
def __init__(self, parent = None, **kw):
|
|
|
|
# Define the megawidget options.
|
|
INITOPT = Pmw.INITOPT
|
|
optiondefs = (
|
|
('borderframe', 0, INITOPT),
|
|
('columnheader', 0, INITOPT),
|
|
('hscrollmode', 'dynamic', self._hscrollMode),
|
|
('labelmargin', 0, INITOPT),
|
|
('labelpos', None, INITOPT),
|
|
('rowcolumnheader',0, INITOPT),
|
|
('rowheader', 0, INITOPT),
|
|
('scrollmargin', 2, INITOPT),
|
|
('usehullsize', 0, INITOPT),
|
|
('vscrollmode', 'dynamic', self._vscrollMode),
|
|
)
|
|
self.defineoptions(kw, optiondefs)
|
|
|
|
# Initialise the base class (after defining the options).
|
|
Pmw.MegaWidget.__init__(self, parent)
|
|
|
|
# Create the components.
|
|
interior = self.interior()
|
|
|
|
if self['usehullsize']:
|
|
interior.grid_propagate(0)
|
|
|
|
if self['borderframe']:
|
|
# Create a frame widget to act as the border of the text
|
|
# widget. Later, pack the text widget so that it fills
|
|
# the frame. This avoids a problem in Tk, where window
|
|
# items in a text widget may overlap the border of the
|
|
# text widget.
|
|
self._borderframe = self.createcomponent('borderframe',
|
|
(), None,
|
|
Tkinter.Frame, (interior,),
|
|
relief = 'sunken',
|
|
borderwidth = 2,
|
|
)
|
|
self._borderframe.grid(row = 4, column = 4, sticky = 'news')
|
|
|
|
# Create the text widget.
|
|
self._textbox = self.createcomponent('text',
|
|
(), None,
|
|
Tkinter.Text, (self._borderframe,),
|
|
highlightthickness = 0,
|
|
borderwidth = 0,
|
|
)
|
|
self._textbox.pack(fill = 'both', expand = 1)
|
|
|
|
bw = self._borderframe.cget('borderwidth'),
|
|
ht = self._borderframe.cget('highlightthickness'),
|
|
else:
|
|
# Create the text widget.
|
|
self._textbox = self.createcomponent('text',
|
|
(), None,
|
|
Tkinter.Text, (interior,),
|
|
)
|
|
self._textbox.grid(row = 4, column = 4, sticky = 'news')
|
|
|
|
bw = self._textbox.cget('borderwidth'),
|
|
ht = self._textbox.cget('highlightthickness'),
|
|
|
|
# Create the header text widgets
|
|
if self['columnheader']:
|
|
self._columnheader = self.createcomponent('columnheader',
|
|
(), 'Header',
|
|
Tkinter.Text, (interior,),
|
|
height=1,
|
|
wrap='none',
|
|
borderwidth = bw,
|
|
highlightthickness = ht,
|
|
)
|
|
self._columnheader.grid(row = 2, column = 4, sticky = 'ew')
|
|
self._columnheader.configure(
|
|
xscrollcommand = self._columnheaderscrolled)
|
|
|
|
if self['rowheader']:
|
|
self._rowheader = self.createcomponent('rowheader',
|
|
(), 'Header',
|
|
Tkinter.Text, (interior,),
|
|
wrap='none',
|
|
borderwidth = bw,
|
|
highlightthickness = ht,
|
|
)
|
|
self._rowheader.grid(row = 4, column = 2, sticky = 'ns')
|
|
self._rowheader.configure(
|
|
yscrollcommand = self._rowheaderscrolled)
|
|
|
|
if self['rowcolumnheader']:
|
|
self._rowcolumnheader = self.createcomponent('rowcolumnheader',
|
|
(), 'Header',
|
|
Tkinter.Text, (interior,),
|
|
height=1,
|
|
wrap='none',
|
|
borderwidth = bw,
|
|
highlightthickness = ht,
|
|
)
|
|
self._rowcolumnheader.grid(row = 2, column = 2, sticky = 'nsew')
|
|
|
|
interior.grid_rowconfigure(4, weight = 1, minsize = 0)
|
|
interior.grid_columnconfigure(4, weight = 1, minsize = 0)
|
|
|
|
# Create the horizontal scrollbar
|
|
self._horizScrollbar = self.createcomponent('horizscrollbar',
|
|
(), 'Scrollbar',
|
|
Tkinter.Scrollbar, (interior,),
|
|
orient='horizontal',
|
|
command=self._textbox.xview
|
|
)
|
|
|
|
# Create the vertical scrollbar
|
|
self._vertScrollbar = self.createcomponent('vertscrollbar',
|
|
(), 'Scrollbar',
|
|
Tkinter.Scrollbar, (interior,),
|
|
orient='vertical',
|
|
command=self._textbox.yview
|
|
)
|
|
|
|
self.createlabel(interior, childCols = 5, childRows = 5)
|
|
|
|
# Initialise instance variables.
|
|
self._horizScrollbarOn = 0
|
|
self._vertScrollbarOn = 0
|
|
self.scrollTimer = None
|
|
self._scrollRecurse = 0
|
|
self._horizScrollbarNeeded = 0
|
|
self._vertScrollbarNeeded = 0
|
|
self._textWidth = None
|
|
|
|
# These four variables avoid an infinite loop caused by the
|
|
# row or column header's scrollcommand causing the main text
|
|
# widget's scrollcommand to be called and vice versa.
|
|
self._textboxLastX = None
|
|
self._textboxLastY = None
|
|
self._columnheaderLastX = None
|
|
self._rowheaderLastY = None
|
|
|
|
# Check keywords and initialise options.
|
|
self.initialiseoptions()
|
|
|
|
def destroy(self):
|
|
if self.scrollTimer is not None:
|
|
self.after_cancel(self.scrollTimer)
|
|
self.scrollTimer = None
|
|
Pmw.MegaWidget.destroy(self)
|
|
|
|
# ======================================================================
|
|
|
|
# Public methods.
|
|
|
|
def clear(self):
|
|
self.settext('')
|
|
|
|
def importfile(self, fileName, where = 'end'):
|
|
file = open(fileName, 'r')
|
|
self._textbox.insert(where, file.read())
|
|
file.close()
|
|
|
|
def exportfile(self, fileName):
|
|
file = open(fileName, 'w')
|
|
file.write(self._textbox.get('1.0', 'end'))
|
|
file.close()
|
|
|
|
def settext(self, text):
|
|
disabled = (str(self._textbox.cget('state')) == 'disabled')
|
|
if disabled:
|
|
self._textbox.configure(state='normal')
|
|
self._textbox.delete('0.0', 'end')
|
|
self._textbox.insert('end', text)
|
|
if disabled:
|
|
self._textbox.configure(state='disabled')
|
|
|
|
# Override Tkinter.Text get method, so that if it is called with
|
|
# no arguments, return all text (consistent with other widgets).
|
|
def get(self, first=None, last=None):
|
|
if first is None:
|
|
return self._textbox.get('1.0', 'end')
|
|
else:
|
|
return self._textbox.get(first, last)
|
|
|
|
def getvalue(self):
|
|
return self.get()
|
|
|
|
def setvalue(self, text):
|
|
return self.settext(text)
|
|
|
|
def appendtext(self, text):
|
|
oldTop, oldBottom = self._textbox.yview()
|
|
|
|
disabled = (str(self._textbox.cget('state')) == 'disabled')
|
|
if disabled:
|
|
self._textbox.configure(state='normal')
|
|
self._textbox.insert('end', text)
|
|
if disabled:
|
|
self._textbox.configure(state='disabled')
|
|
|
|
if oldBottom == 1.0:
|
|
self._textbox.yview('moveto', 1.0)
|
|
|
|
# ======================================================================
|
|
|
|
# Configuration methods.
|
|
|
|
def _hscrollMode(self):
|
|
# The horizontal scroll mode has been configured.
|
|
|
|
mode = self['hscrollmode']
|
|
|
|
if mode == 'static':
|
|
if not self._horizScrollbarOn:
|
|
self._toggleHorizScrollbar()
|
|
elif mode == 'dynamic':
|
|
if self._horizScrollbarNeeded != self._horizScrollbarOn:
|
|
self._toggleHorizScrollbar()
|
|
elif mode == 'none':
|
|
if self._horizScrollbarOn:
|
|
self._toggleHorizScrollbar()
|
|
else:
|
|
message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
|
|
raise ValueError, message
|
|
|
|
self._configureScrollCommands()
|
|
|
|
def _vscrollMode(self):
|
|
# The vertical scroll mode has been configured.
|
|
|
|
mode = self['vscrollmode']
|
|
|
|
if mode == 'static':
|
|
if not self._vertScrollbarOn:
|
|
self._toggleVertScrollbar()
|
|
elif mode == 'dynamic':
|
|
if self._vertScrollbarNeeded != self._vertScrollbarOn:
|
|
self._toggleVertScrollbar()
|
|
elif mode == 'none':
|
|
if self._vertScrollbarOn:
|
|
self._toggleVertScrollbar()
|
|
else:
|
|
message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
|
|
raise ValueError, message
|
|
|
|
self._configureScrollCommands()
|
|
|
|
# ======================================================================
|
|
|
|
# Private methods.
|
|
|
|
def _configureScrollCommands(self):
|
|
# If both scrollmodes are not dynamic we can save a lot of
|
|
# time by not having to create an idle job to handle the
|
|
# scroll commands.
|
|
|
|
# Clean up previous scroll commands to prevent memory leak.
|
|
tclCommandName = str(self._textbox.cget('xscrollcommand'))
|
|
if tclCommandName != '':
|
|
self._textbox.deletecommand(tclCommandName)
|
|
tclCommandName = str(self._textbox.cget('yscrollcommand'))
|
|
if tclCommandName != '':
|
|
self._textbox.deletecommand(tclCommandName)
|
|
|
|
if self['hscrollmode'] == self['vscrollmode'] == 'dynamic':
|
|
self._textbox.configure(
|
|
xscrollcommand=self._scrollBothLater,
|
|
yscrollcommand=self._scrollBothLater
|
|
)
|
|
else:
|
|
self._textbox.configure(
|
|
xscrollcommand=self._scrollXNow,
|
|
yscrollcommand=self._scrollYNow
|
|
)
|
|
|
|
def _scrollXNow(self, first, last):
|
|
self._horizScrollbar.set(first, last)
|
|
self._horizScrollbarNeeded = ((first, last) != ('0', '1'))
|
|
|
|
# This code is the same as in _scrollBothNow. Keep it that way.
|
|
if self['hscrollmode'] == 'dynamic':
|
|
currentWidth = self._textbox.winfo_width()
|
|
if self._horizScrollbarNeeded != self._horizScrollbarOn:
|
|
if self._horizScrollbarNeeded or \
|
|
self._textWidth != currentWidth:
|
|
self._toggleHorizScrollbar()
|
|
self._textWidth = currentWidth
|
|
|
|
if self['columnheader']:
|
|
if self._columnheaderLastX != first:
|
|
self._columnheaderLastX = first
|
|
self._columnheader.xview('moveto', first)
|
|
|
|
def _scrollYNow(self, first, last):
|
|
if first == '0' and last == '0':
|
|
return
|
|
self._vertScrollbar.set(first, last)
|
|
self._vertScrollbarNeeded = ((first, last) != ('0', '1'))
|
|
|
|
if self['vscrollmode'] == 'dynamic':
|
|
if self._vertScrollbarNeeded != self._vertScrollbarOn:
|
|
self._toggleVertScrollbar()
|
|
|
|
if self['rowheader']:
|
|
if self._rowheaderLastY != first:
|
|
self._rowheaderLastY = first
|
|
self._rowheader.yview('moveto', first)
|
|
|
|
def _scrollBothLater(self, first, last):
|
|
# Called by the text widget to set the horizontal or vertical
|
|
# scrollbar when it has scrolled or changed size or contents.
|
|
|
|
if self.scrollTimer is None:
|
|
self.scrollTimer = self.after_idle(self._scrollBothNow)
|
|
|
|
def _scrollBothNow(self):
|
|
# This performs the function of _scrollXNow and _scrollYNow.
|
|
# If one is changed, the other should be updated to match.
|
|
self.scrollTimer = None
|
|
|
|
# Call update_idletasks to make sure that the containing frame
|
|
# has been resized before we attempt to set the scrollbars.
|
|
# Otherwise the scrollbars may be mapped/unmapped continuously.
|
|
self._scrollRecurse = self._scrollRecurse + 1
|
|
self.update_idletasks()
|
|
self._scrollRecurse = self._scrollRecurse - 1
|
|
if self._scrollRecurse != 0:
|
|
return
|
|
|
|
xview = self._textbox.xview()
|
|
yview = self._textbox.yview()
|
|
|
|
# The text widget returns a yview of (0.0, 0.0) just after it
|
|
# has been created. Ignore this.
|
|
if yview == (0.0, 0.0):
|
|
return
|
|
|
|
if self['columnheader']:
|
|
if self._columnheaderLastX != xview[0]:
|
|
self._columnheaderLastX = xview[0]
|
|
self._columnheader.xview('moveto', xview[0])
|
|
if self['rowheader']:
|
|
if self._rowheaderLastY != yview[0]:
|
|
self._rowheaderLastY = yview[0]
|
|
self._rowheader.yview('moveto', yview[0])
|
|
|
|
self._horizScrollbar.set(xview[0], xview[1])
|
|
self._vertScrollbar.set(yview[0], yview[1])
|
|
|
|
self._horizScrollbarNeeded = (xview != (0.0, 1.0))
|
|
self._vertScrollbarNeeded = (yview != (0.0, 1.0))
|
|
|
|
# If both horizontal and vertical scrollmodes are dynamic and
|
|
# currently only one scrollbar is mapped and both should be
|
|
# toggled, then unmap the mapped scrollbar. This prevents a
|
|
# continuous mapping and unmapping of the scrollbars.
|
|
if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and
|
|
self._horizScrollbarNeeded != self._horizScrollbarOn and
|
|
self._vertScrollbarNeeded != self._vertScrollbarOn and
|
|
self._vertScrollbarOn != self._horizScrollbarOn):
|
|
if self._horizScrollbarOn:
|
|
self._toggleHorizScrollbar()
|
|
else:
|
|
self._toggleVertScrollbar()
|
|
return
|
|
|
|
if self['hscrollmode'] == 'dynamic':
|
|
|
|
# The following test is done to prevent continuous
|
|
# mapping and unmapping of the horizontal scrollbar.
|
|
# This may occur when some event (scrolling, resizing
|
|
# or text changes) modifies the displayed text such
|
|
# that the bottom line in the window is the longest
|
|
# line displayed. If this causes the horizontal
|
|
# scrollbar to be mapped, the scrollbar may "cover up"
|
|
# the bottom line, which would mean that the scrollbar
|
|
# is no longer required. If the scrollbar is then
|
|
# unmapped, the bottom line will then become visible
|
|
# again, which would cause the scrollbar to be mapped
|
|
# again, and so on...
|
|
#
|
|
# The idea is that, if the width of the text widget
|
|
# has not changed and the scrollbar is currently
|
|
# mapped, then do not unmap the scrollbar even if it
|
|
# is no longer required. This means that, during
|
|
# normal scrolling of the text, once the horizontal
|
|
# scrollbar has been mapped it will not be unmapped
|
|
# (until the width of the text widget changes).
|
|
|
|
currentWidth = self._textbox.winfo_width()
|
|
if self._horizScrollbarNeeded != self._horizScrollbarOn:
|
|
if self._horizScrollbarNeeded or \
|
|
self._textWidth != currentWidth:
|
|
self._toggleHorizScrollbar()
|
|
self._textWidth = currentWidth
|
|
|
|
if self['vscrollmode'] == 'dynamic':
|
|
if self._vertScrollbarNeeded != self._vertScrollbarOn:
|
|
self._toggleVertScrollbar()
|
|
|
|
def _columnheaderscrolled(self, first, last):
|
|
if self._textboxLastX != first:
|
|
self._textboxLastX = first
|
|
self._textbox.xview('moveto', first)
|
|
|
|
def _rowheaderscrolled(self, first, last):
|
|
if self._textboxLastY != first:
|
|
self._textboxLastY = first
|
|
self._textbox.yview('moveto', first)
|
|
|
|
def _toggleHorizScrollbar(self):
|
|
|
|
self._horizScrollbarOn = not self._horizScrollbarOn
|
|
|
|
interior = self.interior()
|
|
if self._horizScrollbarOn:
|
|
self._horizScrollbar.grid(row = 6, column = 4, sticky = 'news')
|
|
interior.grid_rowconfigure(5, minsize = self['scrollmargin'])
|
|
else:
|
|
self._horizScrollbar.grid_forget()
|
|
interior.grid_rowconfigure(5, minsize = 0)
|
|
|
|
def _toggleVertScrollbar(self):
|
|
|
|
self._vertScrollbarOn = not self._vertScrollbarOn
|
|
|
|
interior = self.interior()
|
|
if self._vertScrollbarOn:
|
|
self._vertScrollbar.grid(row = 4, column = 6, sticky = 'news')
|
|
interior.grid_columnconfigure(5, minsize = self['scrollmargin'])
|
|
else:
|
|
self._vertScrollbar.grid_forget()
|
|
interior.grid_columnconfigure(5, minsize = 0)
|
|
|
|
# Need to explicitly forward this to override the stupid
|
|
# (grid_)bbox method inherited from Tkinter.Frame.Grid.
|
|
def bbox(self, index):
|
|
return self._textbox.bbox(index)
|
|
|
|
Pmw.forwardmethods(ScrolledText, Tkinter.Text, '_textbox')
|