diff --git a/docs/changes.txt b/docs/changes.txt index 4531ba349b..79e8774988 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -104,6 +104,7 @@ All (GUI): smaller than the minimal size. - Added parameter to wxScrolledWindow XRC handler. - wxRichTextCtrl performance has been improved considerably. +- Several wxRichTextCtrl style, paste and undo bugs fixed. - wxNotebook RTTI corrected, so now wxDynamicCast(notebook, wxBookCtrlBase) works. - When focus is set to wxScrolledWindow child, scroll it into view. diff --git a/samples/richtext/richtext.cpp b/samples/richtext/richtext.cpp index 894045e0ae..54b354b7a8 100644 --- a/samples/richtext/richtext.cpp +++ b/samples/richtext/richtext.cpp @@ -166,6 +166,8 @@ public: void OnDemoteList(wxCommandEvent& event); void OnClearList(wxCommandEvent& event); + void OnReload(wxCommandEvent& event); + void OnViewHTML(wxCommandEvent& event); void OnSwitchStyleSheets(wxCommandEvent& event); @@ -182,6 +184,9 @@ public: // Forward command events to the current rich text control, if any bool ProcessEvent(wxEvent& event); + // Write text + void WriteInitialText(); + private: // any class wishing to process wxWidgets events must use this macro DECLARE_EVENT_TABLE() @@ -207,6 +212,8 @@ enum ID_FORMAT_PARAGRAPH, ID_FORMAT_CONTENT, + ID_RELOAD, + ID_INSERT_SYMBOL, ID_INSERT_URL, @@ -292,6 +299,8 @@ BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_MENU(ID_FORMAT_PARAGRAPH_SPACING_MORE, MyFrame::OnParagraphSpacingMore) EVT_MENU(ID_FORMAT_PARAGRAPH_SPACING_LESS, MyFrame::OnParagraphSpacingLess) + EVT_MENU(ID_RELOAD, MyFrame::OnReload) + EVT_MENU(ID_INSERT_SYMBOL, MyFrame::OnInsertSymbol) EVT_MENU(ID_INSERT_URL, MyFrame::OnInsertURL) @@ -561,6 +570,8 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, fileMenu->Append(wxID_SAVE, _T("&Save\tCtrl+S"), _T("Save a file")); fileMenu->Append(wxID_SAVEAS, _T("&Save As...\tF12"), _T("Save to a new file")); fileMenu->AppendSeparator(); + fileMenu->Append(ID_RELOAD, _T("&Reload Text\tF2"), _T("Reload the initial text")); + fileMenu->AppendSeparator(); fileMenu->Append(ID_PAGE_SETUP, _T("Page Set&up..."), _T("Page setup")); fileMenu->Append(ID_PRINT, _T("&Print...\tCtrl+P"), _T("Print")); fileMenu->Append(ID_PREVIEW, _T("Print Pre&view"), _T("Print preview")); @@ -714,8 +725,16 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, styleListCtrl->SetRichTextCtrl(m_richTextCtrl); styleListCtrl->UpdateStyles(); + WriteInitialText(); +} + +// Write text +void MyFrame::WriteInitialText() +{ wxRichTextCtrl& r = *m_richTextCtrl; + r.SetDefaultStyle(wxRichTextAttr()); + r.BeginSuppressUndo(); r.BeginParagraphSpacing(0, 20); @@ -727,9 +746,9 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, wxString lineBreak = (wxChar) 29; - r.WriteText(wxString(wxT("Welcome to wxRichTextCtrl, a wxWidgets control")) + lineBreak + wxT("for editing and presenting styled text and images")); + r.WriteText(wxString(wxT("Welcome to wxRichTextCtrl, a wxWidgets control")) + lineBreak + wxT("for editing and presenting styled text and images\n")); r.EndFontSize(); - r.Newline(); + //r.Newline(); r.BeginItalic(); r.WriteText(wxT("by Julian Smart")); @@ -740,10 +759,10 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, r.WriteImage(wxBitmap(zebra_xpm)); - r.EndAlignment(); + r.Newline(); + r.Newline(); - r.Newline(); - r.Newline(); + r.EndAlignment(); r.WriteText(wxT("What can you do with this thing? ")); @@ -778,57 +797,57 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, r.WriteText(wxT(" Next we'll show an indented paragraph.")); + r.Newline(); + r.BeginLeftIndent(60); - r.Newline(); - r.WriteText(wxT("It was in January, the most down-trodden month of an Edinburgh winter. An attractive woman came into the cafe, which is nothing remarkable.")); - r.EndLeftIndent(); - r.Newline(); + r.EndLeftIndent(); + r.WriteText(wxT("Next, we'll show a first-line indent, achieved using BeginLeftIndent(100, -40).")); - r.BeginLeftIndent(100, -40); r.Newline(); + r.BeginLeftIndent(100, -40); + r.WriteText(wxT("It was in January, the most down-trodden month of an Edinburgh winter. An attractive woman came into the cafe, which is nothing remarkable.")); + r.Newline(); + r.EndLeftIndent(); - r.Newline(); - r.WriteText(wxT("Numbered bullets are possible, again using subindents:")); + r.Newline(); r.BeginNumberedBullet(1, 100, 60); + r.WriteText(wxT("This is my first item. Note that wxRichTextCtrl can apply numbering and bullets automatically based on list styles, but this list is formatted explicitly by setting indents.")); r.Newline(); - r.WriteText(wxT("This is my first item. Note that wxRichTextCtrl can apply numbering and bullets automatically based on list styles, but this list is formatted explicitly by setting indents.")); r.EndNumberedBullet(); r.BeginNumberedBullet(2, 100, 60); + r.WriteText(wxT("This is my second item.")); r.Newline(); - r.WriteText(wxT("This is my second item.")); r.EndNumberedBullet(); - r.Newline(); - r.WriteText(wxT("The following paragraph is right-indented:")); + r.Newline(); r.BeginRightIndent(200); - r.Newline(); r.WriteText(wxT("It was in January, the most down-trodden month of an Edinburgh winter. An attractive woman came into the cafe, which is nothing remarkable.")); - r.EndRightIndent(); - r.Newline(); + r.EndRightIndent(); + r.WriteText(wxT("The following paragraph is right-aligned with 1.5 line spacing:")); + r.Newline(); r.BeginAlignment(wxTEXT_ALIGNMENT_RIGHT); r.BeginLineSpacing(wxTEXT_ATTR_LINE_SPACING_HALF); - r.Newline(); - r.WriteText(wxT("It was in January, the most down-trodden month of an Edinburgh winter. An attractive woman came into the cafe, which is nothing remarkable.")); + r.Newline(); r.EndLineSpacing(); r.EndAlignment(); @@ -842,48 +861,46 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, attr.SetTabs(tabs); r.SetDefaultStyle(attr); - r.Newline(); r.WriteText(wxT("This line contains tabs:\tFirst tab\tSecond tab\tThird tab")); - r.Newline(); + r.WriteText(wxT("Other notable features of wxRichTextCtrl include:")); + r.Newline(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("Compatibility with wxTextCtrl API")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("Easy stack-based BeginXXX()...EndXXX() style setting in addition to SetStyle()")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("XML loading and saving")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("Undo/Redo, with batching option and Undo suppressing")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("Clipboard copy and paste")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("wxRichTextStyleSheet with named character and paragraph styles, and control for applying named styles")); + r.Newline(); r.EndSymbolBullet(); r.BeginSymbolBullet(wxT('*'), 100, 60); - r.Newline(); r.WriteText(wxT("A design that can easily be extended to other content types, ultimately with text boxes, tables, controls, and so on")); - r.EndSymbolBullet(); - r.Newline(); + r.EndSymbolBullet(); // Make a style suitable for showing a URL wxRichTextAttr urlStyle; @@ -902,6 +919,8 @@ MyFrame::MyFrame(const wxString& title, wxWindowID id, const wxPoint& pos, r.WriteText(wxT("Note: this sample content was generated programmatically from within the MyFrame constructor in the demo. The images were loaded from inline XPMs. Enjoy wxRichTextCtrl!")); + r.Newline(); + r.EndParagraphSpacing(); r.EndSuppressUndo(); @@ -1310,6 +1329,12 @@ void MyFrame::OnParagraphSpacingLess(wxCommandEvent& WXUNUSED(event)) } } +void MyFrame::OnReload(wxCommandEvent& WXUNUSED(event)) +{ + m_richTextCtrl->Clear(); + WriteInitialText(); +} + void MyFrame::OnViewHTML(wxCommandEvent& WXUNUSED(event)) { wxDialog dialog(this, wxID_ANY, _("HTML"), wxDefaultPosition, wxSize(500, 400), wxDEFAULT_DIALOG_STYLE); diff --git a/src/richtext/richtextbuffer.cpp b/src/richtext/richtextbuffer.cpp index 2dce9e255b..97adc64873 100644 --- a/src/richtext/richtextbuffer.cpp +++ b/src/richtext/richtextbuffer.cpp @@ -1006,15 +1006,18 @@ wxRichTextRange wxRichTextParagraphLayoutBox::AddParagraphs(const wxString& text wxChar ch = text[i]; if (ch == wxT('\n') || ch == wxT('\r')) { - wxRichTextPlainText* plainText = (wxRichTextPlainText*) para->GetChildren().GetFirst()->GetData(); - plainText->SetText(line); + if (i != (len-1)) + { + wxRichTextPlainText* plainText = (wxRichTextPlainText*) para->GetChildren().GetFirst()->GetData(); + plainText->SetText(line); - para = new wxRichTextParagraph(wxEmptyString, this, pStyle, cStyle); + para = new wxRichTextParagraph(wxEmptyString, this, pStyle, cStyle); - AppendChild(para); + AppendChild(para); - lastPara = para; - line = wxEmptyString; + lastPara = para; + line = wxEmptyString; + } } else line += ch; @@ -1072,6 +1075,8 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag wxRichTextParagraph* para = GetParagraphAtPosition(position); if (para) { + wxTextAttrEx originalAttr = para->GetAttributes(); + wxRichTextObjectList::compatibility_iterator node = m_children.Find(para); // Now split at this position, returning the object to insert the new @@ -1092,11 +1097,6 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag wxRichTextParagraph* firstPara = wxDynamicCast(firstParaNode->GetData(), wxRichTextParagraph); wxASSERT (firstPara != NULL); - // Apply the new paragraph attributes to the existing paragraph - wxTextAttrEx attr(para->GetAttributes()); - wxRichTextApplyStyle(attr, firstPara->GetAttributes()); - para->SetAttributes(attr); - wxRichTextObjectList::compatibility_iterator objectNode = firstPara->GetChildren().GetFirst(); while (objectNode) { @@ -1144,13 +1144,27 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag wxRichTextParagraph* firstPara = wxDynamicCast(firstParaNode->GetData(), wxRichTextParagraph); wxASSERT(firstPara != NULL); + para->SetAttributes(firstPara->GetAttributes()); + + // Save empty paragraph attributes for appending later + // These are character attributes deliberately set for a new paragraph. Without this, + // we couldn't pass default attributes when appending a new paragraph. + wxTextAttrEx emptyParagraphAttributes; + wxRichTextObjectList::compatibility_iterator objectNode = firstPara->GetChildren().GetFirst(); + + if (objectNode && firstPara->GetChildren().GetCount() == 1 && objectNode->GetData()->IsEmpty()) + emptyParagraphAttributes = objectNode->GetData()->GetAttributes(); + while (objectNode) { - wxRichTextObject* newObj = objectNode->GetData()->Clone(); + if (!objectNode->GetData()->IsEmpty()) + { + wxRichTextObject* newObj = objectNode->GetData()->Clone(); - // Append - para->AppendChild(newObj); + // Append + para->AppendChild(newObj); + } objectNode = objectNode->GetNext(); } @@ -1164,22 +1178,9 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag wxRichTextObjectList::compatibility_iterator i = fragment.GetChildren().GetFirst()->GetNext(); wxRichTextParagraph* finalPara = para; - // If there was only one paragraph, we need to insert a new one. - if (!i) - { - finalPara = new wxRichTextParagraph; + bool needExtraPara = (!i || !fragment.GetPartialParagraph()); - // TODO: These attributes should come from the subsequent paragraph - // when originally deleted, since the subsequent para takes on - // the previous para's attributes. - finalPara->SetAttributes(firstPara->GetAttributes()); - - if (nextParagraph) - InsertChild(finalPara, nextParagraph); - else - AppendChild(finalPara); - } - else while (i) + while (i) { wxRichTextParagraph* para = wxDynamicCast(i->GetData(), wxRichTextParagraph); wxASSERT( para != NULL ); @@ -1194,6 +1195,18 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag i = i->GetNext(); } + // If there was only one paragraph, or we have full paragraphs in our fragment, + // we need to insert a new one. + if (needExtraPara) + { + finalPara = new wxRichTextParagraph; + + if (nextParagraph) + InsertChild(finalPara, nextParagraph); + else + AppendChild(finalPara); + } + // 4. Add back the remaining content. if (finalPara) { @@ -1203,11 +1216,15 @@ bool wxRichTextParagraphLayoutBox::InsertFragment(long position, wxRichTextParag if (finalPara->GetChildCount() == 0) { wxRichTextPlainText* text = new wxRichTextPlainText(wxEmptyString); + text->SetAttributes(emptyParagraphAttributes); finalPara->AppendChild(text); } } + if (finalPara && finalPara != para) + finalPara->SetAttributes(originalAttr); + return true; } } @@ -1391,6 +1408,7 @@ bool wxRichTextParagraphLayoutBox::DeleteRange(const wxRichTextRange& range) { wxRichTextObjectList::compatibility_iterator node = m_children.GetFirst(); + wxRichTextParagraph* firstPara = NULL; while (node) { wxRichTextParagraph* obj = wxDynamicCast(node->GetData(), wxRichTextParagraph); @@ -1402,61 +1420,78 @@ bool wxRichTextParagraphLayoutBox::DeleteRange(const wxRichTextRange& range) if (!obj->GetRange().IsOutside(range)) { + wxRichTextRange thisRange = obj->GetRange(); + // Deletes the content of this object within the given range obj->DeleteRange(range); // If the whole paragraph is within the range to delete, // delete the whole thing. - if (range.GetStart() <= obj->GetRange().GetStart() && range.GetEnd() >= obj->GetRange().GetEnd()) + if (range.GetStart() <= thisRange.GetStart() && range.GetEnd() >= thisRange.GetEnd()) { // Delete the whole object RemoveChild(obj, true); + obj = NULL; } + else if (!firstPara) + firstPara = obj; + // If the range includes the paragraph end, we need to join this // and the next paragraph. - else if (range.Contains(obj->GetRange().GetEnd())) + if (range.GetEnd() <= thisRange.GetEnd()) { // We need to move the objects from the next paragraph // to this paragraph - if (next) + wxRichTextParagraph* nextParagraph = NULL; + if ((range.GetEnd() < thisRange.GetEnd()) && obj) + nextParagraph = obj; + else { - wxRichTextParagraph* nextParagraph = wxDynamicCast(next->GetData(), wxRichTextParagraph); - next = next->GetNext(); - if (nextParagraph) - { - // Delete the stuff we need to delete - nextParagraph->DeleteRange(range); - - // Move the objects to the previous para - wxRichTextObjectList::compatibility_iterator node1 = nextParagraph->GetChildren().GetFirst(); - - while (node1) - { - wxRichTextObject* obj1 = node1->GetData(); - - // If the object is empty, optimise it out - if (obj1->IsEmpty()) - { - delete obj1; - } - else - { - obj->AppendChild(obj1); - } - - wxRichTextObjectList::compatibility_iterator next1 = node1->GetNext(); - nextParagraph->GetChildren().Erase(node1); - - node1 = next1; - } - - // Delete the paragraph - RemoveChild(nextParagraph, true); - - } + // We're ending at the end of the paragraph, so merge the _next_ paragraph. + if (next) + nextParagraph = wxDynamicCast(next->GetData(), wxRichTextParagraph); } + bool applyFinalParagraphStyle = firstPara && nextParagraph && nextParagraph != firstPara; + + wxTextAttrEx nextParaAttr; + if (applyFinalParagraphStyle) + nextParaAttr = nextParagraph->GetAttributes(); + + if (firstPara && nextParagraph && firstPara != nextParagraph) + { + // Move the objects to the previous para + wxRichTextObjectList::compatibility_iterator node1 = nextParagraph->GetChildren().GetFirst(); + + while (node1) + { + wxRichTextObject* obj1 = node1->GetData(); + + // If the object is empty, optimise it out + if (obj1->IsEmpty()) + { + delete obj1; + } + else + { + firstPara->AppendChild(obj1); + } + + wxRichTextObjectList::compatibility_iterator next1 = node1->GetNext(); + nextParagraph->GetChildren().Erase(node1); + + node1 = next1; + } + + // Delete the paragraph + RemoveChild(nextParagraph, true); + } + + if (applyFinalParagraphStyle) + firstPara->SetAttributes(nextParaAttr); + + return true; } } @@ -4729,6 +4764,7 @@ bool wxRichTextPlainText::Merge(wxRichTextObject* object) if (textObject) { m_text += textObject->GetText(); + wxRichTextApplyStyle(m_attributes, textObject->GetAttributes()); return true; } else @@ -4861,21 +4897,14 @@ bool wxRichTextBuffer::InsertParagraphsWithUndo(long pos, const wxRichTextParagr action->GetNewParagraphs() = paragraphs; - if (p) - { - wxRichTextObjectList::compatibility_iterator node = m_children.GetLast(); - while (node) - { - wxRichTextParagraph* obj = (wxRichTextParagraph*) node->GetData(); - obj->SetAttributes(*p); - node = node->GetPrevious(); - } - } - action->SetPosition(pos); + wxRichTextRange range = wxRichTextRange(pos, pos + paragraphs.GetRange().GetEnd() - 1); + if (!paragraphs.GetPartialParagraph()) + range.SetEnd(range.GetEnd()+1); + // Set the range we'll need to delete in Undo - action->SetRange(wxRichTextRange(pos, pos + paragraphs.GetRange().GetEnd() - 1)); + action->SetRange(range); SubmitAction(action); @@ -4945,6 +4974,10 @@ bool wxRichTextBuffer::InsertNewlineWithUndo(long pos, wxRichTextCtrl* ctrl, int if (p) newPara->SetAttributes(*p); + // Use the default character style + if (!GetDefaultStyle().IsDefault() && newPara->GetChildren().GetFirst()) + newPara->GetChildren().GetFirst()->GetData()->SetAttributes(GetDefaultStyle()); + // Set the range we'll need to delete in Undo action->SetRange(wxRichTextRange(pos, pos)); @@ -5065,26 +5098,6 @@ bool wxRichTextBuffer::DeleteRangeWithUndo(const wxRichTextRange& range, wxRichT // Copy the fragment that we'll need to restore in Undo CopyFragment(range, action->GetOldParagraphs()); - // Special case: if there is only one (non-partial) paragraph, - // we must save the *next* paragraph's style, because that - // is the style we must apply when inserting the content back - // when undoing the delete. (This is because we're merging the - // paragraph with the previous paragraph and throwing away - // the style, and we need to restore it.) - if (!action->GetOldParagraphs().GetPartialParagraph() && action->GetOldParagraphs().GetChildCount() == 1) - { - wxRichTextParagraph* lastPara = GetParagraphAtPosition(range.GetStart()); - if (lastPara) - { - wxRichTextParagraph* nextPara = GetParagraphAtPosition(range.GetEnd()+1); - if (nextPara) - { - wxRichTextParagraph* para = (wxRichTextParagraph*) action->GetOldParagraphs().GetChild(0); - para->SetAttributes(nextPara->GetAttributes()); - } - } - } - SubmitAction(action); return true;