1 | /* |
2 | * Copyright (C) 2008-2009 Stephan Kulow <coolo@kde.org> |
3 | * Copyright (C) 2008-2009 Parker Coates <coates@kde.org> |
4 | * |
5 | * This program is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU General Public License as |
7 | * published by the Free Software Foundation; either version 2 of |
8 | * the License, or (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License |
16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | */ |
18 | |
19 | #include "gameselectionscene.h" |
20 | |
21 | #include "dealerinfo.h" |
22 | #include "renderer.h" |
23 | |
24 | #include <KColorUtils> |
25 | #include <KDebug> |
26 | #include <KLocale> |
27 | #include <KStandardDirs> |
28 | |
29 | #include <QtGui/QGraphicsObject> |
30 | #include <QtGui/QKeyEvent> |
31 | #include <QtGui/QPainter> |
32 | #include <QtGui/QStyleOptionGraphicsItem> |
33 | #include <QtCore/QPropertyAnimation> |
34 | |
35 | #include <cmath> |
36 | |
37 | |
38 | namespace |
39 | { |
40 | const qreal boxPaddingRatio = 0.037; |
41 | const qreal spacingRatio = 0.10; |
42 | const qreal textToBoxHeightRatio = 1 / 6.0; |
43 | const qreal textToBoxWidthRatio = 0.57; |
44 | const int hoverTransitionDuration = 300; |
45 | const int minimumFontSize = 5; |
46 | } |
47 | |
48 | |
49 | class GameSelectionScene::GameSelectionBox : public QGraphicsObject |
50 | { |
51 | Q_OBJECT |
52 | Q_PROPERTY( qreal fade READ hoverFadeAmount WRITE setHoverFadeAmount ) |
53 | |
54 | public: |
55 | GameSelectionBox( const QString & name, int id ) |
56 | : m_label( name ), |
57 | m_gameId( id ), |
58 | m_anim( new QPropertyAnimation( this, "fade" , this ) ), |
59 | m_highlightFadeAmount( 0 ), |
60 | m_previewPath( KStandardDirs::locate( "data" , QString( "kpat/previews/%1.png" ).arg( id ) ) ) |
61 | { |
62 | setAcceptHoverEvents( true ); |
63 | m_anim->setDuration( hoverTransitionDuration ); |
64 | m_anim->setStartValue( qreal( 0.0 ) ); |
65 | m_anim->setEndValue( qreal( 1.0 ) ); |
66 | m_anim->setEasingCurve( QEasingCurve::InOutSine ); |
67 | } |
68 | |
69 | void setSize( const QSize & size ) |
70 | { |
71 | if ( size != m_size ) |
72 | { |
73 | m_size = size; |
74 | m_preview = QPixmap(); |
75 | } |
76 | } |
77 | |
78 | virtual QRectF boundingRect() const |
79 | { |
80 | return QRectF( QPointF(), m_size ); |
81 | } |
82 | |
83 | QString label() const |
84 | { |
85 | return m_label; |
86 | } |
87 | |
88 | int id() const |
89 | { |
90 | return m_gameId; |
91 | } |
92 | |
93 | void setHighlighted( bool highlighted ) |
94 | { |
95 | if ( highlighted ) |
96 | { |
97 | m_anim->setDirection( QAbstractAnimation::Forward ); |
98 | if ( m_anim->state() != QAbstractAnimation::Running ) |
99 | m_anim->start(); |
100 | } |
101 | else |
102 | { |
103 | m_anim->setDirection( QAbstractAnimation::Backward ); |
104 | if ( m_anim->state() != QAbstractAnimation::Running ) |
105 | m_anim->start(); |
106 | } |
107 | } |
108 | |
109 | static bool lessThan( const GameSelectionBox * a, const GameSelectionBox * b ) |
110 | { |
111 | return a->m_label < b->m_label; |
112 | } |
113 | |
114 | signals: |
115 | void selected( int gameId ); |
116 | void hoverChanged( GameSelectionBox * box, bool hovered ); |
117 | |
118 | protected: |
119 | virtual void mousePressEvent( QGraphicsSceneMouseEvent * event ) |
120 | { |
121 | Q_UNUSED( event ) |
122 | emit selected( m_gameId ); |
123 | } |
124 | |
125 | virtual void hoverEnterEvent( QGraphicsSceneHoverEvent * event ) |
126 | { |
127 | Q_UNUSED( event ) |
128 | emit hoverChanged( this, true ); |
129 | } |
130 | |
131 | virtual void hoverLeaveEvent( QGraphicsSceneHoverEvent * event ) |
132 | { |
133 | Q_UNUSED( event ) |
134 | emit hoverChanged( this, false ); |
135 | } |
136 | |
137 | qreal hoverFadeAmount() const |
138 | { |
139 | return m_highlightFadeAmount; |
140 | } |
141 | |
142 | void setHoverFadeAmount( qreal amount ) |
143 | { |
144 | m_highlightFadeAmount = amount; |
145 | update(); |
146 | } |
147 | |
148 | virtual void paint( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = 0 ) |
149 | { |
150 | Q_UNUSED( option ) |
151 | Q_UNUSED( widget ) |
152 | |
153 | Renderer * r = Renderer::self(); |
154 | int textAreaHeight = m_size.height() * textToBoxHeightRatio; |
155 | int padding = boxPaddingRatio * m_size.width(); |
156 | QSize previewSize( m_size.height() - padding * 2, m_size.height() - padding * 2 - textAreaHeight ); |
157 | QRect textRect( 0, 0, m_size.width(), textAreaHeight ); |
158 | |
159 | if ( m_highlightFadeAmount < 1 ) |
160 | painter->drawPixmap( 0, 0, r->spritePixmap( "bubble" , m_size ) ); |
161 | |
162 | if ( m_highlightFadeAmount > 0 ) |
163 | { |
164 | if ( m_highlightFadeAmount < 1 ) |
165 | { |
166 | // Using QPainter::setOpacity is currently very inefficient, so to |
167 | // paint a semitransparent pixmap, we have to do some fiddling. |
168 | QPixmap transPix( m_size ); |
169 | transPix.fill( Qt::transparent ); |
170 | QPainter p( &transPix ); |
171 | p.drawPixmap( 0, 0, r->spritePixmap( "bubble_hover" , m_size ) ); |
172 | p.setCompositionMode( QPainter::CompositionMode_DestinationIn ); |
173 | p.fillRect( transPix.rect(), QColor( 0, 0, 0, m_highlightFadeAmount * 255 ) ); |
174 | painter->drawPixmap( 0, 0, transPix ); |
175 | } |
176 | else |
177 | { |
178 | painter->drawPixmap( 0, 0, r->spritePixmap( "bubble_hover" , m_size ) ); |
179 | } |
180 | } |
181 | |
182 | // Draw game preview |
183 | if ( m_preview.isNull() ) |
184 | { |
185 | QPixmap previewPix( m_previewPath ); |
186 | m_preview = previewPix.scaled( previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation ); |
187 | } |
188 | painter->drawPixmap( ( m_size.width() - previewSize.width() ) / 2, |
189 | padding + textAreaHeight, |
190 | m_preview ); |
191 | |
192 | // Draw label |
193 | painter->setFont( scene()->font() ); |
194 | painter->setPen( KColorUtils::mix( r->colorOfElement( "bubble_text_color" ), |
195 | r->colorOfElement( "bubble_hover_text_color" ), |
196 | m_highlightFadeAmount ) ); |
197 | painter->drawText( textRect, Qt::AlignCenter, m_label ); |
198 | } |
199 | |
200 | private: |
201 | QString m_label; |
202 | int m_gameId; |
203 | QSize m_size; |
204 | QPropertyAnimation * m_anim; |
205 | qreal m_highlightFadeAmount; |
206 | QPixmap m_preview; |
207 | QString m_previewPath; |
208 | }; |
209 | |
210 | |
211 | GameSelectionScene::GameSelectionScene( QObject * parent ) |
212 | : QGraphicsScene( parent ), |
213 | m_selectionIndex( -1 ) |
214 | { |
215 | foreach (const DealerInfo * i, DealerInfoList::self()->games()) |
216 | { |
217 | GameSelectionBox * box = new GameSelectionBox( i->baseName(), i->baseId() ); |
218 | m_boxes.append( box ); |
219 | addItem( box ); |
220 | |
221 | connect( box, SIGNAL(selected(int)), this, SIGNAL(gameSelected(int)) ); |
222 | connect( box, SIGNAL(hoverChanged(GameSelectionBox*,bool)), this, SLOT(boxHoverChanged(GameSelectionBox*,bool)) ); |
223 | } |
224 | |
225 | qSort( m_boxes.begin(), m_boxes.end(), GameSelectionBox::lessThan ); |
226 | } |
227 | |
228 | |
229 | GameSelectionScene::~GameSelectionScene() |
230 | { |
231 | } |
232 | |
233 | |
234 | void GameSelectionScene::resizeScene( const QSize & size ) |
235 | { |
236 | int numBoxes = m_boxes.size(); |
237 | qreal boxAspect = Renderer::self()->aspectRatioOfElement( "bubble" ); |
238 | qreal sceneAspect = qreal( size.width() ) / size.height(); |
239 | |
240 | // Determine the optimal number of rows/columns for the grid |
241 | m_columns = 1; |
242 | int numRows = 1; |
243 | int bestNumRows = 1; |
244 | qreal lowestError = 10e10; |
245 | for ( numRows = 1; numRows <= numBoxes; ++numRows ) |
246 | { |
247 | m_columns = ceil( qreal( numBoxes ) / numRows ); |
248 | int numNonEmptyRows = ceil( qreal( numBoxes ) / m_columns ); |
249 | qreal gridAspect = boxAspect * m_columns / numNonEmptyRows; |
250 | qreal error = gridAspect > sceneAspect ? gridAspect / sceneAspect |
251 | : sceneAspect / gridAspect; |
252 | if ( error < lowestError ) |
253 | { |
254 | lowestError = error; |
255 | bestNumRows = numRows; |
256 | } |
257 | } |
258 | numRows = bestNumRows; |
259 | m_columns = ceil( qreal( numBoxes ) / bestNumRows ); |
260 | |
261 | // Calculate the box and grid dimensions |
262 | qreal gridAspect = boxAspect * ( m_columns + spacingRatio * ( m_columns + 1 ) ) |
263 | / ( numRows + spacingRatio * ( numRows + 1 ) ); |
264 | int boxWidth, boxHeight; |
265 | if ( gridAspect > sceneAspect ) |
266 | { |
267 | boxWidth = size.width() / ( m_columns + spacingRatio * ( m_columns + 1 ) ); |
268 | boxHeight = boxWidth / boxAspect; |
269 | } |
270 | else |
271 | { |
272 | boxHeight = size.height() / ( numRows + spacingRatio * ( numRows + 1 ) ); |
273 | boxWidth = boxHeight * boxAspect; |
274 | } |
275 | int gridWidth = boxWidth * ( m_columns + spacingRatio * ( m_columns + 1 ) ); |
276 | int gridHeight = boxHeight * ( numRows + spacingRatio * ( numRows + 1 ) ); |
277 | |
278 | int xOffset = (gridWidth - size.width()) / 2 - boxWidth * spacingRatio; |
279 | int yOffset = (gridHeight - size.height()) / 2 - boxHeight * spacingRatio; |
280 | |
281 | // Set up the sceneRect so that the grid is centered |
282 | setSceneRect( xOffset, yOffset, size.width(), size.height() ); |
283 | |
284 | QPixmap pix( 1, 1 ); |
285 | QPainter p( &pix ); |
286 | QFont f; |
287 | |
288 | // Initial font size estimate |
289 | int pixelFontSize = boxHeight * (textToBoxHeightRatio - 1.5 * boxPaddingRatio); |
290 | f.setPixelSize( pixelFontSize ); |
291 | p.setFont( f ); |
292 | |
293 | qreal maxLabelWidth = boxWidth * textToBoxWidthRatio; |
294 | int row = 0; |
295 | int col = 0; |
296 | |
297 | foreach ( GameSelectionBox * box, m_boxes ) |
298 | { |
299 | // Reduce font size until the label fits |
300 | while ( pixelFontSize > minimumFontSize |
301 | && p.boundingRect( QRectF(), box->label() ).width() > maxLabelWidth ) |
302 | { |
303 | f.setPixelSize( --pixelFontSize ); |
304 | p.setFont( f ); |
305 | } |
306 | |
307 | // Position and size the boxes |
308 | box->setPos( col * ( boxWidth * ( 1 + spacingRatio ) ), |
309 | row * ( boxHeight * ( 1 + spacingRatio ) ) ); |
310 | box->setSize( QSize( boxWidth, boxHeight ) ); |
311 | |
312 | // Increment column and row |
313 | ++col; |
314 | if ( col == m_columns ) |
315 | { |
316 | col = 0; |
317 | ++row; |
318 | } |
319 | } |
320 | |
321 | setFont( f ); |
322 | } |
323 | |
324 | |
325 | void GameSelectionScene::keyReleaseEvent( QKeyEvent * event ) |
326 | { |
327 | if ( m_selectionIndex == -1 ) |
328 | { |
329 | m_selectionIndex = 0; |
330 | m_boxes.at( m_selectionIndex )->setHighlighted( true ); |
331 | } |
332 | else if ( event->key() == Qt::Key_Up |
333 | && m_selectionIndex / m_columns > 0 ) |
334 | { |
335 | m_boxes.at( m_selectionIndex )->setHighlighted( false ); |
336 | m_selectionIndex -= m_columns; |
337 | m_boxes.at( m_selectionIndex )->setHighlighted( true ); |
338 | } |
339 | else if ( event->key() == Qt::Key_Down |
340 | && m_selectionIndex + m_columns < m_boxes.size() ) |
341 | { |
342 | m_boxes.at( m_selectionIndex )->setHighlighted( false ); |
343 | m_selectionIndex += m_columns; |
344 | m_boxes.at( m_selectionIndex )->setHighlighted( true ); |
345 | } |
346 | else if ( event->key() == Qt::Key_Left |
347 | && m_selectionIndex % m_columns > 0 ) |
348 | { |
349 | m_boxes.at( m_selectionIndex )->setHighlighted( false ); |
350 | --m_selectionIndex; |
351 | m_boxes.at( m_selectionIndex )->setHighlighted( true ); |
352 | } |
353 | else if ( event->key() == Qt::Key_Right |
354 | && m_selectionIndex % m_columns < m_columns - 1 |
355 | && m_selectionIndex < m_boxes.size() - 1 ) |
356 | { |
357 | m_boxes.at( m_selectionIndex )->setHighlighted( false ); |
358 | ++m_selectionIndex; |
359 | m_boxes.at( m_selectionIndex )->setHighlighted( true ); |
360 | } |
361 | else if ( ( event->key() == Qt::Key_Return |
362 | || event->key() ==Qt::Key_Enter |
363 | || event->key() ==Qt::Key_Space ) |
364 | && m_selectionIndex != -1 ) |
365 | { |
366 | emit gameSelected( m_boxes.at( m_selectionIndex )->id() ); |
367 | } |
368 | } |
369 | |
370 | |
371 | void GameSelectionScene::boxHoverChanged( GameSelectionScene::GameSelectionBox * box, bool hovered ) |
372 | { |
373 | if ( hovered ) |
374 | { |
375 | if ( m_selectionIndex != -1 ) |
376 | m_boxes.at( m_selectionIndex )->setHighlighted( false ); |
377 | |
378 | m_selectionIndex = m_boxes.indexOf( box ); |
379 | box->setHighlighted( true ); |
380 | } |
381 | else |
382 | { |
383 | if ( m_boxes.indexOf( box ) == m_selectionIndex ) |
384 | { |
385 | m_selectionIndex = -1; |
386 | box->setHighlighted( false ); |
387 | } |
388 | } |
389 | } |
390 | |
391 | |
392 | #include "gameselectionscene.moc" |
393 | #include "moc_gameselectionscene.cpp" |
394 | |