+/////////////////////////////////////////////////////////////////////////////
+// Name: univ/textctrl.cpp
+// Purpose: wxTextCtrl
+// Author: Vadim Zeitlin
+// Modified by:
+// Created: 15.09.00
+// RCS-ID: $Id$
+// Copyright: (c) 2000 Vadim Zeitlin
+// Licence: wxWindows license
+/////////////////////////////////////////////////////////////////////////////
+
+/*
+ TODO
+
++ 1. update vert scrollbar when any line length changes for WrapLines()
++ 2. cursor movement ("Hello,^" -> "^verse!" on Arrow Down)?
+ -> maybe save the x position and use it instead of current in handling
+ DOWN/UP actions (this would make up/down always return the cursor to
+ the same location)?
+ 3. split file into chunks
++? 4. rewrite Replace() refresh logic to deal with wrapping lines
++? 5. cache info found by GetPartOfWrappedLine() - performance must be horrible
+ with lots of text
+
+ 6. backspace refreshes too much (until end of line)
+ */
+
+/*
+ Optimisation hints from PureQuantify:
+
+ +1. wxStringTokenize is the slowest part of Replace
+ 2. GetDC/ReleaseDC are very slow, avoid calling them several times
+ +3. GetCharHeight() should be cached too
+ 4. wxClientDC construction/destruction in HitTestLine is horribly expensive
+
+ For line wrapping controls HitTest2 takes 50% of program time. The results
+ of GetRowsPerLine and GetPartOfWrappedLine *MUST* be cached.
+
+ Search for "OPT!" for things which must be optimized.
+ */
+
+/*
+ Some terminology:
+
+ Everywhere in this file LINE refers to a logical line of text, and ROW to a
+ physical line of text on the display. They are the same unless WrapLines()
+ is TRUE in which case a single LINE may correspond to multiple ROWs.
+
+ A text position is an unsigned int (which for reasons of compatibility is
+ still a long) from 0 to GetLastPosition() inclusive. The positions
+ correspond to the gaps between the letters so the position 0 is just
+ before the first character and the last position is the one beyond the last
+ character. For an empty text control GetLastPosition() returns 0.
+
+ Lines and columns returned/accepted by XYToPosition() and PositionToXY()
+ start from 0. The y coordinate is a LINE, not a ROW. Columns correspond to
+ the characters, the first column of a line is the first character in it,
+ the last one is length(line text). For compatibility, again, lines and
+ columns are also longs.
+
+ When translating lines/column coordinates to/from positions, the line and
+ column give the character after the given position. Thus, GetLastPosition()
+ doesn't have any corresponding column.
+
+ An example of positions and lines/columns for a control without wrapping
+ containing the text "Hello, Universe!\nGoodbye"
+
+ 1 1 1 1 1 1 1
+ pos: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
+ H e l l o , U n i v e r s e ! line 0
+ col: 0 1 2 3 4 5 6 7 8 9 1 1 1 1 1 1
+ 0 1 2 3 4 5
+
+ pos: 1 1 1 2 2 2 2 2
+ 7 8 9 0 1 2 3 4
+ G o o d b y e line 1
+ col: 0 1 2 3 4 5 6
+
+
+ The same example for a control with line wrap assuming "Universe" is too
+ long to fit on the same line with "Hello,":
+
+ pos: 0 1 2 3 4 5
+ H e l l o , line 0 (row 0)
+ col: 0 1 2 3 4 5
+
+ 1 1 1 1 1 1 1
+ pos: 6 7 8 9 0 1 2 3 4 5 6
+ U n i v e r s e ! line 0 (row 1)
+ col: 6 7 8 9 1 1 1 1 1 1
+ 0 1 2 3 4 5
+
+ (line 1 == row 2 same as above)
+
+ Note that there is still the same number of columns and positions and that
+ there is no (logical) position at the end of the first ROW. This position
+ is identified with the preceding one (which is not how Windows does it: it
+ identifies it with the next one, i.e. the first position of the next line,
+ but much more logical IMHO).
+ */
+
+/*
+ Search for "OPT" for possible optimizations
+
+ A possible global optimization would be to always store the coords in the
+ text in triplets (pos, col, line) and update them simultaneously instead of
+ recalculating col and line from pos each time it is needed. Currently we
+ only do it for the current position but we might also do it for the
+ selection start and end.
+ */
+
+// ============================================================================
+// declarations
+// ============================================================================
+
+// ----------------------------------------------------------------------------
+// headers
+// ----------------------------------------------------------------------------
+
+#ifdef __GNUG__
+ #pragma implementation "univtextctrl.h"
+#endif
+
+#include "wx/wxprec.h"
+
+#ifdef __BORLANDC__
+ #pragma hdrstop
+#endif
+
+#if wxUSE_TEXTCTRL
+
+#ifndef WX_PRECOMP
+ #include "wx/log.h"
+
+ #include "wx/dcclient.h"
+ #include "wx/validate.h"
+ #include "wx/textctrl.h"
+#endif
+
+#include "wx/clipbrd.h"
+#include "wx/textfile.h"
+
+#include "wx/caret.h"
+
+#include "wx/univ/inphand.h"
+#include "wx/univ/renderer.h"
+#include "wx/univ/colschem.h"
+#include "wx/univ/theme.h"
+
+#include "wx/cmdproc.h"
+
+// turn extra wxTextCtrl-specific debugging on/off
+#define WXDEBUG_TEXT
+
+// turn wxTextCtrl::Replace() debugging on (slows down code a *lot*!)
+#define WXDEBUG_TEXT_REPLACE
+
+#ifndef __WXDEBUG__
+ #undef WXDEBUG_TEXT
+ #undef WXDEBUG_TEXT_REPLACE
+#endif
+
+// wxStringTokenize only needed for debug checks
+#ifdef WXDEBUG_TEXT_REPLACE
+ #include "wx/tokenzr.h"
+#endif // WXDEBUG_TEXT_REPLACE
+
+// ----------------------------------------------------------------------------
+// private functions
+// ----------------------------------------------------------------------------
+
+// exchange two positions so that from is always less than or equal to to
+static inline void OrderPositions(wxTextPos& from, wxTextPos& to)
+{
+ if ( from > to )
+ {
+ wxTextPos tmp = from;
+ from = to;
+ to = tmp;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// constants
+// ----------------------------------------------------------------------------
+
+// names of text ctrl commands
+#define wxTEXT_COMMAND_INSERT _T("insert")
+#define wxTEXT_COMMAND_REMOVE _T("remove")
+
+// the value which is never used for text position, even not -1 which is
+// sometimes used for some special meaning
+static const wxTextPos INVALID_POS_VALUE = -2;
+
+// overlap between pages (when using PageUp/Dn) in lines
+static const size_t PAGE_OVERLAP_IN_LINES = 1;
+
+// ----------------------------------------------------------------------------
+// private data of wxTextCtrl
+// ----------------------------------------------------------------------------
+
+// the data only used by single line text controls
+struct WXDLLEXPORT wxTextSingleLineData
+{
+ // the position of the first visible pixel and the first visible column
+ wxCoord m_ofsHorz;
+ wxTextCoord m_colStart;
+
+ // and the last ones (m_posLastVisible is the width but m_colLastVisible
+ // is an absolute value)
+ wxCoord m_posLastVisible;
+ wxTextCoord m_colLastVisible;
+
+ // def ctor
+ wxTextSingleLineData()
+ {
+ m_colStart = 0;
+ m_ofsHorz = 0;
+
+ m_colLastVisible = -1;
+ m_posLastVisible = -1;
+ }
+
+};
+
+// the data only used by multi line text controls
+struct WXDLLEXPORT wxTextMultiLineData
+{
+ // the lines of text
+ wxArrayString m_lines;
+
+ // the current ranges of the scrollbars
+ int m_scrollRangeX,
+ m_scrollRangeY;
+
+ // should we adjust the horz/vert scrollbar?
+ bool m_updateScrollbarX,
+ m_updateScrollbarY;
+
+ // the max line length in pixels
+ wxCoord m_widthMax;
+
+ // the index of the line which has the length of m_widthMax
+ wxTextCoord m_lineLongest;
+
+ // the rect in which text appears: it is even less than m_rectText because
+ // only the last _complete_ line is shown, hence there is an unoccupied
+ // horizontal band at the bottom of it
+ wxRect m_rectTextReal;
+
+ // the x-coordinate of the caret before we started moving it vertically:
+ // this is used to ensure that moving the caret up and then down will
+ // return it to the same position as if we always round it in one direction
+ // we would shift it in that direction
+ //
+ // when m_xCaret == -1, we don't have any remembered position
+ wxCoord m_xCaret;
+
+ // the def ctor
+ wxTextMultiLineData()
+ {
+ m_scrollRangeX =
+ m_scrollRangeY = 0;
+
+ m_updateScrollbarX =
+ m_updateScrollbarY = FALSE;
+
+ m_widthMax = -1;
+ m_lineLongest = 0;
+
+ m_xCaret = -1;
+ }
+};
+
+// the data only used by multi line text controls in line wrap mode
+class WXDLLEXPORT wxWrappedLineData
+{
+ // these functions set all our values, so give them access to them
+friend void wxTextCtrl::LayoutLine(wxTextCoord line,
+ wxWrappedLineData& lineData) const;
+friend void wxTextCtrl::LayoutLines(wxTextCoord) const;
+
+public:
+ // def ctor
+ wxWrappedLineData()
+ {
+ m_rowFirst = -1;
+ }
+
+ // get the start of any row (remember that accessing m_rowsStart doesn't work
+ // for the first one)
+ wxTextCoord GetRowStart(wxTextCoord row) const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return row ? m_rowsStart[row - 1] : 0;
+ }
+
+ // get the length of the row (using the total line length which we don't
+ // have here but need to calculate the length of the last row, so it must
+ // be given to us)
+ wxTextCoord GetRowLength(wxTextCoord row, wxTextCoord lenLine) const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ // note that m_rowsStart[row] is the same as GetRowStart(row + 1) (but
+ // slightly more efficient) and lenLine is the same as the start of the
+ // first row of the next line
+ return ((size_t)row == m_rowsStart.GetCount() ? lenLine : m_rowsStart[row])
+ - GetRowStart(row);
+ }
+
+ // return the width of the row in pixels
+ wxCoord GetRowWidth(wxTextCoord row) const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowsWidth[row];
+ }
+
+ // return the number of rows
+ size_t GetRowCount() const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowsStart.GetCount() + 1;
+ }
+
+ // return the number of additional (i.e. after the first one) rows
+ size_t GetExtraRowCount() const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowsStart.GetCount();
+ }
+
+ // return the first row of this line
+ wxTextCoord GetFirstRow() const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowFirst;
+ }
+
+ // return the first row of the next line
+ wxTextCoord GetNextRow() const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowFirst + m_rowsStart.GetCount() + 1;
+ }
+
+ // this just provides direct access to m_rowsStart aerray for efficiency
+ wxTextCoord GetExtraRowStart(wxTextCoord row) const
+ {
+ wxASSERT_MSG( IsValid(), _T("this line hadn't been laid out") );
+
+ return m_rowsStart[row];
+ }
+
+ // this code is unused any longer
+#if 0
+ // return TRUE if the column is in the start of the last row (hence the row
+ // it is in is not wrapped)
+ bool IsLastRow(wxTextCoord colRowStart) const
+ {
+ return colRowStart == GetRowStart(m_rowsStart.GetCount());
+ }
+
+ // return TRUE if the column is the last column of the row starting in
+ // colRowStart
+ bool IsLastColInRow(wxTextCoord colRowStart,
+ wxTextCoord colRowEnd,
+ wxTextCoord lenLine) const
+ {
+ // find the row which starts with colRowStart
+ size_t nRows = GetRowCount();
+ for ( size_t n = 0; n < nRows; n++ )
+ {
+ if ( GetRowStart(n) == colRowStart )
+ {
+ wxTextCoord colNextRowStart = n == nRows - 1
+ ? lenLine
+ : GetRowStart(n + 1);
+
+ wxASSERT_MSG( colRowEnd < colNextRowStart,
+ _T("this column is not in this row at all!") );
+
+ return colRowEnd == colNextRowStart - 1;
+ }
+ }
+
+ // caller got it wrong
+ wxFAIL_MSG( _T("this column is not in the start of the row!") );
+
+ return FALSE;
+ }
+#endif // 0
+
+ // is this row the last one in its line?
+ bool IsLastRow(wxTextCoord row) const
+ {
+ return (size_t)row == GetExtraRowCount();
+ }
+
+ // the line is valid if it had been laid out correctly: note that just
+ // shiwting the line (because one of previous lines changed) doesn't make
+ // it invalid
+ bool IsValid() const { return !m_rowsWidth.IsEmpty(); }
+
+ // invalidating line will relayout it
+ void Invalidate() { m_rowsWidth.Empty(); }
+
+private:
+ // for each line we remember the starting columns of all its rows after the
+ // first one (which always starts at 0), i.e. if a line is wrapped twice
+ // (== takes 3 rows) its m_rowsStart[0] may be 10 and m_rowsStart[1] == 15
+ wxArrayLong m_rowsStart;
+
+ // and the width of each row in pixels (this array starts from 0, as usual)
+ wxArrayInt m_rowsWidth;
+
+ // and also its starting row (0 for the first line, first lines'
+ // m_rowsStart.GetCount() + 1 for the second &c): it is set to -1 initially
+ // and this means that the struct hadn't yet been initialized
+ wxTextCoord m_rowFirst;
+
+ // the last modification "time"-stamp used by LayoutLines()
+ size_t m_timestamp;
+};
+
+WX_DECLARE_OBJARRAY(wxWrappedLineData, wxArrayWrappedLinesData);
+#include "wx/arrimpl.cpp"
+WX_DEFINE_OBJARRAY(wxArrayWrappedLinesData);
+
+struct WXDLLEXPORT wxTextWrappedData : public wxTextMultiLineData
+{
+ // the width of the column to the right of the text rect used for the
+ // indicator mark display for the wrapped lines
+ wxCoord m_widthMark;
+
+ // the data for each line
+ wxArrayWrappedLinesData m_linesData;
+
+ // flag telling us to recalculate all starting rows starting from this line
+ // (if it is -1, we don't have to recalculate anything) - it is set when
+ // the number of the rows in the middle of the control changes
+ wxTextCoord m_rowFirstInvalid;
+
+ // the current timestamp used by LayoutLines()
+ size_t m_timestamp;
+
+ // invalidate starting rows of all lines (NOT rows!) after this one
+ void InvalidateLinesBelow(wxTextCoord line)
+ {
+ if ( m_rowFirstInvalid == -1 || m_rowFirstInvalid > line )
+ {
+ m_rowFirstInvalid = line;
+ }
+ }
+
+ // check if this line is valid: i.e. before the first invalid one
+ bool IsValidLine(wxTextCoord line) const
+ {
+ return ((m_rowFirstInvalid == -1) || (line < m_rowFirstInvalid)) &&
+ m_linesData[line].IsValid();
+ }
+
+ // def ctor
+ wxTextWrappedData()
+ {
+ m_widthMark = 0;
+ m_rowFirstInvalid = -1;
+ m_timestamp = 0;
+ }
+};
+
+// ----------------------------------------------------------------------------
+// private classes for undo/redo management
+// ----------------------------------------------------------------------------
+
+/*
+ We use custom versions of wxWindows command processor to implement undo/redo
+ as we want to avoid storing the backpointer to wxTextCtrl in wxCommand
+ itself: this is a waste of memory as all commands in the given command
+ processor always have the same associated wxTextCtrl and so it makes sense
+ to store the backpointer there.
+
+ As for the rest of the implementation, it's fairly standard: we have 2
+ command classes corresponding to adding and removing text.
+ */
+
+// a command corresponding to a wxTextCtrl action
+class wxTextCtrlCommand : public wxCommand
+{
+public:
+ wxTextCtrlCommand(const wxString& name) : wxCommand(TRUE, name) { }
+
+ // we don't use these methods as they don't make sense for us as we need a
+ // wxTextCtrl to be applied
+ virtual bool Do() { wxFAIL_MSG(_T("shouldn't be called")); return FALSE; }
+ virtual bool Undo() { wxFAIL_MSG(_T("shouldn't be called")); return FALSE; }
+
+ // instead, our command processor uses these methods
+ virtual bool Do(wxTextCtrl *text) = 0;
+ virtual bool Undo(wxTextCtrl *text) = 0;
+};
+
+// insert text command
+class wxTextCtrlInsertCommand : public wxTextCtrlCommand
+{
+public:
+ wxTextCtrlInsertCommand(const wxString& textToInsert)
+ : wxTextCtrlCommand(wxTEXT_COMMAND_INSERT), m_text(textToInsert)
+ {
+ m_from = -1;
+ }
+
+ // combine the 2 commands together
+ void Append(wxTextCtrlInsertCommand *other);
+
+ virtual bool CanUndo() const;
+ virtual bool Do(wxTextCtrl *text);
+ virtual bool Undo(wxTextCtrl *text);
+
+private:
+ // the text we insert
+ wxString m_text;
+
+ // the position where we inserted the text
+ wxTextPos m_from;
+};
+
+// remove text command
+class wxTextCtrlRemoveCommand : public wxTextCtrlCommand
+{
+public:
+ wxTextCtrlRemoveCommand(wxTextPos from, wxTextPos to)
+ : wxTextCtrlCommand(wxTEXT_COMMAND_REMOVE)
+ {
+ m_from = from;
+ m_to = to;
+ }
+
+ virtual bool CanUndo() const;
+ virtual bool Do(wxTextCtrl *text);
+ virtual bool Undo(wxTextCtrl *text);
+
+private:
+ // the range of text to delete
+ wxTextPos m_from,
+ m_to;
+
+ // the text which was deleted when this command was Do()ne
+ wxString m_textDeleted;
+};
+
+// a command processor for a wxTextCtrl
+class wxTextCtrlCommandProcessor : public wxCommandProcessor
+{
+public:
+ wxTextCtrlCommandProcessor(wxTextCtrl *text)
+ {
+ m_compressInserts = FALSE;
+
+ m_text = text;
+ }
+
+ // override Store() to compress multiple wxTextCtrlInsertCommand into one
+ virtual void Store(wxCommand *command);
+
+ // stop compressing insert commands when this is called
+ void StopCompressing() { m_compressInserts = FALSE; }
+
+ // accessors
+ wxTextCtrl *GetTextCtrl() const { return m_text; }
+ bool IsCompressing() const { return m_compressInserts; }
+
+protected:
+ virtual bool DoCommand(wxCommand& cmd)
+ { return ((wxTextCtrlCommand &)cmd).Do(m_text); }
+ virtual bool UndoCommand(wxCommand& cmd)
+ { return ((wxTextCtrlCommand &)cmd).Undo(m_text); }
+
+ // check if this command is a wxTextCtrlInsertCommand and return it casted
+ // to the right type if it is or NULL otherwise
+ wxTextCtrlInsertCommand *IsInsertCommand(wxCommand *cmd);
+
+private:
+ // the control we're associated with
+ wxTextCtrl *m_text;
+
+ // if the flag is TRUE we're compressing subsequent insert commands into
+ // one so that the entire typing could be undone in one call to Undo()
+ bool m_compressInserts;
+};
+
+// ============================================================================
+// implementation
+// ============================================================================
+
+BEGIN_EVENT_TABLE(wxTextCtrl, wxControl)
+ EVT_CHAR(wxTextCtrl::OnChar)
+
+ EVT_SIZE(wxTextCtrl::OnSize)
+
+ EVT_IDLE(wxTextCtrl::OnIdle)
+END_EVENT_TABLE()
+
+IMPLEMENT_DYNAMIC_CLASS(wxTextCtrl, wxControl)
+
+// ----------------------------------------------------------------------------
+// creation
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::Init()
+{
+ m_selAnchor =
+ m_selStart =
+ m_selEnd = -1;
+
+ m_isModified = FALSE;
+ m_isEditable = TRUE;
+
+ m_posLast =
+ m_curPos =
+ m_curCol =
+ m_curRow = 0;
+
+ m_heightLine =
+ m_widthAvg = -1;
+
+ // init wxScrollHelper
+ SetWindow(this);
+
+ // init the undo manager
+ m_cmdProcessor = new wxTextCtrlCommandProcessor(this);
+
+ // no data yet
+ m_data.data = NULL;
+}
+
+bool wxTextCtrl::Create(wxWindow *parent,
+ wxWindowID id,
+ const wxString& value,
+ const wxPoint& pos,
+ const wxSize& size,
+ long style,
+ const wxValidator& validator,
+ const wxString &name)
+{
+ if ( style & wxTE_MULTILINE )
+ {
+ // for compatibility with wxMSW we create the controls with vertical
+ // scrollbar always shown unless they have wxTE_RICH style (because
+ // Windows text controls always has vert scrollbar but richedit one
+ // doesn't)
+ if ( !(style & wxTE_RICH) )
+ {
+ style |= wxALWAYS_SHOW_SB;
+ }
+
+ if ( style & wxTE_WORDWRAP )
+ {
+ // wrapping words means wrapping, hence no horz scrollbar
+ style &= ~wxHSCROLL;
+ }
+
+ // TODO: support wxTE_NO_VSCROLL (?)
+
+ // create data object for normal multiline or for controls with line
+ // wrap as needed
+ if ( style & wxHSCROLL )
+ m_data.mdata = new wxTextMultiLineData;
+ else
+ m_data.wdata = new wxTextWrappedData;
+ }
+ else
+ {
+ // this doesn't make sense for single line controls
+ style &= ~wxHSCROLL;
+
+ // create data object for single line controls
+ m_data.sdata = new wxTextSingleLineData;
+ }
+
+ if ( !wxControl::Create(parent, id, pos, size, style,
+ validator, name) )
+ {
+ return FALSE;
+ }
+
+ SetCursor(wxCURSOR_IBEAM);
+
+ if ( style & wxTE_MULTILINE )
+ {
+ // we should always have at least one line in a multiline control
+ MData().m_lines.Add(wxEmptyString);
+
+ if ( !(style & wxHSCROLL) )
+ {
+ WData().m_linesData.Add(new wxWrappedLineData);
+ WData().InvalidateLinesBelow(0);
+ }
+
+ // we might support it but it's quite useless and other ports don't
+ // support it anyhow
+ wxASSERT_MSG( !(style & wxTE_PASSWORD),
+ _T("wxTE_PASSWORD can't be used with multiline ctrls") );
+ }
+
+ RecalcFontMetrics();
+ SetValue(value);
+ SetBestSize(size);
+
+ m_isEditable = !(style & wxTE_READONLY);
+
+ CreateCaret();
+ InitInsertionPoint();
+
+ // we can't show caret right now as we're not shown yet and so it would
+ // result in garbage on the screen - we'll do it after first OnPaint()
+ m_hasCaret = FALSE;
+
+ CreateInputHandler(wxINP_HANDLER_TEXTCTRL);
+
+ return TRUE;
+}
+
+wxTextCtrl::~wxTextCtrl()
+{
+ delete m_cmdProcessor;
+
+ if ( m_data.data )
+ {
+ if ( IsSingleLine() )
+ delete m_data.sdata;
+ else if ( WrapLines() )
+ delete m_data.wdata;
+ else
+ delete m_data.mdata;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// set/get the value
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::SetValue(const wxString& value)
+{
+ if ( IsSingleLine() && (value == GetValue()) )
+ {
+ // nothing changed
+ return;
+ }
+
+ Replace(0, GetLastPosition(), value);
+
+ if ( IsSingleLine() )
+ {
+ SetInsertionPoint(0);
+ }
+
+ // TODO: should we generate the event or not, finally?
+}
+
+const wxArrayString& wxTextCtrl::GetLines() const
+{
+ return MData().m_lines;
+}
+
+size_t wxTextCtrl::GetLineCount() const
+{
+ return MData().m_lines.GetCount();
+}
+
+wxString wxTextCtrl::GetValue() const
+{
+ // for multiline controls we don't always store the total value but only
+ // recompute it when asked - and to invalidate it we just empty it in
+ // Replace()
+ if ( !IsSingleLine() && m_value.empty() )
+ {
+ // recalculate: note that we always do it for empty multilien control,
+ // but then it's so quick that it's not important
+
+ // the first line is special as there is no \n before it, so it's
+ // outside the loop
+ const wxArrayString& lines = GetLines();
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ self->m_value << lines[0u];
+ size_t count = lines.GetCount();
+ for ( size_t n = 1; n < count; n++ )
+ {
+ self->m_value << _T('\n') << lines[n];
+ }
+ }
+
+ return m_value;
+}
+
+void wxTextCtrl::Clear()
+{
+ SetValue(_T(""));
+}
+
+bool wxTextCtrl::ReplaceLine(wxTextCoord line,
+ const wxString& text)
+{
+ if ( WrapLines() )
+ {
+ // first, we have to relayout the line entirely
+ //
+ // OPT: we might try not to recalc the unchanged part of line
+
+ wxWrappedLineData& lineData = WData().m_linesData[line];
+
+ // if we had some number of rows before, use this number, otherwise
+ // just make sure that the test below (rowsNew != rowsOld) will be true
+ int rowsOld;
+ if ( lineData.IsValid() )
+ {
+ rowsOld = lineData.GetExtraRowCount();
+ }
+ else // line wasn't laid out yet
+ {
+ // assume it changed entirely as we can't do anything better
+ rowsOld = -1;
+ }
+
+ // now change the line
+ MData().m_lines[line] = text;
+
+ // OPT: we choose to lay it our immediately instead of delaying it
+ // until it is needed because it allows us to avoid invalidating
+ // lines further down if the number of rows didn't chnage, but
+ // maybe we can imporve this even further?
+ LayoutLine(line, lineData);
+
+ int rowsNew = lineData.GetExtraRowCount();
+
+ if ( rowsNew != rowsOld )
+ {
+ // we have to update the line wrap marks as this is normally done
+ // by LayoutLines() which we bypassed by calling LayoutLine()
+ // directly
+ wxTextCoord rowFirst = lineData.GetFirstRow(),
+ rowCount = wxMax(rowsOld, rowsNew);
+ RefreshLineWrapMarks(rowFirst, rowFirst + rowCount);
+
+ // next, if this is not the last line, as the number of rows in it
+ // changed, we need to shift all the lines below it
+ if ( (size_t)line < WData().m_linesData.GetCount() )
+ {
+ // number of rows changed shifting all lines below
+ WData().InvalidateLinesBelow(line + 1);
+ }
+
+ // the number of rows changed
+ return TRUE;
+ }
+ }
+ else // no line wrap
+ {
+ MData().m_lines[line] = text;
+ }
+
+ // the number of rows didn't change
+ return FALSE;
+}
+
+void wxTextCtrl::RemoveLine(wxTextCoord line)
+{
+ MData().m_lines.RemoveAt(line);
+ if ( WrapLines() )
+ {
+ // we need to recalculate all the starting rows from this line, but we
+ // can avoid doing it if this line was never calculated: this means
+ // that we will recalculate all lines below it anyhow later if needed
+ if ( WData().IsValidLine(line) )
+ {
+ WData().InvalidateLinesBelow(line);
+ }
+
+ WData().m_linesData.RemoveAt(line);
+ }
+}
+
+void wxTextCtrl::InsertLine(wxTextCoord line, const wxString& text)
+{
+ MData().m_lines.Insert(text, line);
+ if ( WrapLines() )
+ {
+ WData().m_linesData.Insert(new wxWrappedLineData, line);
+
+ // invalidate everything below it
+ WData().InvalidateLinesBelow(line);
+ }
+}
+
+void wxTextCtrl::Replace(wxTextPos from, wxTextPos to, const wxString& text)
+{
+ wxTextCoord colStart, colEnd,
+ lineStart, lineEnd;
+
+ if ( (from > to) ||
+ !PositionToXY(from, &colStart, &lineStart) ||
+ !PositionToXY(to, &colEnd, &lineEnd) )
+ {
+ wxFAIL_MSG(_T("invalid range in wxTextCtrl::Replace"));
+
+ return;
+ }
+
+#ifdef WXDEBUG_TEXT_REPLACE
+ // a straighforward (but very inefficient) way of calculating what the new
+ // value should be
+ wxString textTotal = GetValue();
+ wxString textTotalNew(textTotal, (size_t)from);
+ textTotalNew += text;
+ if ( (size_t)to < textTotal.length() )
+ textTotalNew += textTotal.c_str() + (size_t)to;
+#endif // WXDEBUG_TEXT_REPLACE
+
+ // remember the old selection and reset it immediately: we must do it
+ // before calling Refresh(anything) as, at least under GTK, this leads to
+ // an _immediate_ repaint (under MSW it is delayed) and hence parts of
+ // text would be redrawn as selected if we didn't reset the selection
+ int selStartOld = m_selStart,
+ selEndOld = m_selEnd;
+
+ m_selStart =
+ m_selEnd = -1;
+
+ if ( IsSingleLine() )
+ {
+ // replace the part of the text with the new value
+ wxString valueNew(m_value, (size_t)from);
+
+ // remember it for later use
+ wxCoord startNewText = GetTextWidth(valueNew);
+
+ valueNew += text;
+ if ( (size_t)to < m_value.length() )
+ {
+ valueNew += m_value.c_str() + (size_t)to;
+ }
+
+ // we usually refresh till the end of line except of the most common case
+ // when some text is appended to the end of the string in which case we
+ // refresh just it
+ wxCoord widthNewText;
+
+ if ( (size_t)from < m_value.length() )
+ {
+ // refresh till the end of line
+ widthNewText = 0;
+ }
+ else // text appended, not replaced
+ {
+ // refresh only the new text
+ widthNewText = GetTextWidth(text);
+ }
+
+ m_value = valueNew;
+
+ // force SData().m_colLastVisible update
+ SData().m_colLastVisible = -1;
+
+ // repaint
+ RefreshPixelRange(0, startNewText, widthNewText);
+ }
+ else // multiline
+ {
+ //OPT: special case for replacements inside single line?
+
+ /*
+ Join all the lines in the replacement range into one string, then
+ replace a part of it with the new text and break it into lines again.
+ */
+
+ // (0) we want to know if this replacement changes the number of rows
+ // as if it does we need to refresh everything below the changed
+ // text (it will be shifted...) and we can avoid it if there is no
+ // row relayout
+ bool rowsNumberChanged = FALSE;
+
+ // (1) join lines
+ const wxArrayString& linesOld = GetLines();
+ wxString textOrig;
+ wxTextCoord line;
+ for ( line = lineStart; line <= lineEnd; line++ )
+ {
+ if ( line > lineStart )
+ {
+ // from the previous line
+ textOrig += _T('\n');
+ }
+
+ textOrig += linesOld[line];
+ }
+
+ // we need to append the '\n' for the last line unless there is no
+ // following line
+ size_t countOld = linesOld.GetCount();
+
+ // (2) replace text in the combined string
+
+ // (2a) leave the part before replaced area unchanged
+ wxString textNew(textOrig, colStart);
+
+ // these values will be used to refresh the changed area below
+ wxCoord widthNewText,
+ startNewText = GetTextWidth(textNew);
+ if ( (size_t)colStart == linesOld[lineStart].length() )
+ {
+ // text appended, refresh just enough to show the new text
+ widthNewText = GetTextWidth(text.BeforeFirst(_T('\n')));
+ }
+ else // text inserted, refresh till the end of line
+ {
+ widthNewText = 0;
+ }
+
+ // (2b) insert new text
+ textNew += text;
+
+ // (2c) and append the end of the old text
+
+ // adjust for index shift: to is relative to colStart, not 0
+ size_t toRel = (size_t)((to - from) + colStart);
+ if ( toRel < textOrig.length() )
+ {
+ textNew += textOrig.c_str() + toRel;
+ }
+
+ // (3) break it into lines
+
+ wxArrayString lines;
+ const wxChar *curLineStart = textNew.c_str();
+ for ( const wxChar *p = textNew.c_str(); ; p++ )
+ {
+ // end of line/text?
+ if ( !*p || *p == _T('\n') )
+ {
+ lines.Add(wxString(curLineStart, p));
+ if ( !*p )
+ break;
+
+ curLineStart = p + 1;
+ }
+ }
+
+#ifdef WXDEBUG_TEXT_REPLACE
+ // (3a) all empty tokens should be counted as replacing with "foo" and
+ // with "foo\n" should have different effects
+ wxArrayString lines2 = wxStringTokenize(textNew, _T("\n"),
+ wxTOKEN_RET_EMPTY_ALL);
+
+ if ( lines2.IsEmpty() )
+ {
+ lines2.Add(wxEmptyString);
+ }
+
+ wxASSERT_MSG( lines.GetCount() == lines2.GetCount(),
+ _T("Replace() broken") );
+ for ( size_t n = 0; n < lines.GetCount(); n++ )
+ {
+ wxASSERT_MSG( lines[n] == lines2[n], _T("Replace() broken") );
+ }
+#endif // WXDEBUG_TEXT_REPLACE
+
+ // (3b) special case: if we replace everything till the end we need to
+ // keep an empty line or the lines would disappear completely
+ // (this also takes care of never leaving m_lines empty)
+ if ( ((size_t)lineEnd == countOld - 1) && lines.IsEmpty() )
+ {
+ lines.Add(wxEmptyString);
+ }
+
+ size_t nReplaceCount = lines.GetCount(),
+ nReplaceLine = 0;
+
+ // (4) merge into the array
+
+ // (4a) replace
+ for ( line = lineStart; line <= lineEnd; line++, nReplaceLine++ )
+ {
+ if ( nReplaceLine < nReplaceCount )
+ {
+ // we have the replacement line for this one
+ if ( ReplaceLine(line, lines[nReplaceLine]) )
+ {
+ rowsNumberChanged = TRUE;
+ }
+
+ UpdateMaxWidth(line);
+ }
+ else // no more replacement lines
+ {
+ // (4b) delete all extra lines (note that we need to delete
+ // them backwards because indices shift while we do it)
+ bool deletedLongestLine = FALSE;
+ for ( wxTextCoord lineDel = lineEnd; lineDel >= line; lineDel-- )
+ {
+ if ( lineDel == MData().m_lineLongest )
+ {
+ // we will need to recalc the max line width
+ deletedLongestLine = TRUE;
+ }
+
+ RemoveLine(lineDel);
+ }
+
+ if ( deletedLongestLine )
+ {
+ RecalcMaxWidth();
+ }
+
+ // even the line number changed
+ rowsNumberChanged = TRUE;
+
+ // update line to exit the loop
+ line = lineEnd + 1;
+ }
+ }
+
+ // (4c) insert the new lines
+ if ( nReplaceLine < nReplaceCount )
+ {
+ // even the line number changed
+ rowsNumberChanged = TRUE;
+
+ do
+ {
+ InsertLine(++lineEnd, lines[nReplaceLine++]);
+
+ UpdateMaxWidth(lineEnd);
+ }
+ while ( nReplaceLine < nReplaceCount );
+ }
+
+ // (5) now refresh the changed area
+
+ // update the (cached) last position first as refresh functions use it
+ m_posLast += text.length() - to + from;
+
+ // we may optimize refresh if the number of rows didn't change - but if
+ // it did we have to refresh everything below the part we chanegd as
+ // well as it might have moved
+ if ( !rowsNumberChanged )
+ {
+ // refresh the line we changed
+ if ( !WrapLines() )
+ {
+ RefreshPixelRange(lineStart++, startNewText, widthNewText);
+ }
+ else
+ {
+ //OPT: we shouldn't refresh the unchanged part of the line in
+ // this case, but instead just refresh the tail of it - the
+ // trouble is that we don't know here where does this tail
+ // start
+ }
+
+ // number of rows didn't change, refresh the updated rows and the
+ // last one
+ if ( lineStart <= lineEnd )
+ RefreshLineRange(lineStart, lineEnd);
+ }
+ else // rows number did change
+ {
+ if ( !WrapLines() )
+ {
+ // refresh only part of the first line
+ RefreshPixelRange(lineStart++, startNewText, widthNewText);
+ }
+ //else: we have to refresh everything as some part of the text
+ // could be in the previous row before but moved to the next
+ // one now (due to word wrap)
+
+ wxTextCoord lineEnd = GetLines().GetCount() - 1;
+ if ( lineStart <= lineEnd )
+ RefreshLineRange(lineStart, lineEnd);
+
+ // refresh text rect left below
+ RefreshLineRange(lineEnd + 1, 0);
+
+ // the vert scrollbar might [dis]appear
+ MData().m_updateScrollbarY = TRUE;
+ }
+
+ // must recalculate it - will do later
+ m_value.clear();
+ }
+
+#ifdef WXDEBUG_TEXT_REPLACE
+ // optimized code above should give the same result as straightforward
+ // computation in the beginning
+ wxASSERT_MSG( GetValue() == textTotalNew, _T("error in Replace()") );
+#endif // WXDEBUG_TEXT_REPLACE
+
+ // update the current position: note that we always put the cursor at the
+ // end of the replacement text
+ DoSetInsertionPoint(from + text.length());
+
+ // and the selection: this is complicated by the fact that selection coords
+ // must be first updated to reflect change in text coords, i.e. if we had
+ // selection from 17 to 19 and we just removed this range, we don't have to
+ // refresh anything, so we can't just use ClearSelection() here
+ if ( selStartOld != -1 )
+ {
+ // refresh the parst of the selection outside the changed text (which
+ // we already refreshed)
+ if ( selStartOld < from )
+ RefreshTextRange(selStartOld, from);
+ if ( to < selEndOld )
+ RefreshTextRange(to, selEndOld);
+
+ }
+
+ // now call it to do the rest (not related to refreshing)
+ ClearSelection();
+}
+
+void wxTextCtrl::Remove(wxTextPos from, wxTextPos to)
+{
+ // Replace() only works with correctly ordered arguments, so exchange them
+ // if necessary
+ OrderPositions(from, to);
+
+ Replace(from, to, _T(""));
+}
+
+void wxTextCtrl::WriteText(const wxString& text)
+{
+ // replace the selection with the new text
+ RemoveSelection();
+
+ Replace(m_curPos, m_curPos, text);
+}
+
+void wxTextCtrl::AppendText(const wxString& text)
+{
+ SetInsertionPointEnd();
+ WriteText(text);
+}
+
+// ----------------------------------------------------------------------------
+// current position
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::SetInsertionPoint(wxTextPos pos)
+{
+ wxCHECK_RET( pos >= 0 && pos <= GetLastPosition(),
+ _T("insertion point position out of range") );
+
+ // don't do anything if it didn't change
+ if ( pos != m_curPos )
+ {
+ DoSetInsertionPoint(pos);
+ }
+
+ if ( !IsSingleLine() )
+ {
+ // moving cursor should reset the stored abscissa (even if the cursor
+ // position didn't actually change!)
+ MData().m_xCaret = -1;
+ }
+
+ ClearSelection();
+}
+
+void wxTextCtrl::InitInsertionPoint()
+{
+ // so far always put it in the beginning
+ DoSetInsertionPoint(0);
+
+ // this will also set the selection anchor correctly
+ ClearSelection();
+}
+
+void wxTextCtrl::MoveInsertionPoint(wxTextPos pos)
+{
+ wxASSERT_MSG( pos >= 0 && pos <= GetLastPosition(),
+ _T("DoSetInsertionPoint() can only be called with valid pos") );
+
+ m_curPos = pos;
+ PositionToXY(m_curPos, &m_curCol, &m_curRow);
+}
+
+void wxTextCtrl::DoSetInsertionPoint(wxTextPos pos)
+{
+ MoveInsertionPoint(pos);
+
+ ShowPosition(pos);
+}
+
+void wxTextCtrl::SetInsertionPointEnd()
+{
+ SetInsertionPoint(GetLastPosition());
+}
+
+wxTextPos wxTextCtrl::GetInsertionPoint() const
+{
+ return m_curPos;
+}
+
+wxTextPos wxTextCtrl::GetLastPosition() const
+{
+ wxTextPos pos;
+ if ( IsSingleLine() )
+ {
+ pos = m_value.length();
+ }
+ else // multiline
+ {
+#ifdef WXDEBUG_TEXT
+ pos = 0;
+ size_t nLineCount = GetLineCount();
+ for ( size_t nLine = 0; nLine < nLineCount; nLine++ )
+ {
+ // +1 is because the positions at the end of this line and of the
+ // start of the next one are different
+ pos += GetLines()[nLine].length() + 1;
+ }
+
+ if ( pos > 0 )
+ {
+ // the last position is at the end of the last line, not in the
+ // beginning of the next line after it
+ pos--;
+ }
+
+ // more probable reason of this would be to forget to update m_posLast
+ wxASSERT_MSG( pos == m_posLast, _T("bug in GetLastPosition()") );
+#endif // WXDEBUG_TEXT
+
+ pos = m_posLast;
+ }
+
+ return pos;
+}
+
+// ----------------------------------------------------------------------------
+// selection
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::GetSelection(wxTextPos* from, wxTextPos* to) const
+{
+ if ( from )
+ *from = m_selStart;
+ if ( to )
+ *to = m_selEnd;
+}
+
+wxString wxTextCtrl::GetSelectionText() const
+{
+ wxString sel;
+
+ if ( HasSelection() )
+ {
+ if ( IsSingleLine() )
+ {
+ sel = m_value.Mid(m_selStart, m_selEnd - m_selStart);
+ }
+ else // multiline
+ {
+ wxTextCoord colStart, lineStart,
+ colEnd, lineEnd;
+ PositionToXY(m_selStart, &colStart, &lineStart);
+ PositionToXY(m_selEnd, &colEnd, &lineEnd);
+
+ // as always, we need to check for the special case when the start
+ // and end line are the same
+ if ( lineEnd == lineStart )
+ {
+ sel = GetLines()[lineStart].Mid(colStart, colEnd - colStart);
+ }
+ else // sel on multiple lines
+ {
+ // take the end of the first line
+ sel = GetLines()[lineStart].c_str() + colStart;
+ sel += _T('\n');
+
+ // all intermediate ones
+ for ( wxTextCoord line = lineStart + 1; line < lineEnd; line++ )
+ {
+ sel << GetLines()[line] << _T('\n');
+ }
+
+ // and the start of the last one
+ sel += GetLines()[lineEnd].Left(colEnd);
+ }
+ }
+ }
+
+ return sel;
+}
+
+void wxTextCtrl::SetSelection(wxTextPos from, wxTextPos to)
+{
+ // selecting till -1 is the same as selecting to the end
+ if ( to == -1 && from != -1 )
+ {
+ to = GetLastPosition();
+ }
+
+ if ( from == -1 || to == from )
+ {
+ ClearSelection();
+ }
+ else // valid sel range
+ {
+ OrderPositions(from, to);
+
+ wxCHECK_RET( to <= GetLastPosition(),
+ _T("invalid range in wxTextCtrl::SetSelection") );
+
+ if ( from != m_selStart || to != m_selEnd )
+ {
+ // we need to use temp vars as RefreshTextRange() may call DoDraw()
+ // directly and so m_selStart/End must be reset by then
+ wxTextPos selStartOld = m_selStart,
+ selEndOld = m_selEnd;
+
+ m_selStart = from;
+ m_selEnd = to;
+
+ wxLogTrace(_T("text"), _T("Selection range is %ld-%ld"),
+ m_selStart, m_selEnd);
+
+ // refresh only the part of text which became (un)selected if
+ // possible
+ if ( selStartOld == m_selStart )
+ {
+ RefreshTextRange(selEndOld, m_selEnd);
+ }
+ else if ( selEndOld == m_selEnd )
+ {
+ RefreshTextRange(m_selStart, selStartOld);
+ }
+ else
+ {
+ // OPT: could check for other cases too but it is probably not
+ // worth it as the two above are the most common ones
+ if ( selStartOld != -1 )
+ RefreshTextRange(selStartOld, selEndOld);
+ if ( m_selStart != -1 )
+ RefreshTextRange(m_selStart, m_selEnd);
+ }
+
+ // we need to fully repaint the invalidated areas of the window
+ // before scrolling it (from DoSetInsertionPoint which is typically
+ // called after SetSelection()), otherwise they may stay unpainted
+ m_targetWindow->Update();
+ }
+ //else: nothing to do
+
+ // the insertion point is put at the end of selection
+ DoSetInsertionPoint(to);
+ }
+}
+
+void wxTextCtrl::ClearSelection()
+{
+ if ( HasSelection() )
+ {
+ // we need to use temp vars as RefreshTextRange() may call DoDraw()
+ // directly (see above as well)
+ wxTextPos selStart = m_selStart,
+ selEnd = m_selEnd;
+
+ // no selection any more
+ m_selStart =
+ m_selEnd = -1;
+
+ // refresh the old selection
+ RefreshTextRange(selStart, selEnd);
+ }
+
+ // the anchor should be moved even if there was no selection previously
+ m_selAnchor = m_curPos;
+}
+
+void wxTextCtrl::RemoveSelection()
+{
+ if ( !HasSelection() )
+ return;
+
+ Remove(m_selStart, m_selEnd);
+}
+
+bool wxTextCtrl::GetSelectedPartOfLine(wxTextCoord line,
+ wxTextPos *start, wxTextPos *end) const
+{
+ if ( start )
+ *start = -1;
+ if ( end )
+ *end = -1;
+
+ if ( !HasSelection() )
+ {
+ // no selection at all, hence no selection in this line
+ return FALSE;
+ }
+
+ wxTextCoord lineStart, colStart;
+ PositionToXY(m_selStart, &colStart, &lineStart);
+ if ( lineStart > line )
+ {
+ // this line is entirely above the selection
+ return FALSE;
+ }
+
+ wxTextCoord lineEnd, colEnd;
+ PositionToXY(m_selEnd, &colEnd, &lineEnd);
+ if ( lineEnd < line )
+ {
+ // this line is entirely below the selection
+ return FALSE;
+ }
+
+ if ( line == lineStart )
+ {
+ if ( start )
+ *start = colStart;
+ if ( end )
+ *end = lineEnd == lineStart ? colEnd : GetLineLength(line);
+ }
+ else if ( line == lineEnd )
+ {
+ if ( start )
+ *start = lineEnd == lineStart ? colStart : 0;
+ if ( end )
+ *end = colEnd;
+ }
+ else // the line is entirely inside the selection
+ {
+ if ( start )
+ *start = 0;
+ if ( end )
+ *end = GetLineLength(line);
+ }
+
+ return TRUE;
+}
+
+// ----------------------------------------------------------------------------
+// flags
+// ----------------------------------------------------------------------------
+
+bool wxTextCtrl::IsModified() const
+{
+ return m_isModified;
+}
+
+bool wxTextCtrl::IsEditable() const
+{
+ // disabled control can never be edited
+ return m_isEditable && IsEnabled();
+}
+
+void wxTextCtrl::DiscardEdits()
+{
+ m_isModified = FALSE;
+}
+
+void wxTextCtrl::SetEditable(bool editable)
+{
+ if ( editable != m_isEditable )
+ {
+ m_isEditable = editable;
+
+ // the caret (dis)appears
+ CreateCaret();
+
+ // the appearance of the control might have changed
+ Refresh();
+ }
+}
+
+// ----------------------------------------------------------------------------
+// col/lines <-> position correspondence
+// ----------------------------------------------------------------------------
+
+/*
+ A few remarks about this stuff:
+
+ o The numbering of the text control columns/rows starts from 0.
+ o Start of first line is position 0, its last position is line.length()
+ o Start of the next line is the last position of the previous line + 1
+ */
+
+int wxTextCtrl::GetLineLength(wxTextCoord line) const
+{
+ if ( IsSingleLine() )
+ {
+ wxASSERT_MSG( line == 0, _T("invalid GetLineLength() parameter") );
+
+ return m_value.length();
+ }
+ else // multiline
+ {
+ wxCHECK_MSG( (size_t)line < GetLineCount(), -1,
+ _T("line index out of range") );
+
+ return GetLines()[line].length();
+ }
+}
+
+wxString wxTextCtrl::GetLineText(wxTextCoord line) const
+{
+ if ( IsSingleLine() )
+ {
+ wxASSERT_MSG( line == 0, _T("invalid GetLineLength() parameter") );
+
+ return m_value;
+ }
+ else // multiline
+ {
+ wxCHECK_MSG( (size_t)line < GetLineCount(), _T(""),
+ _T("line index out of range") );
+
+ return GetLines()[line];
+ }
+}
+
+int wxTextCtrl::GetNumberOfLines() const
+{
+ // there is always 1 line, even if the text is empty
+ return IsSingleLine() ? 1 : GetLineCount();
+}
+
+wxTextPos wxTextCtrl::XYToPosition(wxTextCoord x, wxTextCoord y) const
+{
+ // note that this method should accept any values of x and y and return -1
+ // if they are out of range
+ if ( IsSingleLine() )
+ {
+ return x > GetLastPosition() || y > 0 ? -1 : x;
+ }
+ else // multiline
+ {
+ if ( (size_t)y >= GetLineCount() )
+ {
+ // this position is below the text
+ return GetLastPosition();
+ }
+
+ wxTextPos pos = 0;
+ for ( size_t nLine = 0; nLine < (size_t)y; nLine++ )
+ {
+ // +1 is because the positions at the end of this line and of the
+ // start of the next one are different
+ pos += GetLines()[nLine].length() + 1;
+ }
+
+ // take into account also the position in line
+ if ( (size_t)x > GetLines()[y].length() )
+ {
+ // don't return position in the next line
+ x = GetLines()[y].length();
+ }
+
+ return pos + x;
+ }
+}
+
+bool wxTextCtrl::PositionToXY(wxTextPos pos,
+ wxTextCoord *x, wxTextCoord *y) const
+{
+ if ( IsSingleLine() )
+ {
+ if ( (size_t)pos > m_value.length() )
+ return FALSE;
+
+ if ( x )
+ *x = pos;
+ if ( y )
+ *y = 0;
+
+ return TRUE;
+ }
+ else // multiline
+ {
+ wxTextPos posCur = 0;
+ size_t nLineCount = GetLineCount();
+ for ( size_t nLine = 0; nLine < nLineCount; nLine++ )
+ {
+ // +1 is because the start the start of the next line is one
+ // position after the end of this one
+ wxTextPos posNew = posCur + GetLines()[nLine].length() + 1;
+ if ( posNew > pos )
+ {
+ // we've found the line, now just calc the column
+ if ( x )
+ *x = pos - posCur;
+
+ if ( y )
+ *y = nLine;
+
+#ifdef WXDEBUG_TEXT
+ wxASSERT_MSG( XYToPosition(pos - posCur, nLine) == pos,
+ _T("XYToPosition() or PositionToXY() broken") );
+#endif // WXDEBUG_TEXT
+
+ return TRUE;
+ }
+ else // go further down
+ {
+ posCur = posNew;
+ }
+ }
+
+ // beyond the last line
+ return FALSE;
+ }
+}
+
+wxTextCoord wxTextCtrl::GetRowsPerLine(wxTextCoord line) const
+{
+ // a normal line has one row
+ wxTextCoord numRows = 1;
+
+ if ( WrapLines() )
+ {
+ // add the number of additional rows
+ numRows += WData().m_linesData[line].GetExtraRowCount();
+ }
+
+ return numRows;
+}
+
+wxTextCoord wxTextCtrl::GetRowCount() const
+{
+ wxTextCoord count = GetLineCount();
+ if ( WrapLines() )
+ {
+ count = GetFirstRowOfLine(count - 1) +
+ WData().m_linesData[count - 1].GetRowCount();
+ }
+
+ return count;
+}
+
+wxTextCoord wxTextCtrl::GetRowAfterLine(wxTextCoord line) const
+{
+ if ( !WrapLines() )
+ return line + 1;
+
+ if ( !WData().IsValidLine(line) )
+ {
+ LayoutLines(line);
+ }
+
+ return WData().m_linesData[line].GetNextRow();
+}
+
+wxTextCoord wxTextCtrl::GetFirstRowOfLine(wxTextCoord line) const
+{
+ if ( !WrapLines() )
+ return line;
+
+ if ( !WData().IsValidLine(line) )
+ {
+ LayoutLines(line);
+ }
+
+ return WData().m_linesData[line].GetFirstRow();
+}
+
+bool wxTextCtrl::PositionToLogicalXY(wxTextPos pos,
+ wxCoord *xOut,
+ wxCoord *yOut) const
+{
+ wxTextCoord col, line;
+
+ // optimization for special (but common) case when we already have the col
+ // and line
+ if ( pos == m_curPos )
+ {
+ col = m_curCol;
+ line = m_curRow;
+ }
+ else // must really calculate col/line from pos
+ {
+ if ( !PositionToXY(pos, &col, &line) )
+ return FALSE;
+ }
+
+ int hLine = GetLineHeight();
+ wxCoord x, y;
+ wxString textLine = GetLineText(line);
+ if ( IsSingleLine() || !WrapLines() )
+ {
+ x = GetTextWidth(textLine.Left(col));
+ y = line*hLine;
+ }
+ else // difficult case: multline control with line wrap
+ {
+ y = GetFirstRowOfLine(line);
+
+ wxTextCoord colRowStart;
+ y += GetRowInLine(line, col, &colRowStart);
+
+ y *= hLine;
+
+ // x is the width of the text before this position in this row
+ x = GetTextWidth(textLine.Mid(colRowStart, col - colRowStart));
+ }
+
+ if ( xOut )
+ *xOut = x;
+ if ( yOut )
+ *yOut = y;
+
+ return TRUE;
+}
+
+bool wxTextCtrl::PositionToDeviceXY(wxTextPos pos,
+ wxCoord *xOut,
+ wxCoord *yOut) const
+{
+ wxCoord x, y;
+ if ( !PositionToLogicalXY(pos, &x, &y) )
+ return FALSE;
+
+ // finally translate the logical text rect coords into physical client
+ // coords
+ CalcScrolledPosition(m_rectText.x + x, m_rectText.y + y, xOut, yOut);
+
+ return TRUE;
+}
+
+wxPoint wxTextCtrl::GetCaretPosition() const
+{
+ wxCoord xCaret, yCaret;
+ if ( !PositionToDeviceXY(m_curPos, &xCaret, &yCaret) )
+ {
+ wxFAIL_MSG( _T("Caret can't be beyond the text!") );
+ }
+
+ return wxPoint(xCaret, yCaret);
+}
+
+// pos may be -1 to show the current position
+void wxTextCtrl::ShowPosition(wxTextPos pos)
+{
+ HideCaret();
+
+ if ( IsSingleLine() )
+ {
+ ShowHorzPosition(GetTextWidth(m_value.Left(pos)));
+ }
+ else if ( MData().m_scrollRangeX || MData().m_scrollRangeY ) // multiline with scrollbars
+ {
+ int xStart, yStart;
+ GetViewStart(&xStart, &yStart);
+
+ if ( pos == -1 )
+ pos = m_curPos;
+
+ wxCoord x, y;
+ PositionToLogicalXY(pos, &x, &y);
+
+ wxRect rectText = GetRealTextArea();
+
+ // scroll the position vertically into view: if it is currently above
+ // it, make it the first one, otherwise the last one
+ if ( MData().m_scrollRangeY )
+ {
+ y /= GetLineHeight();
+
+ if ( y < yStart )
+ {
+ Scroll(0, y);
+ }
+ else // we are currently in or below the view area
+ {
+ // find the last row currently shown
+ wxTextCoord yEnd;
+
+ if ( WrapLines() )
+ {
+ // to find the last row we need to use the generic HitTest
+ wxTextCoord col;
+
+ // OPT this is a bit silly: we undo this in HitTest(), so
+ // it would be better to factor out the common
+ // functionality into a separate function (OTOH it
+ // won't probably save us that much)
+ wxPoint pt(0, rectText.height - 1);
+ pt += GetClientAreaOrigin();
+ pt += m_rectText.GetPosition();
+ HitTest(pt, &col, &yEnd);
+
+ // find the row inside the line
+ yEnd = GetFirstRowOfLine(yEnd) + GetRowInLine(yEnd, col);
+ }
+ else
+ {
+ // finding the last line is easy if each line has exactly
+ // one row
+ yEnd = yStart + rectText.height / GetLineHeight() - 1;
+ }
+
+ if ( yEnd < y )
+ {
+ // scroll down: the current item should appear at the
+ // bottom of the view
+ Scroll(0, y - (yEnd - yStart));
+ }
+ }
+ }
+
+ // scroll the position horizontally into view
+ //
+ // we follow what I believe to be Windows behaviour here, that is if
+ // the position is already entirely in the view we do nothing, but if
+ // we do have to scroll the window to bring it into view, we scroll it
+ // not just enough to show the position but slightly more so that this
+ // position is at 1/3 of the window width from the closest border to it
+ // (I'm not sure that Windows does exactly this but it looks like this)
+ if ( MData().m_scrollRangeX )
+ {
+ // unlike for the rows, xStart doesn't correspond to the starting
+ // column as they all have different widths, so we need to
+ // translate everything to pixels
+
+ // we want the text between x and x2 be entirely inside the view
+ // (i.e. the current character)
+
+ // make xStart the first visible pixel (and not position)
+ int wChar = GetAverageWidth();
+ xStart *= wChar;
+
+ if ( x < xStart )
+ {
+ // we want the position of this column be 1/3 to the right of
+ // the left edge
+ x -= rectText.width / 3;
+ if ( x < 0 )
+ x = 0;
+ Scroll(x / wChar, y);
+ }
+ else // maybe we're beyond the right border of the view?
+ {
+ wxTextCoord col, row;
+ if ( PositionToXY(pos, &col, &row) )
+ {
+ wxString lineText = GetLineText(row);
+ wxCoord x2 = x + GetTextWidth(lineText[(size_t)col]);
+ if ( x2 > xStart + rectText.width )
+ {
+ // we want the position of this column be 1/3 to the
+ // left of the right edge, i.e. 2/3 right of the left
+ // one
+ x2 -= (2*rectText.width)/3;
+ if ( x2 < 0 )
+ x2 = 0;
+ Scroll(x2 / wChar, row);
+ }
+ }
+ }
+ }
+ }
+ //else: multiline but no scrollbars, hence nothing to do
+
+ ShowCaret();
+}
+
+// ----------------------------------------------------------------------------
+// word stuff
+// ----------------------------------------------------------------------------
+
+/*
+ TODO: we could have (easy to do) vi-like options for word movement, i.e.
+ distinguish between inlusive/exclusive words and between words and
+ WORDS (in vim sense) and also, finally, make the set of characters
+ which make up a word configurable - currently we use the exclusive
+ WORDS only (coincidentally, this is what Windows edit control does)
+
+ For future references, here is what vim help says:
+
+ A word consists of a sequence of letters, digits and underscores, or
+ a sequence of other non-blank characters, separated with white space
+ (spaces, tabs, <EOL>). This can be changed with the 'iskeyword'
+ option.
+
+ A WORD consists of a sequence of non-blank characters, separated with
+ white space. An empty line is also considered to be a word and a
+ WORD.
+ */
+
+static inline bool IsWordChar(wxChar ch)
+{
+ return !wxIsspace(ch);
+}
+
+wxTextPos wxTextCtrl::GetWordStart() const
+{
+ if ( m_curPos == -1 || m_curPos == 0 )
+ return 0;
+
+ if ( m_curCol == 0 )
+ {
+ // go to the end of the previous line
+ return m_curPos - 1;
+ }
+
+ // it shouldn't be possible to learn where the word starts in the password
+ // text entry zone
+ if ( IsPassword() )
+ return 0;
+
+ // start at the previous position
+ const wxChar *p0 = GetLineText(m_curRow).c_str();
+ const wxChar *p = p0 + m_curCol - 1;
+
+ // find the end of the previous word
+ while ( (p > p0) && !IsWordChar(*p) )
+ p--;
+
+ // now find the beginning of this word
+ while ( (p > p0) && IsWordChar(*p) )
+ p--;
+
+ // we might have gone too far
+ if ( !IsWordChar(*p) )
+ p++;
+
+ return (m_curPos - m_curCol) + p - p0;
+}
+
+wxTextPos wxTextCtrl::GetWordEnd() const
+{
+ if ( m_curPos == -1 )
+ return 0;
+
+ wxString line = GetLineText(m_curRow);
+ if ( (size_t)m_curCol == line.length() )
+ {
+ // if we're on the last position in the line, go to the next one - if
+ // it exists
+ wxTextPos pos = m_curPos;
+ if ( pos < GetLastPosition() )
+ pos++;
+
+ return pos;
+ }
+
+ // it shouldn't be possible to learn where the word ends in the password
+ // text entry zone
+ if ( IsPassword() )
+ return GetLastPosition();
+
+ // start at the current position
+ const wxChar *p0 = line.c_str();
+ const wxChar *p = p0 + m_curCol;
+
+ // find the start of the next word
+ while ( *p && !IsWordChar(*p) )
+ p++;
+
+ // now find the end of it
+ while ( *p && IsWordChar(*p) )
+ p++;
+
+ // and find the start of the next word
+ while ( *p && !IsWordChar(*p) )
+ p++;
+
+ return (m_curPos - m_curCol) + p - p0;
+}
+
+// ----------------------------------------------------------------------------
+// clipboard stuff
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::Copy()
+{
+#if wxUSE_CLIPBOARD
+ if ( HasSelection() )
+ {
+ wxClipboardLocker clipLock;
+
+ // wxTextFile::Translate() is needed to transform all '\n' into "\r\n"
+ wxString text = wxTextFile::Translate(GetTextToShow(GetSelectionText()));
+ wxTextDataObject *data = new wxTextDataObject(text);
+ wxTheClipboard->SetData(data);
+ }
+#endif // wxUSE_CLIPBOARD
+}
+
+void wxTextCtrl::Cut()
+{
+ (void)DoCut();
+}
+
+bool wxTextCtrl::DoCut()
+{
+ if ( !HasSelection() )
+ return FALSE;
+
+ Copy();
+
+ RemoveSelection();
+
+ return TRUE;
+}
+
+void wxTextCtrl::Paste()
+{
+ (void)DoPaste();
+}
+
+bool wxTextCtrl::DoPaste()
+{
+#if wxUSE_CLIPBOARD
+ wxClipboardLocker clipLock;
+
+ wxTextDataObject data;
+ if ( wxTheClipboard->IsSupported(data.GetFormat())
+ && wxTheClipboard->GetData(data) )
+ {
+ // reverse transformation: '\r\n\" -> '\n'
+ wxString text = wxTextFile::Translate(data.GetText(),
+ wxTextFileType_Unix);
+ if ( !text.empty() )
+ {
+ WriteText(text);
+
+ return TRUE;
+ }
+ }
+#endif // wxUSE_CLIPBOARD
+
+ return FALSE;
+}
+
+// ----------------------------------------------------------------------------
+// Undo and redo
+// ----------------------------------------------------------------------------
+
+wxTextCtrlInsertCommand *
+wxTextCtrlCommandProcessor::IsInsertCommand(wxCommand *command)
+{
+ return (wxTextCtrlInsertCommand *)
+ (command && (command->GetName() == wxTEXT_COMMAND_INSERT)
+ ? command : NULL);
+}
+
+void wxTextCtrlCommandProcessor::Store(wxCommand *command)
+{
+ wxTextCtrlInsertCommand *cmdIns = IsInsertCommand(command);
+ if ( cmdIns )
+ {
+ if ( IsCompressing() )
+ {
+ wxTextCtrlInsertCommand *
+ cmdInsLast = IsInsertCommand(GetCurrentCommand());
+
+ // it is possible that we don't have any last command at all if,
+ // for example, it was undone since the last Store(), so deal with
+ // this case too
+ if ( cmdInsLast )
+ {
+ cmdInsLast->Append(cmdIns);
+
+ delete cmdIns;
+
+ // don't need to call the base class version
+ return;
+ }
+ }
+
+ // append the following insert commands to this one
+ m_compressInserts = TRUE;
+
+ // let the base class version will do the job normally
+ }
+ else // not an insert command
+ {
+ // stop compressing insert commands - this won't work with the last
+ // command not being an insert one anyhow
+ StopCompressing();
+
+ // let the base class version will do the job normally
+ }
+
+ wxCommandProcessor::Store(command);
+}
+
+void wxTextCtrlInsertCommand::Append(wxTextCtrlInsertCommand *other)
+{
+ m_text += other->m_text;
+}
+
+bool wxTextCtrlInsertCommand::CanUndo() const
+{
+ return m_from != -1;
+}
+
+bool wxTextCtrlInsertCommand::Do(wxTextCtrl *text)
+{
+ // the text is going to be inserted at the current position, remember where
+ // exactly it is
+ m_from = text->GetInsertionPoint();
+
+ // and now do insert it
+ text->WriteText(m_text);
+
+ return TRUE;
+}
+
+bool wxTextCtrlInsertCommand::Undo(wxTextCtrl *text)
+{
+ wxCHECK_MSG( CanUndo(), FALSE, _T("impossible to undo insert cmd") );
+
+ // remove the text from where we inserted it
+ text->Remove(m_from, m_from + m_text.length());
+
+ return TRUE;
+}
+
+bool wxTextCtrlRemoveCommand::CanUndo() const
+{
+ // if we were executed, we should have the text we removed
+ return !m_textDeleted.empty();
+}
+
+bool wxTextCtrlRemoveCommand::Do(wxTextCtrl *text)
+{
+ text->SetSelection(m_from, m_to);
+ m_textDeleted = text->GetSelectionText();
+ text->RemoveSelection();
+
+ return TRUE;
+}
+
+bool wxTextCtrlRemoveCommand::Undo(wxTextCtrl *text)
+{
+ // it is possible that the text was deleted and that we can't restore text
+ // at the same position we removed it any more
+ wxTextPos posLast = text->GetLastPosition();
+ text->SetInsertionPoint(m_from > posLast ? posLast : m_from);
+ text->WriteText(m_textDeleted);
+
+ return TRUE;
+}
+
+void wxTextCtrl::Undo()
+{
+ // the caller must check it
+ wxASSERT_MSG( CanUndo(), _T("can't call Undo() if !CanUndo()") );
+
+ m_cmdProcessor->Undo();
+}
+
+void wxTextCtrl::Redo()
+{
+ // the caller must check it
+ wxASSERT_MSG( CanRedo(), _T("can't call Undo() if !CanUndo()") );
+
+ m_cmdProcessor->Redo();
+}
+
+bool wxTextCtrl::CanUndo() const
+{
+ return IsEditable() && m_cmdProcessor->CanUndo();
+}
+
+bool wxTextCtrl::CanRedo() const
+{
+ return IsEditable() && m_cmdProcessor->CanRedo();
+}
+
+// ----------------------------------------------------------------------------
+// geometry
+// ----------------------------------------------------------------------------
+
+wxSize wxTextCtrl::DoGetBestClientSize() const
+{
+ // when we're called for the very first time from Create() we must
+ // calculate the font metrics here because we can't do it before calling
+ // Create() (there is no window yet and wxGTK crashes) but we need them
+ // here
+ if ( m_heightLine == -1 )
+ {
+ wxConstCast(this, wxTextCtrl)->RecalcFontMetrics();
+ }
+
+ wxCoord w, h;
+ GetTextExtent(GetTextToShow(GetLineText(0)), &w, &h);
+
+ int wChar = GetAverageWidth(),
+ hChar = GetLineHeight();
+
+ int widthMin = wxMax(10*wChar, 100);
+ if ( w < widthMin )
+ w = widthMin;
+ if ( h < hChar )
+ h = hChar;
+
+ if ( !IsSingleLine() )
+ {
+ // let the control have a reasonable number of lines
+ int lines = GetNumberOfLines();
+ if ( lines < 5 )
+ lines = 5;
+ else if ( lines > 10 )
+ lines = 10;
+ h *= 10;
+ }
+
+ wxRect rectText;
+ rectText.width = w;
+ rectText.height = h;
+ wxRect rectTotal = GetRenderer()->GetTextTotalArea(this, rectText);
+ return wxSize(rectTotal.width, rectTotal.height);
+}
+
+void wxTextCtrl::UpdateTextRect()
+{
+ wxRect rectTotal(wxPoint(0, 0), GetClientSize());
+ wxCoord *extraSpace = WrapLines() ? &WData().m_widthMark : NULL;
+ m_rectText = GetRenderer()->GetTextClientArea(this, rectTotal, extraSpace);
+
+ // code elsewhere is confused by negative rect size
+ if ( m_rectText.width <= 0 )
+ m_rectText.width = 1;
+ if ( m_rectText.height <= 0 )
+ m_rectText.height = 1;
+
+ if ( !IsSingleLine() )
+ {
+ // invalidate it so that GetRealTextArea() will recalc it
+ MData().m_rectTextReal.width = 0;
+
+ // only scroll this rect when the window is scrolled: note that we have
+ // to scroll not only the text but the line wrap marks too if we show
+ // them
+ wxRect rectText = GetRealTextArea();
+ if ( extraSpace && *extraSpace )
+ {
+ rectText.width += *extraSpace;
+ }
+ SetTargetRect(rectText);
+
+ // relayout all lines
+ if ( WrapLines() )
+ {
+ WData().m_rowFirstInvalid = 0;
+
+ // increase timestamp: this means that the lines which had been
+ // laid out before will be relayd out the next time LayoutLines()
+ // is called because their timestamp will be smaller than the
+ // current one
+ WData().m_timestamp++;
+ }
+ }
+
+ UpdateLastVisible();
+}
+
+void wxTextCtrl::UpdateLastVisible()
+{
+ // this method is only used for horizontal "scrollbarless" scrolling which
+ // is used only with single line controls
+ if ( !IsSingleLine() )
+ return;
+
+ // use (efficient) HitTestLine to find the last visible character
+ wxString text = m_value.Mid((size_t)SData().m_colStart /* to the end */);
+ wxTextCoord col;
+ switch ( HitTestLine(text, m_rectText.width, &col) )
+ {
+ case wxTE_HT_BEYOND:
+ // everything is visible
+ SData().m_colLastVisible = text.length();
+
+ // calc it below
+ SData().m_posLastVisible = -1;
+ break;
+
+ /*
+ case wxTE_HT_BEFORE:
+ case wxTE_HT_BELOW:
+ */
+ default:
+ wxFAIL_MSG(_T("unexpected HitTestLine() return value"));
+ // fall through
+
+ case wxTE_HT_ON_TEXT:
+ if ( col > 0 )
+ {
+ // the last entirely seen character is the previous one because
+ // this one is only partly visible - unless the width of the
+ // string is exactly the max width
+ SData().m_posLastVisible = GetTextWidth(text.Truncate(col + 1));
+ if ( SData().m_posLastVisible > m_rectText.width )
+ {
+ // this character is not entirely visible, take the
+ // previous one
+ col--;
+
+ // recalc it
+ SData().m_posLastVisible = -1;
+ }
+ //else: we can just see it
+
+ SData().m_colLastVisible = col;
+ }
+ break;
+ }
+
+ // calculate the width of the text really shown
+ if ( SData().m_posLastVisible == -1 )
+ {
+ SData().m_posLastVisible = GetTextWidth(text.Truncate(SData().m_colLastVisible + 1));
+ }
+
+ // current value is relative the start of the string text which starts at
+ // SData().m_colStart, we need an absolute offset into string
+ SData().m_colLastVisible += SData().m_colStart;
+
+ wxLogTrace(_T("text"), _T("Last visible column/position is %d/%ld"),
+ SData().m_colLastVisible, SData().m_posLastVisible);
+}
+
+void wxTextCtrl::OnSize(wxSizeEvent& event)
+{
+ UpdateTextRect();
+
+ if ( !IsSingleLine() )
+ {
+#if 0
+ // update them immediately because if we are called for the first time,
+ // we need to create them in order for the base class version to
+ // position the scrollbars correctly - if we don't do it now, it won't
+ // happen at all if we don't get more size events
+ UpdateScrollbars();
+#endif // 0
+
+ MData().m_updateScrollbarX =
+ MData().m_updateScrollbarY = TRUE;
+ }
+
+ event.Skip();
+}
+
+wxCoord wxTextCtrl::GetTotalWidth() const
+{
+ wxCoord w;
+ CalcUnscrolledPosition(m_rectText.width, 0, &w, NULL);
+ return w;
+}
+
+wxCoord wxTextCtrl::GetTextWidth(const wxString& text) const
+{
+ wxCoord w;
+ GetTextExtent(GetTextToShow(text), &w, NULL);
+ return w;
+}
+
+wxRect wxTextCtrl::GetRealTextArea() const
+{
+ // for single line text control it's just the same as text rect
+ if ( IsSingleLine() )
+ return m_rectText;
+
+ // the real text area always holds an entire number of lines, so the only
+ // difference with the text area is a narrow strip along the bottom border
+ wxRect rectText = MData().m_rectTextReal;
+ if ( !rectText.width )
+ {
+ // recalculate it
+ rectText = m_rectText;
+
+ // when we're called for the very first time, the line height might not
+ // had been calculated yet, so do get it now
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ self->RecalcFontMetrics();
+
+ int hLine = GetLineHeight();
+ rectText.height = (m_rectText.height / hLine) * hLine;
+
+ // cache the result
+ self->MData().m_rectTextReal = rectText;
+ }
+
+ return rectText;
+}
+
+wxTextCoord wxTextCtrl::GetRowInLine(wxTextCoord line,
+ wxTextCoord col,
+ wxTextCoord *colRowStart) const
+{
+ wxASSERT_MSG( WrapLines(), _T("shouldn't be called") );
+
+ const wxWrappedLineData& lineData = WData().m_linesData[line];
+
+ if ( !WData().IsValidLine(line) )
+ LayoutLines(line);
+
+ // row is here counted a bit specially: 0 is the 2nd row of the line (1st
+ // extra row)
+ size_t row = 0,
+ rowMax = lineData.GetExtraRowCount();
+ if ( rowMax )
+ {
+ row = 0;
+ while ( (row < rowMax) && (col >= lineData.GetExtraRowStart(row)) )
+ row++;
+
+ // it's ok here that row is 1 greater than needed: like this, it is
+ // counted as a normal (and not extra) row
+ }
+ //else: only one row anyhow
+
+ if ( colRowStart )
+ {
+ // +1 because we need a real row number, not the extra row one
+ *colRowStart = lineData.GetRowStart(row);
+
+ // this can't happen, of course
+ wxASSERT_MSG( *colRowStart <= col, _T("GetRowInLine() is broken") );
+ }
+
+ return row;
+}
+
+void wxTextCtrl::LayoutLine(wxTextCoord line, wxWrappedLineData& lineData) const
+{
+ // FIXME: this uses old GetPartOfWrappedLine() which is not used anywhere
+ // else now and has rather awkward interface for our needs here
+
+ lineData.m_rowsStart.Empty();
+ lineData.m_rowsWidth.Empty();
+
+ const wxString& text = GetLineText(line);
+ wxCoord widthRow;
+ size_t colRowStart = 0;
+ do
+ {
+ size_t lenRow = GetPartOfWrappedLine
+ (
+ text.c_str() + colRowStart,
+ &widthRow
+ );
+
+ // remember the start of this row (not for the first one as
+ // it's always 0) and its width
+ if ( colRowStart )
+ lineData.m_rowsStart.Add(colRowStart);
+ lineData.m_rowsWidth.Add(widthRow);
+
+ colRowStart += lenRow;
+ }
+ while ( colRowStart < text.length() );
+
+ // put the current timestamp on it
+ lineData.m_timestamp = WData().m_timestamp;
+}
+
+void wxTextCtrl::LayoutLines(wxTextCoord lineLast) const
+{
+ wxASSERT_MSG( WrapLines(), _T("should only be used for line wrapping") );
+
+ // if we were called, some line was dirty and if it was dirty we must have
+ // had m_rowFirstInvalid set to something too
+ wxTextCoord lineFirst = WData().m_rowFirstInvalid;
+ wxASSERT_MSG( lineFirst != -1, _T("nothing to layout?") );
+
+ wxTextCoord rowFirst, rowCur;
+ if ( lineFirst )
+ {
+ // start after the last known valid line
+ const wxWrappedLineData& lineData = WData().m_linesData[lineFirst - 1];
+ rowFirst = lineData.GetFirstRow() + lineData.GetRowCount();
+ }
+ else // no valid lines, start at row 0
+ {
+ rowFirst = 0;
+ }
+
+ rowCur = rowFirst;
+ for ( wxTextCoord line = lineFirst; line <= lineLast; line++ )
+ {
+ // set the starting row for this line
+ wxWrappedLineData& lineData = WData().m_linesData[line];
+ lineData.m_rowFirst = rowCur;
+
+ // had the line been already broken into rows?
+ //
+ // if so, compare its timestamp with the current one: if nothing has
+ // been changed, don't relayout it
+ if ( !lineData.IsValid() ||
+ (lineData.m_timestamp < WData().m_timestamp) )
+ {
+ // now do break it in rows
+ LayoutLine(line, lineData);
+ }
+
+ rowCur += lineData.GetRowCount();
+ }
+
+ // we are now valid at least up to this line, but if it is the last one we
+ // just don't have any more invalid rows at all
+ if ( (size_t)lineLast == WData().m_linesData.GetCount() -1 )
+ {
+ lineLast = -1;
+ }
+
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ self->WData().m_rowFirstInvalid = lineLast;
+
+ // also refresh the line end indicators (FIXME shouldn't do it always!)
+ self->RefreshLineWrapMarks(rowFirst, rowCur);
+}
+
+size_t wxTextCtrl::GetPartOfWrappedLine(const wxChar* text,
+ wxCoord *widthReal) const
+{
+ // this function is slow, it shouldn't be called unless really needed
+ wxASSERT_MSG( WrapLines(), _T("shouldn't be called") );
+
+ wxString s(text);
+ wxTextCoord col;
+ wxCoord wReal = -1;
+ switch ( HitTestLine(s, m_rectText.width, &col) )
+ {
+ /*
+ case wxTE_HT_BEFORE:
+ case wxTE_HT_BELOW:
+ */
+ default:
+ wxFAIL_MSG(_T("unexpected HitTestLine() return value"));
+ // fall through
+
+ case wxTE_HT_ON_TEXT:
+ if ( col > 0 )
+ {
+ // the last entirely seen character is the previous one because
+ // this one is only partly visible - unless the width of the
+ // string is exactly the max width
+ wReal = GetTextWidth(s.Truncate(col + 1));
+ if ( wReal > m_rectText.width )
+ {
+ // this character is not entirely visible, take the
+ // previous one
+ col--;
+
+ // recalc the width
+ wReal = -1;
+ }
+ //else: we can just see it
+
+ // wrap at any character or only at words boundaries?
+ if ( !(GetWindowStyle() & wxTE_LINEWRAP) )
+ {
+ // find the (last) not word char before this word
+ wxTextCoord colWordStart;
+ for ( colWordStart = col;
+ colWordStart && IsWordChar(s[(size_t)colWordStart]);
+ colWordStart-- )
+ ;
+
+ if ( colWordStart > 0 )
+ {
+ if ( colWordStart != col )
+ {
+ // will have to recalc the real width
+ wReal = -1;
+
+ col = colWordStart;
+ }
+ }
+ //else: only a single word, have to wrap it here
+ }
+ }
+ break;
+
+ case wxTE_HT_BEYOND:
+ break;
+ }
+
+ // we return the number of characters, not the index of the last one
+ if ( (size_t)col < s.length() )
+ {
+ // but don't return more than this (empty) string has
+ col++;
+ }
+
+ if ( widthReal )
+ {
+ if ( wReal == -1 )
+ {
+ // calc it if not done yet
+ wReal = GetTextWidth(s.Truncate(col));
+ }
+
+ *widthReal = wReal;
+ }
+
+ // VZ: old, horribly inefficient code which can still be used for checking
+ // the result (in line, not word, wrap mode only) - to be removed later
+#if 0
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ wxClientDC dc(self);
+ dc.SetFont(GetFont());
+ self->DoPrepareDC(dc);
+
+ wxCoord widthMax = m_rectText.width;
+
+ // the text which we can keep in this ROW
+ wxString str;
+ wxCoord w, wOld;
+ for ( wOld = w = 0; *text && (w <= widthMax); )
+ {
+ wOld = w;
+ str += *text++;
+ dc.GetTextExtent(str, &w, NULL);
+ }
+
+ if ( w > widthMax )
+ {
+ // if we wrapped, the last letter was one too much
+ if ( str.length() > 1 )
+ {
+ // remove it
+ str.erase(str.length() - 1, 1);
+ }
+ else // but always keep at least one letter in each row
+ {
+ // the real width then is the last value of w and not teh one
+ // before last
+ wOld = w;
+ }
+ }
+ else // we didn't wrap
+ {
+ wOld = w;
+ }
+
+ wxASSERT( col == str.length() );
+
+ if ( widthReal )
+ {
+ wxASSERT( *widthReal == wOld );
+
+ *widthReal = wOld;
+ }
+
+ //return str.length();
+#endif
+
+ return col;
+}
+
+// OPT: this function is called a lot - would be nice to optimize it but I
+// don't really know how yet
+wxTextCtrlHitTestResult wxTextCtrl::HitTestLine(const wxString& line,
+ wxCoord x,
+ wxTextCoord *colOut) const
+{
+ wxTextCtrlHitTestResult res = wxTE_HT_ON_TEXT;
+
+ int col;
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ wxClientDC dc(self);
+ dc.SetFont(GetFont());
+ self->DoPrepareDC(dc);
+
+ wxCoord width;
+ dc.GetTextExtent(line, &width, NULL);
+ if ( x >= width )
+ {
+ // clicking beyond the end of line is equivalent to clicking at
+ // the end of it, so return the last line column
+ col = line.length();
+ if ( col )
+ {
+ // unless the line is empty and so doesn't have any column at all -
+ // in this case return 0, what else can we do?
+ col--;
+ }
+
+ res = wxTE_HT_BEYOND;
+ }
+ else if ( x < 0 )
+ {
+ col = 0;
+
+ res = wxTE_HT_BEFORE;
+ }
+ else // we're inside the line
+ {
+ // now calculate the column: first, approximate it with fixed-width
+ // value and then calculate the correct value iteratively: note that
+ // we use the first character of the line instead of (average)
+ // GetCharWidth(): it is common to have lines of dashes, for example,
+ // and this should give us much better approximation in such case
+ //
+ // OPT: maybe using (cache) m_widthAvg would be still faster? profile!
+ dc.GetTextExtent(line[0], &width, NULL);
+
+ col = x / width;
+ if ( col < 0 )
+ {
+ col = 0;
+ }
+ else if ( (size_t)col > line.length() )
+ {
+ col = line.length();
+ }
+
+ // matchDir is the direction in which we should move to reach the
+ // character containing the given position
+ enum
+ {
+ Match_Left = -1,
+ Match_None = 0,
+ Match_Right = 1
+ } matchDir = Match_None;
+ for ( ;; )
+ {
+ // check that we didn't go beyond the line boundary
+ if ( col < 0 )
+ {
+ col = 0;
+ break;
+ }
+ if ( (size_t)col > line.length() )
+ {
+ col = line.length();
+ break;
+ }
+
+ wxString strBefore(line, (size_t)col);
+ dc.GetTextExtent(strBefore, &width, NULL);
+ if ( width > x )
+ {
+ if ( matchDir == Match_Right )
+ {
+ // we were going to the right and, finally, moved beyond
+ // the original position - stop on the previous one
+ col--;
+
+ break;
+ }
+
+ if ( matchDir == Match_None )
+ {
+ // we just started iterating, now we know that we should
+ // move to the left
+ matchDir = Match_Left;
+ }
+ //else: we are still to the right of the target, continue
+ }
+ else // width < x
+ {
+ // invert the logic above
+ if ( matchDir == Match_Left )
+ {
+ // with the exception that we don't need to backtrack here
+ break;
+ }
+
+ if ( matchDir == Match_None )
+ {
+ // go to the right
+ matchDir = Match_Right;
+ }
+ }
+
+ // this is not supposed to happen
+ wxASSERT_MSG( matchDir, _T("logic error in wxTextCtrl::HitTest") );
+
+ if ( matchDir == Match_Right )
+ col++;
+ else
+ col--;
+ }
+ }
+
+ // check that we calculated it correctly
+#ifdef WXDEBUG_TEXT
+ if ( res == wxTE_HT_ON_TEXT )
+ {
+ wxCoord width1;
+ wxString text = line.Left(col);
+ dc.GetTextExtent(text, &width1, NULL);
+ if ( (size_t)col < line.length() )
+ {
+ wxCoord width2;
+
+ text += line[col];
+ dc.GetTextExtent(text, &width2, NULL);
+
+ wxASSERT_MSG( (width1 <= x) && (x < width2),
+ _T("incorrect HitTestLine() result") );
+ }
+ else // we return last char
+ {
+ wxASSERT_MSG( x >= width1, _T("incorrect HitTestLine() result") );
+ }
+ }
+#endif // WXDEBUG_TEXT
+
+ if ( colOut )
+ *colOut = col;
+
+ return res;
+}
+
+wxTextCtrlHitTestResult wxTextCtrl::HitTest(const wxPoint& pos,
+ wxTextCoord *colOut,
+ wxTextCoord *rowOut) const
+{
+ return HitTest2(pos.y, pos.x, 0, rowOut, colOut, NULL, NULL);
+}
+
+wxTextCtrlHitTestResult wxTextCtrl::HitTestLogical(const wxPoint& pos,
+ wxTextCoord *colOut,
+ wxTextCoord *rowOut) const
+{
+ return HitTest2(pos.y, pos.x, 0, rowOut, colOut, NULL, NULL, FALSE);
+}
+
+wxTextCtrlHitTestResult wxTextCtrl::HitTest2(wxCoord y0,
+ wxCoord x10,
+ wxCoord x20,
+ wxTextCoord *rowOut,
+ wxTextCoord *colStart,
+ wxTextCoord *colEnd,
+ wxTextCoord *colRowStartOut,
+ bool deviceCoords) const
+{
+ // is the point in the text area or to the right or below it?
+ wxTextCtrlHitTestResult res = wxTE_HT_ON_TEXT;
+
+ // translate the window coords x0 and y0 into the client coords in the text
+ // area by adjusting for both the client and text area offsets (unless this
+ // was already done)
+ int x1, y;
+ if ( deviceCoords )
+ {
+ wxPoint pt = GetClientAreaOrigin() + m_rectText.GetPosition();
+ CalcUnscrolledPosition(x10 - pt.x, y0 - pt.y, &x1, &y);
+ }
+ else
+ {
+ y = y0;
+ x1 = x10;
+ }
+
+ // calculate the row (it is really a LINE, not a ROW)
+ wxTextCoord row;
+
+ // these vars are used only for WrapLines() case
+ wxTextCoord colRowStart = 0;
+ size_t rowLen = 0;
+
+ if ( colRowStartOut )
+ *colRowStartOut = 0;
+
+ int hLine = GetLineHeight();
+ if ( y < 0 )
+ {
+ // and clicking before it is the same as clicking on the first one
+ row = 0;
+
+ res = wxTE_HT_BEFORE;
+ }
+ else // y >= 0
+ {
+ wxTextCoord rowLast = GetNumberOfLines() - 1;
+ row = y / hLine;
+ if ( IsSingleLine() || !WrapLines() )
+ {
+ // in this case row calculation is simple as all lines have the
+ // same height and so row is the same as line
+ if ( row > rowLast )
+ {
+ // clicking below the text is the same as clicking on the last
+ // line
+ row = rowLast;
+
+ res = wxTE_HT_BELOW;
+ }
+ }
+ else // multline control with line wrap
+ {
+ // use binary search to find the line containing this row
+ const wxArrayWrappedLinesData& linesData = WData().m_linesData;
+ size_t lo = 0,
+ hi = linesData.GetCount(),
+ cur;
+ while ( lo < hi )
+ {
+ cur = (lo + hi)/2;
+ const wxWrappedLineData& lineData = linesData[cur];
+ if ( !WData().IsValidLine(cur) )
+ LayoutLines(cur);
+ wxTextCoord rowFirst = lineData.GetFirstRow();
+
+ if ( row < rowFirst )
+ {
+ hi = cur;
+ }
+ else
+ {
+ // our row is after the first row of the cur line:
+ // obviously, if cur is the last line, it contains this
+ // row, otherwise we have to test that it is before the
+ // first row of the next line
+ bool found = cur == linesData.GetCount() - 1;
+ if ( found )
+ {
+ // if the row is beyond the end of text, adjust it to
+ // be the last one and set res accordingly
+ if ( (size_t)(row - rowFirst) >= lineData.GetRowCount() )
+ {
+ res = wxTE_HT_BELOW;
+
+ row = lineData.GetRowCount() + rowFirst - 1;
+ }
+ }
+ else // not the last row
+ {
+ const wxWrappedLineData&
+ lineNextData = linesData[cur + 1];
+ if ( !WData().IsValidLine(cur + 1) )
+ LayoutLines(cur + 1);
+ found = row < lineNextData.GetFirstRow();
+ }
+
+ if ( found )
+ {
+ colRowStart = lineData.GetRowStart(row - rowFirst);
+ rowLen = lineData.GetRowLength(row - rowFirst,
+ GetLines()[cur].length());
+ row = cur;
+
+ break;
+ }
+ else
+ {
+ lo = cur;
+ }
+ }
+ }
+ }
+ }
+
+ if ( res == wxTE_HT_ON_TEXT )
+ {
+ // now find the position in the line
+ wxString lineText = GetLineText(row),
+ rowText;
+
+ if ( colRowStart || rowLen )
+ {
+ // look in this row only, not in whole line
+ rowText = lineText.Mid(colRowStart, rowLen);
+ }
+ else
+ {
+ // just take the whole string
+ rowText = lineText;
+ }
+
+ if ( colStart )
+ {
+ res = HitTestLine(GetTextToShow(rowText), x1, colStart);
+
+ if ( colRowStart )
+ {
+ if ( colRowStartOut )
+ {
+ // give them the column offset in this ROW in pixels
+ *colRowStartOut = colRowStart;
+ }
+
+ // take into account that the ROW doesn't start in the
+ // beginning of the LINE
+ *colStart += colRowStart;
+ }
+
+ if ( colEnd )
+ {
+ // the hit test result we return is for x1, so throw out
+ // the result for x2 here
+ int x2 = x1 + x20 - x10;
+ (void)HitTestLine(GetTextToShow(rowText), x2, colEnd);
+
+ *colEnd += colRowStart;
+ }
+ }
+ }
+ else // before/after vertical text span
+ {
+ if ( colStart )
+ {
+ // fill the column with the first/last position in the
+ // corresponding line
+ if ( res == wxTE_HT_BEFORE )
+ *colStart = 0;
+ else // res == wxTE_HT_BELOW
+ *colStart = GetLineText(GetNumberOfLines() - 1).length();
+ }
+ }
+
+ if ( rowOut )
+ {
+ // give them the row in text coords (as is)
+ *rowOut = row;
+ }
+
+ return res;
+}
+
+bool wxTextCtrl::GetLineAndRow(wxTextCoord row,
+ wxTextCoord *lineOut,
+ wxTextCoord *rowInLineOut) const
+{
+ wxTextCoord line,
+ rowInLine = 0;
+
+ if ( row < 0 )
+ return FALSE;
+
+ int nLines = GetNumberOfLines();
+ if ( WrapLines() )
+ {
+ const wxArrayWrappedLinesData& linesData = WData().m_linesData;
+ for ( line = 0; line < nLines; line++ )
+ {
+ if ( !WData().IsValidLine(line) )
+ LayoutLines(line);
+
+ if ( row < linesData[line].GetNextRow() )
+ {
+ // we found the right line
+ rowInLine = row - linesData[line].GetFirstRow();
+
+ break;
+ }
+ }
+
+ if ( line == nLines )
+ {
+ // the row is out of range
+ return FALSE;
+ }
+ }
+ else // no line wrapping, everything is easy
+ {
+ if ( row >= nLines )
+ return FALSE;
+
+ line = row;
+ }
+
+ if ( lineOut )
+ *lineOut = line;
+ if ( rowInLineOut )
+ *rowInLineOut = rowInLine;
+
+ return TRUE;
+}
+
+// ----------------------------------------------------------------------------
+// scrolling
+// ----------------------------------------------------------------------------
+
+/*
+ wxTextCtrl has not one but two scrolling mechanisms: one is semi-automatic
+ scrolling in both horizontal and vertical direction implemented using
+ wxScrollHelper and the second one is manual scrolling implemented using
+ SData().m_ofsHorz and used by the single line controls without scroll bar.
+
+ The first version (the standard one) always scrolls by fixed amount which is
+ fine for vertical scrolling as all lines have the same height but is rather
+ ugly for horizontal scrolling if proportional font is used. This is why we
+ manually update and use SData().m_ofsHorz which contains the length of the string
+ which is hidden beyond the left borde. An important property of text
+ controls using this kind of scrolling is that an entire number of characters
+ is always shown and that parts of characters never appear on display -
+ neither in the leftmost nor rightmost positions.
+
+ Once again, for multi line controls SData().m_ofsHorz is always 0 and scrolling is
+ done as usual for wxScrollWindow.
+ */
+
+void wxTextCtrl::ShowHorzPosition(wxCoord pos)
+{
+ wxASSERT_MSG( IsSingleLine(), _T("doesn't work for multiline") );
+
+ // pos is the logical position to show
+
+ // SData().m_ofsHorz is the fisrt logical position shown
+ if ( pos < SData().m_ofsHorz )
+ {
+ // scroll backwards
+ wxTextCoord col;
+ HitTestLine(m_value, pos, &col);
+ ScrollText(col);
+ }
+ else
+ {
+ wxCoord width = m_rectText.width;
+ if ( !width )
+ {
+ // if we are called from the ctor, m_rectText is not initialized
+ // yet, so do it now
+ UpdateTextRect();
+ width = m_rectText.width;
+ }
+
+ // SData().m_ofsHorz + width is the last logical position shown
+ if ( pos > SData().m_ofsHorz + width)
+ {
+ // scroll forward
+ wxTextCoord col;
+ HitTestLine(m_value, pos - width, &col);
+ ScrollText(col + 1);
+ }
+ }
+}
+
+// scroll the window horizontally so that the first visible character becomes
+// the one at this position
+void wxTextCtrl::ScrollText(wxTextCoord col)
+{
+ wxASSERT_MSG( IsSingleLine(),
+ _T("ScrollText() is for single line controls only") );
+
+ // never scroll beyond the left border
+ if ( col < 0 )
+ col = 0;
+
+ // OPT: could only get the extent of the part of the string between col
+ // and SData().m_colStart
+ wxCoord ofsHorz = GetTextWidth(GetLineText(0).Left(col));
+
+ if ( ofsHorz != SData().m_ofsHorz )
+ {
+ // remember the last currently used pixel
+ int posLastVisible = SData().m_posLastVisible;
+ if ( posLastVisible == -1 )
+ {
+ // this may happen when we're called very early, during the
+ // controls construction
+ UpdateLastVisible();
+
+ posLastVisible = SData().m_posLastVisible;
+ }
+
+ // NB1: to scroll to the right, offset must be negative, hence the
+ // order of operands
+ int dx = SData().m_ofsHorz - ofsHorz;
+
+ // NB2: we call Refresh() below which results in a call to
+ // DoDraw(), so we must update SData().m_ofsHorz before calling it
+ SData().m_ofsHorz = ofsHorz;
+ SData().m_colStart = col;
+
+ // after changing m_colStart, recalc the last visible position: we need
+ // to recalc the last visible position beore scrolling in order to make
+ // it appear exactly at the right edge of the text area after scrolling
+ UpdateLastVisible();
+
+#if 0 // do we?
+ if ( dx < 0 )
+ {
+ // we want to force the update of it after scrolling
+ SData().m_colLastVisible = -1;
+ }
+#endif
+
+ // scroll only the rectangle inside which there is the text
+ wxRect rect = m_rectText;
+ rect.width = posLastVisible;
+
+ rect = ScrollNoRefresh(dx, 0, &rect);
+
+ /*
+ we need to manually refresh the part which ScrollWindow() doesn't
+ refresh (with new API this means the part outside the rect returned
+ by ScrollNoRefresh): indeed, if we had this:
+
+ ********o
+
+ where '*' is text and 'o' is blank area at the end (too small to
+ hold the next char) then after scrolling by 2 positions to the left
+ we're going to have
+
+ ******RRo
+
+ where 'R' is the area refreshed by ScrollWindow() - but we still
+ need to refresh the 'o' at the end as it may be now big enough to
+ hold the new character shifted into view.
+
+ when we are scrolling to the right, we need to update this rect as
+ well because it might have contained something before but doesn't
+ contain anything any more
+ */
+
+ // we can combine both rectangles into one when scrolling to the left,
+ // but we need two separate Refreshes() otherwise
+ if ( dx > 0 )
+ {
+ // refresh the uncovered part on the left
+ Refresh(TRUE, &rect);
+
+ // and now the area on the right
+ rect.x = m_rectText.x + posLastVisible;
+ rect.width = m_rectText.width - posLastVisible;
+ }
+ else // scrolling to the left
+ {
+ // just extend the rect covering the uncovered area to the edge of
+ // the text rect
+ rect.width += m_rectText.width - posLastVisible;
+ }
+
+ Refresh(TRUE, &rect);
+
+ // I don't know exactly why is this needed here but without it we may
+ // scroll the window again (from the same method) before the previously
+ // invalidated area is repainted when typing *very* quickly - and this
+ // may lead to the display corruption
+ Update();
+ }
+}
+
+void wxTextCtrl::CalcUnscrolledPosition(int x, int y, int *xx, int *yy) const
+{
+ if ( IsSingleLine() )
+ {
+ // we don't use wxScrollHelper
+ if ( xx )
+ *xx = x + SData().m_ofsHorz;
+ if ( yy )
+ *yy = y;
+ }
+ else
+ {
+ // let the base class do it
+ wxScrollHelper::CalcUnscrolledPosition(x, y, xx, yy);
+ }
+}
+
+void wxTextCtrl::CalcScrolledPosition(int x, int y, int *xx, int *yy) const
+{
+ if ( IsSingleLine() )
+ {
+ // we don't use wxScrollHelper
+ if ( xx )
+ *xx = x - SData().m_ofsHorz;
+ if ( yy )
+ *yy = y;
+ }
+ else
+ {
+ // let the base class do it
+ wxScrollHelper::CalcScrolledPosition(x, y, xx, yy);
+ }
+}
+
+void wxTextCtrl::DoPrepareDC(wxDC& dc)
+{
+ // for single line controls we only have to deal with SData().m_ofsHorz and it's
+ // useless to call base class version as they don't use normal scrolling
+ if ( IsSingleLine() && SData().m_ofsHorz )
+ {
+ // adjust the DC origin if the text is shifted
+ wxPoint pt = dc.GetDeviceOrigin();
+ dc.SetDeviceOrigin(pt.x - SData().m_ofsHorz, pt.y);
+ }
+ else
+ {
+ wxScrollHelper::DoPrepareDC(dc);
+ }
+}
+
+void wxTextCtrl::UpdateMaxWidth(wxTextCoord line)
+{
+ // OPT!
+
+ // check if the max width changes after this line was modified
+ wxCoord widthMaxOld = MData().m_widthMax,
+ width;
+ GetTextExtent(GetLineText(line), &width, NULL);
+
+ if ( line == MData().m_lineLongest )
+ {
+ // this line was the longest one, is it still?
+ if ( width > MData().m_widthMax )
+ {
+ MData().m_widthMax = width;
+ }
+ else if ( width < MData().m_widthMax )
+ {
+ // we need to find the new longest line
+ RecalcMaxWidth();
+ }
+ //else: its length didn't change, nothing to do
+ }
+ else // it wasn't the longest line, but maybe it became it?
+ {
+ // GetMaxWidth() and not MData().m_widthMax as it might be not calculated yet
+ if ( width > GetMaxWidth() )
+ {
+ MData().m_widthMax = width;
+ MData().m_lineLongest = line;
+ }
+ }
+
+ MData().m_updateScrollbarX = MData().m_widthMax != widthMaxOld;
+}
+
+void wxTextCtrl::RecalcFontMetrics()
+{
+ m_heightLine = GetCharHeight();
+ m_widthAvg = GetCharWidth();
+}
+
+void wxTextCtrl::RecalcMaxWidth()
+{
+ wxASSERT_MSG( !IsSingleLine(), _T("only used for multiline") );
+
+ MData().m_widthMax = -1;
+ (void)GetMaxWidth();
+}
+
+wxCoord wxTextCtrl::GetMaxWidth() const
+{
+ if ( MData().m_widthMax == -1 )
+ {
+ // recalculate it
+
+ // OPT: should we remember the widths of all the lines?
+
+ wxTextCtrl *self = wxConstCast(this, wxTextCtrl);
+ wxClientDC dc(self);
+ dc.SetFont(GetFont());
+
+ self->MData().m_widthMax = 0;
+
+ size_t count = GetLineCount();
+ for ( size_t n = 0; n < count; n++ )
+ {
+ wxCoord width;
+ dc.GetTextExtent(GetLines()[n], &width, NULL);
+ if ( width > MData().m_widthMax )
+ {
+ // remember the width and the line which has it
+ self->MData().m_widthMax = width;
+ self->MData().m_lineLongest = n;
+ }
+ }
+ }
+
+ wxASSERT_MSG( MData().m_widthMax != -1, _T("should have at least 1 line") );
+
+ return MData().m_widthMax;
+}
+
+void wxTextCtrl::UpdateScrollbars()
+{
+ wxASSERT_MSG( !IsSingleLine(), _T("only used for multiline") );
+
+ wxSize size = GetRealTextArea().GetSize();
+
+ // is our height enough to show all items?
+ wxTextCoord nRows = GetRowCount();
+ wxCoord lineHeight = GetLineHeight();
+ bool showScrollbarY = nRows*lineHeight > size.y;
+
+ // is our width enough to show the longest line?
+ wxCoord charWidth, maxWidth;
+ bool showScrollbarX;
+ if ( !WrapLines() )
+ {
+ charWidth = GetAverageWidth();
+ maxWidth = GetMaxWidth();
+ showScrollbarX = maxWidth > size.x;
+ }
+ else // never show the horz scrollbar
+ {
+ // just to suppress compiler warnings about using uninit vars below
+ charWidth = maxWidth = 0;
+
+ showScrollbarX = FALSE;
+ }
+
+ // calc the scrollbars ranges
+ int scrollRangeX = showScrollbarX
+ ? (maxWidth + 2*charWidth - 1) / charWidth
+ : 0;
+ int scrollRangeY = showScrollbarY ? nRows : 0;
+
+ int scrollRangeXOld = MData().m_scrollRangeX,
+ scrollRangeYOld = MData().m_scrollRangeY;
+ if ( (scrollRangeY != scrollRangeYOld) || (scrollRangeX != scrollRangeXOld) )
+ {
+ int x, y;
+ GetViewStart(&x, &y);
+
+#if 0
+ // we want to leave the scrollbars at the same position which means
+ // that x and y have to be adjusted as the number of positions may have
+ // changed
+ //
+ // the number of positions is calculated from knowing that last
+ // position = range - thumbSize and thumbSize == pageSize which is
+ // equal to the window width / pixelsPerLine
+ if ( scrollRangeXOld )
+ {
+ x *= scrollRangeX - m_rectText.width / charWidth;
+ x /= scrollRangeXOld - m_rectText.width / charWidth;
+ }
+
+ if ( scrollRangeYOld )
+ y *= scrollRangeY / scrollRangeYOld;
+#endif // 0
+
+ SetScrollbars(charWidth, lineHeight,
+ scrollRangeX, scrollRangeY,
+ x, y,
+ TRUE /* no refresh */);
+
+ if ( scrollRangeXOld )
+ {
+ x *= scrollRangeX - m_rectText.width / charWidth;
+ x /= scrollRangeXOld - m_rectText.width / charWidth;
+ Scroll(x, y);
+ }
+
+ MData().m_scrollRangeX = scrollRangeX;
+ MData().m_scrollRangeY = scrollRangeY;
+
+ // bring the current position in view
+ ShowPosition(-1);
+ }
+
+ MData().m_updateScrollbarX =
+ MData().m_updateScrollbarY = FALSE;
+}
+
+void wxTextCtrl::OnIdle(wxIdleEvent& event)
+{
+ // notice that single line text control never has scrollbars
+ if ( !IsSingleLine() &&
+ (MData().m_updateScrollbarX || MData().m_updateScrollbarY) )
+ {
+ UpdateScrollbars();
+ }
+
+ event.Skip();
+}
+
+bool wxTextCtrl::SendAutoScrollEvents(wxScrollWinEvent& event) const
+{
+ bool forward = event.GetEventType() == wxEVT_SCROLLWIN_LINEDOWN;
+ if ( event.GetOrientation() == wxHORIZONTAL )
+ {
+ return forward ? m_curCol <= GetLineLength(m_curRow) : m_curCol > 0;
+ }
+ else // wxVERTICAL
+ {
+ return forward ? m_curRow < GetNumberOfLines() : m_curRow > 0;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// refresh
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::RefreshSelection()
+{
+ if ( HasSelection() )
+ {
+ RefreshTextRange(m_selStart, m_selEnd);
+ }
+}
+
+void wxTextCtrl::RefreshLineRange(wxTextCoord lineFirst, wxTextCoord lineLast)
+{
+ wxASSERT_MSG( lineFirst <= lineLast || !lineLast,
+ _T("no lines to refresh") );
+
+ wxRect rect;
+ // rect.x is already 0
+ rect.width = m_rectText.width;
+ wxCoord h = GetLineHeight();
+
+ wxTextCoord rowFirst;
+ if ( lineFirst < GetNumberOfLines() )
+ {
+ rowFirst = GetFirstRowOfLine(lineFirst);
+ }
+ else // lineFirst == GetNumberOfLines()
+ {
+ // lineFirst may be beyond the last line only if we refresh till
+ // the end, otherwise it's illegal
+ wxASSERT_MSG( lineFirst == GetNumberOfLines() && !lineLast,
+ _T("invalid line range") );
+
+ rowFirst = GetRowAfterLine(lineFirst - 1);
+ }
+
+ rect.y = rowFirst*h;
+
+ if ( lineLast )
+ {
+ // refresh till this line (inclusive)
+ wxTextCoord rowLast = GetRowAfterLine(lineLast);
+
+ rect.height = (rowLast - rowFirst + 1)*h;
+ }
+ else // lineLast == 0 means to refresh till the end
+ {
+ // FIXME: calc it exactly
+ rect.height = 32000;
+ }
+
+ RefreshTextRect(rect);
+}
+
+void wxTextCtrl::RefreshTextRange(wxTextPos start, wxTextPos end)
+{
+ wxCHECK_RET( start != -1 && end != -1,
+ _T("invalid RefreshTextRange() arguments") );
+
+ // accept arguments in any order as it is more conenient for the caller
+ OrderPositions(start, end);
+
+ // this is acceptable but we don't do anything in this case
+ if ( start == end )
+ return;
+
+ wxTextPos colStart, lineStart;
+ if ( !PositionToXY(start, &colStart, &lineStart) )
+ {
+ // the range is entirely beyond the end of the text, nothing to do
+ return;
+ }
+
+ wxTextCoord colEnd, lineEnd;
+ if ( !PositionToXY(end, &colEnd, &lineEnd) )
+ {
+ // the range spans beyond the end of text, refresh to the end
+ colEnd = -1;
+ lineEnd = GetNumberOfLines() - 1;
+ }
+
+ // refresh all lines one by one
+ for ( wxTextCoord line = lineStart; line <= lineEnd; line++ )
+ {
+ // refresh the first line from the start of the range to the end, the
+ // intermediate ones entirely and the last one from the beginning to
+ // the end of the range
+ wxTextPos posStart = line == lineStart ? colStart : 0;
+ size_t posCount;
+ if ( (line != lineEnd) || (colEnd == -1) )
+ {
+ // intermediate line or the last one but we need to refresh it
+ // until the end anyhow - do it
+ posCount = wxSTRING_MAXLEN;
+ }
+ else // last line
+ {
+ // refresh just the positions in between the start and the end one
+ posCount = colEnd - posStart;
+ }
+
+ if ( posCount )
+ RefreshColRange(line, posStart, posCount);
+ }
+}
+
+void wxTextCtrl::RefreshColRange(wxTextCoord line,
+ wxTextPos start,
+ size_t count)
+{
+ wxString text = GetLineText(line);
+
+ wxASSERT_MSG( (size_t)start <= text.length() && count,
+ _T("invalid RefreshColRange() parameter") );
+
+ RefreshPixelRange(line,
+ GetTextWidth(text.Left((size_t)start)),
+ GetTextWidth(text.Mid((size_t)start, (size_t)count)));
+}
+
+// this method accepts "logical" coords in the sense that they are coordinates
+// in a logical line but it can span several rows if we wrap lines and
+// RefreshPixelRange() will then refresh several rows
+void wxTextCtrl::RefreshPixelRange(wxTextCoord line,
+ wxCoord start,
+ wxCoord width)
+{
+ // we will use line text only in line wrap case
+ wxString text;
+ if ( WrapLines() )
+ {
+ text = GetLineText(line);
+ }
+
+ // special case: width == 0 means to refresh till the end of line
+ if ( width == 0 )
+ {
+ // refresh till the end of visible line
+ width = GetTotalWidth();
+
+ if ( WrapLines() )
+ {
+ // refresh till the end of text
+ wxCoord widthAll = GetTextWidth(text);
+
+ // extend width to the end of ROW
+ width = widthAll - widthAll % width + width;
+ }
+
+ // no need to refresh beyond the end of line
+ width -= start;
+ }
+ //else: just refresh the specified part
+
+ wxCoord h = GetLineHeight();
+ wxRect rect;
+ rect.x = start;
+ rect.y = GetFirstRowOfLine(line)*h;
+ rect.height = h;
+
+ if ( WrapLines() )
+ {
+ // (1) skip all rows which we don't touch at all
+ const wxWrappedLineData& lineData = WData().m_linesData[line];
+ if ( !WData().IsValidLine(line) )
+ LayoutLines(line);
+
+ wxCoord wLine = 0; // suppress compiler warning about uninit var
+ size_t rowLast = lineData.GetRowCount(),
+ row = 0;
+ while ( (row < rowLast) &&
+ (rect.x > (wLine = lineData.GetRowWidth(row++))) )
+ {
+ rect.x -= wLine;
+ rect.y += h;
+ }
+
+ // (2) now refresh all lines except the last one: note that the first
+ // line is refreshed from the given start to the end, all the next
+ // ones - entirely
+ while ( (row < rowLast) && (width > wLine - rect.x) )
+ {
+ rect.width = GetTotalWidth() - rect.x;
+ RefreshTextRect(rect);
+
+ width -= wLine - rect.x;
+ rect.x = 0;
+ rect.y += h;
+
+ wLine = lineData.GetRowWidth(row++);
+ }
+
+ // (3) the code below will refresh the last line
+ }
+
+ rect.width = width;
+
+ RefreshTextRect(rect);
+}
+
+void wxTextCtrl::RefreshTextRect(const wxRect& rectClient, bool textOnly)
+{
+ wxRect rect;
+ CalcScrolledPosition(rectClient.x, rectClient.y, &rect.x, &rect.y);
+ rect.width = rectClient.width;
+ rect.height = rectClient.height;
+
+ // account for the text area offset
+ rect.Offset(m_rectText.GetPosition());
+
+ // don't refresh beyond the text area unless we're refreshing the line wrap
+ // marks in which case textOnly is FALSE
+ if ( textOnly )
+ {
+ if ( rect.GetRight() > m_rectText.GetRight() )
+ {
+ rect.SetRight(m_rectText.GetRight());
+
+ if ( rect.width <= 0 )
+ {
+ // nothing to refresh
+ return;
+ }
+ }
+ }
+
+ // check the bottom boundary always, even for the line wrap marks
+ if ( rect.GetBottom() > m_rectText.GetBottom() )
+ {
+ rect.SetBottom(m_rectText.GetBottom());
+
+ if ( rect.height <= 0 )
+ {
+ // nothing to refresh
+ return;
+ }
+ }
+
+ // never refresh before the visible rect
+ if ( rect.x < m_rectText.x )
+ rect.x = m_rectText.x;
+
+ if ( rect.y < m_rectText.y )
+ rect.y = m_rectText.y;
+
+ wxLogTrace(_T("text"), _T("Refreshing (%d, %d)-(%d, %d)"),
+ rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
+
+ Refresh(TRUE, &rect);
+}
+
+void wxTextCtrl::RefreshLineWrapMarks(wxTextCoord rowFirst,
+ wxTextCoord rowLast)
+{
+ if ( WData().m_widthMark )
+ {
+ wxRect rectMarks;
+ rectMarks.x = m_rectText.width;
+ rectMarks.width = WData().m_widthMark;
+ rectMarks.y = rowFirst*GetLineHeight();
+ rectMarks.height = (rowLast - rowFirst)*GetLineHeight();
+
+ RefreshTextRect(rectMarks, FALSE /* don't limit to text area */);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// border drawing
+// ----------------------------------------------------------------------------
+
+void wxTextCtrl::DoDrawBorder(wxDC& dc, const wxRect& rect)
+{
+ m_renderer->DrawTextBorder(dc, GetBorder(), rect, GetStateFlags());
+}
+
+// ----------------------------------------------------------------------------
+// client area drawing
+// ----------------------------------------------------------------------------
+
+/*
+ Several remarks about wxTextCtrl redraw logic:
+
+ 1. only the regions which must be updated are redrawn, this means that we
+ never Refresh() the entire window but use RefreshPixelRange() and
+ ScrollWindow() which only refresh small parts of it and iterate over the
+ update region in our DoDraw()
+
+ 2. the text displayed on the screen is obtained using GetTextToShow(): it
+ should be used for all drawing/measuring
+ */
+
+wxString wxTextCtrl::GetTextToShow(const wxString& text) const
+{
+ wxString textShown;
+ if ( IsPassword() )
+ textShown = wxString(_T('*'), text.length());
+ else
+ textShown = text;
+
+ return textShown;
+}
+
+void wxTextCtrl::DoDrawTextInRect(wxDC& dc, const wxRect& rectUpdate)
+{
+ // debugging trick to see the update rect visually
+#ifdef WXDEBUG_TEXT
+ static int s_countUpdates = -1;
+ if ( s_countUpdates != -1 )
+ {
+ wxWindowDC dc(this);
+ dc.SetBrush(*(++s_countUpdates % 2 ? wxRED_BRUSH : wxGREEN_BRUSH));
+ dc.SetPen(*wxTRANSPARENT_PEN);
+ dc.DrawRectangle(rectUpdate);
+ }
+#endif // WXDEBUG_TEXT
+
+ // calculate the range lineStart..lineEnd of lines to redraw
+ wxTextCoord lineStart, lineEnd;
+ if ( IsSingleLine() )
+ {
+ lineStart =
+ lineEnd = 0;
+ }
+ else // multiline
+ {
+ wxPoint pt = rectUpdate.GetPosition();
+ (void)HitTest(pt, NULL, &lineStart);
+
+ pt.y += rectUpdate.height;
+ (void)HitTest(pt, NULL, &lineEnd);
+ }
+
+ // prepare for drawing
+ wxCoord hLine = GetLineHeight();
+
+ // these vars will be used for hit testing of the current row
+ wxCoord y = rectUpdate.y;
+ const wxCoord x1 = rectUpdate.x;
+ const wxCoord x2 = rectUpdate.x + rectUpdate.width;
+
+ wxRect rectText;
+ rectText.height = hLine;
+ wxCoord yClient = y - GetClientAreaOrigin().y;
+
+ // we want to always start at the top of the line, otherwise if we redraw a
+ // rect whose top is in the middle of a line, we'd draw this line shifted
+ yClient -= (yClient - m_rectText.y) % hLine;
+
+ if ( IsSingleLine() )
+ {
+ rectText.y = yClient;
+ }
+ else // multiline, adjust for scrolling
+ {
+ CalcUnscrolledPosition(0, yClient, NULL, &rectText.y);
+ }
+
+ wxRenderer *renderer = GetRenderer();
+
+ // do draw the invalidated parts of each line: note that we iterate here
+ // over ROWs, not over LINEs
+ for ( wxTextCoord line = lineStart;
+ y < rectUpdate.y + rectUpdate.height;
+ y += hLine,
+ rectText.y += hLine )
+ {
+ // calculate the update rect in text positions for this line
+ wxTextCoord colStart, colEnd, colRowStart;
+ wxTextCtrlHitTestResult ht = HitTest2(y, x1, x2,
+ &line, &colStart, &colEnd,
+ &colRowStart);
+
+ if ( (ht == wxTE_HT_BEYOND) || (ht == wxTE_HT_BELOW) )
+ {
+ wxASSERT_MSG( line <= lineEnd, _T("how did we get that far?") );
+
+ if ( line == lineEnd )
+ {
+ // we redrew everything
+ break;
+ }
+
+ // the update rect is beyond the end of line, no need to redraw
+ // anything on this line - but continue with the remaining ones
+ continue;
+ }
+
+ // for single line controls we may additionally cut off everything
+ // which is to the right of the last visible position
+ if ( IsSingleLine() )
+ {
+ // don't show the columns which are scrolled out to the left
+ if ( colStart < SData().m_colStart )
+ colStart = SData().m_colStart;
+
+ // colEnd may be less than colStart if colStart was changed by the
+ // assignment above
+ if ( colEnd < colStart )
+ colEnd = colStart;
+
+ // don't draw the chars beyond the rightmost one
+ if ( SData().m_colLastVisible == -1 )
+ {
+ // recalculate this rightmost column
+ UpdateLastVisible();
+ }
+
+ if ( colStart > SData().m_colLastVisible )
+ {
+ // don't bother redrawing something that is beyond the last
+ // visible position
+ continue;
+ }
+
+ if ( colEnd > SData().m_colLastVisible )
+ {
+ colEnd = SData().m_colLastVisible;
+ }
+ }
+
+ // extract the part of line we need to redraw
+ wxString textLine = GetTextToShow(GetLineText(line));
+ wxString text = textLine.Mid(colStart, colEnd - colStart + 1);
+
+ // now deal with the selection: only do something if at least part of
+ // the line is selected
+ wxTextPos selStart, selEnd;
+ if ( GetSelectedPartOfLine(line, &selStart, &selEnd) )
+ {
+ // and if this part is (at least partly) in the current row
+ if ( (selStart <= colEnd) &&
+ (selEnd >= wxMax(colStart, colRowStart)) )
+ {
+ // these values are relative to the start of the line while the
+ // string passed to DrawTextLine() is only part of it, so
+ // adjust the selection range accordingly
+ selStart -= colStart;
+ selEnd -= colStart;
+
+ if ( selStart < 0 )
+ selStart = 0;
+
+ if ( (size_t)selEnd >= text.length() )
+ selEnd = text.length();
+ }
+ else
+ {
+ // reset selStart and selEnd to avoid passing them to
+ // DrawTextLine() below
+ selStart =
+ selEnd = -1;
+ }
+ }
+
+ // calculate the text coords on screen
+ wxASSERT_MSG( colStart >= colRowStart, _T("invalid string part") );
+ wxCoord ofsStart = GetTextWidth(
+ textLine.Mid(colRowStart,
+ colStart - colRowStart));
+ rectText.x = m_rectText.x + ofsStart;
+ rectText.width = GetTextWidth(text);
+
+ // do draw the text
+ renderer->DrawTextLine(dc, text, rectText, selStart, selEnd,
+ GetStateFlags());
+ wxLogTrace(_T("text"), _T("Line %ld: positions %ld-%ld redrawn."),
+ line, colStart, colEnd);
+ }
+}
+
+void wxTextCtrl::DoDrawLineWrapMarks(wxDC& dc, const wxRect& rectUpdate)
+{
+ wxASSERT_MSG( WrapLines() && WData().m_widthMark,
+ _T("shouldn't be called at all") );
+
+ wxRenderer *renderer = GetRenderer();
+
+ wxRect rectMark;
+ rectMark.x = rectUpdate.x;
+ rectMark.width = rectUpdate.width;
+ wxCoord yTop = GetClientAreaOrigin().y;
+ CalcUnscrolledPosition(0, rectUpdate.y - yTop, NULL, &rectMark.y);
+ wxCoord hLine = GetLineHeight();
+ rectMark.height = hLine;
+
+ wxTextCoord line, rowInLine;
+
+ wxCoord yBottom;
+ CalcUnscrolledPosition(0, rectUpdate.GetBottom() - yTop, NULL, &yBottom);
+ for ( ; rectMark.y < yBottom; rectMark.y += hLine )
+ {
+ if ( !GetLineAndRow(rectMark.y / hLine, &line, &rowInLine) )
+ {
+ // we went beyond the end of text
+ break;
+ }
+
+ // is this row continued on the next one?
+ if ( !WData().m_linesData[line].IsLastRow(rowInLine) )
+ {
+ renderer->DrawLineWrapMark(dc, rectMark);
+ }
+ }
+}
+
+void wxTextCtrl::DoDraw(wxControlRenderer *renderer)
+{
+ // hide the caret while we're redrawing the window and show it after we are
+ // done with it
+ wxCaretSuspend cs(this);
+
+ // prepare the DC
+ wxDC& dc = renderer->GetDC();
+ dc.SetFont(GetFont());
+ dc.SetTextForeground(GetForegroundColour());
+
+ // get the intersection of the update region with the text area: note that
+ // the update region is in window coords and text area is in the client
+ // ones, so it must be shifted before computing intersection
+ wxRegion rgnUpdate = GetUpdateRegion();
+ wxRect rectTextArea = GetRealTextArea();
+ wxPoint pt = GetClientAreaOrigin();
+ wxRect rectTextAreaAdjusted = rectTextArea;
+ rectTextAreaAdjusted.x += pt.x;
+ rectTextAreaAdjusted.y += pt.y;
+ rgnUpdate.Intersect(rectTextAreaAdjusted);
+
+ // even though the drawing is already clipped to the update region, we must
+ // explicitly clip it to the rect we will use as otherwise parts of letters
+ // might be drawn outside of it (if even a small part of a charater is
+ // inside, HitTest() will return its column and DrawText() can't draw only
+ // the part of the character, of course)
+#ifdef __WXMSW__
+ // FIXME: is this really a bug in wxMSW?
+ rectTextArea.width--;
+#endif // __WXMSW__
+ dc.SetClippingRegion(rectTextArea);
+
+ // adjust for scrolling
+ DoPrepareDC(dc);
+
+ // and now refresh the invalidated parts of the window
+ wxRegionIterator iter(rgnUpdate);
+ for ( ; iter.HaveRects(); iter++ )
+ {
+ wxRect r = iter.GetRect();
+
+ // this is a workaround for wxGTK::wxRegion bug
+#ifdef __WXGTK__
+ if ( !r.width || !r.height )
+ {
+ // ignore invalid rect
+ continue;
+ }
+#endif // __WXGTK__
+
+ DoDrawTextInRect(dc, r);
+ }
+
+ // now redraw the line wrap marks (if we draw them)
+ if ( WrapLines() && WData().m_widthMark )
+ {
+ // this is the rect inside which line wrap marks are drawn
+ wxRect rectMarks;
+ rectMarks.x = rectTextAreaAdjusted.GetRight() + 1;
+ rectMarks.y = rectTextAreaAdjusted.y;
+ rectMarks.width = WData().m_widthMark;
+ rectMarks.height = rectTextAreaAdjusted.height;
+
+ rgnUpdate = GetUpdateRegion();
+ rgnUpdate.Intersect(rectMarks);
+
+ wxRect rectUpdate = rgnUpdate.GetBox();
+ if ( rectUpdate.width && rectUpdate.height )
+ {
+ // the marks are outside previously set clipping region
+ dc.DestroyClippingRegion();
+
+ DoDrawLineWrapMarks(dc, rectUpdate);
+ }
+ }
+
+ // show caret first time only: we must show it after drawing the text or
+ // the display can be corrupted when it's hidden
+ if ( !m_hasCaret && GetCaret() )
+ {
+ ShowCaret();
+
+ m_hasCaret = TRUE;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// caret
+// ----------------------------------------------------------------------------
+
+bool wxTextCtrl::SetFont(const wxFont& font)
+{
+ if ( !wxControl::SetFont(font) )
+ return FALSE;
+
+ // and refresh everything, of course
+ InitInsertionPoint();
+ ClearSelection();
+
+ // update geometry parameters
+ UpdateTextRect();
+ RecalcFontMetrics();
+ if ( !IsSingleLine() )
+ {
+ UpdateScrollbars();
+ RecalcMaxWidth();
+ }
+
+ // recreate it, in fact
+ CreateCaret();
+
+ Refresh();
+
+ return TRUE;
+}
+
+bool wxTextCtrl::Enable(bool enable)
+{
+ if ( !wxTextCtrlBase::Enable(enable) )
+ return FALSE;
+
+ ShowCaret(enable);
+
+ return TRUE;
+}
+
+void wxTextCtrl::CreateCaret()
+{
+ wxCaret *caret;
+
+ if ( IsEditable() )
+ {
+ // FIXME use renderer
+ caret = new wxCaret(this, 1, GetLineHeight());
+#ifndef __WXMSW__
+ caret->SetBlinkTime(0);
+#endif // __WXMSW__
+ }
+ else
+ {
+ // read only controls don't have the caret
+ caret = (wxCaret *)NULL;
+ }
+
+ // SetCaret() will delete the old caret if any
+ SetCaret(caret);
+}
+
+void wxTextCtrl::ShowCaret(bool show)
+{
+ wxCaret *caret = GetCaret();
+ if ( caret )
+ {
+ // (re)position caret correctly
+ caret->Move(GetCaretPosition());
+
+ // and show it there
+ caret->Show(show);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// vertical scrolling (multiline only)
+// ----------------------------------------------------------------------------
+
+size_t wxTextCtrl::GetLinesPerPage() const
+{
+ if ( IsSingleLine() )
+ return 1;
+
+ return GetRealTextArea().height / GetLineHeight();
+}
+
+wxTextPos wxTextCtrl::GetPositionAbove()
+{
+ wxCHECK_MSG( !IsSingleLine(), INVALID_POS_VALUE,
+ _T("can't move cursor vertically in a single line control") );
+
+ // move the cursor up by one ROW not by one LINE: this means that
+ // we should really use HitTest() and not just go to the same
+ // position in the previous line
+ wxPoint pt = GetCaretPosition() - m_rectText.GetPosition();
+ if ( MData().m_xCaret == -1 )
+ {
+ // remember the initial cursor abscissa
+ MData().m_xCaret = pt.x;
+ }
+ else
+ {
+ // use the remembered abscissa
+ pt.x = MData().m_xCaret;
+ }
+
+ CalcUnscrolledPosition(pt.x, pt.y, &pt.x, &pt.y);
+ pt.y -= GetLineHeight();
+
+ wxTextCoord col, row;
+ if ( HitTestLogical(pt, &col, &row) == wxTE_HT_BEFORE )
+ {
+ // can't move further
+ return INVALID_POS_VALUE;
+ }
+
+ return XYToPosition(col, row);
+}
+
+wxTextPos wxTextCtrl::GetPositionBelow()
+{
+ wxCHECK_MSG( !IsSingleLine(), INVALID_POS_VALUE,
+ _T("can't move cursor vertically in a single line control") );
+
+ // see comments for wxACTION_TEXT_UP
+ wxPoint pt = GetCaretPosition() - m_rectText.GetPosition();
+ if ( MData().m_xCaret == -1 )
+ {
+ // remember the initial cursor abscissa
+ MData().m_xCaret = pt.x;
+ }
+ else
+ {
+ // use the remembered abscissa
+ pt.x = MData().m_xCaret;
+ }
+
+ CalcUnscrolledPosition(pt.x, pt.y, &pt.x, &pt.y);
+ pt.y += GetLineHeight();
+
+ wxTextCoord col, row;
+ if ( HitTestLogical(pt, &col, &row) == wxTE_HT_BELOW )
+ {
+ // can't go further down
+ return INVALID_POS_VALUE;
+ }
+
+ // note that wxTE_HT_BEYOND is ok: it happens when we go down
+ // from a longer line to a shorter one, for example (OTOH
+ // wxTE_HT_BEFORE can never happen)
+ return XYToPosition(col, row);
+}
+
+// ----------------------------------------------------------------------------
+// input
+// ----------------------------------------------------------------------------
+
+bool wxTextCtrl::PerformAction(const wxControlAction& actionOrig,
+ long numArg,
+ const wxString& strArg)
+{
+ // has the text changed as result of this action?
+ bool textChanged = FALSE;
+
+ // the remembered cursor abscissa for multiline text controls is usually
+ // reset after each user action but for ones which do use it (UP and DOWN
+ // for example) we shouldn't do it - as indicated by this flag
+ bool rememberAbscissa = FALSE;
+
+ // the command this action corresponds to or NULL if this action doesn't
+ // change text at all or can't be undone
+ wxTextCtrlCommand *command = (wxTextCtrlCommand *)NULL;
+
+ wxString action;
+ bool del = FALSE,
+ sel = FALSE;
+ if ( actionOrig.StartsWith(wxACTION_TEXT_PREFIX_DEL, &action) )
+ {
+ if ( IsEditable() )
+ del = TRUE;
+ }
+ else if ( actionOrig.StartsWith(wxACTION_TEXT_PREFIX_SEL, &action) )
+ {
+ sel = TRUE;
+ }
+ else // not selection nor delete action
+ {
+ action = actionOrig;
+ }
+
+ // set newPos to -2 as it can't become equal to it in the assignments below
+ // (but it can become -1)
+ wxTextPos newPos = INVALID_POS_VALUE;
+
+ if ( action == wxACTION_TEXT_HOME )
+ {
+ newPos = m_curPos - m_curCol;
+ }
+ else if ( action == wxACTION_TEXT_END )
+ {
+ newPos = m_curPos + GetLineLength(m_curRow) - m_curCol;
+ }
+ else if ( (action == wxACTION_TEXT_GOTO) ||
+ (action == wxACTION_TEXT_FIRST) ||
+ (action == wxACTION_TEXT_LAST) )
+ {
+ if ( action == wxACTION_TEXT_FIRST )
+ numArg = 0;
+ else if ( action == wxACTION_TEXT_LAST )
+ numArg = GetLastPosition();
+ //else: numArg already contains the position
+
+ newPos = numArg;
+ }
+ else if ( action == wxACTION_TEXT_UP )
+ {
+ if ( !IsSingleLine() )
+ {
+ newPos = GetPositionAbove();
+
+ if ( newPos != INVALID_POS_VALUE )
+ {
+ // remember where the cursor original had been
+ rememberAbscissa = TRUE;
+ }
+ }
+ }
+ else if ( action == wxACTION_TEXT_DOWN )
+ {
+ if ( !IsSingleLine() )
+ {
+ newPos = GetPositionBelow();
+
+ if ( newPos != INVALID_POS_VALUE )
+ {
+ // remember where the cursor original had been
+ rememberAbscissa = TRUE;
+ }
+ }
+ }
+ else if ( action == wxACTION_TEXT_LEFT )
+ {
+ newPos = m_curPos - 1;
+ }
+ else if ( action == wxACTION_TEXT_WORD_LEFT )
+ {
+ newPos = GetWordStart();
+ }
+ else if ( action == wxACTION_TEXT_RIGHT )
+ {
+ newPos = m_curPos + 1;
+ }
+ else if ( action == wxACTION_TEXT_WORD_RIGHT )
+ {
+ newPos = GetWordEnd();
+ }
+ else if ( action == wxACTION_TEXT_INSERT )
+ {
+ if ( IsEditable() && !strArg.empty() )
+ {
+ // inserting text can be undone
+ command = new wxTextCtrlInsertCommand(strArg);
+
+ textChanged = TRUE;
+ }
+ }
+ else if ( (action == wxACTION_TEXT_PAGE_UP) ||
+ (action == wxACTION_TEXT_PAGE_DOWN) )
+ {
+ if ( !IsSingleLine() )
+ {
+ size_t count = GetLinesPerPage();
+ if ( count > PAGE_OVERLAP_IN_LINES )
+ {
+ // pages should overlap slightly to allow the reader to keep
+ // orientation in the text
+ count -= PAGE_OVERLAP_IN_LINES;
+ }
+
+ // remember where the cursor original had been
+ rememberAbscissa = TRUE;
+
+ bool goUp = action == wxACTION_TEXT_PAGE_UP;
+ for ( size_t line = 0; line < count; line++ )
+ {
+ wxTextPos pos = goUp ? GetPositionAbove() : GetPositionBelow();
+ if ( pos == INVALID_POS_VALUE )
+ {
+ // can't move further
+ break;
+ }
+
+ MoveInsertionPoint(pos);
+ newPos = pos;
+ }
+
+ // we implement the Unix scrolling model here: cursor will always
+ // be on the first line after Page Down and on the last one after
+ // Page Up
+ //
+ // Windows programs usually keep the cursor line offset constant
+ // but do we really need it?
+ wxCoord y;
+ if ( goUp )
+ {
+ // find the line such that when it is the first one, the
+ // current position is in the last line
+ wxTextPos pos = 0;
+ for ( size_t line = 0; line < count; line++ )
+ {
+ pos = GetPositionAbove();
+ if ( pos == INVALID_POS_VALUE )
+ break;
+
+ MoveInsertionPoint(pos);
+ }
+
+ MoveInsertionPoint(newPos);
+
+ PositionToLogicalXY(pos, NULL, &y);
+ }
+ else // scrolled down
+ {
+ PositionToLogicalXY(newPos, NULL, &y);
+ }
+
+ // scroll vertically only
+ Scroll(-1, y);
+ }
+ }
+ else if ( action == wxACTION_TEXT_SEL_WORD )
+ {
+ SetSelection(GetWordStart(), GetWordEnd());
+ }
+ else if ( action == wxACTION_TEXT_ANCHOR_SEL )
+ {
+ newPos = numArg;
+ }
+ else if ( action == wxACTION_TEXT_EXTEND_SEL )
+ {
+ SetSelection(m_selAnchor, numArg);
+ }
+ else if ( action == wxACTION_TEXT_COPY )
+ {
+ Copy();
+ }
+ else if ( action == wxACTION_TEXT_CUT )
+ {
+ if ( IsEditable() )
+ Cut();
+ }
+ else if ( action == wxACTION_TEXT_PASTE )
+ {
+ if ( IsEditable() )
+ Paste();
+ }
+ else if ( action == wxACTION_TEXT_UNDO )
+ {
+ if ( CanUndo() )
+ Undo();
+ }
+ else if ( action == wxACTION_TEXT_REDO )
+ {
+ if ( CanRedo() )
+ Redo();
+ }
+ else
+ {
+ return wxControl::PerformAction(action, numArg, strArg);
+ }
+
+ if ( newPos != INVALID_POS_VALUE )
+ {
+ // bring the new position into the range
+ if ( newPos < 0 )
+ newPos = 0;
+
+ wxTextPos posLast = GetLastPosition();
+ if ( newPos > posLast )
+ newPos = posLast;
+
+ if ( del )
+ {
+ // if we have the selection, remove just it
+ wxTextPos from, to;
+ if ( HasSelection() )
+ {
+ from = m_selStart;
+ to = m_selEnd;
+ }
+ else
+ {
+ // otherwise delete everything between current position and
+ // the new one
+ if ( m_curPos != newPos )
+ {
+ from = m_curPos;
+ to = newPos;
+ }
+ else // nothing to delete
+ {
+ // prevent test below from working
+ from = INVALID_POS_VALUE;
+
+ // and this is just to silent the compiler warning
+ to = 0;
+ }
+ }
+
+ if ( from != INVALID_POS_VALUE )
+ {
+ command = new wxTextCtrlRemoveCommand(from, to);
+ }
+ }
+ else // cursor movement command
+ {
+ // just go there
+ DoSetInsertionPoint(newPos);
+
+ if ( sel )
+ {
+ SetSelection(m_selAnchor, m_curPos);
+ }
+ else // simple movement
+ {
+ // clear the existing selection
+ ClearSelection();
+ }
+ }
+
+ if ( !rememberAbscissa && !IsSingleLine() )
+ {
+ MData().m_xCaret = -1;
+ }
+ }
+
+ if ( command )
+ {
+ // execute and remember it to be able to undo it later
+ m_cmdProcessor->Submit(command);
+
+ // undoable commands always change text
+ textChanged = TRUE;
+ }
+ else // no undoable command
+ {
+ // m_cmdProcessor->StopCompressing()
+ }
+
+ if ( textChanged )
+ {
+ wxASSERT_MSG( IsEditable(), _T("non editable control changed?") );
+
+ wxCommandEvent event(wxEVT_COMMAND_TEXT_UPDATED, GetId());
+ InitCommandEvent(event);
+ event.SetString(GetValue());
+ GetEventHandler()->ProcessEvent(event);
+
+ // as the text changed...
+ m_isModified = TRUE;
+ }
+
+ return TRUE;
+}
+
+void wxTextCtrl::OnChar(wxKeyEvent& event)
+{
+ // only process the key events from "simple keys" here
+ if ( !event.HasModifiers() )
+ {
+ int keycode = event.GetKeyCode();
+ if ( keycode == WXK_RETURN )
+ {
+ if ( IsSingleLine() || (GetWindowStyle() & wxTE_PROCESS_ENTER) )
+ {
+ wxCommandEvent event(wxEVT_COMMAND_TEXT_ENTER, GetId());
+ InitCommandEvent(event);
+ event.SetString(GetValue());
+ GetEventHandler()->ProcessEvent(event);
+ }
+ else // interpret <Enter> normally: insert new line
+ {
+ PerformAction(wxACTION_TEXT_INSERT, -1, _T('\n'));
+ }
+ }
+ else if ( keycode < 255 && isprint(keycode) )
+ {
+ PerformAction(wxACTION_TEXT_INSERT, -1, (wxChar)keycode);
+
+ // skip event.Skip() below
+ return;
+ }
+ }
+#ifdef __WXDEBUG__
+ // Ctrl-R refreshes the control in debug mode
+ else if ( event.ControlDown() && event.GetKeyCode() == 'r' )
+ Refresh();
+#endif // __WXDEBUG__
+
+ event.Skip();
+}
+
+// ----------------------------------------------------------------------------
+// wxStdTextCtrlInputHandler
+// ----------------------------------------------------------------------------
+
+wxStdTextCtrlInputHandler::wxStdTextCtrlInputHandler(wxInputHandler *inphand)
+ : wxStdInputHandler(inphand)
+{
+ m_winCapture = (wxTextCtrl *)NULL;
+}
+
+/* static */
+wxTextPos wxStdTextCtrlInputHandler::HitTest(const wxTextCtrl *text,
+ const wxPoint& pt)
+{
+ wxTextCoord col, row;
+ wxTextCtrlHitTestResult ht = text->HitTest(pt, &col, &row);
+
+ wxTextPos pos = text->XYToPosition(col, row);
+
+ // if the point is after the last column we must adjust the position to be
+ // the last position in the line (unless it is already the last)
+ if ( (ht == wxTE_HT_BEYOND) && (pos < text->GetLastPosition()) )
+ {
+ pos++;
+ }
+
+ return pos;
+}
+
+bool wxStdTextCtrlInputHandler::HandleKey(wxControl *control,
+ const wxKeyEvent& event,
+ bool pressed)
+{
+ // we're only interested in key presses
+ if ( !pressed )
+ return FALSE;
+
+ int keycode = event.GetKeyCode();
+
+ wxControlAction action;
+ wxString str;
+ bool ctrlDown = event.ControlDown(),
+ shiftDown = event.ShiftDown();
+ if ( shiftDown )
+ {
+ action = wxACTION_TEXT_PREFIX_SEL;
+ }
+
+ // the only key combination with Alt we recognize is Alt-Bksp for undo, so
+ // treat it first separately
+ if ( event.AltDown() )
+ {
+ if ( keycode == WXK_BACK && !ctrlDown && !shiftDown )
+ action = wxACTION_TEXT_UNDO;
+ }
+ else switch ( keycode )
+ {
+ // cursor movement
+ case WXK_HOME:
+ action << (ctrlDown ? wxACTION_TEXT_FIRST
+ : wxACTION_TEXT_HOME);
+ break;
+
+ case WXK_END:
+ action << (ctrlDown ? wxACTION_TEXT_LAST
+ : wxACTION_TEXT_END);
+ break;
+
+ case WXK_UP:
+ if ( !ctrlDown )
+ action << wxACTION_TEXT_UP;
+ break;
+
+ case WXK_DOWN:
+ if ( !ctrlDown )
+ action << wxACTION_TEXT_DOWN;
+ break;
+
+ case WXK_LEFT:
+ action << (ctrlDown ? wxACTION_TEXT_WORD_LEFT
+ : wxACTION_TEXT_LEFT);
+ break;
+
+ case WXK_RIGHT:
+ action << (ctrlDown ? wxACTION_TEXT_WORD_RIGHT
+ : wxACTION_TEXT_RIGHT);
+ break;
+
+ case WXK_PAGEDOWN:
+ case WXK_NEXT:
+ // we don't map Ctrl-PgUp/Dn to anything special - what should it
+ // to? for now, it's the same as without control
+ action << wxACTION_TEXT_PAGE_DOWN;
+ break;
+
+ case WXK_PAGEUP:
+ case WXK_PRIOR:
+ action << wxACTION_TEXT_PAGE_UP;
+ break;
+
+ // delete
+ case WXK_DELETE:
+ if ( !ctrlDown )
+ action << wxACTION_TEXT_PREFIX_DEL << wxACTION_TEXT_RIGHT;
+ break;
+
+ case WXK_BACK:
+ if ( !ctrlDown )
+ action << wxACTION_TEXT_PREFIX_DEL << wxACTION_TEXT_LEFT;
+ break;
+
+ // something else
+ default:
+ // reset the action as it could be already set to one of the
+ // prefixes
+ action = wxACTION_NONE;
+
+ if ( ctrlDown )
+ {
+ switch ( keycode )
+ {
+ case 'A':
+ action = wxACTION_TEXT_REDO;
+ break;
+
+ case 'C':
+ action = wxACTION_TEXT_COPY;
+ break;
+
+ case 'V':
+ action = wxACTION_TEXT_PASTE;
+ break;
+
+ case 'X':
+ action = wxACTION_TEXT_CUT;
+ break;
+
+ case 'Z':
+ action = wxACTION_TEXT_UNDO;
+ break;
+ }
+ }
+ }
+
+ if ( (action != wxACTION_NONE) && (action != wxACTION_TEXT_PREFIX_SEL) )
+ {
+ control->PerformAction(action, -1, str);
+
+ return TRUE;
+ }
+
+ return wxStdInputHandler::HandleKey(control, event, pressed);
+}
+
+bool wxStdTextCtrlInputHandler::HandleMouse(wxControl *control,
+ const wxMouseEvent& event)
+{
+ if ( event.LeftDown() )
+ {
+ wxASSERT_MSG( !m_winCapture, _T("left button going down twice?") );
+
+ wxTextCtrl *text = wxStaticCast(control, wxTextCtrl);
+
+ m_winCapture = text;
+ m_winCapture->CaptureMouse();
+
+ text->HideCaret();
+
+ wxTextPos pos = HitTest(text, event.GetPosition());
+ if ( pos != -1 )
+ {
+ text->PerformAction(wxACTION_TEXT_ANCHOR_SEL, pos);
+ }
+ }
+ else if ( event.LeftDClick() )
+ {
+ // select the word the cursor is on
+ control->PerformAction(wxACTION_TEXT_SEL_WORD);
+ }
+ else if ( event.LeftUp() )
+ {
+ if ( m_winCapture )
+ {
+ m_winCapture->ShowCaret();
+
+ m_winCapture->ReleaseMouse();
+ m_winCapture = (wxTextCtrl *)NULL;
+ }
+ }
+
+ return wxStdInputHandler::HandleMouse(control, event);
+}
+
+bool wxStdTextCtrlInputHandler::HandleMouseMove(wxControl *control,
+ const wxMouseEvent& event)
+{
+ if ( m_winCapture )
+ {
+ // track it
+ wxTextCtrl *text = wxStaticCast(m_winCapture, wxTextCtrl);
+ wxTextPos pos = HitTest(text, event.GetPosition());
+ if ( pos != -1 )
+ {
+ text->PerformAction(wxACTION_TEXT_EXTEND_SEL, pos);
+ }
+ }
+
+ return wxStdInputHandler::HandleMouseMove(control, event);
+}
+
+bool wxStdTextCtrlInputHandler::HandleFocus(wxControl *control,
+ const wxFocusEvent& event)
+{
+ wxTextCtrl *text = wxStaticCast(control, wxTextCtrl);
+
+ // the selection appearance changes depending on whether we have the focus
+ text->RefreshSelection();
+
+ // never refresh entirely
+ return FALSE;
+}
+
+#endif // wxUSE_TEXTCTRL