W Widgets
[ Styled Text Window ]

Styled Text Editor Widget


Writing Widgets
Bevel Button / Styled Text Editor
Exercises

The standard text widgets only allow one text style in the entire widget. This is fine for things like code editors, but WASTE has built-in support for styled text, so it would be nice to have a text widget which can use those features. We'll subclass the W.TextEditor class to provide these services.

As before, we have to import some modules to help us, and define a couple of constants which are used in the menu handlers.

import W
import Wapplication

import WASTEconst
import waste
import FrameWork

import Res
import Fm
import Menu

# Style and size menu. Note that style order is important (tied to bit values)
STYLES = [
    ("Bold", "B"), ("Italic", "I"), ("Underline", "U"), ("Outline", "O"),
    ("Shadow", ""), ("Condensed", ""), ("Extended", "")
    ]
SIZES = [ 9, 10, 12, 14, 18, 24]

Now comes the main definition. We use W.TextEditor as our base, but we now allow the passing of style and object information along with the text, via the styles and soup parameters. To cope with this change, we redefine the set() and get() methods, and make sure that open() and close() use the new information. For convenience, we define settext() and gettext() methods, so that we can get our hands on the text without having to worry about the style information. They key changes are highlighted in red.

class StyledEditor(W.TextEditor):
    
    def __init__(self, possize, text = "", styles = None, soup = None, callback=None, wrap=1,
                inset=(4, 4), fontsettings=None, tabsettings=(32, 0), readonly=0):
        W.TextEditor.__init__(self, possize, text = "", callback=None, wrap=1,
                inset=(4, 4), fontsettings=None, tabsettings=(32, 0), readonly=0)
        self.tempstyles = styles
        self.tempsoup = soup
    def open(self):
        if not hasattr(self._parent, "_barx"):
            self._parent._barx = None
        if not hasattr(self._parent, "_bary"):
            self._parent._bary = None
        self._calcbounds()
        self.SetPort()
        viewrect, destrect = self._calctextbounds()
        flags = self._getflags()
        self.ted = waste.WENew(destrect, viewrect, flags)
        self.ted.WEInstallTabHooks()
        self.setfontsettings(self.fontsettings)
        self.settabsettings(self.tabsettings)
        self.ted.WEInsert(self.temptext, self.tempstyles, self.tempsoup)
        self.ted.WECalText()
        self.ted.WEResetModCount()
        if self.selection:
            self.setselection(self.selection[0], self.selection[1])
            self.selection = None
        else:
            self.selview()
        self.temptext = None
        self.tempstyles = None
        self.tempsoup = None
        self.updatescrollbars()
        self.bind("pageup", self.scrollpageup)
        self.bind("pagedown", self.scrollpagedown)
        self.bind("top", self.scrolltop)
        self.bind("bottom", self.scrollbottom)
        self.selchanged = 0
    
    def close(self):
        self.tempstyles = None
        self.tempsoup = None
        W.TextEditor.close(self)

    def set(self, text, style=None, soup=None):
        if not self.ted:
            self.temptext = text
            self.tempstyles = style
            self.tempsoup = soup
        else:
            texthandle = Res.Resource(text)
            self.ted.WESetSelection(0, self.tedWEGetTextLength())
            self.ted.WEDelete()
            self.ted.WEInsert(text, style, soup)
            self.ted.WESetSelection(0,0)
            self.ted.WECalText()
            self.SetPort()
            viewrect, destrect = self._calctextbounds()
            self.ted.WESetViewRect(viewrect)
            self.ted.WESetDestRect(destrect)
            rgn = Qd.NewRgn()
            Qd.RectRgn(rgn, viewrect)
            Qd.EraseRect(viewrect)
            self.draw(rgn)
            self.updatescrollbars()
    
    def get(self):
        if not self.ted:
            return self.temptext, self.tempstyles, self.tempsoup
        else:
            texthandle = Res.Resource('')
            styles = Res.Resource('')
            soup = Res.Resource('')
            self.ted.WECopyRange(0, self.tedWEGetTextLength(), texthandle, styles, soup)
            return text.data, styles, soup
    
    def settext(self, text):
        return W.TextEditor.set(self, text)
    
    def gettext(self):
        return W.TextEditor.get(self)

We need a general routine which allows us to set the style of the selection in response to menu items or other commands. setstyle() does this job. Which is flags from WASTEconst, which tell WASTE which aspects of the text style of the selection we want to change. Fontsettings is a standard tuple which tells WASTE the font, face, size and color.

    def setstyle(self, which, fontsettings):
        self.ted.WESelView()
        self.ted.WESetStyle(which, fontsettings)

We also need to override the _getflags() method to let WASTE know we we want a widget with styled text.

    def _getflags(self):
        flags = WASTEconst.weDoAutoScroll | WASTEconst.weDoOutlineHilite
        if self.readonly:
            flags = flags | WASTEconst.weDoReadOnly
        else:
            flags = flags | WASTEconst.weDoUndo
        return flags

Finally we need to set hooks for Font, Style and Size menus, if they are available. The domenu_ methods will change the appropriate aspect of the selected text in the widget; the can_ methods handle putting checkmarks in the menu for those styles which span the whole selection. All that is needed to make these work is to create a Font menu (it should contain a list of all 'FOND' resources which don't start with '.' or '%' - or some appropriate subset); a Style menu whose items are listed in the STYLES variable (the order here is important); and a Size menu (the sizes listed in SIZES are a good selection, but any menu containing numbers is OK). All the menu items should have callbacks of the form 'setfont', 'setface' or 'setsize' as appropriate.

    def domenu_setfont(self, id, item, window, event):
        text = Menu.GetMenuHandle(id).GetMenuItemText(item)
        font = W.GetFNum(text)
        self.setstyle(WASTEconst.weDoFont, (font, 0, 0, (0, 0, 0)))
    
    def can_setfont(self, item):
        any, mode, (font, face, size, color) = self.ted.WEContinuousStyle(WASTEconst.weDoFont)
        if any and Fm.GetFontName(font) == item.menu.items[item.item-1][0]:
            item.check(1)
        else:
            item.check(0)
        return 1
    
    def domenu_setface(self, id, item, window, event):
        face = (1 << (item-1))
        self.setstyle(WASTEconst.weDoFace | WASTEconst.weDoToggleFace,
                (0, face, 0, (0, 0, 0)))
    
    def can_setface(self, item):
        any, mode, (font, face, size, color) = self.ted.WEContinuousStyle(WASTEconst.weDoFace)
        if any and item.menu.items[item.item-1][0] in getfaces(face):
            item.check(1)
        else:
            item.check(0)
        return 1
        
    def domenu_setsize(self, id, item, window, event):
        size = Menu.GetMenuHandle(id).GetMenuItemText(item)
        self.setstyle(WASTEconst.weDoSize,
                (0, 0, int(size), (0, 0, 0)))
    
    def can_setsize(self, item):
        any, mode, (font, face, size, color) = self.ted.WEContinuousStyle(WASTEconst.weDoSize)
        if any and item.menu.items[item.item-1][0] == str(size):
            item.check(1)
        else:
            item.check(0)
        return 1

Finally, we define some utility routines which help is set up the various menus, if the don't already exist. Unfortunately, we have to do this by hand, since otherwise we are likely to get multiple copies of the menus. These would likely be created by any application which used this widget.

def MakeFontMenu(menubar, where = 0):
        app = W.getapplication()
        fontmenu = Wapplication.Menu(menubar, "Font", where)
        for font in getfontnames():
                item = FrameWork.MenuItem(fontmenu, font, "", "setfont")
                app._menustocheck.append(item)
        return fontmenu

def MakeFaceMenu(menubar, where = 0):
        app = W.getapplication()
        facemenu = Wapplication.Menu(menubar, "Style", where)
        for face, key in STYLES:
                item = FrameWork.MenuItem(facemenu, face, key, "setface")
                app._menustocheck.append(item)
        return facemenu

def MakeSizeMenu(menubar, where=0):
        app = W.getapplication()
        sizemenu = Wapplication.Menu(menubar, "Size", where)
        for size in SIZES:
                item = FrameWork.MenuItem(sizemenu, str(size), "", "setsize")
                app._menustocheck.append(item)
        return sizemenu

def MakeNewMenus():
        mbar = W.getapplication().menubar
        MakeFontMenu(mbar)
        MakeFaceMenu(mbar)
        MakeSizeMenu(mbar)

def getfontnames():
        fontnames = []
        for i in range(1, Res.CountResources('FOND') + 1):
                r = Res.GetIndResource('FOND', i)
                name = r.GetResInfo()[2]
                if name[0] not in [".", "%"]:
                        fontnames.append(name)
        fontnames.sort()
        return fontnames
        
def getfaces(face):
        faces = []
        for i in range(len(STYLES)):
                if face & (1 << i):
                        faces.append(STYLES[i][0])
        return faces

You can view the code put together into a complete module. Run it as main in the IDE to see it in action. To get the most out of it, create Font, Style and Size menus for the IDE by modularizing it, and typing:

>>> import stylededitor
>>> stylededitor.MakeNewMenus()
in the console window.

Ideally we'd add in more hooks here for other useful font-based menu items, like "Larger", "Smaller" and "Other" menu items for the Size menu, and maybe a "Plain" option for the Style menu. We could also add hooks for a Color menu and justifying the text. With a bit more work, it might be possible to jack this widget up to create a simple editor roughly equivalent to SimpleText in power, or perhaps a simple HTML viewer.

Exercises

  1. Add domenu_ and commands for "Larger" and "Smaller" menu items in the Size menu.
  2. Adjust domenu_setface and can_setface to allow a "Plain" menu item in the Style menu.
  3. Add support for an "Other" menu item in the Size menu. This should bring up a dialog box which allows the user to type in the size they want.
  4. As defined, the Style menu is not very portable: the items listed in STYLES variable must appear first and in that order (if you did exercise 2, you may have noticed this). Redesign it to be more robust.