1# pyenchant
2#
3# Copyright (C) 2004-2008, Ryan Kelly
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20# In addition, as a special exception, you are
21# given permission to link the code of this program with
22# non-LGPL Spelling Provider libraries (eg: a MSFT Office
23# spell checker backend) and distribute linked combinations including
24# the two.  You must obey the GNU Lesser General Public License in all
25# respects for all of the code used other than said providers.  If you modify
26# this file, you may extend this exception to your version of the
27# file, but you are not obligated to do so.  If you do not wish to
28# do so, delete this exception statement from your version.
29#
30# Major code cleanup and re-write thanks to Phil Mayes, 2007
31#
32"""
33
34    enchant.checker.wxSpellCheckerDialog: wxPython spellchecker interface
35
36    This module provides the class wxSpellCheckerDialog, which provides
37    a wxPython dialog that can be used as an interface to a spell checking
38    session.  Currently it is intended as a proof-of-concept and demonstration
39    class, but it should be suitable for general-purpose use in a program.
40
41    The class must be given an enchant.checker.SpellChecker object with
42    which to operate.  It can (in theory...) be used in modal and non-modal
43    modes.  Use Show() when operating on an array of characters as it will
44    modify the array in place, meaning other work can be done at the same
45    time.  Use ShowModal() when operating on a static string.
46
47"""
48_DOC_ERRORS = ["ShowModal"]
49
50import wx
51
52
53class wxSpellCheckerDialog(wx.Dialog):
54    """Simple spellcheck dialog for wxPython
55
56    This class implements a simple spellcheck interface for wxPython,
57    in the form of a dialog.  It's intended mainly of an example of
58    how to do this, although it should be useful for applications that
59    just need a simple graphical spellchecker.
60
61    To use, a SpellChecker instance must be created and passed to the
62    dialog before it is shown:
63
64        >>> dlg = wxSpellCheckerDialog(None,-1,"")
65        >>> chkr = SpellChecker("en_AU",text)
66        >>> dlg.SetSpellChecker(chkr)
67        >>> dlg.Show()
68
69    This is most useful when the text to be checked is in the form of
70    a character array, as it will be modified in place as the user
71    interacts with the dialog.  For checking strings, the final result
72    will need to be obtained from the SpellChecker object:
73
74        >>> dlg = wxSpellCheckerDialog(None,-1,"")
75        >>> chkr = SpellChecker("en_AU",text)
76        >>> dlg.SetSpellChecker(chkr)
77        >>> dlg.ShowModal()
78        >>> text = dlg.GetSpellChecker().get_text()
79
80    Currently the checker must deal with strings of the same type as
81    returned by wxPython - unicode or normal string depending on the
82    underlying system.  This needs to be fixed, somehow...
83    """
84
85    _DOC_ERRORS = [
86        "dlg",
87        "chkr",
88        "dlg",
89        "SetSpellChecker",
90        "chkr",
91        "dlg",
92        "dlg",
93        "chkr",
94        "dlg",
95        "SetSpellChecker",
96        "chkr",
97        "dlg",
98        "ShowModal",
99        "dlg",
100        "GetSpellChecker",
101    ]
102
103    # Remember dialog size across invocations by storing it on the class
104    sz = (300, 70)
105
106    def __init__(self, parent=None, id=-1, title="Checking Spelling..."):
107        super().__init__(
108            parent,
109            id,
110            title,
111            size=wxSpellCheckerDialog.sz,
112            style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
113        )
114        self._numContext = 40
115        self._checker = None
116        self._buttonsEnabled = True
117        self.error_text = wx.TextCtrl(
118            self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH
119        )
120        self.replace_text = wx.TextCtrl(self, -1, "", style=wx.TE_PROCESS_ENTER)
121        self.replace_list = wx.ListBox(self, -1, style=wx.LB_SINGLE)
122        self.InitLayout()
123        wx.EVT_LISTBOX(self, self.replace_list.GetId(), self.OnReplSelect)
124        wx.EVT_LISTBOX_DCLICK(self, self.replace_list.GetId(), self.OnReplace)
125
126    def InitLayout(self):
127        """Lay out controls and add buttons."""
128        sizer = wx.BoxSizer(wx.HORIZONTAL)
129        txtSizer = wx.BoxSizer(wx.VERTICAL)
130        btnSizer = wx.BoxSizer(wx.VERTICAL)
131        replaceSizer = wx.BoxSizer(wx.HORIZONTAL)
132        txtSizer.Add(
133            wx.StaticText(self, -1, "Unrecognised Word:"), 0, wx.LEFT | wx.TOP, 5
134        )
135        txtSizer.Add(self.error_text, 1, wx.ALL | wx.EXPAND, 5)
136        replaceSizer.Add(
137            wx.StaticText(self, -1, "Replace with:"),
138            0,
139            wx.ALL | wx.ALIGN_CENTER_VERTICAL,
140            5,
141        )
142        replaceSizer.Add(self.replace_text, 1, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
143        txtSizer.Add(replaceSizer, 0, wx.EXPAND, 0)
144        txtSizer.Add(self.replace_list, 2, wx.ALL | wx.EXPAND, 5)
145        sizer.Add(txtSizer, 1, wx.EXPAND, 0)
146        self.buttons = []
147        for label, action, tip in (
148            ("Ignore", self.OnIgnore, "Ignore this word and continue"),
149            (
150                "Ignore All",
151                self.OnIgnoreAll,
152                "Ignore all instances of this word and continue",
153            ),
154            ("Replace", self.OnReplace, "Replace this word"),
155            ("Replace All", self.OnReplaceAll, "Replace all instances of this word"),
156            ("Add", self.OnAdd, "Add this word to the dictionary"),
157            ("Done", self.OnDone, "Finish spell-checking and accept changes"),
158        ):
159            btn = wx.Button(self, -1, label)
160            btn.SetToolTip(wx.ToolTip(tip))
161            btnSizer.Add(btn, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
162            btn.Bind(wx.EVT_BUTTON, action)
163            self.buttons.append(btn)
164        sizer.Add(btnSizer, 0, wx.ALL | wx.EXPAND, 5)
165        self.SetAutoLayout(True)
166        self.SetSizer(sizer)
167        sizer.Fit(self)
168
169    def Advance(self):
170        """Advance to the next error.
171
172        This method advances the SpellChecker to the next error, if
173        any.  It then displays the error and some surrounding context,
174        and well as listing the suggested replacements.
175        """
176        # Disable interaction if no checker
177        if self._checker is None:
178            self.EnableButtons(False)
179            return False
180        # Advance to next error, disable if not available
181        try:
182            self._checker.next()
183        except StopIteration:
184            self.EnableButtons(False)
185            self.error_text.SetValue("")
186            self.replace_list.Clear()
187            self.replace_text.SetValue("")
188            if self.IsModal():  # test needed for SetSpellChecker call
189                # auto-exit when checking complete
190                self.EndModal(wx.ID_OK)
191            return False
192        self.EnableButtons()
193        # Display error context with erroneous word in red.
194        # Restoring default style was misbehaving under win32, so
195        # I am forcing the rest of the text to be black.
196        self.error_text.SetValue("")
197        self.error_text.SetDefaultStyle(wx.TextAttr(wx.BLACK))
198        lContext = self._checker.leading_context(self._numContext)
199        self.error_text.AppendText(lContext)
200        self.error_text.SetDefaultStyle(wx.TextAttr(wx.RED))
201        self.error_text.AppendText(self._checker.word)
202        self.error_text.SetDefaultStyle(wx.TextAttr(wx.BLACK))
203        tContext = self._checker.trailing_context(self._numContext)
204        self.error_text.AppendText(tContext)
205        # Display suggestions in the replacements list
206        suggs = self._checker.suggest()
207        self.replace_list.Set(suggs)
208        self.replace_text.SetValue(suggs and suggs[0] or "")
209        return True
210
211    def EnableButtons(self, state=True):
212        """Enable the checking-related buttons"""
213        if state != self._buttonsEnabled:
214            for btn in self.buttons[:-1]:
215                btn.Enable(state)
216            self._buttonsEnabled = state
217
218    def GetRepl(self):
219        """Get the chosen replacement string."""
220        repl = self.replace_text.GetValue()
221        return repl
222
223    def OnAdd(self, evt):
224        """Callback for the "add" button."""
225        self._checker.add()
226        self.Advance()
227
228    def OnDone(self, evt):
229        """Callback for the "close" button."""
230        wxSpellCheckerDialog.sz = self.error_text.GetSizeTuple()
231        if self.IsModal():
232            self.EndModal(wx.ID_OK)
233        else:
234            self.Close()
235
236    def OnIgnore(self, evt):
237        """Callback for the "ignore" button.
238        This simply advances to the next error.
239        """
240        self.Advance()
241
242    def OnIgnoreAll(self, evt):
243        """Callback for the "ignore all" button."""
244        self._checker.ignore_always()
245        self.Advance()
246
247    def OnReplace(self, evt):
248        """Callback for the "replace" button."""
249        repl = self.GetRepl()
250        if repl:
251            self._checker.replace(repl)
252        self.Advance()
253
254    def OnReplaceAll(self, evt):
255        """Callback for the "replace all" button."""
256        repl = self.GetRepl()
257        self._checker.replace_always(repl)
258        self.Advance()
259
260    def OnReplSelect(self, evt):
261        """Callback when a new replacement option is selected."""
262        sel = self.replace_list.GetSelection()
263        if sel == -1:
264            return
265        opt = self.replace_list.GetString(sel)
266        self.replace_text.SetValue(opt)
267
268    def GetSpellChecker(self):
269        """Get the spell checker object."""
270        return self._checker
271
272    def SetSpellChecker(self, chkr):
273        """Set the spell checker, advancing to the first error.
274        Return True if error(s) to correct, else False."""
275        self._checker = chkr
276        return self.Advance()
277
278
279def _test():
280    class TestDialog(wxSpellCheckerDialog):
281        def __init__(self, *args):
282            super().__init__(*args)
283            wx.EVT_CLOSE(self, self.OnClose)
284
285        def OnClose(self, evnt):
286            chkr = dlg.GetSpellChecker()
287            if chkr is not None:
288                print(["AFTER:", chkr.get_text()])
289            self.Destroy()
290
291    from enchant.checker import SpellChecker
292
293    text = "This is sme text with a fw speling errors in it. Here are a fw more to tst it ut."
294    print(["BEFORE:", text])
295    app = wx.App(False)
296    dlg = TestDialog()
297    chkr = SpellChecker("en_US", text)
298    dlg.SetSpellChecker(chkr)
299    dlg.Show()
300    app.MainLoop()
301
302
303if __name__ == "__main__":
304    _test()
305