#-----------------------------------------------------------------------------
# Name:        HelpBook.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     2003
# RCS-ID:      $Id: HelpBook.plug-in.py,v 1.10 2005/05/18 12:11:50 riaan Exp $
# Copyright:   (c) 2003 - 2005
# Licence:     GPL
#-----------------------------------------------------------------------------
#Boa:Dialog:HelpBookItemDlg

import wx

def createIndexDlg(parent, keyword, location, anchors):
    return HelpBookItemDlg(parent, 'Help Book - Index', keyword, 'Keyword(s)', '',
                     location, 'Location', anchors)

def createContentsDlg(parent, title, htmlTitle, location, anchors):
    return HelpBookItemDlg(parent, 'Help Book - Contents', title, 'Title', htmlTitle,
                     location, 'Location', anchors)

[wxID_HELPBOOKITEMDLG, wxID_HELPBOOKITEMDLGBTNREADTITLE, 
 wxID_HELPBOOKITEMDLGBUTTON2, wxID_HELPBOOKITEMDLGBUTTON3, 
 wxID_HELPBOOKITEMDLGCBBANCHORS, wxID_HELPBOOKITEMDLGSTATICTEXT1, 
 wxID_HELPBOOKITEMDLGSTATICTEXT2, wxID_HELPBOOKITEMDLGTXTPAGE, 
 wxID_HELPBOOKITEMDLGTXTTITLE, 
] = [wx.NewId() for _init_ctrls in range(9)]

class HelpBookItemDlg(wx.Dialog):
    def _init_ctrls(self, prnt):
        # generated method, don't edit
        wx.Dialog.__init__(self, id=wxID_HELPBOOKITEMDLG,
              name='HelpBookItemDlg', parent=prnt, pos=wx.Point(452, 359),
              size=wx.Size(446, 136), style=wx.DEFAULT_DIALOG_STYLE,
              title=self.caption)
        self.SetClientSize(wx.Size(438, 109))

        self.staticText1 = wx.StaticText(id=wxID_HELPBOOKITEMDLGSTATICTEXT1,
              label=self.nameLabel, name='staticText1', parent=self,
              pos=wx.Point(8, 16), size=wx.Size(56, 13), style=0)

        self.txtTitle = wx.TextCtrl(id=wxID_HELPBOOKITEMDLGTXTTITLE,
              name='txtTitle', parent=self, pos=wx.Point(72, 8),
              size=wx.Size(232, 21), style=0, value=self.name)

        self.staticText2 = wx.StaticText(id=wxID_HELPBOOKITEMDLGSTATICTEXT2,
              label=self.valueLabel, name='staticText2', parent=self,
              pos=wx.Point(8, 48), size=wx.Size(64, 13), style=0)

        self.txtPage = wx.TextCtrl(id=wxID_HELPBOOKITEMDLGTXTPAGE,
              name='txtPage', parent=self, pos=wx.Point(72, 40),
              size=wx.Size(232, 21), style=0, value=self.value)

        self.cbbAnchors = wx.ComboBox(choices=self.valueAnchors,
              id=wxID_HELPBOOKITEMDLGCBBANCHORS, name='cbbAnchors', parent=self,
              pos=wx.Point(304, 40), size=wx.Size(125, 21), style=0,
              value=self.valueAnchorsValue)

        self.btnReadTitle = wx.Button(id=wxID_HELPBOOKITEMDLGBTNREADTITLE,
              label='Title from HTML', name='btnReadTitle', parent=self,
              pos=wx.Point(304, 8), size=wx.Size(123, 23), style=0)
        self.btnReadTitle.Bind(wx.EVT_BUTTON, self.OnBtnreadtitleButton,
              id=wxID_HELPBOOKITEMDLGBTNREADTITLE)

        self.button2 = wx.Button(id=wx.ID_OK, label='OK', name='button2',
              parent=self, pos=wx.Point(272, 72), size=wx.Size(75, 23),
              style=0)

        self.button3 = wx.Button(id=wx.ID_CANCEL, label='Cancel',
              name='button3', parent=self, pos=wx.Point(353, 72),
              size=wx.Size(75, 23), style=0)

    def __init__(self, parent, caption, name, nameLabel, nameDefault,
                 value, valueLabel, valueAnchors):

        self.caption = 'Help Book'
        self.caption = caption
        self.name = ''
        self.name = name
        self.nameLabel = 'Name'
        self.nameLabel = nameLabel
        self.nameDefault = ''
        self.nameDefault = nameDefault
        self.value = ''
        self.value = self.getFilenameValue(value)
        self.valueLabel = 'Value'
        self.valueLabel = valueLabel
        self.valueAnchors = []
        self.valueAnchors = valueAnchors
        self.valueAnchorsValue = ''
        self.valueAnchorsValue = self.getAnchorValue(value)

        self._init_ctrls(parent)

        self.titleFromHtml = ''

        if not self.nameDefault:
            self.btnReadTitle.Disable()

        self.button2.SetDefault()

    def GetResult(self):
        name = self.txtTitle.GetValue()

        anch = self.cbbAnchors.GetValue().strip()
        value = self.txtPage.GetValue()
        if anch:
            value = value + '#' + anch

        return name, value


    def OnBtnreadtitleButton(self, event):
        self.txtTitle.SetValue(self.titleFromHtml)


#-------------------------------------------------------------------------------

    def getAnchorValue(self, value):
        if value.find('#') != -1:
            return value.split('#', 1)[1]
        else:
            return ''

    def getFilenameValue(self, value):
        if value.find('#') != -1:
            return value.split('#')[0]
        else:
            return value

import os, sys, htmllib, formatter

if __name__ == '__main__':
    sys.path.append('..')


class HelpConfigParser:
    def __init__(self, lines, defaultName='helpbook'):
        self.options = {'Compatibility': '1.1',
                        'Full-text search': 'Yes',
                        'Default Window': 'wxHelp',
                        'Default topic': 'index.html',
                        'Title': defaultName,
        }
        self.updateFilenameOptions(defaultName)
        self.windows = []
        self.files = []

        self._section = ''

        for line in [l.strip() for l in lines if l.strip()]:
            if line in ('[OPTIONS]', '[WINDOWS]', '[FILES]'):
                self._section = line
            else:
                if self._section == '[OPTIONS]':
                    k, v = line.split('=')
                    self.options[k] = v
                elif self._section == '[WINDOWS]':
                    k, v = line.split('=')
                    self.windows = v.split(',')
                elif self._section == '[FILES]':
                    self.files.append(line)

    def generateConfigData(self):
        res = ['[OPTIONS]']
        res.extend(['%s=%s'%(key, val) for key, val in self.options.items()])
        res.append('')
        res.append('[WINDOWS]')
        # I have no idea what the magic numbers mean
        res.append('wxHelp=,"%(Contents file)s","%(Index file)s",'
              '"%(Default topic)s",,,,,,0x2420,,0x380e,,,,,0,,,'%self.options)
        res.append('')
        res.append('[FILES]')
        res.extend(self.files)

        return '\n'.join(res)


    def updateFilenameOptions(self, name):
        self.options.update({'Contents file': name+'.hhc',
                             'Compiled file': name+'.chm',
                             'Index file': name+'.hhk'})

class HelpBookParser(htmllib.HTMLParser):
    def __init__(self, formatter, verbose=0):
        htmllib.HTMLParser.__init__(self, formatter, verbose)

        self.indexes = None
        self.index = None

        self.workstack = []
        self.result = []

    def start_ul(self, attrs):
        indexes = []

        if self.indexes is not None:
            self.workstack.append(self.indexes)

        if self.indexes:
            self.indexes[-1] = self.indexes[-1][:2]+(indexes,)

        self.indexes = indexes
        self.index = None

    def end_ul(self):
        if not self.workstack:
            self.result = self.indexes
        else:
            self.indexes = self.workstack.pop()

    def do_param(self, attrs):
        if self.indexes is not None and self.index:
            name, value = attrs[0][1], attrs[1][1]
            self.index[name == 'Local'] = value

    def start_object(self, attrs):
        if self.indexes is not None and self.index is None:
            self.index = [None, None, None]

    def end_object(self):
        if self.indexes is not None:
            self.indexes.append(tuple(self.index))
            self.index = None

def writeHelpBook(items):
    res = ['<UL>']
    for name, value, children in items:
        res.append('<LI><OBJECT type="text/sitemap">')
        res.append('<PARAM name="Local" value="%s">'%value)
        res.append('<PARAM name="Name" value="%s">'%name)
        res.append('</OBJECT>')

        if children:
            res.append(writeHelpBook(children))

    res.append('</UL>')

    return '\n'.join(res)

def parseHelpFile(data, Parser=HelpBookParser):
    w = formatter.NullWriter()
    f = formatter.NullFormatter(w)
    p = Parser(f)
    p.feed(data)
    return p


class BreakOnTitle(Exception): pass

class HtmlDocDetailParser(htmllib.HTMLParser):
    def __init__(self, formatter, verbose=0, breakOnTitle=False):
        htmllib.HTMLParser.__init__(self, formatter, verbose)
        self.anchors = []
        self.title = ''
        self.breakOnTitle = breakOnTitle
        self.result = [self.title, self.anchors]

    _in_title = False
    def start_title(self, attrs):
        self._in_title = True
    def end_title(self):
        self._in_title = False

    def start_a(self, attrs):
        if attrs and attrs[0] and attrs[0][0].lower() == 'name':
            self.anchors.append(attrs[0][1])
    def end_a(self):
        pass

    def handle_data(self, data):
        if self._in_title:
            self.title = self.result[0] = data.strip()

            if self.breakOnTitle:
                raise BreakOnTitle, self.title


    def do_param(self, attrs):
        pass#print 'do_param', attrs


def _testHPP():
    from pprint import pprint
    #pprint(parseHelpFile(open('../Docs/python/python.hhc').read()))
    #i = parseHelpFile(open('../Docs/boa/apphelp/apphelp.hhc').read())
    #print i
    ##writeHelpBook(i)
    print parseHelpFile(open('../Docs/boa/apphelp/debugger.html').read(), Parser=HtmlDocDetailParser).result

def _testHCP():
    h = HelpConfigParser(open('../Docs/wxpython/wx/wx.hhp').readlines())
    print h.options, h.windows, h.files


if __name__ == '__main__':
    _testHPP()
    #_testHCP()

#-------------------------------------------------------------------------------

import Preferences, Utils, Plugins
import PaletteStore

from Models import Controllers, EditorHelper, EditorModels
from Views import EditorViews, SourceViews, StyledTextCtrls
from Explorers import Explorer, ExplorerNodes

import ProcessProgressDlg

import glob, zipfile
from cStringIO import StringIO


class HelpBookModel(EditorModels.SourceModel):
    modelIdentifier = 'HelpBook'
    defaultName = 'helpbook'
    bitmap = 'HelpBook.png'
    ext = '.hhp'
    imgIdx = EditorHelper.imgHelpBook

    def __init__(self, data, name, editor, saved):
        EditorModels.SourceModel.__init__(self, data, name, editor, saved)

        contents = os.path.splitext(name)[0]+'.hhc'
        try:
            transport = Explorer.openEx(contents)
            data = transport.load()
        except ExplorerNodes.TransportError:
            transport = None
            data = ''
        self.contentsModel = EditorModels.SourceModel(data, contents, editor, saved)
        self.contentsModel.transport = transport

        indexes = os.path.splitext(name)[0]+'.hhk'
        try:
            transport = Explorer.openEx(indexes)
            data = transport.load()
        except ExplorerNodes.TransportError:
            transport = None
            data = ''
        self.indexesModel = EditorModels.SourceModel(data, indexes, editor, saved)
        self.indexesModel.transport = transport

        self.update()

    def update(self):
        self.config = HelpConfigParser(StringIO(self.data).readlines())
        self.contentsModel.update()
        self.contents = parseHelpFile(self.contentsModel.data).result
        self.indexesModel.update()
        self.indexes = parseHelpFile(self.indexesModel.data).result

    def load(self, notify=True):
        try: self.contentsModel.load(False)
        except ExplorerNodes.TransportError: pass

        try: self.indexesModel.load(False)
        except ExplorerNodes.TransportError: pass

        EditorModels.SourceModel.load(self, notify)

    def save(self, overwriteNewer=False):
        if self.modified:
            gen = '<META name="GENERATOR" content="Boa Constructor - HelpBook">\n'
            if self.contentsModel.modified:
                self.contentsModel.data = gen + writeHelpBook(self.contents)
                self.contentsModel.save(overwriteNewer)
            if self.indexesModel.modified:
                self.indexesModel.data = gen + writeHelpBook(self.indexes)
                self.indexesModel.save(overwriteNewer)

            self.data = self.config.generateConfigData()
            EditorModels.SourceModel.save(self, overwriteNewer)

    def saveAs(self, filename):
        newDir, newName = os.path.split(filename)
        oldDir, oldName = os.path.split(self.filename)
        if self.savedAs and newDir != oldDir:
            raise ExplorerNodes.TransportSaveError, \
                  'Once saved, help books files cannot be moved, only renamed.\n'\
                  'Please move the entrire directory.'

        if newName != oldName:
            self.config.updateFilenameOptions(os.path.splitext(newName)[0])
            self.modified = True

        noExtFilename = os.path.splitext(filename)[0]
        self.contentsModel.saveAs(noExtFilename+'.hhc')
        self.indexesModel.saveAs(noExtFilename+'.hhk')

        EditorModels.SourceModel.saveAs(self, filename)

    def setModified(self, section='config'):
        if section == 'contents':
            self.contentsModel.modified = True
        elif section == 'indexes':
            self.indexesModel.modified = True

        self.modified = True

    def makeHTB(self):
        pass

# XXX Experimental, should just load and save from zip
class HTBHelpBookModel(HelpBookModel):
    modelIdentifier = 'BTBHelpBook'
    defaultName = 'htbhelpbook'
    ext = '.htb'

    def __init__(self, data, name, editor, saved):
        HelpBookModel.__init__(self, data, name, editor, saved)

    def load(self, notify=True):
        pass

    def save(self, overwriteNewer=False):
        pass

##        if self.GetItemCount():
##            itemFrom = self.GetTopItem()
##            itemTo   = self.GetTopItem()+1 + self.GetCountPerPage()
##            itemTo   = min(itemTo, self.GetItemCount()-1)
##            self.RefreshItems(itemFrom, itemTo)


class HelpBookFilesView(EditorViews.VirtualListCtrlView):
    viewName = 'Files'

    addBmp = 'Images/Shared/NewItem.png'
    delBmp = 'Images/Shared/DeleteItem.png'
    def __init__(self, parent, model, provideActions=True):
        if provideActions:
            actions = (('Add file', self.OnAddFile, self.addBmp, ''),
                       ('Add files', self.OnAddFiles, '-', ''),
                       ('Remove file', self.OnRemoveFile, self.delBmp, ''),
                       ('Open file', self.OnOpenFile, '-', ''),
                       ('-', None, '-', ''),
                       ('Normalise paths', self.OnNormalisePaths, '-', ''),)
        else:
            actions = ()
        EditorViews.VirtualListCtrlView.__init__(self, parent, model,
              wx.LC_REPORT, actions, -1)

        self.Bind(wx.EVT_LEFT_DOWN, self.OnFilesLeftDown)

        #self.sortOnColumns = [0, 1, 2]

        self.InsertColumn(0, 'No')
        self.InsertColumn(1, 'Name')
        self.InsertColumn(2, 'Path')
        self.SetColumnWidth(0, 30)
        self.SetColumnWidth(1, 150)
        self.SetColumnWidth(2, 300)

        if Preferences.hbShowDocumentTitles:
            self.InsertColumn(3, 'Title')
            self.SetColumnWidth(3, 300)

        self.active = True

        self.resetCache()

        #self.Bind(wx.EVT_LIST_CACHE_HINT, self.OnCacheHint, id=self.GetId())

    def refreshCtrl(self):
        self.resetCache()
        self.SetItemCount(len(self.model.config.files))

    def OnGetItemText(self, item, col):
        f = self.model.config.files[item]
        if col == 0:
            return `item`
        elif col == 1:
            return os.path.basename(f)
        elif col == 2:
            return f
        elif Preferences.hbShowDocumentTitles and col == 3:
            if not self.cached[item]:
                title = ''
                try:
                    if os.path.splitext(f)[1].lower() not in ('.htm', '.html'):
                        return ''
                    docsDir = os.path.dirname(self.model.filename)
                    try: data = Explorer.openEx(os.path.join(docsDir, f)).load()
                    except ExplorerNodes.TransportError: return ''
                    fmtr = formatter.NullFormatter(formatter.NullWriter())
                    try: HtmlDocDetailParser(fmtr, breakOnTitle=True).feed(data)
                    except BreakOnTitle, title: return str(title)
                    except: return ''
                    else: return ''
                finally:
                    self.cached[item] = title
            else:
                return self.cached[item]

    def doStartDrag(self):
        sel = self.GetSelections()
        if sel:
            filenames = [self.model.config.files[idx] for idx in sel]
            filelist = wx.CustomDataObject(wx.CustomDataFormat('FileList'))
            filelist.SetData(`filenames`)
            #tdo =wx.TextDataObject(filename)
            ds = wx.DropSource(self)
            ds.SetData(filelist)
            ds.DoDragDrop(wx.Drag_DefaultMove)

    def OnFilesLeftDown(self, event):
        event.Skip()
        wx.CallAfter(self.doStartDrag)

    helpBookDirMsg = 'Help documents must be in the same directory as the help '\
                     'book, or a sub-directory. Not allowed outside the tree.'
    def OnAddFile(self, event):
        if not self.model.savedAs:
            wx.LogError('Please save the help book before adding files\n'+self.helpBookDirMsg)
            return

        filename = self.model.editor.openFileDlg()
        if filename:
            helpProjDir = os.path.dirname(self.model.filename)
            if not filename.startswith(helpProjDir):
                wx.LogError(self.helpBookDirMsg)
                return

            self.model.config.files.append(filename[len(helpProjDir)+1:])

            self.refreshCtrl()
            self.updateOtherViews()

            self.model.setModified()
            self.updateEditor()

    def OnRemoveFile(self, event):
        sel = self.GetSelections()
        if sel:
            sel.sort()
            sel.reverse()
            for idx in sel:
                del self.model.config.files[idx]

            self.refreshCtrl()
            self.updateOtherViews()

            self.model.setModified()
            self.updateEditor()

    def updateOtherViews(self):
        views = self.model.views
        if views.has_key('Contents'):
            views['Contents'].files.refreshCtrl()
        if views.has_key('Index'):
            views['Index'].files.refreshCtrl()

    def OnOpenFile(self, event):
        idx = self.selected
        if idx != -1:
            helpBookDir = os.path.dirname(self.model.filename)
            self.model.editor.openOrGotoModule(
                  helpBookDir+'/'+self.model.config.files[idx])

    #def OnCacheHint(self, event):
    #    print 'From', event.GetCacheFrom()
    #    print 'To', event.GetCacheTo()

    def resetCache(self, ):
        self.cached = [False]*len(self.model.config.files)

    def OnNormalisePaths(self, event):
        helpBookDir = os.path.dirname(self.model.localFilename())
        delidxs = []
        for idx, filename in zip(range(len(self.model.config.files)),
                                 self.model.config.files):
            absfn = os.path.normpath(os.path.join(helpBookDir, filename))
            if absfn[:len(helpBookDir)] != helpBookDir:
                if wx.MessageBox('Filename "%s" not inside help book directory.'
                      '\n\nRemove?'%absfn, 'Invalid file',
                      wx.ICON_QUESTION | wx.YES_NO) == wx.YES:
                    delidxs.append(idx)
            else:
                self.model.config.files[idx] = absfn[len(helpBookDir)+1:]

        if delidxs:
            delidxs.reverse()
            for idx in delidxs:
                del self.model.config.files[idx]

        self.refreshCtrl()
        self.updateOtherViews()

        self.model.setModified()
        self.updateEditor()

    def OnAddFiles(self, event):
        helpBookDir = os.path.dirname(self.model.localFilename())
        dlg = wx.DirDialog(self)
        try:
            dlg.SetPath(helpBookDir)
            if dlg.ShowModal() != wx.ID_OK:
                return
            dir = dlg.GetPath()
        finally:
            dlg.Destroy()

        if not dir.startswith(helpBookDir):
            wx.LogError(self.helpBookDirMsg)
            return

        dlg = wx.TextEntryDialog(self, 'Enter wildcard', 'Add Files', '*.*')
        try:
            if dlg.ShowModal() != wx.ID_OK:
                return
            globStr = dlg.GetValue()
        finally:
            dlg.Destroy()

        files = glob.glob(os.path.join(dir, globStr))

        for filename in files:
            self.model.config.files.append(filename[len(helpBookDir)+1:])

        self.refreshCtrl()
        self.updateOtherViews()

        self.model.setModified()
        self.updateEditor()

class FileListDropTarget(wx.PyDropTarget):
    def __init__(self):
        wx.PyDropTarget.__init__(self)
        self.fmt = wx.CustomDataFormat('FileList')
        self.data = wx.CustomDataObject(self.fmt)
        self.SetDataObject(self.data)

    def OnDrop(self, x, y):
        return True

    def OnData(self, x, y, d):
        if self.GetData():
            filelist = eval(self.data.GetData())
            self.OnDropFiles(x, y, filelist)
        return d

class HelpBookIndexDropTarget(FileListDropTarget):
    def __init__(self, indexList):
        FileListDropTarget.__init__(self)
        self.list = indexList

    def OnDropFiles(self, x, y, files):
        docsDir = os.path.dirname(self.list.model.filename)

        for filename in files:
            data = Explorer.openEx(os.path.join(docsDir, filename)).load()
            prs = parseHelpFile(data, HtmlDocDetailParser)
            dlg = createIndexDlg(None, '', filename, prs.anchors)
            try:
                if dlg.ShowModal() != wx.ID_OK:
                    return

                keyword, location = dlg.GetResult()
            finally:
                dlg.Destroy()

            #item, flags = self.list.HitTest( (x, y) )
            self.list.model.indexes.append((keyword, location, None))

        self.list.model.setModified('indexes')
        self.list.updateEditor()

        self.list.refreshCtrl()


class HelpBookIndexView(wx.SplitterWindow, EditorViews.EditorView):
    viewName = 'Index'

    addBmp = 'Images/Shared/NewItem.png'
    delBmp = 'Images/Shared/DeleteItem.png'
    def __init__(self, parent, model):
        wx.SplitterWindow.__init__(self, parent, -1,
              style=wx.CLIP_CHILDREN | wx.NO_3D | wx.SP_3DSASH)

        self.indexes = HelpBookIndexListView(self, model, self)
        self.files = HelpBookFilesView(self, model, False)

        self.SplitVertically(self.indexes, self.files, 350)

        EditorViews.EditorView.__init__(self, model, (
            ('Add index', self.indexes.OnAddIndex, self.addBmp, ''),
            ('Edit index', self.indexes.OnEditIndex, '-', ''),
            ('Delete index', self.indexes.OnDeleteIndex, self.delBmp, ''),
        ), 0)

        self.active = True

    def refreshCtrl(self):
        self.indexes.refreshCtrl()
        self.files.refreshCtrl()


class HelpBookIndexListView(EditorViews.VirtualListCtrlView):
    def __init__(self, parent, model, parentView=None):
        EditorViews.VirtualListCtrlView.__init__(self, parent, model, wx.LC_REPORT,
          (), -1)
        self.parentView = parentView

        self.SetDropTarget(HelpBookIndexDropTarget(self))

        #self.sortOnColumns = [0, 1]

        self.InsertColumn(0, 'Keyword')
        self.InsertColumn(1, 'Location')
        self.SetColumnWidth(0, 100)
        self.SetColumnWidth(1, 400)

        self.active = True

    def refreshCtrl(self):
        self.SetItemCount(len(self.model.indexes))

    def OnGetItemText(self, item, col):
        return self.model.indexes[item][col]

    def OnRightClick(self, event):
        if self.parentView:
            menu = self.parentView.generateMenu()
            event.GetEventObject().PopupMenuXY(menu, event.GetX(), event.GetY())
            menu.Destroy()

    def OnAddIndex(self, event):
        dlg = createIndexDlg(None, '', '', [])
        try:
            if dlg.ShowModal() != wx.ID_OK:
                return

            keyword, location = dlg.GetResult()
        finally:
            dlg.Destroy()

        self.model.indexes.append((keyword, location, None))

        self.model.setModified('indexes')
        self.updateEditor()

        self.refreshCtrl()

    def OnEditIndex(self, event):
        if self.selected != -1:
            keyword, location, c = self.model.indexes[self.selected]

            docsDir = os.path.dirname(self.model.filename)
            try:
                data = Explorer.openEx(
                      os.path.join(docsDir, location.split('#')[0])).load()
            except ExplorerNodes.TransportLoadError:
                data = ''
            prs = parseHelpFile(data, HtmlDocDetailParser)
            dlg = createIndexDlg(None, keyword, location, prs.anchors)
            try:
                if dlg.ShowModal() != wx.ID_OK:
                    return

                res = dlg.GetResult()
            finally:
                dlg.Destroy()

            if res != (keyword, location):
                keyword, location = res
                self.model.indexes[self.selected] = (keyword, location, None)

                self.model.setModified('indexes')
                self.updateEditor()

                self.refreshCtrl()

    def OnDeleteIndex(self, event):
        if self.selected != -1:
            del self.model.indexes[self.selected]

            self.model.setModified('indexes')
            self.updateEditor()

            self.refreshCtrl()

class HelpBookContentsDropTarget(FileListDropTarget):
    def __init__(self, contentsTree):
        FileListDropTarget.__init__(self)
        self.tree = contentsTree

    def OnDropFiles(self, x, y, files):
        item, flags = self.tree.HitTest( (x, y) )
        if not item.Ok(): return

        name = self.tree.GetItemText(item)
        value, items, children = self.tree.GetPyData(item)

        docsDir = os.path.dirname(self.tree.model.filename)

        for filename in files:
            title, page = doContentsDlg(None, filename, docsDir)

            if (title, page) == (None, None):
                continue

            entry = (title, page, None)

            if children is None:
                idx = items.index( (name, value, children) )
                items[idx] = (name, value, [entry])
                children = [entry]
            else:
                children.append(entry)

        self.tree.model.setModified('contents')
        self.tree.updateEditor()

        self.tree.SetItemHasChildren(item, True)
        self.tree.SetItemData(item, wx.TreeItemData((value, items, children)))
        #if self.tree.GetItemImage(item) != 0:
        #    self.tree.SetItemImage(item, 1, 1) # XXX Doesn't work
        self.tree.SelectItem(item)
        self.tree.Collapse(item)
        self.tree.Expand(item)

        #self.tree.refreshCtrl()


def doContentsDlg(title, text, docsDir):
    #text, anchor = (text.split('#')+[''])[:2]
    data = Explorer.openEx(os.path.join(docsDir, text.split('#')[0])).load()
    prs = parseHelpFile(data, HtmlDocDetailParser)
    if title is None:
        title = prs.title
    dlg = createContentsDlg(None, title, prs.title, text, prs.anchors)
    try:
        if dlg.ShowModal() != wx.ID_OK:
            return None, None

        title, location = dlg.GetResult()
    finally:
        dlg.Destroy()
    return title, location

class HelpBookContentsView(wx.SplitterWindow, EditorViews.EditorView):
    viewName = 'Contents'

    addBmp = 'Images/Shared/NewItem.png'
    delBmp = 'Images/Shared/DeleteItem.png'
    def __init__(self, parent, model):
        wx.SplitterWindow.__init__(self, parent, -1,
              style=wx.CLIP_CHILDREN | wx.NO_3D | wx.SP_3DSASH)

        self.contents = HelpBookContentsTreeView(self, model, self)
        self.files = HelpBookFilesView(self, model, False)

        self.SplitVertically(self.contents, self.files, 350)

        actions = (('Edit entry', self.contents.OnEditEntry, '-', ''),
                   ('Delete entry', self.contents.OnDeleteEntry, self.delBmp, ''),
                   ('Import...', self.OnImportContents, '-', ''), )
        EditorViews.EditorView.__init__(self, model, actions, -1)

        self.active = True

    def refreshCtrl(self):
        self.contents.refreshCtrl()
        self.files.refreshCtrl()


    def OnImportContents(self, event):
        pass

class HelpBookContentsTreeView(wx.TreeCtrl, EditorViews.EditorView):
    viewName = 'Contents'

    def __init__(self, parent, model, parentView=None):
        wx.TreeCtrl.__init__(self, parent, -1,
         style=wx.TR_HAS_BUTTONS | wx.SUNKEN_BORDER | wx.TR_DEFAULT_STYLE)
        EditorViews.EditorView.__init__(self, model, (), -1)
        self.parentView = parentView
        #(('Edit entry', self.OnEditEntry, '-', ''),)

        self.SetDropTarget(HelpBookContentsDropTarget(self))

        self.helpImgLst = wx.ImageList(16, 16)
        for artId in (wx.ART_HELP_BOOK, wx.ART_HELP_FOLDER, wx.ART_HELP_PAGE):
            bmp = wx.ArtProvider_GetBitmap(artId, wx.ART_TOOLBAR, (16, 16))
            self.helpImgLst.Add(bmp)
        self.AssignImageList(self.helpImgLst)

        self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnTreeExpand, id=self.GetId())
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeSelChanged, id=self.GetId())

        self.active = True

    def refreshCtrl(self):
        self.DeleteAllItems()
        title = self.model.config.options['Title']
        rootItem = self.AddRoot(title, 0,
              data=wx.TreeItemData((title, None, self.model.contents)))

        self.SetItemHasChildren(rootItem, True)
        self.Expand(rootItem)


    def recurseAddItems(self, items, treeItem):
        # XXX no longer used, build entire tree recursively
        for name, value, children in items:
            item = self.AppendItem(treeItem, name, children and 1 or 2, -1,
                                   wx.TreeItemData((value, items, children)))
            if children:
                self.recurseAddItems(children, item)
                self.SetItemHasChildren(item, True)

    def addTreeItems(self, items, treeItem):
        self.DeleteChildren(treeItem)
        for name, value, children in items:
            item = self.AppendItem(treeItem, name, children and 1 or 2, -1,
                                   wx.TreeItemData((value, items, children)))
            if children:
                self.SetItemHasChildren(item, True)

    def OnRightClick(self, event):
        if self.parentView:
            menu = self.parentView.generateMenu()
            event.GetEventObject().PopupMenuXY(menu, event.GetX(), event.GetY())
            menu.Destroy()

    def OnTreeExpand(self, event):
        item = event.GetItem()
        self.addTreeItems(self.GetPyData(item)[-1], item)

    def OnTreeSelChanged(self, event):
        item = event.GetItem()
        if item.Ok():
            location = self.GetPyData(item)[0]
            self.model.editor.setStatus(location)

    def OnEditEntry(self, event):
        tree = self
        item = tree.GetSelection()
        name = tree.GetItemText(item)
        value, items, children = tree.GetPyData(item)

        if item == tree.GetRootItem():
            options = self.model.config.options
            dlg =  HelpBookItemDlg(self.model.editor, 'Help Book - Contents properties',
                   options['Title'], 'Title', '',
                   options['Default topic'], 'Default topic', [])
            try:
                if dlg.ShowModal() != wx.ID_OK:
                    return

                title, defTopic = dlg.GetResult()
            finally:
                dlg.Destroy()

            if options['Title'] != title or options['Default topic'] != defTopic:
                self.model.setModified('config')
                self.updateEditor()

                options['Title'] = title
                options['Default topic'] = defTopic

                tree.SetItemData(item, wx.TreeItemData((title, defTopic, children)))
                tree.SetItemText(item, title)

            # XXX mark hhp modified
            return


        docsDir = os.path.dirname(self.model.filename)
        title, page = doContentsDlg(name, value, docsDir)

        if (title, page) == (None, None):
            return


        if (name, value, children) != (title, page, children):
            idx = items.index( (name, value, children) )
            items[idx] = (title, page, children)

            self.model.setModified('contents')
            self.updateEditor()

            tree.SetItemData(item, wx.TreeItemData((page, items, children)))
            tree.SetItemText(item, title)

    def OnDeleteEntry(self, event):
        item = self.GetSelection()

        if item == self.GetRootItem():
            wx.LogError('Cannot delete root node.')
            return

        name = self.GetItemText(item)
        value, items, children = self.GetPyData(item)

        idx = items.index( (name, value, children) )

        if wx.MessageBox('Delete %s'%name, 'Delete node and children',
              wx.ICON_WARNING | wx.YES_NO) == wx.YES:
            del items[idx]
            self.model.modified = True
            self.updateEditor()

            self.Collapse(item)
            self.Delete(item)


def visitDir((files, excludes), dirname, names):
    for name in names:
        if name not in excludes:
            filename = os.path.join(dirname, name)
            if os.path.isfile(filename):
                files.append(filename)

wxID_HP_X = wx.NewId()
class HelpBookController(Controllers.SourceController):
    Model = HelpBookModel
    DefaultViews = [HelpBookFilesView, HelpBookContentsView, HelpBookIndexView]

    def actions(self, model):
        actions = [
              ('-', None, '', ''),
              ('Make HTB', self.OnMakeHTB, '', ''),
              ]
        if wx.Platform == '__WXMSW__':
            actions.append( ('Make CHM', self.OnMakeCHM, '', '') )

        return Controllers.SourceController.actions(self, model) + actions

    def OnMakeHTB(self, event):
        model = self.getModel()
        dlg = wx.SingleChoiceDialog(model.editor, 'Choose source files',
              'Make HTB', ['Files list', 'Entire help book directory'])
        try:
            if dlg.ShowModal() != wx.ID_OK:
                return
            selected = dlg.GetSelection()
        finally:
            dlg.Destroy()

        modelFile = model.localFilename()
        zipfilename = os.path.splitext(modelFile)[0]+'.htb'

        docsDir = os.path.dirname(modelFile)
        if selected == 0:
            files = [os.path.join(docsDir, f) for f in model.config.files]
        elif selected == 1:
            files = []
            os.path.walk(docsDir, visitDir,
                         (files, [os.path.basename(zipfilename)]))

        wx.BeginBusyCursor()
        zf = zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED)
        try:
            for filename in files:
                zf.write(filename, filename[len(docsDir)+1:])
        finally:
            wx.EndBusyCursor()
            zf.close()

        wx.LogMessage('Written %s.'%zipfilename)

    def OnMakeCHM(self, event):
        modelFile = model.localFilename()
        dir, name = os.path.split(modelFile)
        cmd = 'hhc %s'%name
        cwd = os.getcwd()
        try:
            os.chdir(runDir)
            dlg = ProcessProgressDlg.ProcessProgressDlg(self.editor, cmd, 'Make CHM')
            try:
                if dlg.ShowModal() == wx.OK:
                    outls = dlg.output
                    errls = dlg.errors
                else:
                    return
            finally:
                dlg.Destroy()
        finally:
            os.chdir(cwd)

        #err = ''.join(errls).strip()

#-------------------------------------------------------------------------------


Plugins.registerPreference('HelpBook', 'hbShowDocumentTitles', 'True',
                           ['Should the document title be parsed from HTML and '
                            'displayed under "Files".'])
Plugins.registerFileType(HelpBookController)
