/* Open Tracker License Terms and Conditions Copyright (c) 1991-2001, Be Incorporated. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice applies to all licensees and shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of Be Incorporated shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Be Incorporated. BeMail(TM), Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks of Be Incorporated in the United States and other countries. Other brand product names are registered trademarks or trademarks of their respective holders. All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MailApp.h" #include "MailSupport.h" #include "MailWindow.h" #include "Messages.h" #include "Content.h" #include "Utilities.h" #include "FieldMsg.h" #include "Words.h" #define DEBUG_SPELLCHECK 0 #if DEBUG_SPELLCHECK # define DSPELL(x) x #else # define DSPELL(x) ; #endif #define B_TRANSLATION_CONTEXT "Mail" const rgb_color kSpellTextColor = {255, 0, 0, 255}; const rgb_color kHeaderColor = {72, 72, 72, 255}; const rgb_color kQuoteColors[] = { {0, 0, 0x80, 0}, // 3rd, 6th, ... quote level color (blue) {0, 0x80, 0, 0}, // 1st, 4th, ... quote level color (green) {0x80, 0, 0, 0} // 2nd, ... (red) }; const int32 kNumQuoteColors = 3; const rgb_color kDiffColors[] = { {0xb0, 0, 0, 0}, // '-', red {0, 0x90, 0, 0}, // '+', green {0x6a, 0x6a, 0x6a, 0} // '@@', dark grey }; void Unicode2UTF8(int32 c, char **out); inline bool IsInitialUTF8Byte(uchar b) { return ((b & 0xC0) != 0x80); } void Unicode2UTF8(int32 c, char **out) { char *s = *out; ASSERT(c < 0x200000); if (c < 0x80) *(s++) = c; else if (c < 0x800) { *(s++) = 0xc0 | (c >> 6); *(s++) = 0x80 | (c & 0x3f); } else if (c < 0x10000) { *(s++) = 0xe0 | (c >> 12); *(s++) = 0x80 | ((c >> 6) & 0x3f); *(s++) = 0x80 | (c & 0x3f); } else if (c < 0x200000) { *(s++) = 0xf0 | (c >> 18); *(s++) = 0x80 | ((c >> 12) & 0x3f); *(s++) = 0x80 | ((c >> 6) & 0x3f); *(s++) = 0x80 | (c & 0x3f); } *out = s; } static bool FilterHTMLTag(int32 &first, char **t, char *end) { const char *newlineTags[] = { "br", "/p", "/div", "/table", "/tr", NULL}; char *a = *t; // check for some common entities (in ISO-Latin-1) if (first == '&') { // filter out and convert decimal values if (a[1] == '#' && sscanf(a + 2, "%" B_SCNd32 ";", &first) == 1) { t[0] += strchr(a, ';') - a; return false; } const struct { const char *name; int32 code; } entities[] = { // this list is sorted alphabetically to be binary searchable // the current implementation doesn't do this, though // "name" is the entity name, // "code" is the corresponding unicode {"AElig;", 0x00c6}, {"Aacute;", 0x00c1}, {"Acirc;", 0x00c2}, {"Agrave;", 0x00c0}, {"Aring;", 0x00c5}, {"Atilde;", 0x00c3}, {"Auml;", 0x00c4}, {"Ccedil;", 0x00c7}, {"Eacute;", 0x00c9}, {"Ecirc;", 0x00ca}, {"Egrave;", 0x00c8}, {"Euml;", 0x00cb}, {"Iacute;", 0x00cd}, {"Icirc;", 0x00ce}, {"Igrave;", 0x00cc}, {"Iuml;", 0x00cf}, {"Ntilde;", 0x00d1}, {"Oacute;", 0x00d3}, {"Ocirc;", 0x00d4}, {"Ograve;", 0x00d2}, {"Ouml;", 0x00d6}, {"Uacute;", 0x00da}, {"Ucirc;", 0x00db}, {"Ugrave;", 0x00d9}, {"Uuml;", 0x00dc}, {"aacute;", 0x00e1}, {"acirc;", 0x00e2}, {"aelig;", 0x00e6}, {"agrave;", 0x00e0}, {"amp;", '&'}, {"aring;", 0x00e5}, {"atilde;", 0x00e3}, {"auml;", 0x00e4}, {"ccedil;", 0x00e7}, {"copy;", 0x00a9}, {"eacute;", 0x00e9}, {"ecirc;", 0x00ea}, {"egrave;", 0x00e8}, {"euml;", 0x00eb}, {"gt;", '>'}, {"iacute;", 0x00ed}, {"icirc;", 0x00ee}, {"igrave;", 0x00ec}, {"iuml;", 0x00ef}, {"lt;", '<'}, {"nbsp;", ' '}, {"ntilde;", 0x00f1}, {"oacute;", 0x00f3}, {"ocirc;", 0x00f4}, {"ograve;", 0x00f2}, {"ouml;", 0x00f6}, {"quot;", '"'}, {"szlig;", 0x00df}, {"uacute;", 0x00fa}, {"ucirc;", 0x00fb}, {"ugrave;", 0x00f9}, {"uuml;", 0x00fc}, {NULL, 0} }; for (int32 i = 0; entities[i].name; i++) { // entities are case-sensitive int32 length = strlen(entities[i].name); if (!strncmp(a + 1, entities[i].name, length)) { t[0] += length; // note that the '&' is included here first = entities[i].code; return false; } } } // no tag to filter if (first != '<') return false; a++; // is the tag one of the newline tags? bool newline = false; for (int i = 0; newlineTags[i]; i++) { int length = strlen(newlineTags[i]); if (!strncasecmp(a, (char *)newlineTags[i], length) && !isalnum(a[length])) { newline = true; break; } } // oh, it's not, so skip it! if (!strncasecmp(a, "head", 4)) { // skip "head" completely for (; a[0] && a < end; a++) { // Find the end of the HEAD section, or the start of the BODY, // which happens for some malformed spam. if (strncasecmp (a, "= startIndex && (isalnum(string.ByteAt(urlPos - 1)) || string.ByteAt(urlPos - 1) == '-')) urlPos--; } else if (mailtoOffset < baseOffset) { type = TYPE_MAILTO; urlPos = mailtoOffset; } // find the end of the URL based on word boundaries const char* str = string.String() + urlPos; urlLength = strcspn(str, " \t<>)\"\\,\r\n"); // filter out some punctuation marks if they are the last character while (urlLength > 0) { char suffix = str[urlLength - 1]; if (suffix != '.' && suffix != ',' && suffix != '?' && suffix != '!' && suffix != ':' && suffix != ';') break; urlLength--; } if (urlString != NULL) *urlString = BString(string.String() + urlPos, urlLength); return type; } static void CopyQuotes(const char *text, size_t length, char *outText, size_t &outLength) { // count qoute level (to be able to wrap quotes correctly) const char *quote = QUOTE; int32 level = 0; for (size_t i = 0; i < length; i++) { if (text[i] == quote[0]) level++; else if (text[i] != ' ' && text[i] != '\t') break; } // if there are too much quotes, try to preserve the quote color level if (level > 10) level = kNumQuoteColors * 3 + (level % kNumQuoteColors); // copy the quotes to outText const int32 quoteLength = strlen(QUOTE); outLength = 0; while (level-- > 0) { strcpy(outText + outLength, QUOTE); outLength += quoteLength; } } int32 diff_mode(char c) { if (c == '+') return 2; if (c == '-') return 1; if (c == '@') return 3; if (c == ' ') return 0; // everything else ends the diff mode return -1; } bool is_quote_char(char c) { return c == '>' || c == '|'; } /*! Fills the specified text_run_array with the correct values for the specified text. If "view" is NULL, it will assume that "line" lies on a line break, if not, it will correctly retrieve the number of quotes the current line already has. */ void FillInQuoteTextRuns(BTextView* view, quote_context* context, const char* line, int32 length, const BFont& font, text_run_array* style, int32 maxStyles) { text_run* runs = style->runs; int32 index = style->count; bool begin; int32 pos = 0; int32 diffMode = 0; bool inDiff = false; bool wasDiff = false; int32 level = 0; // get index to the beginning of the current line if (context != NULL) { level = context->level; diffMode = context->diff_mode; begin = context->begin; inDiff = context->in_diff; wasDiff = context->was_diff; } else if (view != NULL) { int32 start, end; view->GetSelection(&end, &end); begin = view->TextLength() == 0 || view->ByteAt(view->TextLength() - 1) == '\n'; // the following line works only reliable when text wrapping is set to // off; so the complicated version actually used here is necessary: // start = view->OffsetAt(view->CurrentLine()); const char *text = view->Text(); if (!begin) { // if the text is not the start of a new line, go back // to the first character in the current line for (start = end; start > 0; start--) { if (text[start - 1] == '\n') break; } } // get number of nested qoutes for current line if (!begin && start < end) { begin = true; // if there was no text in this line, there may come // more nested quotes diffMode = diff_mode(text[start]); if (diffMode == 0) { for (int32 i = start; i < end; i++) { if (is_quote_char(text[i])) level++; else if (text[i] != ' ' && text[i] != '\t') { begin = false; break; } } } else inDiff = true; if (begin) { // skip leading spaces (tabs & newlines aren't allowed here) while (line[pos] == ' ') pos++; } } } else begin = true; // set styles for all qoute levels in the text to be inserted for (int32 pos = 0; pos < length;) { int32 next; if (begin && is_quote_char(line[pos])) { begin = false; while (pos < length && line[pos] != '\n') { // insert style for each quote level level++; bool search = true; for (next = pos + 1; next < length; next++) { if ((search && is_quote_char(line[next])) || line[next] == '\n') break; else if (search && line[next] != ' ' && line[next] != '\t') search = false; } runs[index].offset = pos; runs[index].font = font; runs[index].color = level > 0 ? kQuoteColors[level % kNumQuoteColors] : ui_color(B_PANEL_TEXT_COLOR); pos = next; if (++index >= maxStyles) break; } } else { if (begin) { if (!inDiff) { inDiff = !strncmp(&line[pos], "--- ", 4); wasDiff = false; } if (inDiff) { diffMode = diff_mode(line[pos]); if (diffMode < 0) { inDiff = false; wasDiff = true; } } } runs[index].offset = pos; runs[index].font = font; if (wasDiff) runs[index].color = kDiffColors[diff_mode('@') - 1]; else if (diffMode <= 0) { runs[index].color = level > 0 ? kQuoteColors[level % kNumQuoteColors] : ui_color(B_PANEL_TEXT_COLOR); } else runs[index].color = kDiffColors[diffMode - 1]; begin = false; for (next = pos; next < length; next++) { if (line[next] == '\n') { begin = true; wasDiff = false; break; } } pos = next; index++; } if (pos < length) begin = line[pos] == '\n'; if (begin) { pos++; level = 0; wasDiff = false; // skip one leading space (tabs & newlines aren't allowed here) if (!inDiff && pos < length && line[pos] == ' ') pos++; } if (index >= maxStyles) break; } style->count = index; if (context) { // update context for next run context->level = level; context->diff_mode = diffMode; context->begin = begin; context->in_diff = inDiff; context->was_diff = wasDiff; } } // #pragma mark - TextRunArray::TextRunArray(size_t entries) : fNumEntries(entries) { fArray = (text_run_array *)malloc(sizeof(int32) + sizeof(text_run) * entries); if (fArray != NULL) fArray->count = 0; } TextRunArray::~TextRunArray() { free(fArray); } // #pragma mark - TContentView::TContentView(bool incoming, BFont* font, bool showHeader, bool coloredQuotes) : BView("m_content", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE), fFocus(false), fIncoming(incoming) { SetViewUIColor(B_PANEL_BACKGROUND_COLOR); BGroupLayout* layout = new BGroupLayout(B_VERTICAL, 0); SetLayout(layout); fTextView = new TTextView(fIncoming, this, font, showHeader, coloredQuotes); BScrollView* scrollView = new BScrollView("", fTextView, 0, true, true); scrollView->SetBorders(BControlLook::B_TOP_BORDER); AddChild(scrollView); } void TContentView::FindString(const char *str) { int32 finish; int32 pass = 0; int32 start = 0; if (str == NULL) return; // // Start from current selection or from the beginning of the pool // const char *text = fTextView->Text(); int32 count = fTextView->TextLength(); fTextView->GetSelection(&start, &finish); if (start != finish) start = finish; if (!count || text == NULL) return; // // Do the find // while (pass < 2) { long found = -1; char lc = tolower(str[0]); char uc = toupper(str[0]); for (long i = start; i < count; i++) { if (text[i] == lc || text[i] == uc) { const char *s = str; const char *t = text + i; while (*s && (tolower(*s) == tolower(*t))) { s++; t++; } if (*s == 0) { found = i; break; } } } // // Select the text if it worked // if (found != -1) { Window()->Activate(); fTextView->Select(found, found + strlen(str)); fTextView->ScrollToSelection(); fTextView->MakeFocus(true); return; } else if (start) { start = 0; text = fTextView->Text(); count = fTextView->TextLength(); pass++; } else { beep(); return; } } } void TContentView::Focus(bool focus) { if (fFocus != focus) { fFocus = focus; Draw(Frame()); } } void TContentView::MessageReceived(BMessage *msg) { switch (msg->what) { case CHANGE_FONT: { BFont *font; msg->FindPointer("font", (void **)&font); fTextView->UpdateFont(font); fTextView->Invalidate(Bounds()); break; } case M_ADD_QUOTE_LEVEL: { int32 start, finish; fTextView->GetSelection(&start, &finish); fTextView->AddQuote(start, finish); break; } case M_SUB_QUOTE_LEVEL: { int32 start, finish; fTextView->GetSelection(&start, &finish); fTextView->RemoveQuote(start, finish); break; } case M_SIGNATURE: { if (fTextView->IsReaderThreadRunning()) { // Do not add the signature until the reader thread // is finished. Resubmit the message for later processing Window()->PostMessage(msg); break; } entry_ref ref; msg->FindRef("ref", &ref); BFile file(&ref, B_READ_ONLY); if (file.InitCheck() == B_OK) { int32 start, finish; fTextView->GetSelection(&start, &finish); off_t size; file.GetSize(&size); if (size > 32768) // safety against corrupt signatures break; char *signature = (char *)malloc(size); if (signature == NULL) break; ssize_t bytesRead = file.Read(signature, size); if (bytesRead < B_OK) { free (signature); break; } const char *text = fTextView->Text(); int32 length = fTextView->TextLength(); // reserve some empty lines before the signature const char* newLines = "\n\n\n\n"; if (length && text[length - 1] == '\n') newLines++; fTextView->Select(length, length); fTextView->Insert(newLines, strlen(newLines)); length += strlen(newLines); // append the signature fTextView->Select(length, length); fTextView->Insert(signature, bytesRead); fTextView->Select(length, length + bytesRead); fTextView->ScrollToSelection(); // set the editing cursor position fTextView->Select(length - 2 , length - 2); fTextView->ScrollToSelection(); free (signature); } else { beep(); BAlert* alert = new BAlert("", B_TRANSLATE("An error occurred trying to open this " "signature."), B_TRANSLATE("Sorry")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(); } break; } case M_FIND: FindString(msg->FindString("findthis")); break; default: BView::MessageReceived(msg); } } // #pragma mark - TTextView::TTextView(bool incoming, TContentView *view, BFont *font, bool showHeader, bool coloredQuotes) : BTextView("", B_WILL_DRAW | B_NAVIGABLE), fHeader(showHeader), fColoredQuotes(coloredQuotes), fReady(false), fYankBuffer(NULL), fLastPosition(-1), fMail(NULL), fFont(font), fParent(view), fStopLoading(false), fThread(0), fPanel(NULL), fIncoming(incoming), fSpellCheck(false), fRaw(false), fCursor(false), fFirstSpellMark(NULL) { fStopSem = create_sem(1, "reader_sem"); SetStylable(true); SetInsets(4, 4, 4, 4); // TODO: have some font size related value here // (ideally the same as in BTextControl, etc. from BControlLook) fEnclosures = new BList(); // Enclosure pop up menu fEnclosureMenu = new BPopUpMenu("Enclosure", false, false); fEnclosureMenu->SetFont(be_plain_font); fEnclosureMenu->AddItem(new BMenuItem( B_TRANSLATE("Save attachment" B_UTF8_ELLIPSIS), new BMessage(M_SAVE))); fEnclosureMenu->AddItem(new BMenuItem(B_TRANSLATE("Open attachment"), new BMessage(M_OPEN))); // Hyperlink pop up menu fLinkMenu = new BPopUpMenu("Link", false, false); fLinkMenu->SetFont(be_plain_font); fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Open this link"), new BMessage(M_OPEN))); fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Copy link location"), new BMessage(M_COPY))); SetDoesUndo(true); //Undo function fUndoBuffer.On(); fInputMethodUndoBuffer.On(); fUndoState.replaced = false; fUndoState.deleted = false; fInputMethodUndoState.active = false; fInputMethodUndoState.replace = false; } TTextView::~TTextView() { ClearList(); delete fPanel; if (fYankBuffer) free(fYankBuffer); delete_sem(fStopSem); } void TTextView::UpdateFont(const BFont* newFont) { fFont = *newFont; // update the text run array safely with new font text_run_array *runArray = RunArray(0, INT32_MAX); for (int i = 0; i < runArray->count; i++) runArray->runs[i].font = *newFont; SetRunArray(0, INT32_MAX, runArray); FreeRunArray(runArray); } void TTextView::AttachedToWindow() { BTextView::AttachedToWindow(); fFont.SetSpacing(B_FIXED_SPACING); SetFontAndColor(&fFont); if (fMail != NULL) { LoadMessage(fMail, false, NULL); if (fIncoming) MakeEditable(false); } } void TTextView::KeyDown(const char *key, int32 count) { char raw; int32 end; int32 start; uint32 mods; BMessage *msg; int32 textLen = TextLength(); msg = Window()->CurrentMessage(); mods = msg->FindInt32("modifiers"); switch (key[0]) { case B_HOME: if (IsSelectable()) { if (IsEditable()) BTextView::KeyDown(key, count); else { // scroll to the beginning Select(0, 0); ScrollToSelection(); } } break; case B_END: if (IsSelectable()) { if (IsEditable()) BTextView::KeyDown(key, count); else { // scroll to the end int32 length = TextLength(); Select(length, length); ScrollToSelection(); } } break; case 0x02: // ^b - back 1 char if (IsSelectable()) { GetSelection(&start, &end); while (!IsInitialUTF8Byte(ByteAt(--start))) { if (start < 0) { start = 0; break; } } if (start >= 0) { Select(start, start); ScrollToSelection(); } } break; case B_DELETE: if (IsSelectable()) { if ((key[0] == B_DELETE) || (mods & B_CONTROL_KEY)) { // ^d if (IsEditable()) { GetSelection(&start, &end); if (start != end) Delete(); else { for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end)); end++) { if (end > textLen) { end = textLen; break; } } Select(start, end); Delete(); } } } else Select(textLen, textLen); ScrollToSelection(); } break; case 0x05: // ^e - end of line if (IsSelectable() && (mods & B_CONTROL_KEY)) { if (CurrentLine() == CountLines() - 1) Select(TextLength(), TextLength()); else { GoToLine(CurrentLine() + 1); GetSelection(&start, &end); Select(start - 1, start - 1); } } break; case 0x06: // ^f - forward 1 char if (IsSelectable()) { GetSelection(&start, &end); if (end > start) start = end; else { for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end)); end++) { if (end > textLen) { end = textLen; break; } } start = end; } Select(start, start); ScrollToSelection(); } break; case 0x0e: // ^n - next line if (IsSelectable()) { raw = B_DOWN_ARROW; BTextView::KeyDown(&raw, 1); } break; case 0x0f: // ^o - open line if (IsEditable()) { GetSelection(&start, &end); Delete(); char newLine = '\n'; Insert(&newLine, 1); Select(start, start); ScrollToSelection(); } break; case B_PAGE_UP: if (mods & B_CONTROL_KEY) { // ^k kill text from cursor to e-o-line if (IsEditable()) { GetSelection(&start, &end); if ((start != fLastPosition) && (fYankBuffer)) { free(fYankBuffer); fYankBuffer = NULL; } fLastPosition = start; if (CurrentLine() < CountLines() - 1) { GoToLine(CurrentLine() + 1); GetSelection(&end, &end); end--; } else end = TextLength(); if (end < start) break; if (start == end) end++; Select(start, end); if (fYankBuffer) { char *result = (char *)realloc(fYankBuffer, strlen(fYankBuffer) + (end - start) + 1); if (result == NULL) { free(fYankBuffer); fYankBuffer = NULL; break; } fYankBuffer = result; GetText(start, end - start, &fYankBuffer[strlen(fYankBuffer)]); } else { fYankBuffer = (char *)malloc(end - start + 1); if (fYankBuffer == NULL) break; GetText(start, end - start, fYankBuffer); } Delete(); ScrollToSelection(); } break; } BTextView::KeyDown(key, count); break; case 0x10: // ^p goto previous line if (IsSelectable()) { raw = B_UP_ARROW; BTextView::KeyDown(&raw, 1); } break; case 0x19: // ^y yank text if (IsEditable() && fYankBuffer) { Delete(); Insert(fYankBuffer); ScrollToSelection(); } break; default: BTextView::KeyDown(key, count); } } void TTextView::MakeFocus(bool focus) { if (!focus) { // ToDo: can someone please translate this? Otherwise I will remove it - axeld. // MakeFocus(false) は、IM も Inactive になり、そのまま確定される。 // しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED) // を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。 fInputMethodUndoState.active = false; // fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加 if (fInputMethodUndoBuffer.CountItems() > 0) { KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1); if (item->History == K_INSERTED) { fUndoBuffer.MakeNewUndoItem(); fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset, item->History, item->CursorPos); fUndoBuffer.MakeNewUndoItem(); } fInputMethodUndoBuffer.MakeEmpty(); } } BTextView::MakeFocus(focus); fParent->Focus(focus); } void TTextView::MessageReceived(BMessage *msg) { switch (msg->what) { case B_SIMPLE_DATA: { if (fIncoming) break; BMessage message(REFS_RECEIVED); bool isEnclosure = false; bool inserted = false; off_t len = 0; int32 end; int32 start; int32 index = 0; entry_ref ref; while (msg->FindRef("refs", index++, &ref) == B_OK) { BFile file(&ref, B_READ_ONLY); if (file.InitCheck() == B_OK) { BNodeInfo node(&file); char type[B_FILE_NAME_LENGTH]; node.GetType(type); off_t size = 0; file.GetSize(&size); if (!strncasecmp(type, "text/", 5) && size > 0) { len += size; char *text = (char *)malloc(size); if (text == NULL) { puts("no memory!"); return; } if (file.Read(text, size) < B_OK) { puts("could not read from file"); free(text); continue; } if (!inserted) { GetSelection(&start, &end); Delete(); inserted = true; } int32 offset = 0; for (int32 loop = 0; loop < size; loop++) { if (text[loop] == '\n') { Insert(&text[offset], loop - offset + 1); offset = loop + 1; } else if (text[loop] == '\r') { text[loop] = '\n'; Insert(&text[offset], loop - offset + 1); if ((loop + 1 < size) && (text[loop + 1] == '\n')) loop++; offset = loop + 1; } } free(text); } else { isEnclosure = true; message.AddRef("refs", &ref); } } } if (index == 1) { // message doesn't contain any refs - maybe the parent class likes it BTextView::MessageReceived(msg); break; } if (inserted) Select(start, start + len); if (isEnclosure) Window()->PostMessage(&message, Window()); break; } case M_HEADER: msg->FindBool("header", &fHeader); SetText(NULL); LoadMessage(fMail, false, NULL); break; case M_RAW: StopLoad(); msg->FindBool("raw", &fRaw); SetText(NULL); LoadMessage(fMail, false, NULL); break; case M_SELECT: if (IsSelectable()) Select(0, TextLength()); break; case M_SAVE: Save(msg); break; case B_NODE_MONITOR: { int32 opcode; if (msg->FindInt32("opcode", &opcode) == B_NO_ERROR) { dev_t device; if (msg->FindInt32("device", &device) < B_OK) break; ino_t inode; if (msg->FindInt64("node", &inode) < B_OK) break; hyper_text *enclosure; for (int32 index = 0; (enclosure = (hyper_text *)fEnclosures->ItemAt(index++)) != NULL;) { if (device == enclosure->node.device && inode == enclosure->node.node) { if (opcode == B_ENTRY_REMOVED) { enclosure->saved = false; enclosure->have_ref = false; } else if (opcode == B_ENTRY_MOVED) { enclosure->ref.device = device; msg->FindInt64("to directory", &enclosure->ref.directory); const char *name; msg->FindString("name", &name); enclosure->ref.set_name(name); } break; } } } break; } // // Tracker has responded to a BMessage that was dragged out of // this email message. It has created a file for us, we just have to // put the stuff in it. // case B_COPY_TARGET: { BMessage data; if (msg->FindMessage("be:originator-data", &data) == B_OK) { entry_ref directory; const char *name; hyper_text *enclosure; if (data.FindPointer("enclosure", (void **)&enclosure) == B_OK && msg->FindString("name", &name) == B_OK && msg->FindRef("directory", &directory) == B_OK) { switch (enclosure->type) { case TYPE_ENCLOSURE: case TYPE_BE_ENCLOSURE: { // // Enclosure. Decode the data and write it out. // BMessage saveMsg(M_SAVE); saveMsg.AddString("name", name); saveMsg.AddRef("directory", &directory); saveMsg.AddPointer("enclosure", enclosure); Save(&saveMsg, false); break; } case TYPE_URL: { const char *replyType; if (msg->FindString("be:filetypes", &replyType) != B_OK) // drag recipient didn't ask for any specific type, // create a bookmark file as default replyType = "application/x-vnd.Be-bookmark"; BDirectory dir(&directory); BFile file(&dir, name, B_READ_WRITE); if (file.InitCheck() == B_OK) { if (strcmp(replyType, "application/x-vnd.Be-bookmark") == 0) { // we got a request to create a bookmark, stuff // it with the url attribute file.WriteAttr("META:url", B_STRING_TYPE, 0, enclosure->name, strlen(enclosure->name) + 1); } else if (strcasecmp(replyType, "text/plain") == 0) { // create a plain text file, stuff it with // the url as text file.Write(enclosure->name, strlen(enclosure->name)); } BNodeInfo fileInfo(&file); fileInfo.SetType(replyType); } break; } case TYPE_MAILTO: { // // Add some attributes to the already created // person file. Strip out the 'mailto:' if // possible. // char *addrStart = enclosure->name; while (true) { if (*addrStart == ':') { addrStart++; break; } if (*addrStart == '\0') { addrStart = enclosure->name; break; } addrStart++; } const char *replyType; if (msg->FindString("be:filetypes", &replyType) != B_OK) // drag recipient didn't ask for any specific type, // create a bookmark file as default replyType = "application/x-vnd.Be-bookmark"; BDirectory dir(&directory); BFile file(&dir, name, B_READ_WRITE); if (file.InitCheck() == B_OK) { if (!strcmp(replyType, "application/x-person")) { // we got a request to create a bookmark, stuff // it with the address attribute file.WriteAttr("META:email", B_STRING_TYPE, 0, addrStart, strlen(enclosure->name) + 1); } else if (!strcasecmp(replyType, "text/plain")) { // create a plain text file, stuff it with the // email as text file.Write(addrStart, strlen(addrStart)); } BNodeInfo fileInfo(&file); fileInfo.SetType(replyType); } break; } } } else { // // Assume this is handled by BTextView... // (Probably drag clipping.) // BTextView::MessageReceived(msg); } } break; } case B_INPUT_METHOD_EVENT: { int32 im_op; if (msg->FindInt32("be:opcode", &im_op) == B_OK) { switch (im_op) { case B_INPUT_METHOD_STARTED: fInputMethodUndoState.replace = true; fInputMethodUndoState.active = true; break; case B_INPUT_METHOD_STOPPED: fInputMethodUndoState.active = false; if (fInputMethodUndoBuffer.CountItems() > 0) { KUndoItem *undo = fInputMethodUndoBuffer.ItemAt( fInputMethodUndoBuffer.CountItems() - 1); if (undo->History == K_INSERTED) { fUndoBuffer.MakeNewUndoItem(); fUndoBuffer.AddUndo(undo->RedoText, undo->Length, undo->Offset, undo->History, undo->CursorPos); fUndoBuffer.MakeNewUndoItem(); } fInputMethodUndoBuffer.MakeEmpty(); } break; case B_INPUT_METHOD_CHANGED: fInputMethodUndoState.active = true; break; case B_INPUT_METHOD_LOCATION_REQUEST: fInputMethodUndoState.active = true; break; } } BTextView::MessageReceived(msg); break; } case M_REDO: Redo(); break; default: BTextView::MessageReceived(msg); } } void TTextView::MouseDown(BPoint where) { if (IsEditable()) { BPoint point; uint32 buttons; GetMouse(&point, &buttons); if (gDictCount && (buttons == B_SECONDARY_MOUSE_BUTTON)) { int32 offset, start, end, length; const char *text = Text(); offset = OffsetAt(where); if (isalpha(text[offset])) { length = TextLength(); //Find start and end of word //FindSpellBoundry(length, offset, &start, &end); char c; bool isAlpha, isApost, isCap; int32 first; for (first = offset; (first >= 0) && (((c = text[first]) == '\'') || isalpha(c)); first--) {} isCap = isupper(text[++first]); for (start = offset, c = text[start], isAlpha = isalpha(c), isApost = (c=='\''); (start >= 0) && (isAlpha || (isApost && (((c = text[start+1]) != 's') || !isCap) && isalpha(c) && isalpha(text[start-1]))); start--, c = text[start], isAlpha = isalpha(c), isApost = (c == '\'')) {} start++; for (end = offset, c = text[end], isAlpha = isalpha(c), isApost = (c == '\''); (end < length) && (isAlpha || (isApost && (((c = text[end + 1]) != 's') || !isCap) && isalpha(c))); end++, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'')) {} length = end - start; BString srcWord; srcWord.SetTo(text + start, length); bool foundWord = false; BList matches; BString *string; BMenuItem *menuItem; BPopUpMenu menu("Words", false, false); for (int32 i = 0; i < gDictCount; i++) gWords[i]->FindBestMatches(&matches, srcWord.String()); if (matches.CountItems()) { sort_word_list(&matches, srcWord.String()); for (int32 i = 0; (string = (BString *)matches.ItemAt(i)) != NULL; i++) { menu.AddItem((menuItem = new BMenuItem(string->String(), NULL))); if (!strcasecmp(string->String(), srcWord.String())) { menuItem->SetEnabled(false); foundWord = true; } delete string; } } else { menuItem = new BMenuItem(B_TRANSLATE("No matches"), NULL); menuItem->SetEnabled(false); menu.AddItem(menuItem); } BMenuItem *addItem = NULL; if (!foundWord && gUserDict >= 0) { menu.AddSeparatorItem(); addItem = new BMenuItem(B_TRANSLATE("Add"), NULL); menu.AddItem(addItem); } point = ConvertToScreen(where); if ((menuItem = menu.Go(point, false, false)) != NULL) { if (menuItem == addItem) { BString newItem(srcWord.String()); newItem << "\n"; gWords[gUserDict]->InitIndex(); gExactWords[gUserDict]->InitIndex(); gUserDictFile->Write(newItem.String(), newItem.Length()); gWords[gUserDict]->BuildIndex(); gExactWords[gUserDict]->BuildIndex(); if (fSpellCheck) CheckSpelling(0, TextLength()); } else { int32 len = strlen(menuItem->Label()); Select(start, start); Delete(start, end); Insert(start, menuItem->Label(), len); Select(start+len, start+len); } } } return; } else if (fSpellCheck && IsEditable()) { int32 start, end; GetSelection(&start, &end); FindSpellBoundry(1, start, &start, &end); CheckSpelling(start, end); } } else { // is not editable, look for enclosures/links int32 clickOffset = OffsetAt(where); int32 items = fEnclosures->CountItems(); for (int32 loop = 0; loop < items; loop++) { hyper_text *enclosure = (hyper_text*) fEnclosures->ItemAt(loop); if (clickOffset < enclosure->text_start || clickOffset >= enclosure->text_end) continue; // // The user is clicking on this attachment // int32 start; int32 finish; Select(enclosure->text_start, enclosure->text_end); GetSelection(&start, &finish); Window()->UpdateIfNeeded(); bool drag = false; bool held = false; uint32 buttons = 0; if (Window()->CurrentMessage()) { Window()->CurrentMessage()->FindInt32("buttons", (int32 *) &buttons); } // // If this is the primary button, wait to see if the user is going // to single click, hold, or drag. // if (buttons != B_SECONDARY_MOUSE_BUTTON) { BPoint point = where; bigtime_t popupDelay; get_click_speed(&popupDelay); popupDelay *= 2; popupDelay += system_time(); while (buttons && abs((int)(point.x - where.x)) < 4 && abs((int)(point.y - where.y)) < 4 && system_time() < popupDelay) { snooze(10000); GetMouse(&point, &buttons); } if (system_time() < popupDelay) { // // The user either dragged this or released the button. // check if it was dragged. // if (!(abs((int)(point.x - where.x)) < 4 && abs((int)(point.y - where.y)) < 4) && buttons) drag = true; } else { // // The user held the button down. // held = true; } } // // If the user has right clicked on this menu, // or held the button down on it for a while, // pop up a context menu. // if (buttons == B_SECONDARY_MOUSE_BUTTON || held) { // // Right mouse click... Display a menu // BPoint point = where; ConvertToScreen(&point); BMenuItem *item; if ((enclosure->type != TYPE_ENCLOSURE) && (enclosure->type != TYPE_BE_ENCLOSURE)) item = fLinkMenu->Go(point, true); else item = fEnclosureMenu->Go(point, true); BMessage *msg; if (item && (msg = item->Message()) != NULL) { if (msg->what == M_SAVE) { if (fPanel) fPanel->SetEnclosure(enclosure); else { fPanel = new TSavePanel(enclosure, this); fPanel->Window()->Show(); } } else if (msg->what == M_COPY) { // copy link location to clipboard if (be_clipboard->Lock()) { be_clipboard->Clear(); BMessage *clip; if ((clip = be_clipboard->Data()) != NULL) { clip->AddData("text/plain", B_MIME_TYPE, enclosure->name, strlen(enclosure->name)); be_clipboard->Commit(); } be_clipboard->Unlock(); } } else Open(enclosure); } } else { // // Left button. If the user single clicks, open this link. // Otherwise, initiate a drag. // if (drag) { BMessage dragMessage(B_SIMPLE_DATA); dragMessage.AddInt32("be:actions", B_COPY_TARGET); dragMessage.AddString("be:types", B_FILE_MIME_TYPE); switch (enclosure->type) { case TYPE_BE_ENCLOSURE: case TYPE_ENCLOSURE: // // Attachment. The type is specified in the message. // dragMessage.AddString("be:types", B_FILE_MIME_TYPE); dragMessage.AddString("be:filetypes", enclosure->content_type ? enclosure->content_type : ""); dragMessage.AddString("be:clip_name", enclosure->name); break; case TYPE_URL: // // URL. The user can drag it into the tracker to // create a bookmark file. // dragMessage.AddString("be:types", B_FILE_MIME_TYPE); dragMessage.AddString("be:filetypes", "application/x-vnd.Be-bookmark"); dragMessage.AddString("be:filetypes", "text/plain"); dragMessage.AddString("be:clip_name", "Bookmark"); dragMessage.AddString("be:url", enclosure->name); break; case TYPE_MAILTO: // // Mailto address. The user can drag it into the // tracker to create a people file. // dragMessage.AddString("be:types", B_FILE_MIME_TYPE); dragMessage.AddString("be:filetypes", "application/x-person"); dragMessage.AddString("be:filetypes", "text/plain"); dragMessage.AddString("be:clip_name", "Person"); dragMessage.AddString("be:email", enclosure->name); break; default: // // Otherwise it doesn't have a type that I know how // to save. It won't have any types and if any // program wants to accept it, more power to them. // (tracker won't.) // dragMessage.AddString("be:clip_name", "Hyperlink"); } BMessage data; data.AddPointer("enclosure", enclosure); dragMessage.AddMessage("be:originator-data", &data); BRegion selectRegion; GetTextRegion(start, finish, &selectRegion); DragMessage(&dragMessage, selectRegion.Frame(), this); } else { // // User Single clicked on the attachment. Open it. // Open(enclosure); } } return; } } BTextView::MouseDown(where); } void TTextView::MouseMoved(BPoint where, uint32 code, const BMessage *msg) { int32 start = OffsetAt(where); for (int32 loop = fEnclosures->CountItems(); loop-- > 0;) { hyper_text *enclosure = (hyper_text *)fEnclosures->ItemAt(loop); if ((start >= enclosure->text_start) && (start < enclosure->text_end)) { if (!fCursor) SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); fCursor = true; return; } } if (fCursor) { SetViewCursor(B_CURSOR_I_BEAM); fCursor = false; } BTextView::MouseMoved(where, code, msg); } void TTextView::ClearList() { hyper_text *enclosure; while ((enclosure = (hyper_text *)fEnclosures->FirstItem()) != NULL) { fEnclosures->RemoveItem(enclosure); if (enclosure->name) free(enclosure->name); if (enclosure->content_type) free(enclosure->content_type); if (enclosure->encoding) free(enclosure->encoding); if (enclosure->have_ref && !enclosure->saved) { BEntry entry(&enclosure->ref); entry.Remove(); } watch_node(&enclosure->node, B_STOP_WATCHING, this); free(enclosure); } } void TTextView::LoadMessage(BEmailMessage *mail, bool quoteIt, const char *text) { StopLoad(); fMail = mail; ClearList(); MakeSelectable(true); MakeEditable(false); if (text) Insert(text, strlen(text)); //attr_info attrInfo; TTextView::Reader *reader = new TTextView::Reader(fHeader, fRaw, quoteIt, fIncoming, text != NULL, true, // I removed the following, because I absolutely can't imagine why it's // there (the mail kit should be able to deal with non-compliant mails) // -- axeld. // fFile->GetAttrInfo(B_MAIL_ATTR_MIME, &attrInfo) == B_OK, this, mail, fEnclosures, fStopSem); resume_thread(fThread = spawn_thread(Reader::Run, "reader", B_NORMAL_PRIORITY, reader)); } void TTextView::Open(hyper_text *enclosure) { switch (enclosure->type) { case TYPE_URL: { const struct {const char *urlType, *handler; } handlerTable[] = { {"http", B_URL_HTTP}, {"https", B_URL_HTTPS}, {"ftp", B_URL_FTP}, {"gopher", B_URL_GOPHER}, {"mailto", B_URL_MAILTO}, {"news", B_URL_NEWS}, {"nntp", B_URL_NNTP}, {"telnet", B_URL_TELNET}, {"rlogin", B_URL_RLOGIN}, {"tn3270", B_URL_TN3270}, {"wais", B_URL_WAIS}, {"file", B_URL_FILE}, {NULL, NULL} }; const char *handlerToLaunch = NULL; const char *colonPos = strchr(enclosure->name, ':'); if (colonPos) { int urlTypeLength = colonPos - enclosure->name; for (int32 index = 0; handlerTable[index].urlType; index++) { if (!strncasecmp(enclosure->name, handlerTable[index].urlType, urlTypeLength)) { handlerToLaunch = handlerTable[index].handler; break; } } } if (handlerToLaunch) { entry_ref appRef; if (be_roster->FindApp(handlerToLaunch, &appRef) != B_OK) handlerToLaunch = NULL; } if (!handlerToLaunch) handlerToLaunch = "application/x-vnd.Be-Bookmark"; status_t result = be_roster->Launch(handlerToLaunch, 1, &enclosure->name); if (result != B_NO_ERROR && result != B_ALREADY_RUNNING) { beep(); BAlert* alert = new BAlert("", B_TRANSLATE("There is no installed handler for " "URL links."), B_TRANSLATE("Sorry")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(); } break; } case TYPE_MAILTO: if (be_roster->Launch(B_MAIL_TYPE, 1, &enclosure->name) < B_OK) { char *argv[] = {(char *)"Mail", enclosure->name}; be_app->ArgvReceived(2, argv); } break; case TYPE_ENCLOSURE: case TYPE_BE_ENCLOSURE: if (!enclosure->have_ref) { BPath path; if (find_directory(B_SYSTEM_TEMP_DIRECTORY, &path) == B_NO_ERROR) { BDirectory dir(path.Path()); if (dir.InitCheck() == B_NO_ERROR) { char name[B_FILE_NAME_LENGTH]; char baseName[B_FILE_NAME_LENGTH]; strcpy(baseName, enclosure->name ? enclosure->name : "enclosure"); strcpy(name, baseName); for (int32 index = 0; dir.Contains(name); index++) { snprintf(name, B_FILE_NAME_LENGTH, "%s_%" B_PRId32, baseName, index); } BEntry entry(path.Path()); entry_ref ref; entry.GetRef(&ref); BMessage save(M_SAVE); save.AddRef("directory", &ref); save.AddString("name", name); save.AddPointer("enclosure", enclosure); if (Save(&save) != B_NO_ERROR) break; enclosure->saved = false; } } } BMessenger tracker("application/x-vnd.Be-TRAK"); if (tracker.IsValid()) { BMessage openMsg(B_REFS_RECEIVED); openMsg.AddRef("refs", &enclosure->ref); tracker.SendMessage(&openMsg); } break; } } status_t TTextView::Save(BMessage *msg, bool makeNewFile) { const char *name; entry_ref ref; BFile file; BPath path; hyper_text *enclosure; status_t result = B_NO_ERROR; char entry_name[B_FILE_NAME_LENGTH]; msg->FindString("name", &name); msg->FindRef("directory", &ref); msg->FindPointer("enclosure", (void **)&enclosure); BDirectory dir; dir.SetTo(&ref); result = dir.InitCheck(); if (result == B_OK) { if (makeNewFile) { // // Search for the file and delete it if it already exists. // (It may not, that's ok.) // BEntry entry; if (dir.FindEntry(name, &entry) == B_NO_ERROR) entry.Remove(); if ((enclosure->have_ref) && (!enclosure->saved)) { entry.SetTo(&enclosure->ref); // // Added true arg and entry_name so MoveTo clobbers as // before. This may not be the correct behaviour, but // it's the preserved behaviour. // entry.GetName(entry_name); result = entry.MoveTo(&dir, entry_name, true); if (result == B_NO_ERROR) { entry.Rename(name); entry.GetRef(&enclosure->ref); entry.GetNodeRef(&enclosure->node); enclosure->saved = true; return result; } } if (result == B_NO_ERROR) { result = dir.CreateFile(name, &file); if (result == B_NO_ERROR && enclosure->content_type) { char type[B_MIME_TYPE_LENGTH]; if (!strcasecmp(enclosure->content_type, "message/rfc822")) strcpy(type, "text/x-email"); else if (!strcasecmp(enclosure->content_type, "message/delivery-status")) strcpy(type, "text/plain"); else strcpy(type, enclosure->content_type); BNodeInfo info(&file); info.SetType(type); } } } else { // // This file was dragged into the tracker or desktop. The file // already exists. // result = file.SetTo(&dir, name, B_WRITE_ONLY); } } if (enclosure->component == NULL) result = B_ERROR; if (result == B_NO_ERROR) { // // Write the data // enclosure->component->GetDecodedData(&file); BEntry entry; dir.FindEntry(name, &entry); entry.GetRef(&enclosure->ref); enclosure->have_ref = true; enclosure->saved = true; entry.GetPath(&path); update_mime_info(path.Path(), false, true, !cistrcmp("application/octet-stream", enclosure->content_type ? enclosure->content_type : B_EMPTY_STRING)); entry.GetNodeRef(&enclosure->node); watch_node(&enclosure->node, B_WATCH_NAME, this); } if (result != B_NO_ERROR) { beep(); BAlert* alert = new BAlert("", B_TRANSLATE("An error occurred trying to save " "the attachment."), B_TRANSLATE("Sorry")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(); } return result; } void TTextView::StopLoad() { Window()->Unlock(); thread_info info; if (fThread != 0 && get_thread_info(fThread, &info) == B_NO_ERROR) { fStopLoading = true; acquire_sem(fStopSem); int32 result; wait_for_thread(fThread, &result); fThread = 0; release_sem(fStopSem); fStopLoading = false; } Window()->Lock(); } bool TTextView::IsReaderThreadRunning() { if (fThread == 0) return false; thread_info info; for (int i = 5; i > 0; i--, usleep(100000)) if (get_thread_info(fThread, &info) != B_OK) return false; return true; } void TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_encoding encoding) { if (mail == NULL) return; int32 textLength = TextLength(); const char *text = Text(); BTextMailComponent *body = mail->Body(); if (body == NULL) { if (mail->SetBody(body = new BTextMailComponent()) < B_OK) return; } body->SetEncoding(encoding, charset); // Just add the text as a whole if we can, or ... if (!wrap) { body->AppendText(text); return; } // ... do word wrapping. BWindow *window = Window(); char *saveText = strdup(text); BRect saveTextRect = TextRect(); // do this before we start messing with the fonts // the user will never know... window->DisableUpdates(); Hide(); BScrollBar *vScroller = ScrollBar(B_VERTICAL); BScrollBar *hScroller = ScrollBar(B_HORIZONTAL); if (vScroller != NULL) vScroller->SetTarget((BView *)NULL); if (hScroller != NULL) hScroller->SetTarget((BView *)NULL); // Temporarily set the font to a fixed width font for line wrapping // calculations. If the font doesn't have as many of the symbols as // the preferred font, go back to using the user's preferred font. bool *boolArray; int missingCharactersFixedWidth = 0; int missingCharactersPreferredFont = 0; int32 numberOfCharacters; numberOfCharacters = BString(text).CountChars(); if (numberOfCharacters > 0 && (boolArray = (bool *)malloc(sizeof(bool) * numberOfCharacters)) != NULL) { memset(boolArray, 0, sizeof (bool) * numberOfCharacters); be_fixed_font->GetHasGlyphs(text, numberOfCharacters, boolArray); for (int i = 0; i < numberOfCharacters; i++) { if (!boolArray[i]) missingCharactersFixedWidth += 1; } memset(boolArray, 0, sizeof (bool) * numberOfCharacters); fFont.GetHasGlyphs(text, numberOfCharacters, boolArray); for (int i = 0; i < numberOfCharacters; i++) { if (!boolArray[i]) missingCharactersPreferredFont += 1; } free(boolArray); } if (missingCharactersFixedWidth > missingCharactersPreferredFont) SetFontAndColor(0, textLength, &fFont); else // All things being equal, the fixed font is better for wrapping. SetFontAndColor(0, textLength, be_fixed_font); // calculate a text rect that is 72 columns wide BRect newTextRect = saveTextRect; newTextRect.right = newTextRect.left + be_fixed_font->StringWidth("m") * 72; SetTextRect(newTextRect); // hard-wrap, based on TextView's soft-wrapping int32 numLines = CountLines(); bool spaceMoved = false; char *content = (char *)malloc(textLength + numLines * 72); // more we'll ever need if (content != NULL) { int32 contentLength = 0; int32 nextUrlAt = 0, nextUrlLength = 0; BString textStr(text); FindURL(text, 0, nextUrlAt, nextUrlLength, NULL); for (int32 i = 0; i < numLines; i++) { int32 startOffset = OffsetAt(i); if (spaceMoved) { startOffset++; spaceMoved = false; } int32 endOffset = OffsetAt(i + 1); int32 lineLength = endOffset - startOffset; // don't break URLs into several parts if (nextUrlAt >= startOffset && nextUrlAt < endOffset && (nextUrlAt + nextUrlLength) > endOffset) { int32 pos = nextUrlAt + nextUrlLength; // find first break character after the URL for (; text[pos]; pos++) { if (isalnum(text[pos]) || isspace(text[pos])) break; } if (text[pos] && isspace(text[pos]) && text[pos] != '\n') pos++; endOffset += pos - endOffset; lineLength = endOffset - startOffset; // insert a newline (and the same number of quotes) after the // URL to make sure the rest of the text is properly wrapped char buffer[64]; if (text[pos] == '\n') buffer[0] = '\0'; else strcpy(buffer, "\n"); size_t quoteLength; CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength); Insert(pos, buffer, strlen(buffer)); numLines = CountLines(); text = Text(); i++; textStr = BString(text); FindURL(text, endOffset, nextUrlAt, nextUrlLength, NULL); } if (text[endOffset - 1] != ' ' && text[endOffset - 1] != '\n' && text[endOffset] == ' ') { // make sure spaces will be part of this line endOffset++; lineLength++; spaceMoved = true; } memcpy(content + contentLength, text + startOffset, lineLength); contentLength += lineLength; // add a newline to every line except for the ones // that already end in newlines, and the last line if ((text[endOffset - 1] != '\n') && (i < numLines - 1)) { content[contentLength++] = '\n'; // copy quote level of the first line size_t quoteLength; CopyQuotes(text + startOffset, lineLength, content + contentLength, quoteLength); contentLength += quoteLength; } } content[contentLength] = '\0'; body->AppendText(content); free(content); } // reset the text rect and font SetTextRect(saveTextRect); SetText(saveText); free(saveText); SetFontAndColor(0, textLength, &fFont); // should be OK to hook these back up now if (vScroller != NULL) vScroller->SetTarget(this); if (hScroller != NULL) hScroller->SetTarget(this); Show(); window->EnableUpdates(); } // #pragma mark - TTextView::Reader::Reader(bool header, bool raw, bool quote, bool incoming, bool stripHeader, bool mime, TTextView *view, BEmailMessage *mail, BList *list, sem_id sem) : fHeader(header), fRaw(raw), fQuote(quote), fIncoming(incoming), fStripHeader(stripHeader), fMime(mime), fView(view), fMail(mail), fEnclosures(list), fStopSem(sem) { } bool TTextView::Reader::ParseMail(BMailContainer *container, BTextMailComponent *ignore) { int32 count = 0; for (int32 i = 0; i < container->CountComponents(); i++) { if (fView->fStopLoading) return false; BMailComponent *component; if ((component = container->GetComponent(i)) == NULL) { if (fView->fStopLoading) return false; hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text)); if (enclosure == NULL) return false; memset((void*)enclosure, 0, sizeof(hyper_text)); enclosure->type = TYPE_ENCLOSURE; const char *name = "\n\n"; fView->GetSelection(&enclosure->text_start, &enclosure->text_end); enclosure->text_start++; enclosure->text_end += strlen(name) - 1; Insert(name, strlen(name), true); fEnclosures->AddItem(enclosure); continue; } count++; if (component == ignore) continue; if (component->ComponentType() == B_MAIL_MULTIPART_CONTAINER) { BMIMEMultipartMailContainer *c = dynamic_cast(container->GetComponent(i)); ASSERT(c != NULL); if (!ParseMail(c, ignore)) count--; } else if (fIncoming) { hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text)); if (enclosure == NULL) return false; memset((void*)enclosure, 0, sizeof(hyper_text)); enclosure->type = TYPE_ENCLOSURE; enclosure->component = component; BString name; char fileName[B_FILE_NAME_LENGTH]; strcpy(fileName, "untitled"); if (BMailAttachment *attachment = dynamic_cast (component)) attachment->FileName(fileName); BPath path(fileName); enclosure->name = strdup(path.Leaf()); BMimeType type; component->MIMEType(&type); enclosure->content_type = strdup(type.Type()); char typeDescription[B_MIME_TYPE_LENGTH]; if (type.GetShortDescription(typeDescription) != B_OK) strcpy(typeDescription, type.Type() ? type.Type() : B_EMPTY_STRING); name = "\n<"; name.Append(B_TRANSLATE_COMMENT("Attachment: %name% (Type: %type%)", "Don't translate the variables %name% and %type%.")); name.Append(">\n"); name.ReplaceFirst("%name%", enclosure->name); name.ReplaceFirst("%type%", typeDescription); fView->GetSelection(&enclosure->text_start, &enclosure->text_end); enclosure->text_start++; enclosure->text_end += strlen(name.String()) - 1; Insert(name.String(), name.Length(), true); fEnclosures->AddItem(enclosure); } // default: // { // PlainTextBodyComponent *body = dynamic_cast(container->GetComponent(i)); // const char *text; // if (body && (text = body->Text()) != NULL) // Insert(text, strlen(text), false); // } } return count > 0; } bool TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader) { char line[522]; int32 count = 0; const BString dataStr(data, data_len); BString nextUrl; int32 nextUrlPos = 0, nextUrlLength = 0; uint8 nextUrlType = FindURL(dataStr, 0, nextUrlPos, nextUrlLength, &nextUrl); for (int32 loop = 0; loop < data_len; loop++) { if (fView->fStopLoading) return false; if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) { strcpy(&line[count], QUOTE); count += strlen(QUOTE); } if (!fRaw && fIncoming && (loop < data_len - 7)) { uint8 type = (nextUrlPos == loop) ? nextUrlType : 0; if (type) { if (!Insert(line, count, false, isHeader)) return false; count = 0; hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text)); if (enclosure == NULL) return false; memset((void*)enclosure, 0, sizeof(hyper_text)); fView->GetSelection(&enclosure->text_start, &enclosure->text_end); enclosure->type = type; enclosure->name = strdup(nextUrl.String()); if (enclosure->name == NULL) { free(enclosure); return false; } Insert(&data[loop], nextUrlLength, true, isHeader); enclosure->text_end += nextUrlLength; loop += nextUrlLength - 1; fEnclosures->AddItem(enclosure); nextUrlType = FindURL(dataStr, loop, nextUrlPos, nextUrlLength, &nextUrl); continue; } } if (!fRaw && fMime && data[loop] == '=') { if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r')) loop += 2; else line[count++] = data[loop]; } else if (data[loop] != '\r') line[count++] = data[loop]; if (count > 511 || (count && loop == data_len - 1)) { if (!Insert(line, count, false, isHeader)) return false; count = 0; } } return true; } bool TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink, bool isHeader) { if (!count) return true; BFont font(fView->Font()); TextRunArray style(count / 8 + 8); if (fView->fColoredQuotes && !isHeader && !isHyperLink) { FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font, &style.Array(), style.MaxEntries()); } else { text_run_array &array = style.Array(); array.count = 1; array.runs[0].offset = 0; if (isHeader) { array.runs[0].color = isHyperLink ? ui_color(B_LINK_TEXT_COLOR) : kHeaderColor; font.SetSize(font.Size() * 0.9); } else { array.runs[0].color = isHyperLink ? ui_color(B_LINK_TEXT_COLOR) : ui_color(B_PANEL_TEXT_COLOR); } array.runs[0].font = font; } if (!fView->Window()->Lock()) return false; fView->Insert(fView->TextLength(), line, count, &style.Array()); fView->Window()->Unlock(); return true; } status_t TTextView::Reader::Run(void *_this) { Reader *reader = (Reader *)_this; TTextView *view = reader->fView; char *msg = NULL; off_t size = 0; int32 len = 0; if (!reader->Lock()) return B_INTERRUPTED; BFile *file = dynamic_cast(reader->fMail->Data()); if (file != NULL) { len = header_len(file); if (reader->fHeader) size = len; if (reader->fRaw || !reader->fMime) file->GetSize(&size); if (size != 0 && (msg = (char *)malloc(size)) == NULL) goto done; file->Seek(0, 0); if (msg) size = file->Read(msg, size); } // show the header? if (reader->fHeader && len) { // strip all headers except "From", "To", "Reply-To", "Subject", and "Date" if (reader->fStripHeader) { const char *header = msg; char *buffer = NULL; while (strncmp(header, "\r\n", 2)) { const char *eol = header; while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2])) eol += 2; if (eol == NULL) break; eol += 2; // CR+LF belong to the line size_t length = eol - header; free(buffer); buffer = (char *)malloc(length + 1); if (buffer == NULL) goto done; memcpy(buffer, header, length); length = rfc2047_to_utf8(&buffer, &length, length); if (!strncasecmp(header, "Reply-To: ", 10) || !strncasecmp(header, "To: ", 4) || !strncasecmp(header, "From: ", 6) || !strncasecmp(header, "Subject: ", 8) || !strncasecmp(header, "Date: ", 6)) reader->Process(buffer, length, true); header = eol; } free(buffer); reader->Process("\r\n", 2, true); } else if (!reader->Process(msg, len, true)) goto done; } if (reader->fRaw) { if (!reader->Process((const char *)msg + len, size - len)) goto done; } else { //reader->fFile->Seek(0, 0); //BEmailMessage *mail = new BEmailMessage(reader->fFile); BEmailMessage *mail = reader->fMail; // at first, insert the mail body BTextMailComponent *body = NULL; if (mail->BodyText() && !view->fStopLoading) { char *bodyText = const_cast(mail->BodyText()); int32 bodyLength = strlen(bodyText); body = mail->Body(); bool isHTML = false; BMimeType type; if (body->MIMEType(&type) == B_OK && type == "text/html") { // strip out HTML tags char *t = bodyText, *a, *end = bodyText + bodyLength; bodyText = (char *)malloc(bodyLength + 1); isHTML = true; // TODO: is it correct to assume that the text is in Latin-1? // because if it isn't, the code below won't work correctly... for (a = bodyText; t < end; t++) { int32 c = *t; // compact spaces bool space = false; while (c && (c == ' ' || c == '\t')) { c = *(++t); space = true; } if (space) { c = ' '; t--; } else if (FilterHTMLTag(c, &t, end)) // the tag filter continue; Unicode2UTF8(c, &a); } *a = 0; bodyLength = strlen(bodyText); body = NULL; // to add the HTML text as enclosure } if (!reader->Process(bodyText, bodyLength)) goto done; if (isHTML) free(bodyText); } if (!reader->ParseMail(mail, body)) goto done; //reader->fView->fMail = mail; } if (!view->fStopLoading && view->Window()->Lock()) { view->Select(0, 0); view->MakeSelectable(true); if (!reader->fIncoming) view->MakeEditable(true); view->Window()->Unlock(); } done: // restore the reading position if available view->Window()->PostMessage(M_READ_POS); reader->Unlock(); delete reader; free(msg); return B_NO_ERROR; } status_t TTextView::Reader::Unlock() { return release_sem(fStopSem); } bool TTextView::Reader::Lock() { if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR) return false; return true; } //==================================================================== // #pragma mark - TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view) : BFilePanel(B_SAVE_PANEL) { fEnclosure = enclosure; fView = view; if (enclosure->name) SetSaveText(enclosure->name); } void TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg) { const char *name = NULL; BMessage save(M_SAVE); entry_ref ref; if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) { save.AddPointer("enclosure", fEnclosure); save.AddString("name", name); save.AddRef("directory", &ref); fView->Window()->PostMessage(&save, fView); } } void TSavePanel::SetEnclosure(hyper_text *enclosure) { fEnclosure = enclosure; if (enclosure->name) SetSaveText(enclosure->name); else SetSaveText(""); if (!IsShowing()) Show(); Window()->Activate(); } //-------------------------------------------------------------------- // #pragma mark - void TTextView::InsertText(const char *insertText, int32 length, int32 offset, const text_run_array *runs) { ContentChanged(); // Undo function int32 cursorPos, dummy; GetSelection(&cursorPos, &dummy); if (fInputMethodUndoState.active) { // IMアクティブ時は、一旦別のバッファへ記憶 fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos); fInputMethodUndoState.replace = false; } else { if (fUndoState.replaced) { fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos); } else { if (length == 1 && insertText[0] == 0x0a) fUndoBuffer.MakeNewUndoItem(); fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos); if (length == 1 && insertText[0] == 0x0a) fUndoBuffer.MakeNewUndoItem(); } } fUndoState.replaced = false; fUndoState.deleted = false; struct text_runs : text_run_array { text_run _runs[1]; } style; if (runs == NULL && IsEditable()) { style.count = 1; style.runs[0].offset = 0; style.runs[0].font = fFont; style.runs[0].color = ui_color(B_PANEL_TEXT_COLOR); runs = &style; } BTextView::InsertText(insertText, length, offset, runs); if (fSpellCheck && IsEditable()) { UpdateSpellMarks(offset, length); rgb_color color; GetFontAndColor(offset - 1, NULL, &color); const char *text = Text(); if (length > 1 || isalpha(text[offset + 1]) || (!isalpha(text[offset]) && text[offset] != '\'') || (color.red == kSpellTextColor.red && color.green == kSpellTextColor.green && color.blue == kSpellTextColor.blue)) { int32 start, end; FindSpellBoundry(length, offset, &start, &end); DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end)); DSPELL(printf("\t\"%10.10s...\"\n", text + start)); CheckSpelling(start, end); } } } void TTextView::DeleteText(int32 start, int32 finish) { ContentChanged(); // Undo function int32 cursorPos, dummy; GetSelection(&cursorPos, &dummy); if (fInputMethodUndoState.active) { if (fInputMethodUndoState.replace) { fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos); fInputMethodUndoState.replace = false; } else { fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos); } } else fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos); fUndoState.deleted = true; fUndoState.replaced = true; BTextView::DeleteText(start, finish); if (fSpellCheck && IsEditable()) { UpdateSpellMarks(start, start - finish); int32 s, e; FindSpellBoundry(1, start, &s, &e); CheckSpelling(s, e); } } void TTextView::ContentChanged(void) { BLooper *looper = Looper(); if (looper == NULL) return; BMessage msg(FIELD_CHANGED); msg.AddInt32("bitmask", FIELD_BODY); msg.AddPointer("source", this); looper->PostMessage(&msg); } void TTextView::CheckSpelling(int32 start, int32 end, int32 flags) { const char *text = Text(); const char *next, *endPtr, *word = NULL; int32 wordLength = 0, wordOffset; int32 nextHighlight = start; BString testWord; bool isCap = false; bool isAlpha; bool isApost; rgb_color normalColor = ui_color(B_PANEL_TEXT_COLOR); for (next = text + start, endPtr = text + end; next <= endPtr; next++) { //printf("next=%c\n", *next); // ToDo: this has to be refined to other languages... // Alpha signifies the start of a word isAlpha = isalpha(*next); isApost = (*next == '\''); if (!word && isAlpha) { //printf("Found word start\n"); word = next; wordLength++; isCap = isupper(*word); } else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1])) && !(isCap && isApost && (next[1] == 's'))) { // Word continues check wordLength++; //printf("Word continues...\n"); } else if (word) { // End of word reached //printf("Word End\n"); // Don't check single characters if (wordLength > 1) { bool isUpper = true; // Look for all uppercase for (int32 i = 0; i < wordLength; i++) { if (word[i] == '\'') break; if (islower(word[i])) { isUpper = false; break; } } // Don't check all uppercase words if (!isUpper) { bool foundMatch = false; wordOffset = word - text; testWord.SetTo(word, wordLength); testWord = testWord.ToLower(); DSPELL(printf("Testing: \"%s\"\n", testWord.String())); int32 key = -1; if (gDictCount) key = gExactWords[0]->GetKey(testWord.String()); // Search all dictionaries for (int32 i = 0; i < gDictCount; i++) { if (gExactWords[i]->Lookup(key) >= 0) { foundMatch = true; break; } } if (!foundMatch) { if (flags & S_CLEAR_ERRORS) RemoveSpellMark(nextHighlight, wordOffset); if (flags & S_SHOW_ERRORS) AddSpellMark(wordOffset, wordOffset + wordLength); } else if (flags & S_CLEAR_ERRORS) RemoveSpellMark(nextHighlight, wordOffset + wordLength); nextHighlight = wordOffset + wordLength; } } // Reset state to looking for word word = NULL; wordLength = 0; } } if (nextHighlight <= end && (flags & S_CLEAR_ERRORS) != 0 && nextHighlight < TextLength()) SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &normalColor); } void TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end) { int32 start, end, textLength; const char *text = Text(); textLength = TextLength(); for (start = offset - 1; start >= 0 && (isalpha(text[start]) || text[start] == '\''); start--) {} start++; for (end = offset + length; end < textLength && (isalpha(text[end]) || text[end] == '\''); end++) {} *_start = start; *_end = end; } TTextView::spell_mark * TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark) { spell_mark *lastMark = NULL; for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) { if (spellMark->start < end && spellMark->end > start) { if (_previousMark) *_previousMark = lastMark; return spellMark; } lastMark = spellMark; } return NULL; } void TTextView::UpdateSpellMarks(int32 offset, int32 length) { DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length)); spell_mark *spellMark; for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) { DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end)); if (spellMark->end < offset) continue; if (spellMark->start > offset) spellMark->start += length; spellMark->end += length; DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end)); } } status_t TTextView::AddSpellMark(int32 start, int32 end) { DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end)); // check if there is already a mark for this passage spell_mark *spellMark = FindSpellMark(start, end); if (spellMark) { if (spellMark->start == start && spellMark->end == end) { DSPELL(printf("\tfound one\n")); return B_OK; } DSPELL(printf("\tremove old one\n")); RemoveSpellMark(start, end); } spellMark = (spell_mark *)malloc(sizeof(spell_mark)); if (spellMark == NULL) return B_NO_MEMORY; spellMark->start = start; spellMark->end = end; spellMark->style = RunArray(start, end); // set the spell marks appearance BFont font(fFont); font.SetFace(B_BOLD_FACE | B_ITALIC_FACE); SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor); // add it to the queue spellMark->next = fFirstSpellMark; fFirstSpellMark = spellMark; return B_OK; } bool TTextView::RemoveSpellMark(int32 start, int32 end) { DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end)); // find spell mark spell_mark *lastMark = NULL; spell_mark *spellMark = FindSpellMark(start, end, &lastMark); if (spellMark == NULL) { DSPELL(printf("\tnot found!\n")); return false; } DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end)); // dequeue the spell mark if (lastMark) lastMark->next = spellMark->next; else fFirstSpellMark = spellMark->next; if (spellMark->start < start) start = spellMark->start; if (spellMark->end > end) end = spellMark->end; // reset old text run array SetRunArray(start, end, spellMark->style); free(spellMark->style); free(spellMark); return true; } void TTextView::RemoveSpellMarks() { spell_mark *spellMark, *nextMark; for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) { nextMark = spellMark->next; // reset old text run array SetRunArray(spellMark->start, spellMark->end, spellMark->style); free(spellMark->style); free(spellMark); } fFirstSpellMark = NULL; } void TTextView::EnableSpellCheck(bool enable) { if (fSpellCheck == enable) return; fSpellCheck = enable; int32 textLength = TextLength(); if (fSpellCheck) { // work-around for a bug in the BTextView class // which causes lots of flicker int32 start, end; GetSelection(&start, &end); if (start != end) Select(start, start); CheckSpelling(0, textLength); if (start != end) Select(start, end); } else RemoveSpellMarks(); } void TTextView::WindowActivated(bool flag) { if (!flag) { // WindowActivated(false) は、IM も Inactive になり、そのまま確定される。 // しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED) // を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。 // Haikuで修正されることを願って暫定処置としている。 fInputMethodUndoState.active = false; // fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加 if (fInputMethodUndoBuffer.CountItems() > 0) { KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1); if (item->History == K_INSERTED) { fUndoBuffer.MakeNewUndoItem(); fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset, item->History, item->CursorPos); fUndoBuffer.MakeNewUndoItem(); } fInputMethodUndoBuffer.MakeEmpty(); } } BTextView::WindowActivated(flag); } void TTextView::AddQuote(int32 start, int32 finish) { BRect rect = Bounds(); int32 lineStart; GoToLine(CurrentLine()); GetSelection(&lineStart, &lineStart); lineStart = LineStart(lineStart); // make sure that we're changing the whole last line, too int32 lineEnd = finish > lineStart ? finish - 1 : finish; { const char *text = Text(); while (text[lineEnd] && text[lineEnd] != '\n') lineEnd++; } Select(lineStart, lineEnd); int32 textLength = lineEnd - lineStart; char *text = (char *)malloc(textLength + 1); if (text == NULL) return; GetText(lineStart, textLength, text); int32 quoteLength = strlen(QUOTE); int32 targetLength = 0; char *target = NULL; int32 lastLine = 0; for (int32 index = 0; index < textLength; index++) { if (text[index] == '\n' || index == textLength - 1) { // add quote to this line int32 lineLength = index - lastLine + 1; char* result = (char *)realloc(target, targetLength + lineLength + quoteLength); if (result == NULL) { free(target); free(text); return; } target = result; // copy the quote sign memcpy(&target[targetLength], QUOTE, quoteLength); targetLength += quoteLength; // copy the rest of the line memcpy(&target[targetLength], &text[lastLine], lineLength); targetLength += lineLength; lastLine = index + 1; } } // replace with quoted text free(text); Delete(); if (fColoredQuotes) { const BFont *font = Font(); TextRunArray style(targetLength / 8 + 8); FillInQuoteTextRuns(NULL, NULL, target, targetLength, font, &style.Array(), style.MaxEntries()); Insert(target, targetLength, &style.Array()); } else Insert(target, targetLength); free(target); // redo the old selection (compute the new start if necessary) Select(start + quoteLength, finish + (targetLength - textLength)); ScrollTo(rect.LeftTop()); } void TTextView::RemoveQuote(int32 start, int32 finish) { BRect rect = Bounds(); GoToLine(CurrentLine()); int32 lineStart; GetSelection(&lineStart, &lineStart); lineStart = LineStart(lineStart); // make sure that we're changing the whole last line, too int32 lineEnd = finish > lineStart ? finish - 1 : finish; const char *text = Text(); while (text[lineEnd] && text[lineEnd] != '\n') lineEnd++; Select(lineStart, lineEnd); int32 length = lineEnd - lineStart; char *target = (char *)malloc(length + 1); if (target == NULL) return; int32 quoteLength = strlen(QUOTE); int32 removed = 0; text += lineStart; for (int32 index = 0; index < length;) { // find out the length of the current line int32 lineLength = 0; while (index + lineLength < length && text[lineLength] != '\n') lineLength++; // include the newline to be part of this line if (text[lineLength] == '\n' && index + lineLength + 1 < length) lineLength++; if (!strncmp(text, QUOTE, quoteLength)) { // remove quote length -= quoteLength; removed += quoteLength; lineLength -= quoteLength; text += quoteLength; } if (lineLength == 0) { target[index] = '\0'; break; } memcpy(&target[index], text, lineLength); text += lineLength; index += lineLength; } if (removed) { Delete(); if (fColoredQuotes) { const BFont *font = Font(); TextRunArray style(length / 8 + 8); FillInQuoteTextRuns(NULL, NULL, target, length, font, &style.Array(), style.MaxEntries()); Insert(target, length, &style.Array()); } else Insert(target, length); // redo old selection bool noSelection = start == finish; if (start > lineStart + quoteLength) start -= quoteLength; else start = lineStart; if (noSelection) finish = start; else finish -= removed; } free(target); Select(start, finish); ScrollTo(rect.LeftTop()); } int32 TTextView::LineStart(int32 offset) { if (offset <= 0) return 0; while (offset > 0) { offset = PreviousByte(offset); if (ByteAt(offset) == B_ENTER) return offset + 1; } return offset; } int32 TTextView::PreviousByte(int32 offset) const { if (offset <= 0) return 0; int32 count = 6; for (--offset; offset > 0 && count; --offset, --count) { if ((ByteAt(offset) & 0xC0) != 0x80) break; } return count ? offset : 0; } void TTextView::Undo(BClipboard */*clipboard*/) { if (fInputMethodUndoState.active) return; int32 length, offset, cursorPos; undo_type history; char *text; status_t status; status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos); if (status == B_OK) { fUndoBuffer.Off(); switch (history) { case K_INSERTED: BTextView::Delete(offset, offset + length); Select(offset, offset); break; case K_DELETED: BTextView::Insert(offset, text, length); Select(offset, offset + length); break; case K_REPLACED: BTextView::Delete(offset, offset + length); status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos); if (status == B_OK && history == K_DELETED) { BTextView::Insert(offset, text, length); Select(offset, offset + length); } else { ::beep(); BAlert* alert = new BAlert("", B_TRANSLATE("Inconsistency occurred in the undo/redo " "buffer."), B_TRANSLATE("OK")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(); } break; } ScrollToSelection(); ContentChanged(); fUndoBuffer.On(); } } void TTextView::Redo() { if (fInputMethodUndoState.active) return; int32 length, offset, cursorPos; undo_type history; char *text; status_t status; bool replaced; status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced); if (status == B_OK) { fUndoBuffer.Off(); switch (history) { case K_INSERTED: BTextView::Insert(offset, text, length); Select(offset, offset + length); break; case K_DELETED: BTextView::Delete(offset, offset + length); if (replaced) { fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced); BTextView::Insert(offset, text, length); } Select(offset, offset + length); break; case K_REPLACED: ::beep(); BAlert* alert = new BAlert("", B_TRANSLATE("Inconsistency occurred in the undo/redo " "buffer."), B_TRANSLATE("OK")); alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); alert->Go(); break; } ScrollToSelection(); ContentChanged(); fUndoBuffer.On(); } }