<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>dict_en.dat</filename>
    </added>
    <added>
      <filename>spellcheck.py</filename>
    </added>
    <added>
      <filename>spellcheckcfgdlg.py</filename>
    </added>
    <added>
      <filename>spellcheckdlg.py</filename>
    </added>
    <added>
      <filename>tools/add_words.py</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -25,6 +25,9 @@ import pml
 import scenereport
 import scriptreport
 import screenplay
+import spellcheck
+import spellcheckdlg
+import spellcheckcfgdlg
 import splash
 import titlesdlg
 import util
@@ -69,10 +72,14 @@ class GlobalData:
 
         self.confFilename = misc.confPath + &quot;/default.conf&quot;
         self.stateFilename = misc.confPath + &quot;/state&quot;
+        self.scDictFilename = misc.confPath + &quot;/spell_checker_dictionary&quot;
 
         # current script config path
         self.scriptSettingsPath = misc.confPath
 
+        # global spell checker (user) dictionary
+        self.scDict = spellcheck.Dict()
+
         if opts.conf:
             self.confFilename = opts.conf
 
@@ -140,6 +147,11 @@ class GlobalData:
         else:
             self.vm = self.vmOverviewLarge
 
+    # save global spell checker dictionary to disk
+    def saveScDict(self):
+        util.writeToFile(self.scDictFilename,
+                         util.toUTF8(self.scDict.save()), mainFrame)
+
 class MyPanel(wxPanel):
 
     def __init__(self, parent, id):
@@ -575,6 +587,16 @@ class MyCtrl(wxControl):
 
         dlg.Destroy()
         
+    def OnSpellCheckerScriptDictionaryDlg(self):
+        dlg = spellcheckcfgdlg.SCDictDlg(mainFrame,
+            copy.deepcopy(self.sp.scDict), False)
+
+        if dlg.ShowModal() == wxID_OK:
+            self.sp.scDict = dlg.scDict
+            self.sp.markChanged()
+
+        dlg.Destroy()
+
     def OnReportDialogueChart(self):
         self.sp.paginate()
         dialoguechart.genDialogueChart(mainFrame, self.sp, not misc.license)
@@ -835,7 +857,53 @@ class MyCtrl(wxControl):
         dlg.Destroy()
 
         self.searchLine = -1
-        self.searchColStart = -1
+        self.searchColumn = -1
+        self.searchWidth = -1
+
+        self.updateScreen()
+
+    def OnSpellCheckerDlg(self):
+        self.clearAutoComp()
+        
+        wasAtStart = self.sp.line == 0
+
+        wxBeginBusyCursor()
+        
+        if not spellcheck.loadDict(mainFrame):
+            wxEndBusyCursor()
+            
+            return
+
+        sc = spellcheck.SpellChecker(self.sp, gd.scDict)
+        found = sc.findNext()
+        
+        wxEndBusyCursor()
+
+        if not found:
+            s = &quot;&quot;
+            
+            if not wasAtStart:
+                s = &quot;\n\n(Starting position was not at\n&quot;\
+                    &quot;the beginning of the script.)&quot;
+            wxMessageBox(&quot;Spell checker found no errors.&quot; + s, &quot;Results&quot;,
+                         wxOK, mainFrame)
+
+            return
+
+        dlg = spellcheckdlg.SpellCheckDlg(mainFrame, self, sc, gd.scDict)
+        dlg.ShowModal()
+        
+        if dlg.didReplaces:
+            self.sp.reformatAll()
+            self.makeLineVisible(self.sp.line)
+
+        if dlg.changedGlobalDict:
+            gd.saveScDict()
+            
+        dlg.Destroy()
+
+        self.searchLine = -1
+        self.searchColumn = -1
         self.searchWidth = -1
 
         self.updateScreen()
@@ -1378,6 +1446,8 @@ class MyFrame(wxFrame):
         tmp.AppendSeparator()
         tmp.Append(ID_SETTINGS_LOAD, &quot;Load...&quot;)
         tmp.Append(ID_SETTINGS_SAVE_AS, &quot;Save as...&quot;)
+        tmp.AppendSeparator()
+        tmp.Append(ID_SETTINGS_SC_DICT, &quot;&amp;Spell checker dictionary...&quot;)
         fileMenu.AppendMenu(ID_FILE_SETTINGS, &quot;Se&amp;ttings&quot;, tmp)
 
         fileMenu.AppendSeparator()
@@ -1428,6 +1498,7 @@ class MyFrame(wxFrame):
         scriptMenu.Append(ID_SCRIPT_HEADERS, &quot;&amp;Headers...&quot;)
         scriptMenu.Append(ID_SCRIPT_LOCATIONS, &quot;&amp;Locations...&quot;)
         scriptMenu.Append(ID_SCRIPT_TITLES, &quot;&amp;Title pages...&quot;)
+        scriptMenu.Append(ID_SCRIPT_SC_DICT, &quot;&amp;Spell checker dictionary...&quot;)
         scriptMenu.AppendSeparator()
 
         tmp = wxMenu()
@@ -1446,6 +1517,7 @@ class MyFrame(wxFrame):
         reportsMenu.Append(ID_REPORTS_DIALOGUE_CHART, &quot;&amp;Dialogue chart...&quot;)
         
         toolsMenu = wxMenu()
+        toolsMenu.Append(ID_TOOLS_SPELL_CHECK, &quot;&amp;Spell checker...&quot;)
         toolsMenu.Append(ID_TOOLS_NAME_DB, &quot;&amp;Name database...&quot;)
         toolsMenu.Append(ID_TOOLS_CHARMAP, &quot;&amp;Character map...&quot;)
         toolsMenu.Append(ID_TOOLS_COMPARE_SCRIPTS, &quot;C&amp;ompare scripts...&quot;)
@@ -1534,6 +1606,7 @@ class MyFrame(wxFrame):
         EVT_MENU(self, ID_SETTINGS_CHANGE, self.OnSettings)
         EVT_MENU(self, ID_SETTINGS_LOAD, self.OnLoadSettings)
         EVT_MENU(self, ID_SETTINGS_SAVE_AS, self.OnSaveSettingsAs)
+        EVT_MENU(self, ID_SETTINGS_SC_DICT, self.OnSpellCheckerDictionaryDlg)
         EVT_MENU(self, ID_FILE_EXIT, self.OnExit)
         EVT_MENU(self, ID_EDIT_CUT, self.OnCut)
         EVT_MENU(self, ID_EDIT_COPY, self.OnCopy)
@@ -1555,6 +1628,8 @@ class MyFrame(wxFrame):
         EVT_MENU(self, ID_SCRIPT_HEADERS, self.OnHeadersDlg)
         EVT_MENU(self, ID_SCRIPT_LOCATIONS, self.OnLocationsDlg)
         EVT_MENU(self, ID_SCRIPT_TITLES, self.OnTitlesDlg)
+        EVT_MENU(self, ID_SCRIPT_SC_DICT,
+                 self.OnSpellCheckerScriptDictionaryDlg)
         EVT_MENU(self, ID_SCRIPT_SETTINGS_CHANGE, self.OnScriptSettings)
         EVT_MENU(self, ID_SCRIPT_SETTINGS_LOAD, self.OnLoadScriptSettings)
         EVT_MENU(self, ID_SCRIPT_SETTINGS_SAVE_AS, self.OnSaveScriptSettingsAs)
@@ -1563,6 +1638,7 @@ class MyFrame(wxFrame):
         EVT_MENU(self, ID_REPORTS_SCRIPT_REP, self.OnReportScript)
         EVT_MENU(self, ID_REPORTS_LOCATION_REP, self.OnReportLocation)
         EVT_MENU(self, ID_REPORTS_SCENE_REP, self.OnReportScene)
+        EVT_MENU(self, ID_TOOLS_SPELL_CHECK, self.OnSpellCheckerDlg)
         EVT_MENU(self, ID_TOOLS_NAME_DB, self.OnNameDatabase)
         EVT_MENU(self, ID_TOOLS_CHARMAP, self.OnCharacterMap)
         EVT_MENU(self, ID_TOOLS_COMPARE_SCRIPTS, self.OnCompareScripts)
@@ -1637,6 +1713,7 @@ class MyFrame(wxFrame):
             &quot;ID_SCRIPT_HEADERS&quot;,
             &quot;ID_SCRIPT_LOCATIONS&quot;,
             &quot;ID_SCRIPT_PAGINATE&quot;,
+            &quot;ID_SCRIPT_SC_DICT&quot;,
             &quot;ID_SCRIPT_SETTINGS&quot;,
             &quot;ID_SCRIPT_SETTINGS_CHANGE&quot;,
             &quot;ID_SCRIPT_SETTINGS_LOAD&quot;,
@@ -1645,9 +1722,11 @@ class MyFrame(wxFrame):
             &quot;ID_SETTINGS_CHANGE&quot;,
             &quot;ID_SETTINGS_LOAD&quot;,
             &quot;ID_SETTINGS_SAVE_AS&quot;,
+            &quot;ID_SETTINGS_SC_DICT&quot;,
             &quot;ID_TOOLS_CHARMAP&quot;,
             &quot;ID_TOOLS_COMPARE_SCRIPTS&quot;,
             &quot;ID_TOOLS_NAME_DB&quot;,
+            &quot;ID_TOOLS_SPELL_CHECK&quot;,
             &quot;ID_VIEW_SHOW_FORMATTING&quot;,
             &quot;ID_VIEW_STYLE_DRAFT&quot;,
             &quot;ID_VIEW_STYLE_LAYOUT&quot;,
@@ -1951,6 +2030,19 @@ class MyFrame(wxFrame):
     def OnLocationsDlg(self, event = None):
         self.panel.ctrl.OnLocationsDlg()
 
+    def OnSpellCheckerDictionaryDlg(self, event = None):
+        dlg = spellcheckcfgdlg.SCDictDlg(self, copy.deepcopy(gd.scDict),
+                                         True)
+
+        if dlg.ShowModal() == wxID_OK:
+            gd.scDict = dlg.scDict
+            gd.saveScDict()
+
+        dlg.Destroy()
+
+    def OnSpellCheckerScriptDictionaryDlg(self, event = None):
+        self.panel.ctrl.OnSpellCheckerScriptDictionaryDlg()
+
     def OnScriptSettings(self, event = None):
         self.panel.ctrl.OnScriptSettings()
 
@@ -2001,6 +2093,9 @@ class MyFrame(wxFrame):
     def OnReportScript(self, event = None):
         self.panel.ctrl.OnReportScript()
 
+    def OnSpellCheckerDlg(self, event = None):
+        self.panel.ctrl.OnSpellCheckerDlg()
+
     def OnNameDatabase(self, event = None):
         if not hasattr(self, &quot;names&quot;):
             self.statusBar.SetStatusText(&quot;Opening name database...&quot;, 1)
@@ -2173,8 +2268,14 @@ class MyApp(wxApp):
 
             if s:
                 gd.cvars.load(gd.cvars.makeVals(s), &quot;&quot;, gd)
-
+                
         gd.setViewMode(gd.viewMode)
+
+        if util.fileExists(gd.scDictFilename):
+            s = util.fromUTF8(util.loadFile(gd.scDictFilename, None))
+
+            if s:
+                gd.scDict.load(s)
         
         mainFrame = MyFrame(NULL, -1, &quot;Blyte&quot;)
         bugreport.mainFrame = mainFrame</diff>
      <filename>blyte.py</filename>
    </modified>
    <modified>
      <diff>@@ -740,7 +740,19 @@ class ConfigGlobal:
                     [util.Key(WXK_SPACE, ctrl = True).toInt()]),
             
             Command(&quot;Settings&quot;, &quot;Change global settings.&quot;, isMenu = True),
+
+            Command(&quot;SpellCheckerDlg&quot;,&quot;Spell check the script.&quot;,
+                    [util.Key(WXK_F8).toInt()], isMenu = True),
             
+            Command(&quot;SpellCheckerDictionaryDlg&quot;,
+                    &quot;Open the global spell checker dictionary dialog.&quot;,
+                    isMenu = True),
+
+            Command(&quot;SpellCheckerScriptDictionaryDlg&quot;,
+                    &quot;Open the script-specific spell checker dictionary&quot;
+                    &quot; dialog.&quot;,
+                    isMenu = True),
+
             Command(&quot;Tab&quot;, &quot;Change current element to the next style or&quot;
                     &quot; create a new element.&quot;, [WXK_TAB], isFixed = True),
 </diff>
      <filename>config.py</filename>
    </modified>
    <modified>
      <diff>@@ -3,7 +3,7 @@ a single byte 0x0A. The file is encoded in UTF-8, and begins with the
 Unicode byte order mark (BOM), i.e. the bytes &quot;EF BB BF&quot;.
 
 After the BOM, the first line contains &quot;#Version x&quot;, where x is the
-file format number, currently either 1 or 2.
+file format number, currently either 1, 2 or 3.
 
 If file format is 2, configuration sections come next. They are of the
 form &quot;#Begin-X &quot;, [lines belonging to X], &quot;#End-X &quot;. Each line in the
@@ -97,11 +97,18 @@ The following config sections exists and appear in the file in this order:
 
    Underlined: (boolean): Whether to underline text.
 
+
  Locations:
 
   Locations (list of lists of strings): Each inner list lists scene names
   to treat as the same location.
 
+
+ Spell-Checker-Dict:
+
+  Words (list of strings): Script-specific spell checker dictionary.
+
+
 After this (in version 1 files after &quot;#Version 1&quot;) there is more
 configuration, but in a different syntax, that is mostly due to historical
 accidents.
@@ -168,8 +175,12 @@ pages exist yet, creates one. Example:
 &quot;Header-Empty-Lines&quot;. Value: integer &gt;= 0. If there are header lines, this
 is the number of empty lines between headers and actual script contents.
 
+If file format is &gt;= 3, between the config data and the actual script data
+is a &quot;#Start-Script &quot; line. With it, config sections can be added without
+increasing the file format version number, as unknown config parts can be
+skipped.
 
-After that, the actual script starts. Each line has two bytes at the
+After the above, the actual script starts. Each line has two bytes at the
 beginning that signal the type of linebreak at the end of that line and
 the general type of the line, and everything after that is the text of the
 line.</diff>
      <filename>fileformat.txt</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,4 @@
-&#65279;#Version 2
+&#65279;#Version 3
 #Begin-Auto-Completion 
 AutoCompletion/Scene/Enabled:True
 AutoCompletion/Scene/Items:0
@@ -135,6 +135,10 @@ Element/Note/Export/Underlined:False
 #Begin-Locations 
 Locations:0
 #End-Locations 
+#Begin-Spell-Checker-Dict 
+Words:1
+Words/1:pilots'
+#End-Spell-Checker-Dict 
 #Title-String 0.000000,80.000000,32,cb,Times,,THE KOALA INCIDENT
 #Title-String 0.000000,100.000000,12,c,Courier,,by
 #Title-String 0.000000,108.470000,12,c,Courier,,Anonymous Person
@@ -142,6 +146,7 @@ Locations:0
 #Title-String 160.000000,208.000000,12,c,Courier,,All rights reserved.
 #Header-String 1,0,r,,${PAGE}.
 #Header-Empty-Lines 1
+#Start-Script 
 .\ext. stonehenge - night
 ..SUPER: &quot;STONEHENGE - 00:30&quot;
 &gt;.A blizzard rages. Snow is everywhere and visibility is</diff>
      <filename>sample.blyte</filename>
    </modified>
    <modified>
      <diff>@@ -23,6 +23,7 @@ import locations
 import mypager
 import pdf
 import pml
+import spellcheck
 import titles
 import util
 
@@ -40,7 +41,8 @@ class Screenplay:
         self.headers = headers.Headers()
         self.locations = locations.Locations()
         self.titles = titles.Titles()
-        
+        self.scDict = spellcheck.Dict()
+
         self.lines = [ Line(LB_LAST, SCENE) ]
 
         self.cfgGl = cfgGl
@@ -110,6 +112,7 @@ class Screenplay:
         sp.headers = copy.deepcopy(self.headers)
         sp.locations = copy.deepcopy(self.locations)
         sp.titles = copy.deepcopy(self.titles)
+        sp.scDict = copy.deepcopy(self.scDict)
 
         # remove the dummy empty line
         sp.lines = []
@@ -126,7 +129,7 @@ class Screenplay:
         output = util.String()
 
         output += codecs.BOM_UTF8
-        output += &quot;#Version 2\n&quot;
+        output += &quot;#Version 3\n&quot;
 
         output += &quot;#Begin-Auto-Completion \n&quot;
         output += util.toUTF8(self.autoCompletion.save())
@@ -139,6 +142,10 @@ class Screenplay:
         output += &quot;#Begin-Locations \n&quot;
         output += util.toUTF8(self.locations.save())
         output += &quot;#End-Locations \n&quot;
+
+        output += &quot;#Begin-Spell-Checker-Dict \n&quot;
+        output += util.toUTF8(self.scDict.save())
+        output += &quot;#End-Spell-Checker-Dict \n&quot;
         
         pgs = self.titles.pages
         for pg in xrange(len(pgs)):
@@ -152,6 +159,8 @@ class Screenplay:
             output += &quot;#Header-String %s\n&quot; % util.toUTF8(str(h))
 
         output += &quot;#Header-Empty-Lines %d\n&quot; % self.headers.emptyLinesAfter
+
+        output += &quot;#Start-Script \n&quot;
         
         for i in xrange(len(self.lines)):
             output += util.toUTF8(str(self.lines[i]) + &quot;\n&quot;)
@@ -208,11 +217,13 @@ class Screenplay:
             raise error.MiscError(&quot;File doesn't seem to be a proper\n&quot;
                                   &quot;screenplay file.&quot;)
 
-        if version not in (&quot;1&quot;, &quot;2&quot;):
+        if version not in (&quot;1&quot;, &quot;2&quot;, &quot;3&quot;):
             raise error.MiscError(&quot;File uses fileformat version '%s',\n&quot;
                                   &quot;which is not supported by this version\n&quot;
                                   &quot;of the program.&quot; % version)
 
+        version = int(version)
+
         # current position at 'lines'
         index = 1
 
@@ -228,6 +239,11 @@ class Screenplay:
         if s:
             sp.locations.load(s)
 
+        s, index = Screenplay.getConfigPart(lines, &quot;Spell-Checker-Dict&quot;,
+                                            index)
+        if s:
+            sp.scDict.load(s)
+
         # used to keep track that element type only changes after a
         # LB_LAST line.
         prevType = None
@@ -241,6 +257,10 @@ class Screenplay:
         # did we encounter unknown config lines
         unknownConfigs = False
 
+        # have we seen the Start-Script line. defaults to True in old
+        # files which didn't have it.
+        startSeen = version &lt; 3
+
         for i in xrange(index, len(lines)):
             s = lines[i]
 
@@ -272,10 +292,18 @@ class Screenplay:
                 elif key == &quot;Header-Empty-Lines&quot;:
                     sp.headers.emptyLinesAfter = util.str2int(val, 1, 0, 5)
 
+                elif key == &quot;Start-Script&quot;:
+                    startSeen = True
+
                 else:
                     unknownConfigs = True
 
             else:
+                if not startSeen:
+                    unknownConfigs = True
+                    
+                    continue
+
                 lb = config.char2lb(s[0], False)
                 lt = config.char2lt(s[1], False)
                 text = s[2:]
@@ -302,6 +330,9 @@ class Screenplay:
                 else:
                     prevType = None
 
+        if not startSeen:
+            raise error.MiscError(&quot;Start-Script line not found.&quot;)
+
         if len(sp.lines) == 0:
             raise error.MiscError(&quot;File doesn't contain any screenplay&quot;
                                   &quot; lines.&quot;)
@@ -1389,6 +1420,60 @@ class Screenplay:
 
         return names
 
+    # return a dictionary of all character names (single-line text
+    # elements only, lower-cased, values = None).
+    def getCharacterNames(self):
+        names = {}
+
+        ul = util.lower
+        
+        for ln in self.lines:
+            if (ln.lt == CHARACTER) and (ln.lb == LB_LAST):
+                names[ul(ln.text)] = None
+
+        return names
+
+    # get next word, starting at (line, col). line must be valid, but col
+    # can point after the line's length, in which case the search starts
+    # at (line + 1, 0). returns (word, line, col), where word is None if
+    # at end of script, and (line, col) point to the start of the word.
+    # note that this only handles words that are on a single line.
+    def getWord(self, line, col):
+        ls = self.lines
+        
+        while 1:
+            if ((line &lt; 0) or (line &gt;= len(ls))):
+                return (None, 0, 0)
+
+            s = ls[line].text
+            
+            if col &gt;= len(s):
+                line += 1
+                col = 0
+
+                continue
+
+            ch = s[col : col + 1]
+            
+            if not util.isWordBoundary(ch):
+                word = ch
+                startCol = col
+                col += 1
+
+                while col &lt; len(s):
+                    ch = s[col : col + 1]
+                    
+                    if util.isWordBoundary(ch):
+                        break
+
+                    word += ch
+                    col += 1
+
+                return (word, line, startCol)
+
+            else:
+                col += 1
+
     # returns True if we're at second-to-last character of PAREN element,
     # and last character is &quot;)&quot;
     def isAtEndOfParen(self):</diff>
      <filename>screenplay.py</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>d63872bcecbb5ddac16dd3d5687d82623158216a</id>
    </parent>
  </parents>
  <author>
    <name>Osku Salerma</name>
    <email>osku@iki.fi</email>
  </author>
  <url>http://github.com/oskusalerma/blyte/commit/093095d6dbf7fb6d0a446433a9954e098bf32ea2</url>
  <id>093095d6dbf7fb6d0a446433a9954e098bf32ea2</id>
  <committed-date>2005-10-04T09:14:39-07:00</committed-date>
  <authored-date>2005-10-04T09:14:39-07:00</authored-date>
  <message>Add spell checker. Fixes #22.</message>
  <tree>9246a0608b52c8d3c2dcbb971567862e6fb463ca</tree>
  <committer>
    <name>Osku Salerma</name>
    <email>osku@iki.fi</email>
  </committer>
</commit>
