Commit 215f35e2 authored by Maciej Suminski's avatar Maciej Suminski

Added mouse cursor drawing.

Added flipping mode.
parent 9b52e9dd
......@@ -119,20 +119,6 @@ EDA_DRAW_PANEL_GAL::~EDA_DRAW_PANEL_GAL()
void EDA_DRAW_PANEL_GAL::onPaint( wxPaintEvent& WXUNUSED( aEvent ) )
{
Refresh();
}
void EDA_DRAW_PANEL_GAL::onSize( wxSizeEvent& aEvent )
{
m_gal->ResizeScreen( aEvent.GetSize().x, aEvent.GetSize().y );
m_view->MarkTargetDirty( KiGfx::TARGET_CACHED );
m_view->MarkTargetDirty( KiGfx::TARGET_NONCACHED );
}
void EDA_DRAW_PANEL_GAL::Refresh( bool eraseBackground, const wxRect* rect )
{
#ifdef __WXDEBUG__
prof_counter time;
......@@ -148,6 +134,7 @@ void EDA_DRAW_PANEL_GAL::Refresh( bool eraseBackground, const wxRect* rect )
if( m_view->IsTargetDirty( KiGfx::TARGET_NONCACHED ) )
m_gal->DrawGrid();
m_view->Redraw();
m_gal->DrawCursor( m_viewControls->GetMousePosition() );
m_gal->EndDrawing();
......@@ -159,6 +146,21 @@ void EDA_DRAW_PANEL_GAL::Refresh( bool eraseBackground, const wxRect* rect )
}
void EDA_DRAW_PANEL_GAL::onSize( wxSizeEvent& aEvent )
{
m_gal->ResizeScreen( aEvent.GetSize().x, aEvent.GetSize().y );
m_view->MarkTargetDirty( KiGfx::TARGET_CACHED );
m_view->MarkTargetDirty( KiGfx::TARGET_NONCACHED );
}
void EDA_DRAW_PANEL_GAL::Refresh( bool eraseBackground, const wxRect* rect )
{
wxPaintEvent redrawEvent;
wxPostEvent( this, redrawEvent );
}
void EDA_DRAW_PANEL_GAL::SwitchBackend( GalType aGalType )
{
// Do not do anything if the currently used GAL is correct
......@@ -213,6 +215,8 @@ void EDA_DRAW_PANEL_GAL::onEvent( wxEvent& aEvent )
printf( "evType %d\n", aEvent.GetEventType() );
m_eventDispatcher->DispatchWxEvent( aEvent );
}
Refresh();
}
......
......@@ -24,7 +24,6 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <wx/dcbuffer.h>
#include <wx/image.h>
#include <wx/log.h>
......@@ -135,6 +134,9 @@ void CAIRO_GAL::EndDrawing()
wxBufferedDC dc;
dc.Init( &client_dc, bmp );
// Now it is the time to blit the mouse cursor
blitCursor( dc );
deinitSurface();
}
......@@ -784,50 +786,12 @@ void CAIRO_GAL::ClearTarget( RenderTarget aTarget )
}
VECTOR2D CAIRO_GAL::ComputeCursorToWorld( const VECTOR2D& aCursorPosition )
{
MATRIX3x3D inverseMatrix = worldScreenMatrix.Inverse();
VECTOR2D cursorPositionWorld = inverseMatrix * aCursorPosition;
return cursorPositionWorld;
}
void CAIRO_GAL::DrawCursor( VECTOR2D aCursorPosition )
void CAIRO_GAL::DrawCursor( const VECTOR2D& aCursorPosition )
{
if( !IsShownOnScreen() )
return;
wxClientDC clientDC( this );
wxMemoryDC cursorSave( *cursorPixelsSaved );
wxMemoryDC cursorShape( *cursorPixels );
// Snap to grid
VECTOR2D cursorPositionWorld = ComputeCursorToWorld( aCursorPosition );
cursorPositionWorld.x = round( cursorPositionWorld.x / gridSize.x ) * gridSize.x;
cursorPositionWorld.y = round( cursorPositionWorld.y / gridSize.y ) * gridSize.y;
aCursorPosition = worldScreenMatrix * cursorPositionWorld;
aCursorPosition = aCursorPosition - VECTOR2D( cursorSize / 2, cursorSize / 2 );
if( !isDeleteSavedPixels )
{
clientDC.Blit( savedCursorPosition.x, savedCursorPosition.y, cursorSize, cursorSize,
&cursorSave, 0, 0 );
}
else
{
isDeleteSavedPixels = false;
}
cursorSave.Blit( 0, 0, cursorSize, cursorSize, &clientDC, aCursorPosition.x,
aCursorPosition.y );
clientDC.Blit( aCursorPosition.x, aCursorPosition.y, cursorSize, cursorSize, &cursorShape, 0,
0, wxOR );
savedCursorPosition.x = (wxCoord) aCursorPosition.x;
savedCursorPosition.y = (wxCoord) aCursorPosition.y;
// Now we should only store the position of the mouse cursor
// The real drawing routines are in EndDrawing()
cursorPosition = VECTOR2D( aCursorPosition.x - cursorSize / 2,
aCursorPosition.y - cursorSize / 2 );
}
......@@ -888,7 +852,7 @@ void CAIRO_GAL::storePath()
}
void CAIRO_GAL::onPaint( wxPaintEvent& aEvent )
void CAIRO_GAL::onPaint( wxPaintEvent& WXUNUSED( aEvent ) )
{
PostPaint();
}
......@@ -922,6 +886,37 @@ void CAIRO_GAL::initCursor( int aCursorSize )
}
void CAIRO_GAL::blitCursor( wxBufferedDC& clientDC )
{
if( !isCursorEnabled )
return;
wxMemoryDC cursorSave( *cursorPixelsSaved );
wxMemoryDC cursorShape( *cursorPixels );
if( !isDeleteSavedPixels )
{
// Restore pixels that were overpainted by the previous cursor
clientDC.Blit( savedCursorPosition.x, savedCursorPosition.y,
cursorSize, cursorSize, &cursorSave, 0, 0 );
}
else
{
isDeleteSavedPixels = false;
}
// Store pixels that are going to be overpainted
cursorSave.Blit( 0, 0, cursorSize, cursorSize, &clientDC, cursorPosition.x, cursorPosition.y );
// Draw the cursor
clientDC.Blit( cursorPosition.x, cursorPosition.y, cursorSize, cursorSize,
&cursorShape, 0, 0, wxOR );
savedCursorPosition.x = (wxCoord) cursorPosition.x;
savedCursorPosition.y = (wxCoord) cursorPosition.y;
}
void CAIRO_GAL::allocateBitmaps()
{
// Create buffer, use the system independent Cairo context backend
......
......@@ -40,9 +40,9 @@ GAL::GAL() :
SetIsStroke( true );
SetFillColor( COLOR4D( 0.0, 0.0, 0.0, 0.0 ) );
SetStrokeColor( COLOR4D( 1.0, 1.0, 1.0, 1.0 ) );
SetIsCursorEnabled( false );
SetZoomFactor( 1.0 );
SetDepthRange( VECTOR2D( -2048, 2047 ) );
SetFlip( false, false );
SetLineWidth( 1.0 );
// Set grid defaults
......@@ -54,6 +54,8 @@ GAL::GAL() :
// Initialize the cursor shape
SetCursorColor( COLOR4D( 1.0, 1.0, 1.0, 1.0 ) );
SetCursorSize( 20 );
SetCursorEnabled( true );
strokeFont.LoadNewStrokeFont( newstroke_font, newstroke_font_bufsize );
}
......@@ -64,6 +66,17 @@ GAL::~GAL()
}
void GAL::SetTextAttributes( const EDA_TEXT* aText )
{
strokeFont.SetGlyphSize( VECTOR2D( aText->GetSize() ) );
strokeFont.SetHorizontalJustify( aText->GetHorizJustify() );
strokeFont.SetVerticalJustify( aText->GetVertJustify() );
strokeFont.SetBold( aText->IsBold() );
strokeFont.SetItalic( aText->IsItalic() );
strokeFont.SetMirrored( aText->IsMirrored() );
}
void GAL::ComputeWorldScreenMatrix()
{
ComputeWorldScale();
......@@ -80,17 +93,17 @@ void GAL::ComputeWorldScreenMatrix()
MATRIX3x3D flip;
flip.SetIdentity();
flip.SetScale( VECTOR2D( 1.0, 1.0 ) );
flip.SetScale( VECTOR2D( flipX, flipY ) );
MATRIX3x3D lookat;
lookat.SetIdentity();
lookat.SetTranslation( -lookAtPoint );
worldScreenMatrix = translation * flip * scale * lookat * worldScreenMatrix;
screenWorldMatrix = worldScreenMatrix.Inverse();
}
void GAL::DrawGrid()
{
if( !gridVisibility )
......@@ -114,9 +127,8 @@ void GAL::DrawGrid()
// Draw the grid
// For the drawing the start points, end points and increments have
// to be calculated in world coordinates
MATRIX3x3D inverseMatrix = worldScreenMatrix.Inverse();
VECTOR2D worldStartPoint = inverseMatrix * VECTOR2D( 0.0, 0.0 );
VECTOR2D worldEndPoint = inverseMatrix * screenSize;
VECTOR2D worldStartPoint = screenWorldMatrix * VECTOR2D( 0.0, 0.0 );
VECTOR2D worldEndPoint = screenWorldMatrix * screenSize;
int gridScreenSizeDense = round( gridSize.x * worldScale );
int gridScreenSizeCoarse = round( gridSize.x * static_cast<double>( gridTick ) * worldScale );
......@@ -218,14 +230,3 @@ void GAL::DrawGrid()
}
}
}
void GAL::SetTextAttributes( const EDA_TEXT* aText )
{
strokeFont.SetGlyphSize( VECTOR2D( aText->GetSize() ) );
strokeFont.SetHorizontalJustify( aText->GetHorizJustify() );
strokeFont.SetVerticalJustify( aText->GetVertJustify() );
strokeFont.SetBold( aText->IsBold() );
strokeFont.SetItalic( aText->IsItalic() );
strokeFont.SetMirrored( aText->IsMirrored() );
}
......@@ -732,73 +732,35 @@ void OPENGL_GAL::ClearTarget( RenderTarget aTarget )
}
VECTOR2D OPENGL_GAL::ComputeCursorToWorld( const VECTOR2D& aCursorPosition )
void OPENGL_GAL::DrawCursor( const VECTOR2D& aCursorPosition )
{
VECTOR2D cursorPosition = aCursorPosition;
cursorPosition.y = screenSize.y - aCursorPosition.y;
MATRIX3x3D inverseMatrix = worldScreenMatrix.Inverse();
VECTOR2D cursorPositionWorld = inverseMatrix * cursorPosition;
return cursorPositionWorld;
}
void OPENGL_GAL::DrawCursor( VECTOR2D aCursorPosition )
{
wxLogWarning( wxT( "Not tested" ) );
SetCurrent( *glContext );
// Draw the cursor on the surface
VECTOR2D cursorPositionWorld = ComputeCursorToWorld( aCursorPosition );
cursorPositionWorld.x = round( cursorPositionWorld.x / gridSize.x ) * gridSize.x;
cursorPositionWorld.y = round( cursorPositionWorld.y / gridSize.y ) * gridSize.y;
if( !isCursorEnabled )
return;
aCursorPosition = worldScreenMatrix * cursorPositionWorld;
compositor.SetBuffer( OPENGL_COMPOSITOR::DIRECT_RENDERING );
// Switch to the main framebuffer and blit the scene
// glBindFramebuffer( GL_FRAMEBUFFER, 0 );
// glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
// Invert y axis
VECTOR2D cursorPosition = VECTOR2D( aCursorPosition.x, screenSize.y - aCursorPosition.y );
glLoadIdentity();
VECTOR2D cursorBegin = ToWorld( cursorPosition -
VECTOR2D( cursorSize / 2, cursorSize / 2 ) );
VECTOR2D cursorEnd = ToWorld( cursorPosition +
VECTOR2D( cursorSize / 2, cursorSize / 2 ) );
VECTOR2D cursorCenter = ( cursorBegin + cursorEnd ) / 2.0;
glDisable( GL_TEXTURE_2D );
glLineWidth( 1.0 );
glColor4d( cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a );
glBegin( GL_TRIANGLES );
glVertex3f( (int) ( aCursorPosition.x - cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y ), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x + cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y ), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x + cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y + 1 ), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x - cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y ), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x - cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y + 1), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x + cursorSize / 2 ) + 1,
(int) ( aCursorPosition.y + 1 ), depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ),
(int) ( aCursorPosition.y - cursorSize / 2 ) + 1, depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ),
(int) ( aCursorPosition.y + cursorSize / 2 ) + 1, depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ) + 1,
(int) ( aCursorPosition.y + cursorSize / 2 ) + 1, depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ),
(int) ( aCursorPosition.y - cursorSize / 2 ) + 1, depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ) + 1,
(int) ( aCursorPosition.y - cursorSize / 2 ) + 1, depthRange.x );
glVertex3f( (int) ( aCursorPosition.x ) + 1,
(int) ( aCursorPosition.y + cursorSize / 2 ) + 1, depthRange.x );
glBegin( GL_LINES );
glVertex3f( cursorCenter.x, cursorBegin.y, GetMinDepth() );
glVertex3f( cursorCenter.x, cursorEnd.y, GetMinDepth() );
glVertex3f( cursorBegin.x, cursorCenter.y, GetMinDepth() );
glVertex3f( cursorEnd.x, cursorCenter.y, GetMinDepth() );
glEnd();
// Blit the current screen contents
SwapBuffers();
// Restore the default color, so textures will be drawn properly
glColor4d( 1.0, 1.0, 1.0, 1.0 );
}
......@@ -823,6 +785,7 @@ void OPENGL_GAL::drawGridLine( const VECTOR2D& aStartPoint, const VECTOR2D& aEnd
glVertex3d( aEndPoint.x, aEndPoint.y, layerDepth );
glEnd();
// Restore the default color, so textures will be drawn properly
glColor4d( 1.0, 1.0, 1.0, 1.0 );
}
......@@ -938,7 +901,7 @@ void OPENGL_GAL::drawStrokedSemiCircle( const VECTOR2D& aCenterPoint, double aRa
}
void OPENGL_GAL::onPaint( wxPaintEvent& aEvent )
void OPENGL_GAL::onPaint( wxPaintEvent& WXUNUSED( aEvent ) )
{
PostPaint();
}
......
......@@ -268,7 +268,7 @@ void VIEW::SetViewport( const BOX2D& aViewport, bool aKeepAspect )
void VIEW::SetMirror( bool aMirrorX, bool aMirrorY )
{
wxASSERT_MSG( false, wxT( "This is not implemented" ) );
m_gal->SetFlip( aMirrorX, aMirrorY );
}
......@@ -292,7 +292,6 @@ void VIEW::SetScale( double aScale, const VECTOR2D& aAnchor )
// Redraw everything after the viewport has changed
MarkTargetDirty( TARGET_CACHED );
MarkTargetDirty( TARGET_NONCACHED );
}
......@@ -304,7 +303,6 @@ void VIEW::SetCenter( const VECTOR2D& aCenter )
// Redraw everything after the viewport has changed
MarkTargetDirty( TARGET_CACHED );
MarkTargetDirty( TARGET_NONCACHED );
}
......@@ -706,6 +704,7 @@ void VIEW::PrepareTargets()
MarkTargetDirty( TARGET_NONCACHED );
MarkTargetDirty( TARGET_CACHED );
MarkTargetDirty( TARGET_OVERLAY );
}
if( IsTargetDirty( TARGET_OVERLAY ) )
......
......@@ -60,17 +60,17 @@ WX_VIEW_CONTROLS::WX_VIEW_CONTROLS( VIEW* aView, wxWindow* aParentPanel ) :
void WX_VIEW_CONTROLS::onMotion( wxMouseEvent& aEvent )
{
VECTOR2D mousePoint( aEvent.GetX(), aEvent.GetY() );
m_mousePosition.x = aEvent.GetX();
m_mousePosition.y = aEvent.GetY();
if( aEvent.Dragging() )
{
if( m_state == DRAG_PANNING )
{
VECTOR2D d = m_dragStartPoint - mousePoint;
VECTOR2D d = m_dragStartPoint - m_mousePosition;
VECTOR2D delta = m_view->ToWorld( d, false );
m_view->SetCenter( m_lookStartPoint + delta );
m_parentPanel->Refresh();
aEvent.StopPropagation();
}
else
......@@ -106,7 +106,6 @@ void WX_VIEW_CONTROLS::onWheel( wxMouseEvent& aEvent )
aEvent.ShiftDown() ? -scrollSpeed : 0.0 );
m_view->SetCenter( m_view->GetCenter() + delta );
m_parentPanel->Refresh();
}
else
{
......@@ -130,7 +129,6 @@ void WX_VIEW_CONTROLS::onWheel( wxMouseEvent& aEvent )
VECTOR2D anchor = m_view->ToWorld( VECTOR2D( aEvent.GetX(), aEvent.GetY() ) );
m_view->SetScale( m_view->GetScale() * zoomScale, anchor );
m_parentPanel->Refresh();
}
aEvent.Skip();
......
......@@ -34,6 +34,7 @@
#include <gal/graphics_abstraction_layer.h>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <wx/dcbuffer.h>
#if defined(__WXMSW__)
#define SCREEN_DEPTH 24
......@@ -228,11 +229,8 @@ public:
// Cursor
// -------
/// @copydoc GAL::ComputeCursorToWorld()
virtual VECTOR2D ComputeCursorToWorld( const VECTOR2D& aCursorPosition );
/// @copydoc GAL::DrawCursor()
virtual void DrawCursor( VECTOR2D aCursorPosition );
virtual void DrawCursor( const VECTOR2D& aCursorPosition );
/**
* Function PostPaint
......@@ -286,6 +284,7 @@ private:
wxBitmap* cursorPixels; ///< Cursor pixels
wxBitmap* cursorPixelsSaved; ///< Saved cursor pixels
int cursorSize; ///< Cursor size
VECTOR2D cursorPosition; ///< Current cursor position
/// Maximum number of arguments for one command
static const int MAX_CAIRO_ARGUMENTS = 6;
......@@ -357,6 +356,11 @@ private:
/// @copydoc GAL::initCursor()
virtual void initCursor( int aCursorSize );
/**
* @brief Blits cursor into current screen.
*/
virtual void blitCursor( wxBufferedDC& clientDC );
/// Prepare Cairo surfaces for drawing
void initSurface();
......
......@@ -551,6 +551,25 @@ public:
return worldScale;
}
/**
* @brief Sets flipping of the screen.
*
* @param xAxis is the flip flag for the X axis.
* @param yAxis is the flip flag for the Y axis.
*/
inline void SetFlip( bool xAxis, bool yAxis )
{
if( xAxis )
flipX = -1.0; // flipped
else
flipX = 1.0; // regular
if( yAxis )
flipY = -1.0; // flipped
else
flipY = 1.0; // regular
}
// ---------------------------
// Buffer manipulation methods
// ---------------------------
......@@ -693,26 +712,36 @@ public:
gridStyle = aGridStyle;
}
// -------
// Cursor
// -------
/**
* @brief Compute the point position in world coordinates from given screen coordinates.
*
* @param aPoint the pointposition in screen coordinates.
* @return the point position in world coordinates.
*/
inline virtual VECTOR2D ToWorld( const VECTOR2D& aPoint ) const
{
return VECTOR2D( screenWorldMatrix * aPoint );
}
/**
* @brief Compute the cursor position in world coordinates from given screen coordinates.
* @brief Compute the point position in screen coordinates from given world coordinates.
*
* @param aCursorPosition is the cursor position in screen coordinates.
* @return the cursor position in world coordinates.
* @param aPoint the pointposition in world coordinates.
* @return the point position in screen coordinates.
*/
virtual VECTOR2D ComputeCursorToWorld( const VECTOR2D& aCursorPosition ) = 0;
inline virtual VECTOR2D ToScreen( const VECTOR2D& aPoint ) const
{
return VECTOR2D( worldScreenMatrix * aPoint );
}
/**
* @brief Enable/Disable cursor.
*
* @param aIsCursorEnabled is true if the cursor should be enabled, else false.
*/
inline void SetIsCursorEnabled( bool aIsCursorEnabled )
inline void SetCursorEnabled( bool aCursorEnabled )
{
isCursorEnabled = aIsCursorEnabled;
isCursorEnabled = aCursorEnabled;
}
/**
......@@ -725,12 +754,22 @@ public:
cursorColor = aCursorColor;
}
/**
* @brief Set the cursor size.
*
* @param aCursorSize is the size of the cursor.
*/
inline void SetCursorSize( unsigned int aCursorSize )
{
cursorSize = aCursorSize;
}
/**
* @brief Draw the cursor.
*
* @param aCursorPosition is the cursor position in screen coordinates.
*/
virtual void DrawCursor( VECTOR2D aCursorPosition ) = 0;
virtual void DrawCursor( const VECTOR2D& aCursorPosition ) = 0;
/**
* @brief Changes the current depth to deeper, so it is possible to draw objects right beneath
......@@ -768,7 +807,10 @@ protected:
double zoomFactor; ///< The zoom factor
MATRIX3x3D worldScreenMatrix; ///< World transformation
MATRIX3x3D screenWorldMatrix; ///< Screen transformation
double worldScale; ///< The scale factor world->screen
double flipX; ///< Flag for X axis flipping
double flipY; ///< Flag for Y axis flipping
double lineWidth; ///< The line width
......@@ -795,8 +837,8 @@ protected:
int gridOriginMarkerSize; ///< Grid origin indicator size (pixels)
bool isCursorEnabled; ///< Is the cursor enabled?
VECTOR2D cursorPosition; ///< The cursor position
COLOR4D cursorColor; ///< Cursor color
int cursorSize; ///< Size of the cursor in pixels
/// Instance of object that stores information about how to draw texts
STROKE_FONT strokeFont;
......
......@@ -221,11 +221,8 @@ public:
// Cursor
// -------
/// @copydoc GAL::ComputeCursorToWorld()
virtual VECTOR2D ComputeCursorToWorld( const VECTOR2D& aCursorPosition );
/// @copydoc GAL::DrawCursor()
virtual void DrawCursor( VECTOR2D aCursorPosition );
virtual void DrawCursor( const VECTOR2D& aCursorPosition );
/**
* @brief Function PostPaint
......@@ -294,12 +291,6 @@ private:
// Shader
SHADER shader; ///< There is only one shader used for different objects
// Cursor
int cursorSize; ///< Size of the cursor in pixels
GLubyte* cursorShape; ///< Cursor pixel storage
GLubyte* cursorSave; ///< Saved cursor pixels
VECTOR2D savedCursorPosition; ///< Last saved cursor position
// Internal flags
bool isGlewInitialized; ///< Is GLEW initialized?
bool isFramebufferInitialized; ///< Are the framebuffers initialized?
......
......@@ -76,6 +76,17 @@ public:
m_autoPanEnabled = true;
}
/**
* Function GetMousePosition()
* Returns the current mouse cursor position in the screen coordinates.
*
* @return The current mouse cursor position.
*/
const VECTOR2D& GetMousePosition() const
{
return m_mousePosition;
}
private:
enum State {
IDLE = 1,
......@@ -89,6 +100,9 @@ private:
/// Current state of VIEW_CONTROLS
State m_state;
/// Current mouse position
VECTOR2D m_mousePosition;
/// Flag for grabbing the mouse cursor
bool m_grabMouse;
/// Flag for turning on autopanning
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment