/* ====================================================================
* Copyright (c) 2003-2006, Martin Hauner
* http://subcommander.tigris.org
*
* Subcommander is licensed as described in the file doc/COPYING, which
* you should have received as part of this distribution.
* ====================================================================
*/
// sc
#include "TextWidget.h"
#include "LinePainter.h"
#include "TextPositionCalculator.h"
#include "ScrollPositionCalculator.h"
#include "PainterSetup.h"
#include "Pair.h"
#include "ColorId.h"
#include "sublib/Tab.h"
#include "sublib/Line.h"
#include "sublib/TextModel.h"
#include "sublib/NullTextModel.h"
#include "sublib/PaintLine.h"
#include "sublib/PaintLineFactory.h"
#include "sublib/LineConfig.h"
#include "sublib/ColorStorage.h"
// sys
#include <stdio.h>
#include <algorithm>
#include <assert.h>
#include "util/max.h"
// qt
#include <qapplication.h>
#include <qtimer.h>
#include <qclipboard.h>
#include <qdragobject.h>
static const int ScrollBorder = 2; // chars
static const int LinePad = 2; // pixel
static NullTextModel NullText;
class TextHighlightCalculator
{
public:
TextHighlightCalculator()
{
}
bool calcHighlight( const CursorPair& cp, int curLine, int maxcol, IntPair& pos )
{
const Cursor& topCursor = cp.getOne();
const Cursor& botCursor = cp.getTwo();
int col1 = 0;
int col2 = 0;
if( curLine > topCursor.line() && curLine < botCursor.line() )
{
// completely highlighted line
col1 = 0;
col2 = maxcol;
}
else if( curLine == topCursor.line() && curLine == botCursor.line() )
{
// possibly only partially highlighted line
col1 = topCursor.column();
col2 = botCursor.column();
}
else if( curLine == topCursor.line() && curLine != botCursor.line() )
{
// partially highlighted first line
col1 = topCursor.column();
col2 = maxcol;
}
else if( curLine == botCursor.line() && curLine != topCursor.line() )
{
// partially highlighted last line
col1 = 0;
col2 = botCursor.column();
}
pos.one = col1;
pos.two = col2;
if( pos.equal() )
{
return false;
}
return true;
}
};
const QColor& getConflictBgColor( ConflictType t )
{
switch( t )
{
case ctConflictAll:
case ctConflictAllEmpty:
{
return ColorStorage::getColor(ColorConflictBgAll);
}
case ctConflictLatestModified:
case ctConflictLatestModifiedEmpty:
{
return ColorStorage::getColor(ColorConflictBgLatestModified);
}
case ctConflictModifiedLatest:
case ctConflictModifiedLatestEmpty:
{
return ColorStorage::getColor(ColorConflictBgModifiedLatest);
}
case ctConflictOriginal:
case ctConflictOriginalEmpty:
{
return ColorStorage::getColor(ColorConflictBgOriginal);
}
case ctNop:
{
return ColorStorage::getColor(ColorNopBg);
}
case ctConflictAllMerged:
case ctConflictLatestModifiedMerged:
case ctConflictModifiedLatestMerged:
case ctConflictOriginalMerged:
case ctConflictAllEmptyMerged:
case ctConflictLatestModifiedEmptyMerged:
case ctConflictModifiedLatestEmptyMerged:
case ctConflictOriginalEmptyMerged:
{
return ColorStorage::getColor(ColorMergedBg);
}
default:
{
return ColorStorage::getColor(ColorNormalBg);
}
}
}
TextWidget::TextWidget( QWidget *parent, const char *name )
: super( parent, name ), _columns(0), _lines(0), _xpos(0), _ypos(0),
_enableRightColMark(true), _rightColMark(72),
_shiftKey(false), _cntrlKey(false), _editable(false), _enableSelection(false)
{
//setEditable(true);
//setWFlags( Qt::WStyle_NormalBorder );
//setMouseTracking(true);
setSizePolicy( QSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding) );
setBackgroundMode( Qt::NoBackground );
setModel(&NullText,&NullText);
_drawSel = false;
_tSel = new QTimer(this);
_selBlockNr = -1;
connect( _tSel, SIGNAL(timeout()), this, SLOT(timerSelection()) );
_tCursor = new QTimer(this);
connect( _tCursor, SIGNAL(timeout()), this, SLOT(timerCursor()) );
setFocusPolicy( QWidget::WheelFocus );
//setAcceptDrops(true);
}
TextWidget::~TextWidget()
{
}
void TextWidget::setModel( TextModel* model, TextModel* diff )
{
_model = model;
_lines = _model->getLineCnt();
_columns = _model->getColumnCnt();
_diff = diff;
updateGeometry();
update();
}
TextModel* TextWidget::getModel() const
{
return _model;
}
void TextWidget::setModelSize( sc::Size cols, sc::Size lines )
{
_lines = lines;
_columns = cols;
updateGeometry();
emit sizeChanged();
}
void TextWidget::setEditable( bool editable )
{
_editable = editable;
}
bool TextWidget::isEditable() const
{
return _editable;
}
void TextWidget::setRightColMark( int column )
{
_rightColMark = column;
}
void TextWidget::enableRightColMark( bool b )
{
_enableRightColMark = b;
}
void TextWidget::enableSelection( bool enable )
{
_enableSelection = enable;
}
#if 0
void TextWidget::enableCursor( bool enable )
{
_enableCursor = enable;
}
#endif
void TextWidget::paintEvent( QPaintEvent *e )
{
QRect pr = e->rect(); //printf("prect: %d %d\n", pr.x(), pr.width() );
// Qt/X11 does crash when pr is null
// todo: why is it null?
if( pr.isNull() )
return;
PainterSetup ps(this,pr);
QPainter& pp = ps.getPainter();
pp.setPen(ColorStorage::getColor(ColorTextFg));
// prepare cursors for highlighted text repaint
CursorPair cp( _model->getCursor(), _model->getCursor2() );
cp.order();
LineConfig lcfg;
Tab tab(_model->getTabWidth());
QFontMetrics m(font());
TextPositionCalculator tpc( m, pr, _xpos, _ypos );
// get first and last line we need to repaint
int topLine = tpc.getTopLine();
int botLine = tpc.getBottomLine();
for( int curLine = topLine; curLine <= botLine; curLine++ )
{
Line line = _model->getLine(curLine);
Line diff = _diff->getLine(curLine);
IntPair pos0(0,0); // result
TextHighlightCalculator thc;
bool highlight = thc.calcHighlight( cp, curLine, tab.calcColumns(line.getStr()), pos0 );
const PaintLine* pline = PaintLineFactory::create( line.getLine(), pos0.one, pos0.two, lcfg );
QString str = QString::fromUtf8( pline->getPaint() );
// draw diff/merge background
pp.setBackgroundColor( getConflictBgColor(line.getType()) );
pp.eraseRect( tpc.getLineX(), tpc.getLineY(curLine), pr.width(), tpc.getFontHeight() );
// draw single character diff background
if( line.isDifference() && ! diff.isEmpty() )
{
const PaintLine* plinediff = PaintLineFactory::create( diff.getLine(), pos0.one, pos0.two, lcfg );
QString strdiff = QString::fromUtf8( plinediff->getPaint() );
unsigned int maxLen = std::max(str.length(),strdiff.length());
unsigned int minLen = std::min(str.length(),strdiff.length());
// we only care for character differences if both lines are nearly equal.
double equalityFactor = 10.0 / 100.0; // in percent
if( maxLen - minLen < maxLen * equalityFactor )
{
unsigned int cntDiff = 0;
for( unsigned int pos = 0; pos < minLen; pos++ )
{
if( str.at(pos) != strdiff.at(pos) )
{
cntDiff++;
}
}
if( cntDiff <= maxLen * equalityFactor )
{
for( unsigned int pos = 0; pos < maxLen; pos++ )
{
if( str.at(pos) != strdiff.at(pos) )
{
pp.setBackgroundColor( getConflictBgColor(line.getType()).dark(110) );
int left = tpc.getCursorX( pos, str, LinePad );
int right = tpc.getCursorX( pos+1, str, LinePad );
pp.eraseRect( left, tpc.getLineY(curLine), right-left, tpc.getFontHeight() );
}
}
}
}
}
// draw highlighted background
if( highlight )
{
int left = tpc.getCursorX( pos0.one, str, LinePad );
int right = tpc.getCursorX( pos0.two, str, LinePad );
pp.setBackgroundColor( ColorStorage::getColor(ColorHighlightedBg) );
pp.eraseRect( left, tpc.getLineY(curLine), right-left, tpc.getFontHeight() );
}
// draw right column marker
if( _enableRightColMark )
{
// fonth width with proportional font??
int dashX = tpc.getFontWidth() * _rightColMark + 2 - _xpos;
pp.setPen( ColorStorage::getColor(ColorDashBg) );
pp.drawLine( dashX, pr.y(), dashX, pr.y()+pr.height() );
pp.setPen( ColorStorage::getColor(ColorDashFg) );
for( int d = pr.y()+(_ypos + pr.y())%2; d < pr.y()+pr.height(); d+=2 )
{
pp.drawPoint( dashX, d );
}
}
// draw text if any.
if( ! line.isEmpty() )
{
LinePainter p( ColorStorage::getColor(ColorTextFg), ColorStorage::getColor(ColorWhitespaceFg),
ColorStorage::getColor(ColorHighlightedTextFg), ColorStorage::getColor(ColorHighlightedWhitespaceFg) );
p.drawLine( pp, tpc.getTextX(LinePad), tpc.getTextY(curLine), pline );
}
delete pline;
}
if( _enableSelection && _drawSel )
{
QPen pen( QColor(0,0,120) );
pen.setWidth(1);
pp.setPen( pen );
pp.drawRect( calcBlockRect(tpc) );
}
if( isEditable() && _model->getCursor().isOn() )
{
pp.setPen( QColor(0,0,0) );
pp.setRasterOp( Qt::NotXorROP );
pp.drawRect( calcCursorRect(tpc,_model->getCursor()) );
}
}
QSize TextWidget::sizeHint() const
{
// fixme: _columns does not include tab adjustments...
QFontMetrics m(font());
sc::Size h = m.height() * _lines;
sc::Size w = m.maxWidth() * _columns + 2*LinePad;
return QSize( (int)w, (int)h );
}
void TextWidget::setScrollPosX( int xpos )
{
ScrollPositionCalculator spc;
int ox = _xpos;
int nx = spc.calcPos( ox, xpos, width(), sizeHint().width() );
if( ox == nx )
{
return;
}
_xpos = nx;
super::scroll( ox - _xpos, 0 );
emit xChanged(_xpos);
}
void TextWidget::setScrollPosY( int ypos )
{
ScrollPositionCalculator spc;
int oy = _ypos;
int ny = spc.calcPos( oy, ypos, height(), sizeHint().height() );
if( oy == ny )
{
return;
}
_ypos = ny;
super::scroll( 0, oy - _ypos );
emit yChanged(_ypos);
}
void TextWidget::timerCursor()
{
Cursor c = _model->getCursor();
c.toggle();
_model->setCursor(c);
update( calcCursorRect(c) );
}
void TextWidget::timerSelection()
{
_tSelCnt++;
_drawSel = (_tSelCnt%2) == 1;
int t = 80;
if( _tSelCnt < 5 )
{
_tSel->start( t, true );
}
update( calcBlockRect() );
}
void TextWidget::focusInEvent( QFocusEvent* e )
{
if( isEditable() )
{
_tCursor->start(750);
Cursor c = _model->getCursor();
c.setOn();
_model->setCursor(c);
update( calcCursorRect(c) );
}
}
void TextWidget::focusOutEvent( QFocusEvent* e )
{
if( isEditable() )
{
_tCursor->stop();
Cursor c = _model->getCursor();
c.setOff();
_model->setCursor( c );
update( calcCursorRect(_model->getCursor()) );
}
}
void TextWidget::setScrollPosCursor( const Cursor& oc, const Cursor& nc )
{
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
int topLine = tpc.getTopLine();
int botLine = tpc.getBottomLine();
int leftCol = tpc.getLeftColumn();
int rightCol = tpc.getRightColumn();
int dx = nc.column() - oc.column();
int dy = nc.line() - oc.line();
//printf( "oc(%d) ol(%d) nc(%d) nl(%d)\n", oc.column(), oc.line(), nc.column(), nc.line() );
//printf( "l(%d) r(%d) t(%d) b(%d)\n", leftCol, rightCol, topLine, botLine );
//printf( "xpos(%d) ypos(%d) dx(%d) dy(%d)\n", _xpos, _ypos, dx, dy );
// right
if( dx > 0 && nc.column() > (rightCol - ScrollBorder) )
{
if( dy != 0 ) // line up by cursor left
{
setScrollPosX( _xpos + tpc.getFontWidth()*(nc.column()-rightCol+ScrollBorder) );
}
else
{
setScrollPosX( _xpos + tpc.getFontWidth()*dx );
}
}
// left
if( dx < 0 && nc.column() <= (leftCol + ScrollBorder) )
{
setScrollPosX( _xpos + tpc.getFontWidth()*dx );
}
// down
if( dy > 0 && nc.line() >= (botLine - ScrollBorder) )
{
setScrollPosY( _ypos + tpc.getFontHeight()*dy );
}
// up
if( dy < 0 && nc.line() < (topLine + ScrollBorder) )
{
setScrollPosY( _ypos + tpc.getFontHeight()*dy );
}
}
void TextWidget::moveCursorRight()
{
updateCursor();
Cursor oc = _model->getCursor();
Cursor nc = _model->moveCursorRight(!_shiftKey);
setScrollPosCursor( oc, nc );
updateCursor();
}
void TextWidget::moveCursorLeft()
{
updateCursor();
Cursor oc = _model->getCursor();
Cursor nc = _model->moveCursorLeft(!_shiftKey);
setScrollPosCursor( oc, nc );
updateCursor();
}
void TextWidget::moveCursorDown()
{
updateCursor();
Cursor oc = _model->getCursor();
Cursor nc = _model->moveCursorDown(!_shiftKey);
setScrollPosCursor( oc, nc );
updateCursor();
}
void TextWidget::moveCursorUp()
{
updateCursor();
Cursor oc = _model->getCursor();
Cursor nc = _model->moveCursorUp(!_shiftKey);
setScrollPosCursor( oc, nc );
updateCursor();
}
//TODO hmm, add cursor parameter?
void TextWidget::updateCursor()
{
update( calcCursorRect(_model->getCursor()) );
update( calcHighlightedRect() );
}
bool TextWidget::event( QEvent* e )
{
if( e->type() == QEvent::KeyPress )
{
QKeyEvent* key = (QKeyEvent*)e;
switch( key->key() )
{
case Qt::Key_Tab:
{
keyPressEvent( key );
return true;
}
}
}
return super::event(e);
}
void TextWidget::keyPressEvent( QKeyEvent* e )
{
//printf("key press\n");
switch( e->key() )
{
case Qt::Key_Shift:
{
_shiftKey = true;
e->accept();
return;
}
case Qt::Key_Control:
{
_cntrlKey = true;
e->accept();
return;
}
case Qt::Key_Z: // undo
{
if( ! _cntrlKey )
{
break;
}
_model->undo();
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
case Qt::Key_Y: // redo
{
if( ! _cntrlKey )
{
break;
}
_model->redo();
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
case Qt::Key_Right:
{
//printf("key right\n");
moveCursorRight();
e->accept();
return;
}
case Qt::Key_Left:
{
//printf("key left\n");
moveCursorLeft();
e->accept();
return;
}
case Qt::Key_Up:
{
//printf("key up\n");
moveCursorUp();
e->accept();
return;
}
case Qt::Key_Down:
{
//printf("key down\n");
moveCursorDown();
e->accept();
return;
}
case Qt::Key_C:
{
if( e->state() == Qt::ControlButton )
{
QString hl = _model->getHighlightedText().getStr();
#if 1
QTextDrag* qtd = new QTextDrag( QString::fromUtf8(hl) );
#else
// this doesn't work on windows.. :(
QTextDrag* qtd = new QTextDrag( hl );
qtd->setSubtype( "utf8" );
#endif
QApplication::clipboard()->setData( qtd );
e->accept();
return;
}
break;
}
default:
{
break;
}
}
if( ! isEditable() )
{
e->ignore();
return;
}
switch( e->key() )
{
case Qt::Key_Backspace:
{
_model->removeTextLeft();
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
case Qt::Key_Delete:
{
_model->removeTextRight();
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
case Qt::Key_V:
{
if( e->state() == Qt::ControlButton )
{
QCString plain("plain");
QString paste = QApplication::clipboard()->text( plain, QClipboard::Clipboard );
_model->addText( sc::String(paste.utf8()) );
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
}
default:
{
QCString s = e->text().utf8();
if( s.size() == 1 && ! QChar(s[0]).isPrint() )
{
break;
}
sc::String t( s );
_model->addText(t);
setModelSize( _model->getColumnCnt(),_model->getLineCnt() );
update();
e->accept();
return;
}
}
e->ignore();
//printf( "cursor l(%d) c(%d)\n", _model->getCursor().line(), _model->getCursor().column() );
}
void TextWidget::keyReleaseEvent( QKeyEvent* e )
{
switch( e->key() )
{
case Qt::Key_Shift:
{
_shiftKey = false;
break;
}
case Qt::Key_Control:
{
_cntrlKey = false;
break;
}
default:
{
e->ignore();
return;
}
}
e->accept();
}
void TextWidget::mousePressEvent( QMouseEvent* e )
{
if( e->button() == Qt::LeftButton )
{
Cursor c = calcCursorPos( e->x(), e->y() );
Cursor nc = _model->calcNearestCursorPos(c);
if( ! _shiftKey )
{
// no shift key, clear highlighted
update( calcCursorRect(_model->getCursor()) );
update( calcHighlightedRect() );
_model->setCursor( nc );
_model->setCursor2( nc );
update( calcCursorRect(_model->getCursor()) );
}
else
{
// shift key
update( calcHighlightedRect() );
_model->setCursor2( nc );
update( calcHighlightedRect() );
}
}
else if( e->button() == Qt::RightButton )
{
Cursor c = calcCursorPos( e->x(), e->y() );
emit mouseLine( c.line() );
}
}
void TextWidget::mouseReleaseEvent( QMouseEvent* e )
{
super::mouseReleaseEvent(e);
}
void TextWidget::mouseDoubleClickEvent( QMouseEvent* e )
{
if( e->button() == Qt::LeftButton )
{
Cursor c = calcCursorPos( e->x(), e->y() );
const Line& l = _model->getLine(c.line());
if( ! _shiftKey )
{
// is it a non selectable block?
if( l.getType() == ctCommon || l.getType() == ctNop )
{
return;
}
// select block
setBlockSelection( l.getBlockNr() );
emit blockChanged( l.getBlockNr() );
_tSelCnt = 0;
_tSel->start( 250, true );
}
}
}
void TextWidget::mouseMoveEvent( QMouseEvent* e )
{
// TODO handle scroll pos when the cursor is moved outside of the scroll rect
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
int topLine = tpc.getTopLine();
int botLine = tpc.getBottomLine();
int leftCol = tpc.getLeftColumn();
int rightCol = tpc.getRightColumn();
if( e->state() == Qt::LeftButton )
{
Cursor c = calcCursorPos( e->x(), e->y() );
Tab tab(_model->getTabWidth());
c.maxColumn( tab.calcColumns(_model->getLine(c.line()).getStr()) ); // hmm
if( c.column() > rightCol-ScrollBorder )
{
setScrollPosX( _xpos+tpc.getFontWidth() );
}
if( c.column() < leftCol+ScrollBorder )
{
setScrollPosX( _xpos-tpc.getFontWidth() );
}
if( c.line() > botLine-ScrollBorder )
{
setScrollPosY( _ypos+tpc.getFontHeight() );
}
if( c.line() < topLine+ScrollBorder )
{
setScrollPosY( _ypos-tpc.getFontHeight() );
}
update( calcHighlightedRect() );
_model->setCursor2( c );
update( calcHighlightedRect() );
}
}
// todo store selection info in "model" (DiffInfo!?)
void TextWidget::setBlockSelection( int block )
{
update( calcBlockRect() );
_drawSel = true;
_selBlock = _model->getBlockInfo( block );
_selBlockNr = block;
update( calcBlockRect() );
}
void TextWidget::clearBlockSelection()
{
_drawSel = false;
update( calcBlockRect() );
}
int TextWidget::getBlockSelection() const
{
return _selBlockNr;
}
QRect TextWidget::calcCursorRect( const TextPositionCalculator& calc, const Cursor& c )
{
const Line& line = _model->getLine(c.line());
const PaintLine* pline = PaintLineFactory::create( line.getLine(), c.column(),
c.column(), LineConfig() );
int x = calc.getCursorX( c.column(), QString::fromUtf8(pline->getPaint()), LinePad );
int y = calc.getLineY(c.line());
int w = 2;
int h = calc.getFontHeight();
return QRect( x, y, w, h );
}
QRect TextWidget::calcCursorRect( const Cursor& c )
{
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
return calcCursorRect(tpc,c);
}
QRect TextWidget::calcBlockRect( const TextPositionCalculator& calc )
{
return QRect(
calc.getTextX(), calc.getLineY(_selBlock.getStart()),
std::max(width(),sizeHint().width()), calc.getHeight(_selBlock.getLength()) );
}
QRect TextWidget::calcBlockRect()
{
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
return calcBlockRect(tpc);
}
QRect TextWidget::calcHighlightedRect()
{
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
CursorPair cp( _model->getCursor(), _model->getCursor2() );
cp.order();
int top = tpc.getLineY( cp.getOne().line()-1 );
int bot = tpc.getLineY( cp.getTwo().line()+1 );
return QRect( tpc.getTextX(), top, sizeHint().width(), bot-top );
}
int TextWidget::calcLineY( int line, bool optimize )
{
// we pass as scroll position 0,0 because we want absolute values
QFontMetrics m(font());
TextPositionCalculator tpc( m, rect(), 0, 0 );
// let's have some space between the widgets top and the target
// line, as long as we have space for it...
if( optimize && (height() > (2*ScrollBorder+1)*m.height()) )
{
line -= ScrollBorder;
}
return tpc.getLineY( line );
}
Cursor TextWidget::calcCursorPos( int x, int y )
{
QFontMetrics m( font() );
TextPositionCalculator tpc( m, rect(), _xpos, _ypos );
int lineno = ( y + _ypos ) / m.height();
const Line& line = _model->getLine(lineno);
const PaintLine* pline = PaintLineFactory::create( line.getLine(), 0, 0, LineConfig() );
int colno = tpc.getColumn( x + _xpos, QString::fromUtf8(pline->getPaint()) );
//printf( "c l(%d) c(%d)\n", lineno, colno );
return Cursor( lineno, colno );
}
void TextWidget::dragEnterEvent(QDragEnterEvent* e)
{
e->accept( QUriDrag::canDecode(e) || QTextDrag::canDecode(e) );
}
void TextWidget::dropEvent(QDropEvent* e)
{
if( e->type() == QEvent::Drop )
{
QDropEvent* de = (QDropEvent*)e;
QStringList drop;
if( QUriDrag::decodeLocalFiles(de,drop) )
{
QString dropped = drop.first();
emit fileDropped(dropped);
return;
}
QString dropped;
if( QTextDrag::decode(de, dropped) )
{
emit fileDropped(dropped);
return;
}
}
}
syntax highlighted by Code2HTML, v. 0.9.1