// FV_Cursor.cpp /* Part of AbiWord. * * Author: Mikael Nordell (tamlin@algonet.se) * !!! Not yet released for public consumption!!! * !!! Don NOT CVS commit this !!! * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. */ #include "fv_Cursor.h" #include "ut_assert.h" #include "ut_timer.h" #include "ut_debugmsg.h" #include "gr_Graphics.h" #include "fp_Run.h" #include "fp_Line.h" #include "fl_BlockLayout.h" #include "fv_View.h" // Since this is not-committed code, and this is only for // development and debugging purposes, I felt it acceptable // (read appropriate) to use the real, suppported by all our // current compilers, datatype 'bool'. Note that this will be // gone before commit. const bool bDebugMsg_setCurrentDocPos = false; const bool bDebugMsg_setDocPos = false; const bool bDebugBeepOnBlink = false; ////////////////////////////////////////////////////////////////// // // DOCUMENT POSITIONS // // PT_DocPosition is "byte offset" within the document. // // fl_BlockLayout::getPosition() returns DocPos of the line // that this block belongs to (close enough explanation I think, // but feel free to fill in/rewrite). // // FV_View::getPoint() returns the "insert point" in DocPos. // This *will* probably go away, since FV_View should get this from // FV_Cursor. // // fp_Run::getBlockOffset() returns offset for this run from its // fl_BlockLayout in DocPos units. // // COORDINATE SYSTEMS - all in "display units/pixels". // This was something that I had to research (since this is not mentioned // anywhere else AFAIK) and it _might_ not be 100% correct. // If so, please fix the comments. // // - fp_Run::getX() fp_Line relative x. // - fp_Line::getX() fl_BlockLayout relative x. // - fp_Run::findPointCoords() fp_Container (fp_Column) relative. // - fp_Line::getOffsets() fp_Container (fp_Column) relative. // - fl_BlockLayout::findPointCoords() fp_Container (fp_Column) relative. // // - fp_Run::mapXYToPosition() wants run-relative x coord. // // class FV_Cursor_impl { public: FV_Cursor_impl(FV_Cursor& owner, FV_View& view, FL_DocLayout& layout) : m_layout(layout), m_caret(owner, view), m_docPos(0), m_nStickyX(0), m_xPoint(0), m_yPoint(0), m_nPointHeight(0), m_bBOL(UT_FALSE), m_bEOL(UT_FALSE) { } void updateInternal(); // The 'bEOL' argument is needed since IP (Insert Position) might // be at BOL at line two while the _cursor_ is at EOL at line 1. // Think about it for a while, you'll get it. fp_Run* setCurrentDocPos( PT_DocPosition dp, UT_Bool bEOL = UT_FALSE); fp_Run* getCurrentRun(); PT_DocPosition clampToDocument(PT_DocPosition dp); FL_DocLayout& m_layout; // The layout for the view FV_Caret m_caret; // The (possibly blinking) gfx caret PT_DocPosition m_docPos; // Absolute DocPos of IP/Caret UT_uint32 m_nStickyX; // fp_Container (fp_Column) relative UT_sint32 m_xPoint; // fp_Container (fp_Column) relative UT_sint32 m_yPoint; // fp_Container (fp_Column) relative UT_sint32 m_nPointHeight; // Height of char at IP UT_Bool m_bBOL; // self explanatory UT_Bool m_bEOL; // self explanatory }; void FV_Cursor_impl::updateInternal() { fp_Run* pRun = setCurrentDocPos(m_docPos, m_bEOL); if (!pRun || !pRun->getLine() || !pRun->getBlock()) { m_bBOL = UT_FALSE; m_bEOL = UT_FALSE; m_nStickyX = 0; } else { if (!pRun->isFirstRunOnLine()) { m_bBOL = UT_FALSE; } if (!pRun->isLastRunOnLine()) { m_bEOL = UT_FALSE; } } } // returns the run it got set to fp_Run* FV_Cursor_impl::setCurrentDocPos(PT_DocPosition docPos, UT_Bool bEOL) { if (!docPos /* || docPos == m_docPos */) { return 0; } fl_BlockLayout* pBlock = m_layout.findBlockAtPosition(docPos); UT_ASSERT(pBlock); if (!pBlock) { // What to do?! Internal error! // Should we crash here, or is it nicer to let the user continue? return 0; } UT_sint32 nPosX; UT_sint32 nPosY; UT_sint32 nPtHeight; fp_Run* pRun = pBlock->findPointCoords(docPos /* [in] */, m_bEOL /* [in] */, nPosX /* [out] */, nPosY /* [out] */, nPtHeight /* [out] */ ); // If we don't get an fp_Run for a DocPos something is severely broken. // What to do?! UT_ASSERT(pRun); if (pRun) { if (bDebugMsg_setCurrentDocPos) { UT_DEBUGMSG(("%s(%d): setCurrentDocPos(%d), EOL=%d\n", __FILE__, __LINE__, docPos, (int)bEOL)); } m_xPoint = nPosX; m_yPoint = nPosY; m_nPointHeight = nPtHeight; m_docPos = docPos; } return pRun; } fp_Run* FV_Cursor_impl::getCurrentRun() { return setCurrentDocPos(m_docPos, m_bEOL); } PT_DocPosition FV_Cursor_impl::clampToDocument(PT_DocPosition dp) { PT_DocPosition dpMin; PT_DocPosition dpMax; const PD_Document* pDoc = m_layout.getDocument(); UT_ASSERT(pDoc); if (!pDoc || !pDoc->getBounds(UT_FALSE, dpMin) || !pDoc->getBounds(UT_TRUE, dpMax)) { // Internal error. What to do? Panic! // When in trouble, when in doubt, run in circles, scream and shout. return 0; } dp = MyMin(dp, dpMax); dp = MyMax(dp, dpMin); return dp; } ////////////////////////////////////////////////////////////////// FV_Cursor::FV_Cursor(FV_View& view, FL_DocLayout& layout) : m_pimpl(new FV_Cursor_impl(*this, view, layout)) { } FV_Cursor::~FV_Cursor() { delete m_pimpl; } fp_Run* FV_Cursor::getCurrentRun() { return m_pimpl->getCurrentRun(); } fp_Line* FV_Cursor::getCurrentLine() { fp_Run* pRun = getCurrentRun(); return pRun ? pRun->getLine() : 0; } // fl_BlockLayout for current DocPos fl_BlockLayout* FV_Cursor::getCurrentBlockLayout() { fp_Line* pLine = getCurrentLine(); return pLine ? pLine->getBlock() : 0; } PT_DocPosition FV_Cursor::getDocPos() const { return m_pimpl->m_docPos; } UT_sint32 FV_Cursor::getContainerRelativeX() const { return m_pimpl->m_xPoint; } UT_sint32 FV_Cursor::getContainerRelativeY() const { return m_pimpl->m_yPoint; } UT_sint32 FV_Cursor::getPointHeight() const { return m_pimpl->m_nPointHeight; } void FV_Cursor::setDocPos(PT_DocPosition dp, UT_Bool bEOL) { if (bDebugMsg_setDocPos) { UT_DEBUGMSG(("%s(%d): setDocPos(%d)\n", __FILE__, __LINE__, dp)); } dp = m_pimpl->clampToDocument(dp); if (m_pimpl->m_docPos != dp) { disable(); m_pimpl->m_docPos = dp; m_pimpl->setCurrentDocPos(dp, bEOL); enable(); } } // painting void FV_Cursor::disable() { m_pimpl->m_caret.disable(); } void FV_Cursor::enable() { m_pimpl->m_caret.enable(); } void FV_Cursor::setBlink(UT_Bool bBlink) { m_pimpl->m_caret.setBlink(bBlink); } ////////////////////////////////////////////////////////////////// // movement commands // I know, it's bad form to use macros. But since all of these are // just forwarders, and I wanted some reporting... #if 1 #define THIS_REPORTER_(fn) UT_DEBUGMSG(("FV_Cursor::"#fn"\n")) #define FV_CURSOR_DEF_FWD_FUN_(fn) \ void FV_Cursor::fn() { \ THIS_REPORTER_(fn); \ _do_movement(_ ## fn); \ } FV_CURSOR_DEF_FWD_FUN_(up) FV_CURSOR_DEF_FWD_FUN_(down) FV_CURSOR_DEF_FWD_FUN_(left) FV_CURSOR_DEF_FWD_FUN_(right) FV_CURSOR_DEF_FWD_FUN_(pageUp) FV_CURSOR_DEF_FWD_FUN_(pageDown) FV_CURSOR_DEF_FWD_FUN_(home) FV_CURSOR_DEF_FWD_FUN_(end) FV_CURSOR_DEF_FWD_FUN_(wordLeft) FV_CURSOR_DEF_FWD_FUN_(wordRight) #undef FV_CURSOR_DEF_FWD_FUN_ #undef THIS_REPORTER_ #else void FV_Cursor::up() { _do_movement(_up); } void FV_Cursor::down() { _do_movement(_down); } void FV_Cursor::left() { _do_movement(_left); } void FV_Cursor::right() { _do_movement(_right); } void FV_Cursor::pageUp() { _do_movement(_pageUp); } void FV_Cursor::pageDown() { _do_movement(_pageDown); } void FV_Cursor::home() { _do_movement(_home); } void FV_Cursor::end() { _do_movement(_end); } void FV_Cursor::wordLeft() { _do_movement(_wordLeft); } void FV_Cursor::wordRight() { _do_movement(_wordRight); } #endif ////////////////////////////////////////////////////////////////// // accessors UT_Bool FV_Cursor::isAtEOL() const { return m_pimpl->m_bEOL; } UT_Bool FV_Cursor::isAtBOL() const { return m_pimpl->m_bBOL; } ////////////////////////////////////////////////////////////////// // protected default handlers // When we entered these, we KNOW that // 1. We have a valid fp_Run pointer. // 2. That run is on a fp_Line. void FV_Cursor::_up() { fp_Line* pLine = _getPrevLine(); if (!pLine || pLine->isEmpty()) { // beep? return; } // now we need to find X-offset in display/layout coords, // save that as stickyX, find closest x-pos from the // previous line, find DocPos for that x-pos, and set // our 'm_docPos' to the found value. if (!_getStickyX()) { _setStickyX(getContainerRelativeX()); } fp_Run* pNewRun = _getRunAtColumnOffset(*pLine, _getStickyX()); UT_ASSERT(pNewRun); UT_Bool bBOL = UT_FALSE; // dummy UT_Bool bEOL = UT_FALSE; // dummy const UT_sint32 nRunRelativeX = _containerToLineX(*pLine, _getStickyX()) - pNewRun->getX(); pNewRun->mapXYToPosition( nRunRelativeX, 0, // unused arg m_pimpl->m_docPos, bBOL, bEOL); } void FV_Cursor::_down() { fp_Line* pLine = _getNextLine(); if (!pLine || pLine->isEmpty()) { // beep? return; } // now we need to find X-offset in display/layout coords, // save that as stickyX, find closest x-pos from the // next line, find DocPos for that x-pos, and set // our 'm_docPos' to the found value. if (!_getStickyX()) { _setStickyX(getContainerRelativeX()); } fp_Run* pNewRun = _getRunAtColumnOffset(*pLine, _getStickyX()); UT_ASSERT(pNewRun); UT_Bool bBOL = UT_FALSE; // dummy UT_Bool bEOL = UT_FALSE; // dummy const UT_sint32 nRunRelativeX = _containerToLineX(*pLine, _getStickyX()) - pNewRun->getX(); pNewRun->mapXYToPosition( nRunRelativeX, 0, // unused arg m_pimpl->m_docPos, bBOL, bEOL); } void FV_Cursor::_left() { PT_DocPosition dpMin; PD_Document* pDoc = m_pimpl->m_layout.getDocument(); UT_ASSERT(pDoc); if (!pDoc || !pDoc->getBounds(UT_FALSE, dpMin)) { // Internal error. What to do? return; } UT_ASSERT(getDocPos() >= dpMin); if (getDocPos() == dpMin) { // Already at BOD return; } if (isAtBOL()) { UT_ASSERT(getCurrentRun()->isFirstRunOnLine()); fp_Line* pLine = _getPrevLine(); if (!pLine) { // we're already at the first line. // beep? return; } } _setStickyX(0); setDocPos(getDocPos() - 1); } void FV_Cursor::_right() { if (isAtEOL()) { // EOL here means that the Caret is at EOL, but IP/DocPos // is really before the first character of the next line. UT_ASSERT(getCurrentRun()->isFirstRunOnLine()); _setBOL(); // don't change docPos! return; } _setStickyX(0); setDocPos(getDocPos() + 1); } void FV_Cursor::_pageUp() { UT_ASSERT(("C'mon, implement me!" == 0)); } void FV_Cursor::_pageDown() { UT_ASSERT(("C'mon, implement me!" == 0)); } void FV_Cursor::_home() { #if 1 if (isAtBOL()) #else // the following is the same as IsAtBOL() if (getCurrentRun()->isFirstRunOnLine() && getCurrentRun()->getBlockOffset() + getCurrentBlockLayout()->getPosition() == m_pimpl->m_docPos) #endif { return; } _setStickyX(0); setDocPos(getCurrentLine()->getFirstRun()->getBlockOffset() + getCurrentBlockLayout()->getPosition()); _setBOL(); } void FV_Cursor::_end() { if (isAtEOL()) { return; } _setStickyX(0); // Note that the usage of 'getCurrentRun()->getLength()' gets us to the // first DocPos _after_ that run. This is correct, and not an oversight. setDocPos(getCurrentLine()->getLastRun()->getBlockOffset() + getCurrentBlockLayout()->getPosition() + getCurrentRun()->getLength()); _setEOL(); } void FV_Cursor::_wordLeft() { UT_ASSERT(("C'mon, implement me!" == 0)); // This is hairy. You'll have to look at FV_View::_getDocPosFromPoint // to even get a basic grip on it. // Look into the switch FV_DOCPOS_BOW; } void FV_Cursor::_wordRight() { UT_ASSERT(("C'mon, implement me!" == 0)); // This is hairy. You'll have to look at FV_View::_getDocPosFromPoint // to even get a basic grip on it. // Look into the switch FV_DOCPOS_EOW; } // end of overridables ////////////////////////////////////////////////////////////////// void FV_Cursor::_updateInternal() { m_pimpl->updateInternal(); } void FV_Cursor::_setEOL(UT_Bool bEOL) { m_pimpl->m_bEOL = bEOL; if (bEOL) { _setStickyX(0); _setBOL(UT_FALSE); } } void FV_Cursor::_setBOL(UT_Bool bBOL) { m_pimpl->m_bBOL = bBOL; if (bBOL) { _setStickyX(0); _setEOL(UT_FALSE); } } void FV_Cursor::_setStickyX(UT_sint32 x) { m_pimpl->m_nStickyX = x; } UT_sint32 FV_Cursor::_getStickyX() { return m_pimpl->m_nStickyX; } fp_Line* FV_Cursor::_getPrevLine() { fl_BlockLayout* pBlockLayout = getCurrentBlockLayout(); UT_ASSERT(pBlockLayout); return pBlockLayout ? pBlockLayout->findPrevLineInDocument(getCurrentLine()) : 0; } fp_Line* FV_Cursor::_getNextLine() { fl_BlockLayout* pBlockLayout = getCurrentBlockLayout(); UT_ASSERT(pBlockLayout); return pBlockLayout ? pBlockLayout->findNextLineInDocument(getCurrentLine()) : 0; } ////////////////////////////////////////////////////////////////// // private (implementation specific) methods // helper function UT_sint32 FV_Cursor::_containerToLineX( const fp_Line& line, UT_sint32 containerX) { UT_ASSERT(!line.isEmpty()); fp_Container* pContainer = line.getContainer(); UT_ASSERT(pContainer); if (!pContainer) { // WHAT THE...?! return 0; } // transpose the fp_Column relative x to fp_Line relative. UT_sint32 xOffs; UT_sint32 yOffs; pContainer->getOffsets(&line, xOffs, yOffs); return containerX - xOffs; } // 'x' is in fp_Container (fp_Column) relative coords. // With legal input, this function returns either the correct run or, if // that's not found, the last run. It returns 0 if no line is given. fp_Run* FV_Cursor::_getRunAtColumnOffset(const fp_Line& line, UT_sint32 x) { x = _containerToLineX(line, x); fp_Run* pRun = line.getFirstRun(); fp_Run* pEnd = line.getLastRun(); UT_ASSERT(pRun); UT_ASSERT(pEnd); // Current X might be on an indented line. stickyX is then // to the left of current run. if (x >= pRun->getX()) { for (; pRun != pEnd; pRun = pRun->getNext()) { UT_ASSERT(x >= pRun->getX()); if (x >= pRun->getX() && x < pRun->getX() + pRun->getWidth()) { break; } } } return pRun; } UT_Bool FV_Cursor::_isRelativeMovementAllowed() { // Implementation detail: If we have a currentBlockLayout, we also // have a currentLine and a currentRun. // If implementation changes, theis must be reviewed. return getCurrentBlockLayout() && !getCurrentLine()->isEmpty(); } void FV_Cursor::_do_movement(void (FV_Cursor::*pmfn)()) { UT_ASSERT(pmfn); UT_ASSERT(getCurrentRun()); if (_isRelativeMovementAllowed()) { FV_CursorDisabler disabler(*this); (this->*pmfn)(); // If this assert triggers it's a sign that we have // a SERIOUS bug! UT_ASSERT(getCurrentRun()); _updateInternal(); } } ////////////////////////////////////////////////////////////////// // implementation of class FV_Caret FV_Caret::FV_Caret(FV_Cursor& cursor, FV_View& view) : m_cursor(cursor), m_view(view), m_pAutoCursorTimer(0), m_nDisableCount(1), m_bCursorBlink(UT_TRUE/*UT_FALSE*/), m_bCursorIsOn(UT_FALSE) { } FV_Caret::~FV_Caret() { delete m_pAutoCursorTimer; } void FV_Caret::enable() { // If it isn't disabled by the time we get this call, something is wrong. UT_ASSERT(m_nDisableCount); UT_DEBUGMSG(("FV_Caret::enable() , count = %d\n", m_nDisableCount)); if (--m_nDisableCount) { return; } // Caret should never be "on" when in disabled state. Let's assert so. UT_ASSERT(!m_bCursorIsOn); // Always start out with the caret in drawn state if (!m_bCursorIsOn) { _blink(); } if (m_bCursorBlink) { if (!m_pAutoCursorTimer && !_createTimer()) { // TODO How should we report this error? UT_ASSERT(UT_SHOULD_NOT_HAPPEN); return; } m_pAutoCursorTimer->set(AUTO_DRAW_POINT); m_pAutoCursorTimer->start(); } // Caret should always be "on" when leaving enable(). UT_ASSERT(m_bCursorIsOn); // post condition } void FV_Caret::disable() { UT_DEBUGMSG(("FV_Caret::disable(), count = %d\n", m_nDisableCount)); if (!m_nDisableCount++) { if (m_pAutoCursorTimer) { m_pAutoCursorTimer->stop(); } if (m_bCursorIsOn) { _blink(); } } // Caret should never be "on" when leaving enable(). UT_ASSERT(!m_bCursorIsOn); // post condition } void FV_Caret::setBlink(UT_Bool bBlink) { m_bCursorBlink = bBlink; } // This is just to have an _instant_ report of what the caret class think // is the carets state. A higher pitch beep (1500 Hz) means the class // think it's drawing a caret, a lower pitch beep (1000 Hz) means it // thinks it's erasing the caret. // It will _probably_ go away before committing. But on the other hand, // this code could be of use when adding another platform. #ifdef UT_DEBUG #ifdef _WIN32 extern "C" __declspec(dllimport) int __stdcall Beep(unsigned long dwFreq, unsigned long dwDuration); #else // _WIN32 void Beep(UT_uint32 freq, UT_uint32 duration) { UT_UNUSED(freq); UT_UNUSED(duration); } #endif // _WIN32 #endif // UT_DEBUG void FV_Caret::_blink() { // toggle this as soon as possible, since timer blinks can possibly // interfere with calls from enable()/disable(). // Since there's no synchronizing mechanism in XP, use a local // (auto/stack) variable. const UT_Bool bTurnCursorOn = !m_bCursorIsOn; fp_Line* pCurrLine = m_cursor.getCurrentLine(); if (m_view.getWindowHeight() <= 0 || !m_view.isSelectionEmpty() || !pCurrLine || !m_cursor.getPointHeight()) { return; } GR_Graphics* pG = m_view.getGraphics(); if (!pG) { return; } UT_sint32 xPageOffs; UT_sint32 yPageOffs; m_view. getPageScreenOffsets( pCurrLine->getContainer()->getPage(), xPageOffs, yPageOffs); const UT_uint32 x = m_cursor.getContainerRelativeX() + xPageOffs; const UT_uint32 yMin = m_cursor.getContainerRelativeY() + yPageOffs; const UT_uint32 yMax = yMin + m_cursor.getPointHeight(); UT_DEBUGMSG(("caret at %d,%d (%d,%d), docpos: %d\n", x, yMin, x - xPageOffs, yMin - yPageOffs, m_cursor.getDocPos())); #ifdef UT_DEBUG // This is just to have an _instant_ report of what the caret class think // is the carets state. A higher pitch beep (1500 Hz) means the class // think it's drawing a caret, a lower pitch beep (1000 Hz) means it // thinks it's erasing the caret. // It will _probably_ go away before committing. But on the other hand, // this code could be of use when adding another platform. if (bDebugBeepOnBlink) { Beep(bTurnCursorOn ? 1500 : 1000, 20); } #endif UT_RGBColor clr(255,255,255); pG->setColor(clr); pG->xorLine(x-1, yMin, x-1, yMax); pG->xorLine(x , yMin, x , yMax); m_bCursorIsOn = !m_bCursorIsOn; } // This is really the only legal (C++ language-wise) function type // we can use for callbacks as we do them now. void FV_Caret_CursorTimerCallback(UT_Timer* pTimer) { UT_ASSERT(pTimer); if (!pTimer) { // If we get no timer, things are really bad. return; } FV_Caret* pCaret = static_cast(pTimer->getInstanceData()); UT_ASSERT(pCaret); FV_Caret::callbackHelper::_callback(*pCaret); } UT_Bool FV_Caret::_createTimer() { if (m_pAutoCursorTimer) { return UT_FALSE; } m_pAutoCursorTimer = UT_Timer::static_constructor(FV_Caret_CursorTimerCallback, this, m_view.getGraphics()); return m_pAutoCursorTimer != 0; }