/* ==================================================================== * 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 #include #include #include "util/max.h" // qt #include #include #include #include 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; } } }