Added URL support to attribute objects and to wxRichTextCtrl,
[wxWidgets.git] / src / richtext / richtexthtml.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/richtext/richtexthtml.cpp
3 // Purpose: HTML I/O for wxRichTextCtrl
4 // Author: Julian Smart
5 // Modified by:
6 // Created: 2005-09-30
7 // RCS-ID: $Id$
8 // Copyright: (c) Julian Smart
9 // Licence: wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
11
12 // For compilers that support precompilation, includes "wx.h".
13 #include "wx/wxprec.h"
14
15 #ifdef __BORLANDC__
16 #pragma hdrstop
17 #endif
18
19 #if wxUSE_RICHTEXT
20
21 #include "wx/richtext/richtexthtml.h"
22
23 #ifndef WX_PRECOMP
24 #endif
25
26 #include "wx/filename.h"
27 #include "wx/wfstream.h"
28 #include "wx/txtstrm.h"
29
30 #if wxUSE_FILESYSTEM
31 #include "wx/filesys.h"
32 #include "wx/fs_mem.h"
33 #endif
34
35 IMPLEMENT_DYNAMIC_CLASS(wxRichTextHTMLHandler, wxRichTextFileHandler)
36
37 int wxRichTextHTMLHandler::sm_fileCounter = 1;
38
39 /// Can we handle this filename (if using files)? By default, checks the extension.
40 bool wxRichTextHTMLHandler::CanHandle(const wxString& filename) const
41 {
42 wxString path, file, ext;
43 wxSplitPath(filename, & path, & file, & ext);
44
45 return (ext.Lower() == wxT("html") || ext.Lower() == wxT("htm"));
46 }
47
48
49 #if wxUSE_STREAMS
50 bool wxRichTextHTMLHandler::DoLoadFile(wxRichTextBuffer *WXUNUSED(buffer), wxInputStream& WXUNUSED(stream))
51 {
52 return false;
53 }
54
55 /*
56 * We need to output only _changes_ in character formatting.
57 */
58
59 bool wxRichTextHTMLHandler::DoSaveFile(wxRichTextBuffer *buffer, wxOutputStream& stream)
60 {
61 ClearTemporaryImageLocations();
62
63 buffer->Defragment();
64
65 wxTextOutputStream str(stream);
66
67 wxTextAttrEx currentParaStyle = buffer->GetAttributes();
68 wxTextAttrEx currentCharStyle = buffer->GetAttributes();
69
70 str << wxT("<html><head></head><body>\n");
71
72 str << wxT("<table border=0 cellpadding=0 cellspacing=0><tr><td width=\"100%\">");
73
74 str << wxString::Format(wxT("<font face=\"%s\" size=\"%ld\" color=\"%s\" >"),
75 currentParaStyle.GetFont().GetFaceName().c_str(), PtToSize(currentParaStyle.GetFont().GetPointSize()),
76 currentParaStyle.GetTextColour().GetAsString(wxC2S_HTML_SYNTAX).c_str());
77
78 m_font = false;
79 m_indent = 0;
80 m_list = false;
81
82 wxRichTextObjectList::compatibility_iterator node = buffer->GetChildren().GetFirst();
83 while (node)
84 {
85 wxRichTextParagraph* para = wxDynamicCast(node->GetData(), wxRichTextParagraph);
86 wxASSERT (para != NULL);
87
88 if (para)
89 {
90 wxTextAttrEx paraStyle(para->GetCombinedAttributes());
91
92 OutputParagraphFormatting(currentParaStyle, paraStyle, stream);
93
94 wxRichTextObjectList::compatibility_iterator node2 = para->GetChildren().GetFirst();
95 while (node2)
96 {
97 wxRichTextObject* obj = node2->GetData();
98 wxRichTextPlainText* textObj = wxDynamicCast(obj, wxRichTextPlainText);
99 if (textObj && !textObj->IsEmpty())
100 {
101 wxTextAttrEx charStyle(para->GetCombinedAttributes(obj->GetAttributes()));
102 BeginCharacterFormatting(currentCharStyle, charStyle, paraStyle, stream);
103
104 str << textObj->GetText();
105
106 EndCharacterFormatting(currentCharStyle, charStyle, paraStyle, stream);
107 }
108
109 wxRichTextImage* image = wxDynamicCast(obj, wxRichTextImage);
110 if( image && !image->IsEmpty())
111 WriteImage( image, stream );
112
113 node2 = node2->GetNext();
114 }
115 str << wxT("\n");
116 }
117 node = node->GetNext();
118 }
119
120 str << wxT("</font></td></tr></table></body></html>\n");
121
122 return true;
123 }
124
125 void wxRichTextHTMLHandler::BeginCharacterFormatting(const wxTextAttrEx& currentStyle, const wxTextAttrEx& thisStyle, const wxTextAttrEx& paraStyle, wxOutputStream& stream)
126 {
127 wxTextOutputStream str(stream);
128
129 // Is the item a bulleted one?
130 if ( paraStyle.GetBulletStyle() != wxTEXT_ATTR_BULLET_STYLE_NONE )
131 {
132 // Is there any opened list?
133 if (m_list)
134 {
135 // Yes there is
136
137 // Is the item among the previous ones?
138 // Is the item one of the previous list tag's child items?
139 if ((paraStyle.GetLeftIndent() == (m_indent + 100)) || (paraStyle.GetLeftIndent() < 100))
140 str << wxT("<li>"); //Yes it is
141 else
142 {
143 // No it isn't, so we should close the list tag
144 str << (m_is_ul ? wxT("</ul>") : wxT("</ol>"));
145
146 // And renavigate to new list's horizontal position
147 NavigateToListPosition(paraStyle, str);
148
149 // Get the appropriate tag, an ol for numerical values, an ul for dot, square etc.
150 wxString tag;
151 TypeOfList(paraStyle, tag);
152 str << tag << wxT("<li>");
153 }
154 }
155 else
156 {
157 // No there isn't a list.
158 // navigate to new list's horizontal position(indent)
159 NavigateToListPosition(paraStyle, str);
160
161 // Get the appropriate tag, an ol for numerical values, an ul for dot, square etc.
162 wxString tag;
163 TypeOfList(paraStyle, tag);
164 str << tag << wxT("<li>");
165
166 // Now we have a list, mark it.
167 m_list = true;
168 }
169 }
170 else if( m_list )
171 {
172 // The item is not bulleted and there is a list what should be closed now.
173 // So close the list
174
175 str << (m_is_ul ? wxT("</ul>") : wxT("</ol>"));
176
177 // And mark as there is no an opened list
178 m_list = false;
179 }
180
181 // does the item have an indentation ?
182 if( paraStyle.GetLeftIndent() )
183 {
184 if (paraStyle.GetBulletStyle() == wxTEXT_ATTR_BULLET_STYLE_NONE)
185 {
186 if (m_indent)
187 {
188 if ((paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent()) == m_indent)
189 {
190 if (paraStyle.GetLeftSubIndent() < 0)
191 {
192 str << SymbolicIndent(~paraStyle.GetLeftSubIndent());
193 }
194 }
195 else
196 {
197 if (paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent() > m_indent)
198 {
199 Indent(paraStyle, str);
200 m_indent = paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent();
201 m_indents.Add( m_indent );
202 }
203 else
204 {
205 int i = m_indents.size() - 1;
206 for (; i > -1; i--)
207 {
208 if (m_indent < (paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent()))
209 {
210 Indent(paraStyle, str);
211 m_indent = paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent();
212 m_indents.Add( m_indent );
213
214 break;
215 }
216 else if (m_indent == (paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent()))
217 {
218 if (paraStyle.GetLeftSubIndent() < 0)
219 {
220 str << SymbolicIndent(~paraStyle.GetLeftSubIndent());
221 }
222 break;
223 }
224 else
225 {
226 str << wxT("</td></tr></table>");
227
228 m_indents.RemoveAt(i);
229
230 if(i < 1)
231 {
232 m_indent=0; break;
233 }
234 m_indent = m_indents[i-1];
235 }
236 }
237 }
238 }
239 }
240 else
241 {
242 Indent(paraStyle, str);
243 m_indent = paraStyle.GetLeftIndent() + paraStyle.GetLeftSubIndent();
244 m_indents.Add( m_indent );
245 }
246 }
247 }
248 else if (m_indent)
249 {
250 // The item is not indented and there is a table(s) that should be closed now.
251
252 for (unsigned int i = 0; i < m_indents.size(); i++)
253 str << wxT("</td></tr></table>");
254
255 m_indent = 0;
256 m_indents.Clear();
257 }
258
259
260 wxString style;
261
262 // Is there any change in the font properties of the item?
263 if (thisStyle.GetFont().GetFaceName() != currentStyle.GetFont().GetFaceName())
264 {
265 wxString faceName(thisStyle.GetFont().GetFaceName());
266 style += wxString::Format(wxT(" face=\"%s\""), faceName.c_str());
267 }
268 if (thisStyle.GetFont().GetPointSize() != currentStyle.GetFont().GetPointSize())
269 style += wxString::Format(wxT(" size=\"%ld\""), PtToSize(thisStyle.GetFont().GetPointSize()));
270 if (thisStyle.GetTextColour() != currentStyle.GetTextColour() )
271 {
272 wxString color(thisStyle.GetTextColour().GetAsString(wxC2S_HTML_SYNTAX));
273 style += wxString::Format(wxT(" color=\"%s\""), color.c_str());
274 }
275
276 if (style.size())
277 {
278 str << wxString::Format(wxT("<font %s >"), style.c_str());
279 m_font = true;
280 }
281
282 if (thisStyle.GetFont().GetWeight() == wxBOLD)
283 str << wxT("<b>");
284 if (thisStyle.GetFont().GetStyle() == wxITALIC)
285 str << wxT("<i>");
286 if (thisStyle.GetFont().GetUnderlined())
287 str << wxT("<u>");
288
289 if (thisStyle.HasURL())
290 str << wxT("<a href=\"") << thisStyle.GetURL() << wxT("\">");
291 }
292
293 void wxRichTextHTMLHandler::EndCharacterFormatting(const wxTextAttrEx& WXUNUSED(currentStyle), const wxTextAttrEx& thisStyle, const wxTextAttrEx& WXUNUSED(paraStyle), wxOutputStream& stream)
294 {
295 wxTextOutputStream str(stream);
296
297 if (thisStyle.HasURL())
298 str << wxT("</a>");
299
300 if (thisStyle.GetFont().GetUnderlined())
301 str << wxT("</u>");
302 if (thisStyle.GetFont().GetStyle() == wxITALIC)
303 str << wxT("</i>");
304 if (thisStyle.GetFont().GetWeight() == wxBOLD)
305 str << wxT("</b>");
306
307 if (m_font)
308 {
309 m_font = false;
310 str << wxT("</font>");
311 }
312 }
313
314 /// Output paragraph formatting
315 void wxRichTextHTMLHandler::OutputParagraphFormatting(const wxTextAttrEx& WXUNUSED(currentStyle), const wxTextAttrEx& thisStyle, wxOutputStream& stream)
316 {
317 // If there is no opened list currently, insert a <p> after every paragraph
318 if (!m_list)
319 {
320 wxTextOutputStream str(stream);
321 wxString align = GetAlignment(thisStyle);
322 str << wxString::Format(wxT("<p align=\"%s\">"), align.c_str());
323 }
324 }
325
326 void wxRichTextHTMLHandler::NavigateToListPosition(const wxTextAttrEx& thisStyle, wxTextOutputStream& str)
327 {
328 // indenting an item using an ul/ol tag is equal to inserting 5 x &nbsp; on its left side.
329 // so we should start from 100 point left
330
331 // Is the second td's left wall of the current indentaion table at the 100+ point-left-side
332 // of the item, horizontally?
333 if (m_indent + 100 < thisStyle.GetLeftIndent())
334 {
335 // yes it is
336 LIndent(thisStyle, str);
337 m_indent = thisStyle.GetLeftIndent() - 100;
338 m_indents.Add( m_indent );
339 return;
340 }
341 // No it isn't
342
343 int i = m_indents.size() - 1;
344 for (; i > -1; i--)
345 {
346 //Is the second td's left wall of the current indentaion table at the 100+ point-left-side
347 //of the item ?
348 if (m_indent + 100 < thisStyle.GetLeftIndent())
349 {
350 // Yes it is
351 LIndent(thisStyle, str);
352 m_indent = thisStyle.GetLeftIndent() - 100;
353 m_indents.Add( m_indent );
354 break;
355 }
356 else if (m_indent + 100 == thisStyle.GetLeftIndent())
357 break; //exact match
358 else
359 {
360 // No it is not, the second td's left wall of the current indentaion table is at the
361 //right side of the current item horizontally, so close it.
362 str << wxT("</td></tr></table>");
363
364 m_indents.RemoveAt(i);
365
366 if (i < 1)
367 {
368 m_indent=0; break;
369 }
370 m_indent = m_indents[i-1];
371 }
372 }
373 }
374 void wxRichTextHTMLHandler::Indent( const wxTextAttrEx& thisStyle, wxTextOutputStream& str )
375 {
376 //There is no way to indent an item in HTML, but we can use tables.
377
378 // Item -> "Hello world"
379 // Its Left Indentation -> 100
380 // Its Left Sub-Indentation ->40
381 // A typical indentation-table for the item will be construct as the following
382
383 // 3 x nbsp = 60
384 // 2 x nbsp = 40
385 // LSI = Left Sub Indent
386 // LI = Left Indent - LSI
387 //
388 // -------------------------------------------
389 // |&nbsp;&nbsp;nbsp;|nbsp;nbsp;Hello World |
390 // | | | | |
391 // | V | V |
392 // | --LI-- | --LSI-- |
393 // -------------------------------------------
394
395 str << wxT("<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>");
396
397 wxString symbolic_indent = SymbolicIndent( (thisStyle.GetLeftIndent() + thisStyle.GetLeftSubIndent()) - m_indent );
398 str << wxString::Format( wxT("<td>%s</td>"), symbolic_indent.c_str() );
399 str << wxT("<td width=\"100%\">");
400
401 if (thisStyle.GetLeftSubIndent() < 0)
402 {
403 str << SymbolicIndent(~thisStyle.GetLeftSubIndent());
404 }
405 }
406
407 void wxRichTextHTMLHandler::LIndent( const wxTextAttrEx& thisStyle, wxTextOutputStream& str )
408 {
409 // Code:
410 // r.BeginNumberedBullet(1, 200, 60);
411 // r.Newline();
412 // r.WriteText(wxT("first item"));
413 // r.EndNumberedBullet();
414 // r.BeginNumberedBullet(2, 200, 60);
415 // r.Newline();
416 // r.WriteText(wxT("second item."));
417 // r.EndNumberedBullet();
418 //
419 // A typical indentation-table for the item will be construct as the following
420
421 // 1 x nbsp = 20 point
422 // ULI -> 100pt (UL/OL tag indents its sub element by 100 point)
423 // <--------- 100 pt ---------->|
424 // ------------------------------------------------------
425 // |&nbsp;&nbsp;nbsp;&nbsp;nbsp;|<ul> |
426 // | |<-ULI-><li>first item |
427 // | |<-ULI-><li>second item |
428 // | |</ul> |
429 // ------------------------------------------------------
430 // |<-100->|
431
432
433 str << wxT("<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr>");
434
435 wxString symbolic_indent = SymbolicIndent( (thisStyle.GetLeftIndent() - m_indent) - 100);
436 str << wxString::Format( wxT("<td>%s</td>"), symbolic_indent.c_str() );
437 str << wxT("<td width=\"100%\">");
438 }
439
440 void wxRichTextHTMLHandler::TypeOfList( const wxTextAttrEx& thisStyle, wxString& tag )
441 {
442 // We can use number attribute of li tag but not all the browsers support it.
443 // also wxHtmlWindow doesn't support type attribute.
444
445 m_is_ul = false;
446 if (thisStyle.GetBulletStyle() == (wxTEXT_ATTR_BULLET_STYLE_ARABIC|wxTEXT_ATTR_BULLET_STYLE_PERIOD))
447 tag = wxT("<ol type=\"1\">");
448 else if (thisStyle.GetBulletStyle() == wxTEXT_ATTR_BULLET_STYLE_LETTERS_UPPER)
449 tag = wxT("<ol type=\"A\">");
450 else if (thisStyle.GetBulletStyle() == wxTEXT_ATTR_BULLET_STYLE_LETTERS_LOWER)
451 tag = wxT("<ol type=\"a\">");
452 else if (thisStyle.GetBulletStyle() == wxTEXT_ATTR_BULLET_STYLE_ROMAN_UPPER)
453 tag = wxT("<ol type=\"I\">");
454 else if (thisStyle.GetBulletStyle() == wxTEXT_ATTR_BULLET_STYLE_ROMAN_LOWER)
455 tag = wxT("<ol type=\"i\">");
456 else
457 {
458 tag = wxT("<ul>");
459 m_is_ul = true;
460 }
461 }
462
463 wxString wxRichTextHTMLHandler::GetAlignment( const wxTextAttrEx& thisStyle )
464 {
465 switch( thisStyle.GetAlignment() )
466 {
467 case wxTEXT_ALIGNMENT_LEFT:
468 return wxT("left");
469 case wxTEXT_ALIGNMENT_RIGHT:
470 return wxT("right");
471 case wxTEXT_ALIGNMENT_CENTER:
472 return wxT("center");
473 case wxTEXT_ALIGNMENT_JUSTIFIED:
474 return wxT("justify");
475 default:
476 return wxT("left");
477 }
478 }
479
480 void wxRichTextHTMLHandler::WriteImage(wxRichTextImage* image, wxOutputStream& stream)
481 {
482 wxTextOutputStream str(stream);
483
484 str << wxT("<img src=\"");
485
486 #if wxUSE_FILESYSTEM
487 if (GetFlags() & wxRICHTEXT_HANDLER_SAVE_IMAGES_TO_MEMORY)
488 {
489 if (!image->GetImage().Ok() && image->GetImageBlock().GetData())
490 image->LoadFromBlock();
491 if (image->GetImage().Ok() && !image->GetImageBlock().GetData())
492 image->MakeBlock();
493
494 if (image->GetImage().Ok())
495 {
496 wxString ext(image->GetImageBlock().GetExtension());
497 wxString tempFilename(wxString::Format(wxT("image%d.%s"), sm_fileCounter, (const wxChar*) ext));
498 wxMemoryFSHandler::AddFile(tempFilename, image->GetImage(), image->GetImageBlock().GetImageType());
499
500 m_imageLocations.Add(tempFilename);
501
502 str << wxT("memory:") << tempFilename;
503 }
504 else
505 str << wxT("memory:?");
506
507 sm_fileCounter ++;
508 }
509 else if (GetFlags() & wxRICHTEXT_HANDLER_SAVE_IMAGES_TO_FILES)
510 {
511 if (!image->GetImage().Ok() && image->GetImageBlock().GetData())
512 image->LoadFromBlock();
513 if (image->GetImage().Ok() && !image->GetImageBlock().GetData())
514 image->MakeBlock();
515
516 if (image->GetImage().Ok())
517 {
518 wxString tempDir(GetTempDir());
519 if (tempDir.IsEmpty())
520 tempDir = wxFileName::GetTempDir();
521
522 wxString ext(image->GetImageBlock().GetExtension());
523 wxString tempFilename(wxString::Format(wxT("%s/image%d.%s"), tempDir, sm_fileCounter, (const wxChar*) ext));
524 image->GetImageBlock().Write(tempFilename);
525
526 m_imageLocations.Add(tempFilename);
527
528 str << wxFileSystem::FileNameToURL(tempFilename);
529 }
530 else
531 str << wxT("file:?");
532
533 sm_fileCounter ++;
534 }
535 else // if (GetFlags() & wxRICHTEXT_HANDLER_SAVE_IMAGES_TO_BASE64) // this is implied
536 #endif
537 {
538 str << wxT("data:");
539 str << GetMimeType(image->GetImageBlock().GetImageType());
540 str << wxT(";base64,");
541
542 if (image->GetImage().Ok() && !image->GetImageBlock().GetData())
543 image->MakeBlock();
544
545 wxChar* data = b64enc( image->GetImageBlock().GetData(), image->GetImageBlock().GetDataSize() );
546 str << data;
547
548 delete[] data;
549 }
550
551 str << wxT("\" />");
552 }
553
554 long wxRichTextHTMLHandler::PtToSize(long size)
555 {
556 // return approximate size
557 if (size < 9 ) return 1;
558 else if( size < 11 ) return 2;
559 else if( size < 14 ) return 3;
560 else if( size < 18 ) return 4;
561 else if( size < 23 ) return 5;
562 else if( size < 30 ) return 6;
563 else return 7;
564 }
565
566 wxString wxRichTextHTMLHandler::SymbolicIndent(long indent)
567 {
568 wxString in;
569 for(;indent > 0; indent -= 20)
570 in.Append( wxT("&nbsp;") );
571 return in;
572 }
573
574 const wxChar* wxRichTextHTMLHandler::GetMimeType(int imageType)
575 {
576 switch(imageType)
577 {
578 case wxBITMAP_TYPE_BMP:
579 return wxT("image/bmp");
580 case wxBITMAP_TYPE_TIF:
581 return wxT("image/tiff");
582 case wxBITMAP_TYPE_GIF:
583 return wxT("image/gif");
584 case wxBITMAP_TYPE_PNG:
585 return wxT("image/png");
586 case wxBITMAP_TYPE_JPEG:
587 return wxT("image/jpeg");
588 default:
589 return wxT("image/unknown");
590 }
591 }
592
593 // exim-style base64 encoder
594 wxChar* wxRichTextHTMLHandler::b64enc( unsigned char* input, size_t in_len )
595 {
596 // elements of enc64 array must be 8 bit values
597 // otherwise encoder will fail
598 // hmmm.. Does wxT macro define a char as 16 bit value
599 // when compiling with UNICODE option?
600 static const wxChar enc64[] = wxT("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
601 wxChar* output = new wxChar[4*((in_len+2)/3)+1];
602 wxChar* p = output;
603
604 while( in_len-- > 0 )
605 {
606 register wxChar a, b;
607
608 a = *input++;
609
610 *p++ = enc64[ (a >> 2) & 0x3f ];
611
612 if( in_len-- == 0 )
613 {
614 *p++ = enc64[ (a << 4 ) & 0x30 ];
615 *p++ = '=';
616 *p++ = '=';
617 break;
618 }
619
620 b = *input++;
621
622 *p++ = enc64[(( a << 4 ) | ((b >> 4) &0xf )) & 0x3f];
623
624 if( in_len-- == 0 )
625 {
626 *p++ = enc64[ (b << 2) & 0x3f ];
627 *p++ = '=';
628 break;
629 }
630
631 a = *input++;
632
633 *p++ = enc64[ ((( b << 2 ) & 0x3f ) | ((a >> 6)& 0x3)) & 0x3f ];
634
635 *p++ = enc64[ a & 0x3f ];
636 }
637 *p = 0;
638
639 return output;
640 }
641 #endif
642 // wxUSE_STREAMS
643
644 /// Delete the in-memory or temporary files generated by the last operation
645 bool wxRichTextHTMLHandler::DeleteTemporaryImages()
646 {
647 return DeleteTemporaryImages(GetFlags(), m_imageLocations);
648 }
649
650 /// Delete the in-memory or temporary files generated by the last operation
651 bool wxRichTextHTMLHandler::DeleteTemporaryImages(int flags, const wxArrayString& imageLocations)
652 {
653 size_t i;
654 for (i = 0; i < imageLocations.GetCount(); i++)
655 {
656 wxString location = imageLocations[i];
657
658 if (flags & wxRICHTEXT_HANDLER_SAVE_IMAGES_TO_MEMORY)
659 {
660 #if wxUSE_FILESYSTEM
661 wxMemoryFSHandler::RemoveFile(location);
662 #endif
663 }
664 else if (flags & wxRICHTEXT_HANDLER_SAVE_IMAGES_TO_FILES)
665 {
666 if (wxFileExists(location))
667 wxRemoveFile(location);
668 }
669 }
670
671 return true;
672 }
673
674
675 #endif
676 // wxUSE_RICHTEXT
677