/* This file is a part of plaYUVer project * Copyright (C) 2014-2015 by Luis Lucas (luisfrlucas@gmail.com) * Joao Carreira (jfmcarreira@gmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /** * \file ViewArea.cpp * \brief Function to manage image display * Based on the work of Ricardo N. Rocha Sardo in SCode project */ #include #include #include #include #include #include #include #include #include #include #include "ViewArea.h" #include "GridManager.h" namespace plaYUVer { static const QColor selectionColor = Qt::cyan; static const QColor imageMaskColor = Qt::green; static const QColor eraserColor = Qt::red; ViewArea::ViewArea( QWidget *parent ) : QWidget( parent ) { setAttribute( Qt::WA_StaticContents ); // setAttribute( Qt::WA_NoBackground ); setMouseTracking( true ); m_pcCurrFrame = NULL; m_pixmap = QPixmap(); m_grid = GridManager(); m_mask = QBitmap(); m_selectedArea = QRect(); m_zoomFactor = 1; m_xOffset = 0; m_yOffset = 0; m_mode = NormalMode; m_tool = NavigationTool; m_gridVisible = false; m_snapToGrid = false; m_blockTrackEnable = false; m_pStream = NULL; } void ViewArea::setImage( PlaYUVerFrame* pcFrame ) { m_pcCurrFrame = pcFrame; m_pcCurrFrame->fillRGBBuffer(); QImage qimg = QImage( m_pcCurrFrame->getRGBBuffer(), m_pcCurrFrame->getWidth(), m_pcCurrFrame->getHeight(), QImage::Format_RGB32 ); setImage( QPixmap::fromImage( qimg ) ); } void ViewArea::setImage( const QPixmap &pixmap ) { m_pixmap = pixmap; m_mask = QBitmap( pixmap.width(), pixmap.height() ); m_mask.clear(); updateSize(); update(); updateGeometry(); } void ViewArea::clear() { m_pixmap = QPixmap(); m_mask = QBitmap(); m_mode = NormalMode; updateSize(); update(); updateGeometry(); } void ViewArea::clearMask() { m_mask.clear(); } Void ViewArea::setZoomFactor( Double f ) { m_zoomFactor = f; updateSize(); update(); } Double ViewArea::scaleZoomFactor( Double scale, QPoint center, QSize minimumSize ) { Double maxZoom = 100.0; Double minZoom = 0.01; //( 1.0 / m_pixmap.width() ); Double new_scale = 1.0; Double zoomFactor = m_zoomFactor * scale; if(!minimumSize.isNull()) { Double cw = m_pixmap.width()*m_zoomFactor; Double ch = m_pixmap.height()*m_zoomFactor; Double fw = m_pixmap.width()*zoomFactor; Double fh = m_pixmap.height()*zoomFactor; Double mw = minimumSize.width(); Double mh = minimumSize.height(); if( (cw < mw) && (ch < mh) && (scale < 1) ) { return new_scale; } if( (fw < mw) && (fh < mh) && (scale < 1) ) { Double wfactor = mw/fw; Double hfactor = mh/fh; if( wfactor < hfactor ) scale = wfactor; else scale = hfactor; zoomFactor = zoomFactor * scale; scale = zoomFactor/m_zoomFactor; } } if( ( m_zoomFactor == minZoom ) && ( scale < 1 ) ) return new_scale; if( ( m_zoomFactor == maxZoom ) && ( scale > 1 ) ) return new_scale; new_scale = scale; if( zoomFactor < minZoom ) { zoomFactor = minZoom; new_scale = zoomFactor/m_zoomFactor; } else { if( zoomFactor > maxZoom ) { zoomFactor = maxZoom; new_scale = zoomFactor/m_zoomFactor; } } setZoomFactor( zoomFactor ); return new_scale; } void ViewArea::setMode( ViewMode mode ) { if( m_mode == mode ) return; m_mode = mode; update(); } void ViewArea::setNormalMode() { setMode( NormalMode ); } void ViewArea::setMaskMode() { setMode( MaskMode ); } void ViewArea::setMaskColor( const QColor &color ) { m_maskColor = color; update(); } void ViewArea::setTool( eTool tool ) { m_tool = tool; } void ViewArea::setMaskTool() { setMode( MaskMode ); setTool( MaskTool ); m_blockTrackEnable = true; } void ViewArea::setEraserTool() { setMode( MaskMode ); setTool( EraserTool ); m_blockTrackEnable = true; } void ViewArea::setNormalSelectionTool() { setMode( NormalMode ); setTool( NormalSelectionTool ); m_blockTrackEnable = false; } void ViewArea::setBlockSelectionTool() { setMode( NormalMode ); setTool( BlockSelectionTool ); m_blockTrackEnable = true; } void ViewArea::setGridVisible( bool enable ) { m_gridVisible = enable; update(); } void ViewArea::setSnapToGrid( bool enable ) { m_snapToGrid = enable; } void ViewArea::setSelectedArea( QRect &rect ) { if( rect.isNull() ) { update(); return; } QRect updateRect; setNormalMode(); if( m_selectedArea.isNull() ) { m_selectedArea = rect.normalized(); updateRect = m_selectedArea; } else { updateRect = m_selectedArea; m_selectedArea = rect.normalized(); updateRect = updateRect.united( m_selectedArea ); } updateRect.adjust( 0, 0, 1, 1 ); update( updateRect.normalized() ); } //////////////////////////////////////////////////////////////////////////////// // Geometry Updates //////////////////////////////////////////////////////////////////////////////// void ViewArea::updateSize() { int w = m_pixmap.width() * m_zoomFactor; int h = m_pixmap.height() * m_zoomFactor; setMinimumSize( w, h ); QWidget *p = parentWidget(); if( p ) { // If the parent size is bigger than the minimum area to view the // image, resize() will call resizeEvent(); otherwise, we need to // perform the necessary updates (updateOffset). resize( p->width(), p->height() ); } if( w <= width() && h <= height() ) updateOffset(); } void ViewArea::updateOffset() { if( width() > m_pixmap.width() * m_zoomFactor ) { m_xOffset = ( width() - m_pixmap.width() * m_zoomFactor ) / 2; } else { m_xOffset = 0; } if( height() > m_pixmap.height() * m_zoomFactor ) { m_yOffset = ( height() - m_pixmap.height() * m_zoomFactor ) / 2; } else { m_yOffset = 0; } } //////////////////////////////////////////////////////////////////////////////// // Resize Event //////////////////////////////////////////////////////////////////////////////// void ViewArea::resizeEvent( QResizeEvent *event ) { if( size().isEmpty() || m_pixmap.isNull() ) return; updateSize(); update(); } //////////////////////////////////////////////////////////////////////////////// // Paint Event //////////////////////////////////////////////////////////////////////////////// void ViewArea::paintEvent( QPaintEvent *event ) { QRect winRect = event->rect(); if( size().isEmpty() || m_pixmap.isNull() ) return; QPainter painter( this ); // Save the actual painter properties and scales the coordinate system. painter.save(); painter.translate( m_xOffset, m_yOffset ); painter.scale( m_zoomFactor, m_zoomFactor ); // This line is for fast paiting. Only visible area of the image is painted. // We take the exposed rect from the event (that gives us scroll/expose optimizations for free – no need // to draw the whole pixmap if your widget is only partially exposed), and reverse map it with the painter matrix. // That gives us the part of the pixmap that has actually been exposed. // See: http://blog.qt.digia.com/blog/2006/05/13/fast-transformed-pixmapimage-drawing/ QRect exposedRect = painter.worldTransform().inverted().mapRect( event->rect() ).adjusted( -1, -1, 1, 1 ); // Draw the pixmap. painter.drawPixmap( exposedRect, m_pixmap, exposedRect ); // Draw the Grid if it's visible. if( m_gridVisible ) { // Do we need to draw the whole grid? // To know that, we need to perform a transformation of the rectangle // area that the painter needs to update - transform the windows // coordinates (origin at the top-left corner of the widget), to the // relatives coordinates of the image at it's original size (origin at // the top-left corner of the image). QRect vr = windowToView( winRect ); // Now we have the (to update) rectangle area on a coordinates system // that has it's origin at the top-left corner of the image. That // is, is referenced to the not scaled image. // To know what image area we need to update, just intersects the // rectangle area with the image area. vr &= QRect( 0, 0, m_pixmap.width(), m_pixmap.height() ); // Set up for the grid drawer. painter.setRenderHint( QPainter::Antialiasing ); // Draw grid. m_grid.drawGrid( m_pixmap, vr, &painter ); } painter.restore(); // Draw a border around the image. /* if( m_xOffset || m_yOffset ) { painter.setPen( Qt::black ); painter.drawRect( m_xOffset - 1, m_yOffset - 1, m_pixmap.width() * m_zoomFactor + 1, m_pixmap.height() * m_zoomFactor + 1 ); }*/ // Draw pixel values in grid if( m_zoomFactor >= 50.0 ) { Int imageWidth = m_pixmap.width(); Int imageHeight = m_pixmap.height(); PlaYUVerFrame::Pixel sPixelValue; QFont font( "Helvetica" ); font.setPixelSize( 12 ); painter.setFont( font ); QRect vr = windowToView( winRect ); vr &= QRect( 0, 0, imageWidth, imageHeight ); for( Int i = vr.x(); i <= vr.right(); i++ ) { for( Int j = vr.y(); j <= vr.bottom(); j++ ) { QPoint pixelTopLeft( i, j ); QRect pixelRect( viewToWindow( pixelTopLeft ), QSize( m_zoomFactor, m_zoomFactor ) ); Int frFormat = m_pcCurrFrame->getColorSpace(); if( frFormat == PlaYUVerFrame::COLOR_YUV ) { sPixelValue = m_pcCurrFrame->getPixelValue( pixelTopLeft.x(), pixelTopLeft.y(), PlaYUVerFrame::COLOR_YUV ); if( sPixelValue.Y() < 128 ) painter.setPen( QColor( Qt::white ) ); else painter.setPen( QColor( Qt::black ) ); painter.drawText( pixelRect, Qt::AlignCenter, "Y: " + QString::number( sPixelValue.Y() ) + "\n" + "U: " + QString::number( sPixelValue.Cb() ) + "\n" + "V: " + QString::number( sPixelValue.Cr() ) ); } if( frFormat == PlaYUVerFrame::COLOR_GRAY ) { sPixelValue = m_pcCurrFrame->getPixelValue( pixelTopLeft.x(), pixelTopLeft.y(), PlaYUVerFrame::COLOR_YUV ); if( sPixelValue.Y() < 128 ) painter.setPen( QColor( Qt::white ) ); else painter.setPen( QColor( Qt::black ) ); painter.drawText( pixelRect, Qt::AlignCenter, "Y: " + QString::number( sPixelValue.Y() ) ); } if( ( frFormat == PlaYUVerFrame::COLOR_RGB ) ) { sPixelValue = m_pcCurrFrame->getPixelValue( pixelTopLeft.x(), pixelTopLeft.y(), PlaYUVerFrame::COLOR_RGB ); if( ( sPixelValue.R() + sPixelValue.G() + sPixelValue.B() ) < 128 * 3 ) painter.setPen( QColor( Qt::white ) ); else painter.setPen( QColor( Qt::black ) ); painter.drawText( pixelRect, Qt::AlignCenter, "R: " + QString::number( sPixelValue.R() ) + "\n" + "G: " + QString::number( sPixelValue.G() ) + "\n" + "B: " + QString::number( sPixelValue.B() ) ); } } } QColor color( Qt::white ); QPen mainPen = QPen( color, 1, Qt::SolidLine ); painter.setPen( mainPen ); // Draw vertical line for( Int x = vr.x(); x <= ( vr.right() + 1 ); x++ ) { // Always draw the full line otherwise the line stippling // varies with the location of view area and we get glitchy // patterns. painter.drawLine( viewToWindow( QPoint( x, 0 ) ), viewToWindow( QPoint( x, imageHeight ) ) ); } // Draw horizontal line for( Int y = vr.y(); y <= ( vr.bottom() + 1 ); y++ ) { painter.drawLine( viewToWindow( QPoint( 0, y ) ), viewToWindow( QPoint( imageWidth, y ) ) ); } } QRect sr = viewToWindow( m_selectedArea ); QRect ir = sr & winRect; if( ( mode() == NormalMode ) && !selectedArea().isNull() ) { if( m_newShape || m_blockTrackEnable ) { // Set the tool color QColor selectColor = selectionColor; selectColor.setAlpha( 120 ); QBrush brush( selectColor ); if( m_blockTrackEnable ) painter.setBrush( Qt::NoBrush ); else painter.setBrush( brush ); painter.setPen( Qt::darkCyan ); // painter.drawRect( sr/*m_selectedArea*/ ); } else { // 1) Cover the image with a light gray except the selected area QColor fill( Qt::lightGray ); fill.setAlpha( 150 ); QBrush brush( fill ); painter.setBrush( fill ); painter.setPen( Qt::NoPen ); QPainterPath myPath; QRect imgr = viewToWindow( QRect( 0, 0, m_pixmap.width(), m_pixmap.height() ) ); myPath.addRect( imgr ); myPath.addRect( sr); //m_selectedArea painter.drawPath( myPath ); // 2) Draw the selection rectangle painter.setBrush( Qt::NoBrush ); painter.setPen( Qt::darkCyan ); // painter.drawRect( sr/*m_selectedArea*/ ); } if( !ir.isNull() ) painter.drawRect( sr/*m_selectedArea*/); } // Mask Mode ---------------------------------------------------------- if( mode() != MaskMode ) return; QColor color; QBrush brush; // Draw Mask // Set the tool color if( m_maskColor.isValid() ) color = m_maskColor; else color = imageMaskColor; color.setAlpha( 120 ); painter.setPen( color ); painter.save(); painter.translate( m_xOffset, m_yOffset ); painter.scale( m_zoomFactor, m_zoomFactor ); painter.drawPixmap( QPoint( 0, 0 ), m_mask ); painter.restore(); if( !m_selectedArea.isNull() ) { if( m_newShape || m_blockTrackEnable ) { switch( tool() ) { case MaskTool: { if( m_maskColor.isValid() ) color = m_maskColor; else color = imageMaskColor; break; } case EraserTool: { color = eraserColor; break; } default: color = selectionColor; // ?Problems! } color.setAlpha( 120 ); brush = QBrush( color ); if( m_blockTrackEnable ) painter.setBrush( Qt::NoBrush ); else painter.setBrush( brush ); painter.setPen( color ); if( !ir.isNull() ) painter.drawRect( sr ); } } } //////////////////////////////////////////////////////////////////////////////// // Mouse Events //////////////////////////////////////////////////////////////////////////////// void ViewArea::wheelEvent( QWheelEvent *event ) { Double scale; Double usedScale; if( event->modifiers() & Qt::ControlModifier ) { scale = 0.001 * event->delta(); if(scale>0) scale=1.25; else scale=0.8; QWidget *p = parentWidget(); QSize minimumSize = QSize( p->size().width() - 5, p->size().height() - 5 ); usedScale = scaleZoomFactor( scale , event->pos(), minimumSize ); if( usedScale != 1.0 ) { emit zoomFactorChanged_byWheel( usedScale, event->pos() ); } } } void ViewArea::mousePressEvent( QMouseEvent *event ) { event->accept(); QPoint vpos = windowToView( event->pos() ); if( event->button() == Qt::MidButton ) { } if( event->button() == Qt::LeftButton ) { if( tool() == NavigationTool ) { if( !( m_xOffset && m_yOffset ) ) setCursor( Qt::ClosedHandCursor ); m_lastWindowPos = event->pos(); } // Is the mouse over the image? If yes, save the mouse position; // otherwise, QPoint( -1, -1 ) indicates an invalid position. if( isPosValid( vpos ) ) { m_lastPos = vpos; } else { m_lastPos = QPoint( -1, -1 ); return; } // If grid tracking if( m_snapToGrid ) { // Find if the cursor is near a grid intersection bool isNear = m_grid.isNear( m_lastPos ); if( isNear ) { // The grid 'near intersection' found when used isNear() m_lastPos = m_grid.nearPos(); } } m_newShape = true; if( tool() == NormalSelectionTool ) { m_blockTrackEnable = false; return; } if( mode() == MaskMode ) { // Disable the block track to enable fill the block m_blockTrackEnable = false; } // Block selection // m_selectedArea = m_grid.rectContains( m_lastPos ); QRect updateRect = viewToWindow( m_selectedArea ); updateRect.adjust( 0, 0, 1, 1 ); update( updateRect.normalized() ); } } void ViewArea::mouseMoveEvent( QMouseEvent *event ) { event->accept(); #ifndef _MSC_VER // Add this code line to avoid slow navigation with some specific mouses // This seems to always appear on windows if( qApp->hasPendingEvents()) return; #endif QPoint actualPos = windowToView( event->pos() ); QRect updateRect; emit positionChanged( actualPos ); // If mouse left button pressed if( event->buttons() == Qt::LeftButton && m_lastPos != QPoint( -1, -1 ) ) { // Grid tracking if( m_snapToGrid ) { // Find if the cursor is near a grid intersection bool isNear = m_grid.isNear( actualPos ); if( isNear ) { // Return the last grid near intersection found actualPos = m_grid.nearPos(); } } if( tool() == NavigationTool ) { QPoint offset = m_lastWindowPos - event->pos(); emit scrollBarMoved( offset ); } updateRect = viewToWindow( m_selectedArea ); if( tool() == NormalSelectionTool ) { // If the selection is only vertical or horizontal then we have one // of the dimentions null. if( actualPos.x() == m_lastPos.x() && actualPos.y() == m_lastPos.y() ) { m_selectedArea = QRect(); } // Selection from top to bottom else if( m_lastPos.y() < actualPos.y() ) { // From left to right if( m_lastPos.x() < actualPos.x() ) { QPoint bottomR = actualPos;// - QPoint( 1, 1 ); m_selectedArea = QRect( m_lastPos, bottomR ); } // From right to left else { QPoint topL( actualPos.x(), m_lastPos.y() ); QPoint bottomR( m_lastPos.x(), actualPos.y() ); m_selectedArea = QRect( topL, bottomR ); } } // Selection from bottom to top else { // From left to right if( m_lastPos.x() < actualPos.x() ) { QPoint topL( m_lastPos.x(), actualPos.y() ); QPoint bottomR( actualPos.x(), m_lastPos.y() ); m_selectedArea = QRect( topL, bottomR ); } // From right to left else { QPoint bottomR = m_lastPos;// - QPoint( 1, 1 ); m_selectedArea = QRect( actualPos, bottomR ); } } } /* else // if tool() == BlockSelectionTool || MaskTool || EraserTool { m_blockTrackEnable = false; // If cursor is inside the selected area, we most redraw // the selection rect because it may be smaller. if( m_selectedArea.contains( actualPos ) ) { m_selectedArea = m_grid.rectContains( m_lastPos ); } m_selectedArea = m_selectedArea.united( m_grid.rectContains( actualPos ) ); } */ if( tool() == NormalSelectionTool ) { // Intercept the selected area with the image area to limit the // selection only to the image area, preventing it to come outside // the image. m_selectedArea &= QRect( 0, 0, m_pixmap.width(), m_pixmap.height() ); // Update only the united area // updateRect = updateRect.united( viewToWindow( m_selectedArea ) ); // "When rendering with a one pixel wide pen the QRect's // boundary line will be rendered to the right and below the // mathematical rectangle's boundary line.", in QT4 doc. // Our selection pen width is 1, let's adjust the rendering area. // updateRect.adjust( 0, 0, 1, 1 ); // update( updateRect.normalized() ); update(); } return; } // If no mouse button is pressed and bloks selection tool is set, then // track the bloks. if( m_blockTrackEnable ) { updateRect = viewToWindow( m_selectedArea ); if( isPosValid( actualPos ) ) { m_selectedArea = m_grid.rectContains( actualPos ); } else { m_selectedArea = QRect(); } updateRect = updateRect.united( viewToWindow( m_selectedArea ) ); updateRect.adjust( 0, 0, 1, 1 ); update( updateRect ); return; } } void ViewArea::mouseReleaseEvent( QMouseEvent *event ) { event->accept(); QPoint vpos = windowToView( event->pos() ); if( event->button() == Qt::LeftButton && m_lastPos != QPoint( -1, -1 ) ) { if( tool() == NavigationTool ) { unsetCursor(); } else if( tool() == NormalSelectionTool ) { // Normal Mode ------------------------------------------------------ if( mode() == NormalMode ) { if( vpos == m_lastPos ) { m_selectedArea = QRect(); if( tool() == BlockSelectionTool ) m_blockTrackEnable = true; } emit selectionChanged( m_selectedArea ); } // Mask Mode ----------------------------------------------------------- else if( mode() == MaskMode ) { if( !m_selectedArea.isNull() ) { updateMask( m_selectedArea ); } m_selectedArea = QRect(); m_blockTrackEnable = true; } m_newShape = false; update(); m_lastPos = QPoint( -1, -1 ); } } } //////////////////////////////////////////////////////////////////////////////// bool ViewArea::isPosValid( const QPoint &pos ) const { if( pos.x() < 0 || pos.y() < 0 || pos.x() >= m_pixmap.width() || pos.y() >= m_pixmap.height() ) return false; else return true; } //////////////////////////////////////////////////////////////////////////////// QPoint ViewArea::windowToView( const QPoint& pt ) const { QPoint p; p.setX( static_cast( ( pt.x() - m_xOffset ) / m_zoomFactor ) ); p.setY( static_cast( ( pt.y() - m_yOffset ) / m_zoomFactor ) ); return p; } QRect ViewArea::windowToView( const QRect& rc ) const { QRect r; r.setTopLeft( windowToView( rc.topLeft() ) ); // r.setRight ( (int)( ceil(( rc.right() - m_xOffset)/m_zoomFactor ))); // r.setBottom( (int)( ceil(( rc.bottom()- m_yOffset)/m_zoomFactor ))); // r.setRight ( static_cast(( rc.right() - m_xOffset ) / m_zoomFactor +1)); // r.setBottom( static_cast(( rc.bottom() - m_xOffset ) / m_zoomFactor+1)); r.setBottomRight( windowToView( rc.bottomRight() ) ); return r; } QPoint ViewArea::viewToWindow( const QPoint& pt ) const { QPoint p; p.setX( static_cast( pt.x() * m_zoomFactor + m_xOffset ) ); p.setY( static_cast( pt.y() * m_zoomFactor + m_yOffset ) ); return p; } QRect ViewArea::viewToWindow( const QRect& rc ) const { QRect r; r.setTopLeft( viewToWindow( rc.topLeft() ) ); // r.setRight ( (int)( ceil(( rc.right() +1+m_xOffset )*m_zoomFactor ) - 1 )); // r.setBottom( (int)( ceil(( rc.bottom()+1+m_yOffset )*m_zoomFactor ) - 1 )); // r.setRight ( (int)( ceil(( rc.right()+0.5)*m_zoomFactor )+ m_xOffset )-1); // r.setBottom( (int)( ceil(( rc.bottom()+0.5)*m_zoomFactor ) +m_yOffset )-1); // qDebug()<<"Right = "<< r.right(); // r.setRight ( static_cast(( rc.right()+1) * m_zoomFactor + m_xOffset -1) ); // r.setBottom( static_cast(( rc.bottom()+1) * m_zoomFactor + m_yOffset -1)); r.setBottomRight( viewToWindow( rc.bottomRight() ) ); return r; } //////////////////////////////////////////////////////////////////////////////// // Masks Management //////////////////////////////////////////////////////////////////////////////// void ViewArea::updateMask( const QRect &rect ) { switch( tool() ) { case MaskTool: { // Add rect to the mask QPainter painter( &m_mask ); painter.setBrush( Qt::color1 ); painter.setPen( Qt::NoPen ); painter.drawRect( rect ); painter.end(); break; } case EraserTool: { // Clears rect area in the mask QPainter painter( &m_mask ); painter.setBrush( Qt::color0 ); painter.setPen( Qt::NoPen ); painter.drawRect( rect ); painter.end(); break; } default: /* Do Nothing */ ; } } void ViewArea::setInputStream( PlaYUVerStream *stream ) { m_pStream = stream; } PlaYUVerStream* ViewArea::getInputStream() { return m_pStream; } } // NameSpace plaYUVer