#include "cTextNavigationAttachment.h" #include #include // =========================================================================== // cTextNavigationAttachment.cpp Version 1.2 ©1998 Joakim Braun All rights reserved. // =========================================================================== // // An highly customizable attachment that works with LEditText, // LTextEditView and LEditField and implements keyboard navigation within text fields. // The following navigations are included: // // * Go to previous word: Positions cursor at start of previous word. // * Go to next word: Positions cursor at start of next word. // * Select previous word: Extends selection backwards to start of previous word, // as defined by Go to previous word routine. // * Select next word: Extends selection forward to end of next word, // as defined by Go to next word routine. // * Delete previous word: Deletes previous word // * Delete next word: Deletes next word // * Select to start: Extends selection backwards to start of text. // * Select to end: Extends selection forward to end of text. // * Select current line: Selects entire line of text where the cursor // (more specifically, the selStart field of the TERecord) is. // * Smart delete: Normalizes spaces when deleting (no more than one space character, // whatever the context of the selection deleted). // // The key combinations that trigger the attachment may either be set in Constructor // (for each attachment), or saved in a single 'tNav' resource (Resorcerer TMPL is included). // In the last case, identical behavior is ensured for all cTextNavigationAttachments // in the entire application. Any of the routines may also be disabled. // The attachment calls FindWordBreaks() to determine where words start and end. // If you don't like the way it works, go complain to Apple - or write your own subclass. // Note also that several bottleneck routines are public and static, // and so may be used without messing around with attachments. // Templates for Constructor and Resorcerer are included. // cTextNavigationAttachment is free for any and all use. // Do not distribute modified source code under my name. // No support promised, no liability accepted. Provided "as is". // That said, I can be reached at braun@swipnet.se. // (Im new at Power Plant. If anyone knows how to implement this at the LWindow level // instead of at the individual edit field level, please tell me...) // Change history: // 1.0 April 5th, 1998 First release // // 1.1 April 7th, 1998 FindNextWordBreak() now advances to beginning of next word, // not end of current word. Slightly less annoying. // // When deleting words or doing SmartDelete (now renamed SelectSmartDelete()), // we don't call TEDelete() ourselves, since that messes up undo of TEActions. // Instead, change selection accordingly and put a kBackspaceCharCode // into the event record for the text object to process. // // 1.2 August 30, 1998 Removed LTextEdit class (now obsolete) // Added call to SetUpdateCommandStatus() when selection changed // =========================================================================== // Ä Constructor // =========================================================================== // cTextNavigationAttachment::cTextNavigationAttachment(MessageT inMessage, Boolean inExecuteHost) : LAttachment(msg_KeyPress, inExecuteHost){ DefaultInit(); } // =========================================================================== // Ä Stream constructor // =========================================================================== // cTextNavigationAttachment::cTextNavigationAttachment(LStream *inStream) : LAttachment(inStream){ Uint8 useValuesFrom = 0; Int16 resID = 0; *inStream >> useValuesFrom; *inStream >> resID; InitFromStream(inStream); if(useValuesFrom == eUseResourceValues) InitFromResource(resID); // Set mMessage to msg_KeyPress, since we don¥t work with anything else. mMessage = msg_KeyPress; } // =========================================================================== // Ä DefaultInit() // Sets every member to zero, meaning we don't do anything at all // =========================================================================== // void cTextNavigationAttachment::DefaultInit(void){ mCmdCharPreviousWord = 0, mCmdCharNextWord = 0, mCmdCharSelectPreviousWord = 0, mCmdCharSelectNextWord = 0, mCmdCharDeletePreviousWord = 0, mCmdCharDeleteNextWord = 0, mCmdCharSelectToStart = 0, mCmdCharSelectToEnd = 0, mCmdCharSelectCurrentLine = 0, mCmdCharSmartDelete = 0, mCmdKeysPreviousWord = 0, mCmdKeysNextWord = 0, mCmdKeysSelectPreviousWord = 0, mCmdKeysSelectNextWord = 0, mCmdKeysDeletePreviousWord = 0, mCmdKeysDeleteNextWord = 0, mCmdKeysSelectToStart = 0, mCmdKeysSelectToEnd = 0, mCmdKeysSelectCurrentLine = 0, mCmdKeysSmartDelete = 0; // Set mMessage to msg_KeyPress, since we don¥t work with anything else. mMessage = msg_KeyPress; } // =========================================================================== // Ä InitFromStream() // Read Constructor data from stream // =========================================================================== // void cTextNavigationAttachment::InitFromStream(LStream* inStream){ *inStream >> mCmdCharPreviousWord; *inStream >> mCmdKeysPreviousWord; *inStream >> mCmdCharNextWord; *inStream >> mCmdKeysNextWord; *inStream >> mCmdCharSelectPreviousWord; *inStream >> mCmdKeysSelectPreviousWord; *inStream >> mCmdCharSelectNextWord; *inStream >> mCmdKeysSelectNextWord; *inStream >> mCmdCharDeletePreviousWord; *inStream >> mCmdKeysDeletePreviousWord; *inStream >> mCmdCharDeleteNextWord; *inStream >> mCmdKeysDeleteNextWord; *inStream >> mCmdCharSelectToStart; *inStream >> mCmdKeysSelectToStart; *inStream >> mCmdCharSelectToEnd; *inStream >> mCmdKeysSelectToEnd; *inStream >> mCmdCharSelectCurrentLine; *inStream >> mCmdKeysSelectCurrentLine; *inStream >> mCmdCharSmartDelete; *inStream >> mCmdKeysSmartDelete; } // =========================================================================== // Ä InitFromResource() // Read data from 'tNav' resource with ID provided. Only searches app's res file. // =========================================================================== // void cTextNavigationAttachment::InitFromResource(Int16 tNavResID){ StResource theResource(eTextNavigationAttachmentResType, tNavResID, false, true); Handle resHandle = NULL; theResource.Detach(); resHandle = theResource; if(resHandle){ LHandleStream theStream(resHandle); InitFromStream(dynamic_cast(&theStream)); } } #pragma mark - // =========================================================================== // Ä ExecuteSelf() // Try to get a TEHandle from host. If we get it, see if key press matches // any of our settings. Note that presses must match EXACTLY, including // odd modifier keys. Cmd-CapsLock-char_UpArrow is treated differently from Cmd-char_UpArrow. // Also note that when using printing characters as "trigger" characters, // you¥ll have to specify them as correct ASCII values in Constructor/Resorcerer. // If you want to trigger an action by option-A, for instance, you won't get away with // specifying optionKey + ASCII 97 ('a'), since that is not what the system generates // when pressing option-A. ASCII 240 will work,in this case. // =========================================================================== // void cTextNavigationAttachment::ExecuteSelf(MessageT inMessage, void *ioParam){ Int16 theKey = (static_cast(ioParam))->message & charCodeMask; EventModifiers theMods = (static_cast(ioParam))->modifiers & 0xFF00; TEHandle theTEH = GetHostMacTEH(); Boolean executeHost = false; // If for some reason we didn't get the active host TEHandle, just disappear nicely if(!theTEH){ return; } // Walk through the code returned from IsWhatKey() switch(IsWhatKey(theKey, theMods)){ case eGotoPreviousWordKey: cTextNavigationAttachment::GoToPreviousWord(theTEH); break; case eGotoNextWordKey: cTextNavigationAttachment::GoToNextWord(theTEH); break; case eSelectPreviousWordKey: cTextNavigationAttachment::SelectPreviousWord(theTEH); break; case eSelectNextWordKey: cTextNavigationAttachment::SelectNextWord(theTEH); break; case eSelectToStartKey: cTextNavigationAttachment::SelectToStart(theTEH); break; case eSelectToEndKey: cTextNavigationAttachment::SelectToEnd(theTEH); break; case eSelectCurrentLineKey: cTextNavigationAttachment::SelectCurrentLine(theTEH); break; case eDeletePreviousWordKey: cTextNavigationAttachment::DeletePreviousWord(theTEH); // Is there a more readable way to do this? // Keep first three bytes of event message intact, put backspace into last byte (static_cast(ioParam))->message = (static_cast(ioParam))->message & 0xFFFFFF00 + (kBackspaceCharCode & charCodeMask); // Let host execute delete action after we've honed the selection executeHost = true; break; case eDeleteNextWordKey: cTextNavigationAttachment::DeleteNextWord(theTEH); (static_cast(ioParam))->message = (static_cast(ioParam))->message & 0xFFFFFF00 + (kBackspaceCharCode & charCodeMask); executeHost = true; break; case eSmartDeleteKey: cTextNavigationAttachment::SelectSmartDelete(theTEH); (static_cast(ioParam))->message = (static_cast(ioParam))->message & 0xFFFFFF00 + (kBackspaceCharCode & charCodeMask); executeHost = true; break; default: executeHost = true; } SetExecuteHost(executeHost); } // =========================================================================== // Ä GoToPreviousWord() // Get start of selection from TEHandle, // then call TEHandle version of GetPrecedingWordBreak() and set the selection. // =========================================================================== // void cTextNavigationAttachment:: GoToPreviousWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selStart = (*macTEH)->selStart; cTextNavigationAttachment:: GetPrecedingWordBreak(macTEH, selStart); ::TESetSelect(selStart, selStart, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä GoToNextWord() // Get end of selection from TEHandle, // then call TEHandle version of GetNextWordBreak() and set the selection. // =========================================================================== // void cTextNavigationAttachment:: GoToNextWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selEnd = (*macTEH)->selEnd; cTextNavigationAttachment:: GetNextWordBreak(macTEH, selEnd); ::TESetSelect(selEnd, selEnd, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä SelectPreviousWord() // Get start of selection from TEHandle, // then call TEHandle version of GetPrecedingWordBreak() and set the selection. // =========================================================================== // void cTextNavigationAttachment:: SelectPreviousWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selStart = (*macTEH)->selStart; cTextNavigationAttachment:: GetPrecedingWordBreak(macTEH, selStart); ::TESetSelect(selStart, (*macTEH)->selEnd, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä SelectNextWord() // Get end of selection from TEHandle, // then call TEHandle version of GetNextWordBreak() and set the selection. // =========================================================================== // void cTextNavigationAttachment:: SelectNextWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selEnd = (*macTEH)->selEnd; cTextNavigationAttachment:: GetNextWordBreak(macTEH, selEnd); ::TESetSelect((*macTEH)->selStart, selEnd, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä DeletePreviousWord() // Call GetPrecedingWordBreak(), then SmartDelete() the resulting selection. // The reason we don't route everything thru SelectPreviousWord() // is we don't want TESetSelect() to flash. // =========================================================================== // void cTextNavigationAttachment:: DeletePreviousWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selStart = (*macTEH)->selStart; cTextNavigationAttachment:: GetPrecedingWordBreak(macTEH, selStart); (*macTEH)->selStart = selStart; SelectSmartDelete(macTEH); } // =========================================================================== // Ä DeleteNextWord() // Call SelectNextWord(), then SmartDelete() the resulting selection. // =========================================================================== // void cTextNavigationAttachment:: DeleteNextWord(TEHandle macTEH){ if(macTEH == nil) return; Uint16 selEnd = (*macTEH)->selEnd; cTextNavigationAttachment:: GetNextWordBreak(macTEH, selEnd); (*macTEH)->selEnd = selEnd; SelectSmartDelete(macTEH); } // =========================================================================== // Ä SelectToStart() // Nothing strange here. // =========================================================================== // void cTextNavigationAttachment:: SelectToStart(TEHandle macTEH){ if(macTEH == nil) return; ::TESetSelect(0, (*macTEH)->selEnd, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä SelectToEnd() // Nothing strange here. // =========================================================================== // void cTextNavigationAttachment:: SelectToEnd(TEHandle macTEH){ if(macTEH == nil) return; ::TESetSelect((*macTEH)->selStart, (*macTEH)->teLength, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä SelectCurrentLine() // Selects line where cursor is (sefined as selStart). // Looks at the line starts table to figure out what line start of selection is in. // =========================================================================== // void cTextNavigationAttachment:: SelectCurrentLine(TEHandle macTEH){ short currentLine = 0, selStart = 0, newSelStart = 0, newSelEnd = 0; if(!macTEH) return; selStart = (*macTEH)->selStart; if(selStart == (*macTEH)->teLength && (*macTEH)->teLength > 1){ selStart--; currentLine = (*macTEH)->nLines - 1; } else{ // Loop to find out which line start of selection is in for(; currentLine < (*macTEH)->nLines; currentLine++){ // If selection start is greater than or equal to start of current line, // while it is smaller than the start of the next line, we have a hit. if(selStart >= (*macTEH)->lineStarts[currentLine] && selStart < (*macTEH)->lineStarts[currentLine + 1]){ break; } } } newSelStart = (*macTEH)->lineStarts[currentLine], newSelEnd = (*macTEH)->lineStarts[currentLine + 1]; ::TESetSelect(newSelStart, newSelEnd, macTEH); LCommander::SetUpdateCommandStatus(true); } // =========================================================================== // Ä SmartDelete() // Set selection to something that when we call TEDelete() we normalize space characters. // TEHandle version gets text from TEHandle and calls char* version of SmartDelete() // =========================================================================== // void cTextNavigationAttachment:: SelectSmartDelete(TEHandle macTEH){ char* ptr = NULL; CharsHandle theText = NULL; Uint16 selStart, selEnd; if(!macTEH){ return; } theText = TEGetText(macTEH); { StHandleLocker theLock((Handle)theText); ptr = &**theText; selStart = (*macTEH)->selStart; selEnd = (*macTEH)->selEnd; cTextNavigationAttachment:: SelectSmartDelete(ptr, (*macTEH)->teLength, selStart, selEnd); (*macTEH)->selStart = selStart; (*macTEH)->selEnd = selEnd; } } // =========================================================================== // Ä SmartDelete() // char* version expands selection to delete so that no multiple spaces occur. // =========================================================================== // void cTextNavigationAttachment:: SelectSmartDelete(char* textStart, Uint16 textLength, Uint16& selStartIO, Uint16& selEndIO){ Uint16 spacesBefore = 0, spacesAfter = 0, cutSpaceBefore = 0, cutSpaceAfter = 0; if(!textStart){ return; } while(selStartIO - spacesBefore > 0 && isspace(textStart[selStartIO - (spacesBefore + 1)])) spacesBefore++; while(selEndIO + spacesAfter < textLength && isspace(textStart[selEndIO + spacesAfter])){ spacesAfter++; } // Extend selection backwards while we still have more than one space for(int i = spacesBefore; i > 0 && i + spacesAfter > 1; cutSpaceBefore++, i--); // Extend selection forward while we still have more than one space for(int i = spacesAfter; i > 0 && i + (spacesBefore - cutSpaceBefore) > 1; cutSpaceAfter++, i--); // Now we should have a maximum of one space character either before or after the selection. // We don't want one if the cut puts it at start of text. // If new selection will start at second character, // and the first is a space, extend selection backwards if(selStartIO - cutSpaceBefore == 1 && isspace(textStart[0])) cutSpaceBefore++; // If new selection will start at first character, // and last character of selection is a space, extend selection forward if(selStartIO - cutSpaceBefore == 0 && isspace(textStart[selEndIO + cutSpaceAfter])) cutSpaceAfter++; selStartIO -= cutSpaceBefore; selEndIO += cutSpaceAfter; } #pragma mark - // =========================================================================== // Ä GetPrecedingWordBreak() // TEHandle version that calls char* version of GetPrecedingWordBreak() // =========================================================================== // void cTextNavigationAttachment:: GetPrecedingWordBreak(TEHandle macTEH, Uint16& selStartIO){ char* ptr = NULL; CharsHandle theText = NULL; if(!macTEH){ return; } theText = TEGetText(macTEH); StHandleLocker theLock((Handle)theText); ptr = &**theText; cTextNavigationAttachment:: GetPrecedingWordBreak(ptr, (*macTEH)->teLength, selStartIO); } // =========================================================================== // Ä GetPrecedingWordBreak() // char* version that calls system's FindWordBreaks() // =========================================================================== // void cTextNavigationAttachment:: GetPrecedingWordBreak(char* textStart, Uint16 textLength, Uint16& selStartIO){ OffsetTable wordOffsets; if(!textStart || selStartIO > textLength) return; // Back up while we have cursor after a space character while(selStartIO > 0 && isspace(textStart[selStartIO - 1])) selStartIO--; FindWordBreaks(textStart, textLength, selStartIO, false, NULL, wordOffsets, smSystemScript); selStartIO = wordOffsets[0].offFirst; } // =========================================================================== // Ä GetNextWordBreak() // TEHandle version that calls char* version of GetNextWordBreak() // =========================================================================== // void cTextNavigationAttachment:: GetNextWordBreak(TEHandle macTEH, Uint16& selEndIO){ char* ptr = NULL; CharsHandle theText = NULL; if(!macTEH) return; theText = TEGetText(macTEH); StHandleLocker theLock((Handle)theText); ptr = &**theText; cTextNavigationAttachment:: GetNextWordBreak(ptr, (*macTEH)->teLength, selEndIO); } // =========================================================================== // Ä GetNextWordBreak() // char* version that calls system's FindWordBreaks() // =========================================================================== // void cTextNavigationAttachment:: GetNextWordBreak(char* textStart, Uint16 textLength, Uint16& selEndIO){ OffsetTable wordOffsets; if(!textStart || selEndIO > textLength) return; // Go forward while we have cursor before a space character while(isspace(textStart[selEndIO]) && selEndIO < textLength) selEndIO++; FindWordBreaks(textStart, textLength, selEndIO, true, NULL, wordOffsets, smSystemScript); // Go forward while we have cursor before a space character while(isspace(textStart[wordOffsets[0].offSecond]) && wordOffsets[0].offSecond < textLength) wordOffsets[0].offSecond++; selEndIO = wordOffsets[0].offSecond; } #pragma mark - // =========================================================================== // Ä IsWhatKey() // Return what kind of key combination was pressed. // Returns eNotOurKey if it¥s not a key for the attachment. // =========================================================================== // Uint16 cTextNavigationAttachment:: IsWhatKey(Int16 key, EventModifiers mods){ Uint16 result = eNotOurKey; if(key == mCmdCharPreviousWord && mods == mCmdKeysPreviousWord) result = eGotoPreviousWordKey; else if (key == mCmdCharNextWord && mods == mCmdKeysNextWord) result = eGotoNextWordKey; else if (key == mCmdCharSelectPreviousWord && mods == mCmdKeysSelectPreviousWord) result = eSelectPreviousWordKey; else if (key == mCmdCharSelectNextWord && mods == mCmdKeysSelectNextWord) result = eSelectNextWordKey; else if (key == mCmdCharDeletePreviousWord && mods == mCmdKeysDeletePreviousWord) result = eDeletePreviousWordKey; else if (key == mCmdCharDeleteNextWord && mods == mCmdKeysDeleteNextWord) result = eDeleteNextWordKey; else if (key == mCmdCharSelectToStart && mods == mCmdKeysSelectToStart) result = eSelectToStartKey; else if (key == mCmdCharSelectToEnd && mods == mCmdKeysSelectToEnd) result = eSelectToEndKey; else if (key == mCmdCharSelectCurrentLine && mods == mCmdKeysSelectCurrentLine) result = eSelectCurrentLineKey; else if (key == mCmdCharSmartDelete && mods == mCmdKeysSmartDelete) result = eSmartDeleteKey; return result; } // =========================================================================== // Ä GetHostMacTEH() // Get TEHandle from a variety of pane types, since there is no common base class // for panes containing TextEdit records. NULL returned means we didn't find any // class that would give us a TEHandle. Supports LTextEdit, LEditText, LTextEditView, LEditField. // =========================================================================== // TEHandle cTextNavigationAttachment:: GetHostMacTEH(void){ LEditText* textEditThing = dynamic_cast(mOwnerHost); LTextEditView* textEditViewThing = dynamic_cast(mOwnerHost); LEditField* textEditFieldThing = dynamic_cast(mOwnerHost); TEHandle result = NULL; if(textEditThing) result = textEditThing->GetMacTEH(); else if(textEditViewThing) result = textEditViewThing->GetMacTEH(); else if(textEditFieldThing) result = textEditFieldThing->GetMacTEH(); return result; }