1 | /*************************************************************************** |
2 | * Copyright (C) 2012 by Peter Penz <peter.penz19@gmail.com> * |
3 | * * |
4 | * This program is free software; you can redistribute it and/or modify * |
5 | * it under the terms of the GNU General Public License as published by * |
6 | * the Free Software Foundation; either version 2 of the License, or * |
7 | * (at your option) any later version. * |
8 | * * |
9 | * This program is distributed in the hope that it will be useful, * |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
12 | * GNU General Public License for more details. * |
13 | * * |
14 | * You should have received a copy of the GNU General Public License * |
15 | * along with this program; if not, write to the * |
16 | * Free Software Foundation, Inc., * |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * |
18 | ***************************************************************************/ |
19 | |
20 | #include "kstandarditemlistwidget.h" |
21 | |
22 | #include "kfileitemlistview.h" |
23 | #include "kfileitemmodel.h" |
24 | |
25 | #include <KIcon> |
26 | #include <KIconEffect> |
27 | #include <KIconLoader> |
28 | #include <KLocale> |
29 | #include <kratingpainter.h> |
30 | #include <KStringHandler> |
31 | #include <KDebug> |
32 | |
33 | #include "private/kfileitemclipboard.h" |
34 | #include "private/kitemlistroleeditor.h" |
35 | #include "private/kpixmapmodifier.h" |
36 | |
37 | #include <QFontMetricsF> |
38 | #include <QGraphicsScene> |
39 | #include <QGraphicsSceneResizeEvent> |
40 | #include <QGraphicsView> |
41 | #include <QPainter> |
42 | #include <QStyleOption> |
43 | #include <QTextLayout> |
44 | #include <QTextLine> |
45 | #include <QPixmapCache> |
46 | |
47 | // #define KSTANDARDITEMLISTWIDGET_DEBUG |
48 | |
49 | KStandardItemListWidgetInformant::KStandardItemListWidgetInformant() : |
50 | KItemListWidgetInformant() |
51 | { |
52 | } |
53 | |
54 | KStandardItemListWidgetInformant::~KStandardItemListWidgetInformant() |
55 | { |
56 | } |
57 | |
58 | void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const |
59 | { |
60 | switch (static_cast<const KStandardItemListView*>(view)->itemLayout()) { |
61 | case KStandardItemListWidget::IconsLayout: |
62 | calculateIconsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); |
63 | break; |
64 | |
65 | case KStandardItemListWidget::CompactLayout: |
66 | calculateCompactLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); |
67 | break; |
68 | |
69 | case KStandardItemListWidget::DetailsLayout: |
70 | calculateDetailsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); |
71 | break; |
72 | |
73 | default: |
74 | Q_ASSERT(false); |
75 | break; |
76 | } |
77 | } |
78 | |
79 | qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray& role, |
80 | int index, |
81 | const KItemListView* view) const |
82 | { |
83 | const QHash<QByteArray, QVariant> values = view->model()->data(index); |
84 | const KItemListStyleOption& option = view->styleOption(); |
85 | |
86 | const QString text = roleText(role, values); |
87 | qreal width = KStandardItemListWidget::columnPadding(option); |
88 | |
89 | const QFontMetrics& normalFontMetrics = option.fontMetrics; |
90 | const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font)); |
91 | |
92 | if (role == "rating" ) { |
93 | width += KStandardItemListWidget::preferredRatingSize(option).width(); |
94 | } else { |
95 | // If current item is a link, we use the customized link font metrics instead of the normal font metrics. |
96 | const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics; |
97 | |
98 | width += fontMetrics.width(text); |
99 | |
100 | if (role == "text" ) { |
101 | if (view->supportsItemExpanding()) { |
102 | // Increase the width by the expansion-toggle and the current expansion level |
103 | const int expandedParentsCount = values.value("expandedParentsCount" , 0).toInt(); |
104 | const qreal height = option.padding * 2 + qMax(option.iconSize, fontMetrics.height()); |
105 | width += (expandedParentsCount + 1) * height; |
106 | } |
107 | |
108 | // Increase the width by the required space for the icon |
109 | width += option.padding * 2 + option.iconSize; |
110 | } |
111 | } |
112 | |
113 | return width; |
114 | } |
115 | |
116 | QString KStandardItemListWidgetInformant::itemText(int index, const KItemListView* view) const |
117 | { |
118 | return view->model()->data(index).value("text" ).toString(); |
119 | } |
120 | |
121 | bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView* view) const |
122 | { |
123 | return false; |
124 | } |
125 | |
126 | QString KStandardItemListWidgetInformant::roleText(const QByteArray& role, |
127 | const QHash<QByteArray, QVariant>& values) const |
128 | { |
129 | if (role == "rating" ) { |
130 | // Always use an empty text, as the rating is shown by the image m_rating. |
131 | return QString(); |
132 | } |
133 | return values.value(role).toString(); |
134 | } |
135 | |
136 | QFont KStandardItemListWidgetInformant::customizedFontForLinks(const QFont& baseFont) const |
137 | { |
138 | return baseFont; |
139 | } |
140 | |
141 | void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const |
142 | { |
143 | const KItemListStyleOption& option = view->styleOption(); |
144 | const QFont& normalFont = option.font; |
145 | const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0); |
146 | |
147 | const qreal itemWidth = view->itemSize().width(); |
148 | const qreal maxWidth = itemWidth - 2 * option.padding; |
149 | const qreal additionalRolesSpacing = additionalRolesCount * option.fontMetrics.lineSpacing(); |
150 | const qreal spacingAndIconHeight = option.iconSize + option.padding * 3; |
151 | |
152 | const QFont linkFont = customizedFontForLinks(normalFont); |
153 | |
154 | QTextOption textOption(Qt::AlignHCenter); |
155 | textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
156 | |
157 | for (int index = 0; index < logicalHeightHints.count(); ++index) { |
158 | if (logicalHeightHints.at(index) > 0.0) { |
159 | continue; |
160 | } |
161 | |
162 | // If the current item is a link, we use the customized link font instead of the normal font. |
163 | const QFont& font = itemIsLink(index, view) ? linkFont : normalFont; |
164 | |
165 | const QString& text = KStringHandler::preProcessWrap(itemText(index, view)); |
166 | |
167 | // Calculate the number of lines required for wrapping the name |
168 | qreal textHeight = 0; |
169 | QTextLayout layout(text, font); |
170 | layout.setTextOption(textOption); |
171 | layout.beginLayout(); |
172 | QTextLine line; |
173 | int lineCount = 0; |
174 | while ((line = layout.createLine()).isValid()) { |
175 | line.setLineWidth(maxWidth); |
176 | line.naturalTextWidth(); |
177 | textHeight += line.height(); |
178 | |
179 | ++lineCount; |
180 | if (lineCount == option.maxTextLines) { |
181 | break; |
182 | } |
183 | } |
184 | layout.endLayout(); |
185 | |
186 | // Add one line for each additional information |
187 | textHeight += additionalRolesSpacing; |
188 | |
189 | logicalHeightHints[index] = textHeight + spacingAndIconHeight; |
190 | } |
191 | |
192 | logicalWidthHint = itemWidth; |
193 | } |
194 | |
195 | void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const |
196 | { |
197 | const KItemListStyleOption& option = view->styleOption(); |
198 | const QFontMetrics& normalFontMetrics = option.fontMetrics; |
199 | const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0); |
200 | |
201 | const QList<QByteArray>& visibleRoles = view->visibleRoles(); |
202 | const bool showOnlyTextRole = (visibleRoles.count() == 1) && (visibleRoles.first() == "text" ); |
203 | const qreal maxWidth = option.maxTextWidth; |
204 | const qreal paddingAndIconWidth = option.padding * 4 + option.iconSize; |
205 | const qreal height = option.padding * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * normalFontMetrics.lineSpacing()); |
206 | |
207 | const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font)); |
208 | |
209 | for (int index = 0; index < logicalHeightHints.count(); ++index) { |
210 | if (logicalHeightHints.at(index) > 0.0) { |
211 | continue; |
212 | } |
213 | |
214 | // If the current item is a link, we use the customized link font metrics instead of the normal font metrics. |
215 | const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics; |
216 | |
217 | // For each row exactly one role is shown. Calculate the maximum required width that is necessary |
218 | // to show all roles without horizontal clipping. |
219 | qreal maximumRequiredWidth = 0.0; |
220 | |
221 | if (showOnlyTextRole) { |
222 | maximumRequiredWidth = fontMetrics.width(itemText(index, view)); |
223 | } else { |
224 | const QHash<QByteArray, QVariant>& values = view->model()->data(index); |
225 | foreach (const QByteArray& role, visibleRoles) { |
226 | const QString& text = roleText(role, values); |
227 | const qreal requiredWidth = fontMetrics.width(text); |
228 | maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth); |
229 | } |
230 | } |
231 | |
232 | qreal width = paddingAndIconWidth + maximumRequiredWidth; |
233 | if (maxWidth > 0 && width > maxWidth) { |
234 | width = maxWidth; |
235 | } |
236 | |
237 | logicalHeightHints[index] = width; |
238 | } |
239 | |
240 | logicalWidthHint = height; |
241 | } |
242 | |
243 | void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const |
244 | { |
245 | const KItemListStyleOption& option = view->styleOption(); |
246 | const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height()); |
247 | logicalHeightHints.fill(height); |
248 | logicalWidthHint = -1.0; |
249 | } |
250 | |
251 | KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) : |
252 | KItemListWidget(informant, parent), |
253 | m_isCut(false), |
254 | m_isHidden(false), |
255 | m_customizedFont(), |
256 | m_customizedFontMetrics(m_customizedFont), |
257 | m_isExpandable(false), |
258 | m_supportsItemExpanding(false), |
259 | m_dirtyLayout(true), |
260 | m_dirtyContent(true), |
261 | m_dirtyContentRoles(), |
262 | m_layout(IconsLayout), |
263 | m_pixmapPos(), |
264 | m_pixmap(), |
265 | m_scaledPixmapSize(), |
266 | m_iconRect(), |
267 | m_hoverPixmap(), |
268 | m_textInfo(), |
269 | m_textRect(), |
270 | m_sortedVisibleRoles(), |
271 | m_expansionArea(), |
272 | m_customTextColor(), |
273 | m_additionalInfoTextColor(), |
274 | m_overlay(), |
275 | m_rating(), |
276 | m_roleEditor(0), |
277 | m_oldRoleEditor(0) |
278 | { |
279 | } |
280 | |
281 | KStandardItemListWidget::~KStandardItemListWidget() |
282 | { |
283 | qDeleteAll(m_textInfo); |
284 | m_textInfo.clear(); |
285 | |
286 | if (m_roleEditor) { |
287 | m_roleEditor->deleteLater(); |
288 | } |
289 | |
290 | if (m_oldRoleEditor) { |
291 | m_oldRoleEditor->deleteLater(); |
292 | } |
293 | } |
294 | |
295 | void KStandardItemListWidget::setLayout(Layout layout) |
296 | { |
297 | if (m_layout != layout) { |
298 | m_layout = layout; |
299 | m_dirtyLayout = true; |
300 | updateAdditionalInfoTextColor(); |
301 | update(); |
302 | } |
303 | } |
304 | |
305 | KStandardItemListWidget::Layout KStandardItemListWidget::layout() const |
306 | { |
307 | return m_layout; |
308 | } |
309 | |
310 | void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding) |
311 | { |
312 | if (m_supportsItemExpanding != supportsItemExpanding) { |
313 | m_supportsItemExpanding = supportsItemExpanding; |
314 | m_dirtyLayout = true; |
315 | update(); |
316 | } |
317 | } |
318 | |
319 | bool KStandardItemListWidget::supportsItemExpanding() const |
320 | { |
321 | return m_supportsItemExpanding; |
322 | } |
323 | |
324 | void KStandardItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) |
325 | { |
326 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
327 | |
328 | KItemListWidget::paint(painter, option, widget); |
329 | |
330 | if (!m_expansionArea.isEmpty()) { |
331 | drawSiblingsInformation(painter); |
332 | } |
333 | |
334 | const KItemListStyleOption& itemListStyleOption = styleOption(); |
335 | if (isHovered()) { |
336 | if (hoverOpacity() < 1.0) { |
337 | /* |
338 | * Linear interpolation between m_pixmap and m_hoverPixmap. |
339 | * |
340 | * Note that this cannot be achieved by painting m_hoverPixmap over |
341 | * m_pixmap, even if the opacities are adjusted. For details see |
342 | * https://git.reviewboard.kde.org/r/109614/ |
343 | */ |
344 | // Paint pixmap1 so that pixmap1 = m_pixmap * (1.0 - hoverOpacity()) |
345 | QPixmap pixmap1(m_pixmap.size()); |
346 | pixmap1.fill(Qt::transparent); |
347 | { |
348 | QPainter p(&pixmap1); |
349 | p.setOpacity(1.0 - hoverOpacity()); |
350 | p.drawPixmap(0, 0, m_pixmap); |
351 | } |
352 | |
353 | // Paint pixmap2 so that pixmap2 = m_hoverPixmap * hoverOpacity() |
354 | QPixmap pixmap2(pixmap1.size()); |
355 | pixmap2.fill(Qt::transparent); |
356 | { |
357 | QPainter p(&pixmap2); |
358 | p.setOpacity(hoverOpacity()); |
359 | p.drawPixmap(0, 0, m_hoverPixmap); |
360 | } |
361 | |
362 | // Paint pixmap2 on pixmap1 using CompositionMode_Plus |
363 | // Now pixmap1 = pixmap2 + m_pixmap * (1.0 - hoverOpacity()) |
364 | // = m_hoverPixmap * hoverOpacity() + m_pixmap * (1.0 - hoverOpacity()) |
365 | { |
366 | QPainter p(&pixmap1); |
367 | p.setCompositionMode(QPainter::CompositionMode_Plus); |
368 | p.drawPixmap(0, 0, pixmap2); |
369 | } |
370 | |
371 | // Finally paint pixmap1 on the widget |
372 | drawPixmap(painter, pixmap1); |
373 | } else { |
374 | drawPixmap(painter, m_hoverPixmap); |
375 | } |
376 | } else { |
377 | drawPixmap(painter, m_pixmap); |
378 | } |
379 | |
380 | painter->setFont(m_customizedFont); |
381 | painter->setPen(textColor()); |
382 | const TextInfo* textInfo = m_textInfo.value("text" ); |
383 | |
384 | if (!textInfo) { |
385 | // It seems that we can end up here even if m_textInfo does not contain |
386 | // the key "text", see bug 306167. According to triggerCacheRefreshing(), |
387 | // this can only happen if the index is negative. This can happen when |
388 | // the item is about to be removed, see KItemListView::slotItemsRemoved(). |
389 | // TODO: try to reproduce the crash and find a better fix. |
390 | return; |
391 | } |
392 | |
393 | painter->drawStaticText(textInfo->pos, textInfo->staticText); |
394 | |
395 | bool clipAdditionalInfoBounds = false; |
396 | if (m_supportsItemExpanding) { |
397 | // Prevent a possible overlapping of the additional-information texts |
398 | // with the icon. This can happen if the user has minimized the width |
399 | // of the name-column to a very small value. |
400 | const qreal minX = m_pixmapPos.x() + m_pixmap.width() + 4 * itemListStyleOption.padding; |
401 | if (textInfo->pos.x() + columnWidth("text" ) > minX) { |
402 | clipAdditionalInfoBounds = true; |
403 | painter->save(); |
404 | painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip); |
405 | } |
406 | } |
407 | |
408 | painter->setPen(m_additionalInfoTextColor); |
409 | painter->setFont(m_customizedFont); |
410 | |
411 | for (int i = 1; i < m_sortedVisibleRoles.count(); ++i) { |
412 | const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles[i]); |
413 | painter->drawStaticText(textInfo->pos, textInfo->staticText); |
414 | } |
415 | |
416 | if (!m_rating.isNull()) { |
417 | const TextInfo* ratingTextInfo = m_textInfo.value("rating" ); |
418 | QPointF pos = ratingTextInfo->pos; |
419 | const Qt::Alignment align = ratingTextInfo->staticText.textOption().alignment(); |
420 | if (align & Qt::AlignHCenter) { |
421 | pos.rx() += (size().width() - m_rating.width()) / 2 - 2; |
422 | } |
423 | painter->drawPixmap(pos, m_rating); |
424 | } |
425 | |
426 | if (clipAdditionalInfoBounds) { |
427 | painter->restore(); |
428 | } |
429 | |
430 | #ifdef KSTANDARDITEMLISTWIDGET_DEBUG |
431 | painter->setBrush(Qt::NoBrush); |
432 | painter->setPen(Qt::green); |
433 | painter->drawRect(m_iconRect); |
434 | |
435 | painter->setPen(Qt::blue); |
436 | painter->drawRect(m_textRect); |
437 | |
438 | painter->setPen(Qt::red); |
439 | painter->drawText(QPointF(0, m_customizedFontMetrics.height()), QString::number(index())); |
440 | painter->drawRect(rect()); |
441 | #endif |
442 | } |
443 | |
444 | QRectF KStandardItemListWidget::iconRect() const |
445 | { |
446 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
447 | return m_iconRect; |
448 | } |
449 | |
450 | QRectF KStandardItemListWidget::textRect() const |
451 | { |
452 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
453 | return m_textRect; |
454 | } |
455 | |
456 | QRectF KStandardItemListWidget::textFocusRect() const |
457 | { |
458 | // In the compact- and details-layout a larger textRect() is returned to be aligned |
459 | // with the iconRect(). This is useful to have a larger selection/hover-area |
460 | // when having a quite large icon size but only one line of text. Still the |
461 | // focus rectangle should be shown as narrow as possible around the text. |
462 | |
463 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
464 | |
465 | switch (m_layout) { |
466 | case CompactLayout: { |
467 | QRectF rect = m_textRect; |
468 | const TextInfo* topText = m_textInfo.value(m_sortedVisibleRoles.first()); |
469 | const TextInfo* bottomText = m_textInfo.value(m_sortedVisibleRoles.last()); |
470 | rect.setTop(topText->pos.y()); |
471 | rect.setBottom(bottomText->pos.y() + bottomText->staticText.size().height()); |
472 | return rect; |
473 | } |
474 | |
475 | case DetailsLayout: { |
476 | QRectF rect = m_textRect; |
477 | const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles.first()); |
478 | rect.setTop(textInfo->pos.y()); |
479 | rect.setBottom(textInfo->pos.y() + textInfo->staticText.size().height()); |
480 | |
481 | const KItemListStyleOption& option = styleOption(); |
482 | if (option.extendedSelectionRegion) { |
483 | const QString text = textInfo->staticText.text(); |
484 | rect.setWidth(m_customizedFontMetrics.width(text) + 2 * option.padding); |
485 | } |
486 | |
487 | return rect; |
488 | } |
489 | |
490 | default: |
491 | break; |
492 | } |
493 | |
494 | return m_textRect; |
495 | } |
496 | |
497 | QRectF KStandardItemListWidget::selectionRect() const |
498 | { |
499 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
500 | |
501 | switch (m_layout) { |
502 | case IconsLayout: |
503 | return m_textRect; |
504 | |
505 | case CompactLayout: |
506 | case DetailsLayout: { |
507 | const int padding = styleOption().padding; |
508 | QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding); |
509 | return adjustedIconRect | m_textRect; |
510 | } |
511 | |
512 | default: |
513 | Q_ASSERT(false); |
514 | break; |
515 | } |
516 | |
517 | return m_textRect; |
518 | } |
519 | |
520 | QRectF KStandardItemListWidget::expansionToggleRect() const |
521 | { |
522 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
523 | return m_isExpandable ? m_expansionArea : QRectF(); |
524 | } |
525 | |
526 | QRectF KStandardItemListWidget::selectionToggleRect() const |
527 | { |
528 | const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing(); |
529 | |
530 | const int iconHeight = styleOption().iconSize; |
531 | |
532 | int toggleSize = KIconLoader::SizeSmall; |
533 | if (iconHeight >= KIconLoader::SizeEnormous) { |
534 | toggleSize = KIconLoader::SizeMedium; |
535 | } else if (iconHeight >= KIconLoader::SizeLarge) { |
536 | toggleSize = KIconLoader::SizeSmallMedium; |
537 | } |
538 | |
539 | QPointF pos = iconRect().topLeft(); |
540 | |
541 | // If the selection toggle has a very small distance to the |
542 | // widget borders, the size of the selection toggle will get |
543 | // increased to prevent an accidental clicking of the item |
544 | // when trying to hit the toggle. |
545 | const int widgetHeight = size().height(); |
546 | const int widgetWidth = size().width(); |
547 | const int minMargin = 2; |
548 | |
549 | if (toggleSize + minMargin * 2 >= widgetHeight) { |
550 | pos.rx() -= (widgetHeight - toggleSize) / 2; |
551 | toggleSize = widgetHeight; |
552 | pos.setY(0); |
553 | } |
554 | if (toggleSize + minMargin * 2 >= widgetWidth) { |
555 | pos.ry() -= (widgetWidth - toggleSize) / 2; |
556 | toggleSize = widgetWidth; |
557 | pos.setX(0); |
558 | } |
559 | |
560 | return QRectF(pos, QSizeF(toggleSize, toggleSize)); |
561 | } |
562 | |
563 | QPixmap KStandardItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem* option, |
564 | QWidget* widget) |
565 | { |
566 | QPixmap pixmap = KItemListWidget::createDragPixmap(option, widget); |
567 | if (m_layout != DetailsLayout) { |
568 | return pixmap; |
569 | } |
570 | |
571 | // Only return the content of the text-column as pixmap |
572 | const int leftClip = m_pixmapPos.x(); |
573 | |
574 | const TextInfo* textInfo = m_textInfo.value("text" ); |
575 | const int rightClip = textInfo->pos.x() + |
576 | textInfo->staticText.size().width() + |
577 | 2 * styleOption().padding; |
578 | |
579 | QPixmap clippedPixmap(rightClip - leftClip + 1, pixmap.height()); |
580 | clippedPixmap.fill(Qt::transparent); |
581 | |
582 | QPainter painter(&clippedPixmap); |
583 | painter.drawPixmap(-leftClip, 0, pixmap); |
584 | |
585 | return clippedPixmap; |
586 | } |
587 | |
588 | |
589 | KItemListWidgetInformant* KStandardItemListWidget::createInformant() |
590 | { |
591 | return new KStandardItemListWidgetInformant(); |
592 | } |
593 | |
594 | void KStandardItemListWidget::invalidateCache() |
595 | { |
596 | m_dirtyLayout = true; |
597 | m_dirtyContent = true; |
598 | } |
599 | |
600 | void KStandardItemListWidget::refreshCache() |
601 | { |
602 | } |
603 | |
604 | bool KStandardItemListWidget::isRoleRightAligned(const QByteArray& role) const |
605 | { |
606 | Q_UNUSED(role); |
607 | return false; |
608 | } |
609 | |
610 | bool KStandardItemListWidget::isHidden() const |
611 | { |
612 | return false; |
613 | } |
614 | |
615 | QFont KStandardItemListWidget::customizedFont(const QFont& baseFont) const |
616 | { |
617 | return baseFont; |
618 | } |
619 | |
620 | QPalette::ColorRole KStandardItemListWidget::normalTextColorRole() const |
621 | { |
622 | return QPalette::Text; |
623 | } |
624 | |
625 | void KStandardItemListWidget::setTextColor(const QColor& color) |
626 | { |
627 | if (color != m_customTextColor) { |
628 | m_customTextColor = color; |
629 | updateAdditionalInfoTextColor(); |
630 | update(); |
631 | } |
632 | } |
633 | |
634 | QColor KStandardItemListWidget::textColor() const |
635 | { |
636 | if (!isSelected()) { |
637 | if (m_isHidden) { |
638 | return m_additionalInfoTextColor; |
639 | } else if (m_customTextColor.isValid()) { |
640 | return m_customTextColor; |
641 | } |
642 | } |
643 | |
644 | const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; |
645 | const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : normalTextColorRole(); |
646 | return styleOption().palette.color(group, role); |
647 | } |
648 | |
649 | void KStandardItemListWidget::setOverlay(const QPixmap& overlay) |
650 | { |
651 | m_overlay = overlay; |
652 | m_dirtyContent = true; |
653 | update(); |
654 | } |
655 | |
656 | QPixmap KStandardItemListWidget::overlay() const |
657 | { |
658 | return m_overlay; |
659 | } |
660 | |
661 | |
662 | QString KStandardItemListWidget::roleText(const QByteArray& role, |
663 | const QHash<QByteArray, QVariant>& values) const |
664 | { |
665 | return static_cast<const KStandardItemListWidgetInformant*>(informant())->roleText(role, values); |
666 | } |
667 | |
668 | void KStandardItemListWidget::dataChanged(const QHash<QByteArray, QVariant>& current, |
669 | const QSet<QByteArray>& roles) |
670 | { |
671 | Q_UNUSED(current); |
672 | |
673 | m_dirtyContent = true; |
674 | |
675 | QSet<QByteArray> dirtyRoles; |
676 | if (roles.isEmpty()) { |
677 | dirtyRoles = visibleRoles().toSet(); |
678 | } else { |
679 | dirtyRoles = roles; |
680 | } |
681 | |
682 | // The URL might have changed (i.e., if the sort order of the items has |
683 | // been changed). Therefore, the "is cut" state must be updated. |
684 | KFileItemClipboard* clipboard = KFileItemClipboard::instance(); |
685 | const KUrl itemUrl = data().value("url" ).value<KUrl>(); |
686 | m_isCut = clipboard->isCut(itemUrl); |
687 | |
688 | // The icon-state might depend from other roles and hence is |
689 | // marked as dirty whenever a role has been changed |
690 | dirtyRoles.insert("iconPixmap" ); |
691 | dirtyRoles.insert("iconName" ); |
692 | |
693 | QSetIterator<QByteArray> it(dirtyRoles); |
694 | while (it.hasNext()) { |
695 | const QByteArray& role = it.next(); |
696 | m_dirtyContentRoles.insert(role); |
697 | } |
698 | } |
699 | |
700 | void KStandardItemListWidget::visibleRolesChanged(const QList<QByteArray>& current, |
701 | const QList<QByteArray>& previous) |
702 | { |
703 | Q_UNUSED(previous); |
704 | m_sortedVisibleRoles = current; |
705 | m_dirtyLayout = true; |
706 | } |
707 | |
708 | void KStandardItemListWidget::columnWidthChanged(const QByteArray& role, |
709 | qreal current, |
710 | qreal previous) |
711 | { |
712 | Q_UNUSED(role); |
713 | Q_UNUSED(current); |
714 | Q_UNUSED(previous); |
715 | m_dirtyLayout = true; |
716 | } |
717 | |
718 | void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption& current, |
719 | const KItemListStyleOption& previous) |
720 | { |
721 | Q_UNUSED(current); |
722 | Q_UNUSED(previous); |
723 | updateAdditionalInfoTextColor(); |
724 | m_dirtyLayout = true; |
725 | } |
726 | |
727 | void KStandardItemListWidget::hoveredChanged(bool hovered) |
728 | { |
729 | Q_UNUSED(hovered); |
730 | m_dirtyLayout = true; |
731 | } |
732 | |
733 | void KStandardItemListWidget::selectedChanged(bool selected) |
734 | { |
735 | Q_UNUSED(selected); |
736 | updateAdditionalInfoTextColor(); |
737 | m_dirtyContent = true; |
738 | } |
739 | |
740 | void KStandardItemListWidget::siblingsInformationChanged(const QBitArray& current, const QBitArray& previous) |
741 | { |
742 | Q_UNUSED(current); |
743 | Q_UNUSED(previous); |
744 | m_dirtyLayout = true; |
745 | } |
746 | |
747 | int KStandardItemListWidget::selectionLength(const QString& text) const |
748 | { |
749 | return text.length(); |
750 | } |
751 | |
752 | void KStandardItemListWidget::editedRoleChanged(const QByteArray& current, const QByteArray& previous) |
753 | { |
754 | Q_UNUSED(previous); |
755 | |
756 | QGraphicsView* parent = scene()->views()[0]; |
757 | if (current.isEmpty() || !parent || current != "text" ) { |
758 | if (m_roleEditor) { |
759 | emit roleEditingCanceled(index(), current, data().value(current)); |
760 | |
761 | disconnect(m_roleEditor, SIGNAL(roleEditingCanceled(QByteArray,QVariant)), |
762 | this, SLOT(slotRoleEditingCanceled(QByteArray,QVariant))); |
763 | disconnect(m_roleEditor, SIGNAL(roleEditingFinished(QByteArray,QVariant)), |
764 | this, SLOT(slotRoleEditingFinished(QByteArray,QVariant))); |
765 | |
766 | if (m_oldRoleEditor) { |
767 | m_oldRoleEditor->deleteLater(); |
768 | } |
769 | m_oldRoleEditor = m_roleEditor; |
770 | m_roleEditor->hide(); |
771 | m_roleEditor = 0; |
772 | } |
773 | return; |
774 | } |
775 | |
776 | Q_ASSERT(!m_roleEditor); |
777 | |
778 | const TextInfo* textInfo = m_textInfo.value("text" ); |
779 | |
780 | m_roleEditor = new KItemListRoleEditor(parent); |
781 | m_roleEditor->setRole(current); |
782 | m_roleEditor->setFont(styleOption().font); |
783 | |
784 | const QString text = data().value(current).toString(); |
785 | m_roleEditor->setPlainText(text); |
786 | |
787 | QTextOption textOption = textInfo->staticText.textOption(); |
788 | m_roleEditor->document()->setDefaultTextOption(textOption); |
789 | |
790 | const int textSelectionLength = selectionLength(text); |
791 | |
792 | if (textSelectionLength > 0) { |
793 | QTextCursor cursor = m_roleEditor->textCursor(); |
794 | cursor.movePosition(QTextCursor::StartOfBlock); |
795 | cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, textSelectionLength); |
796 | m_roleEditor->setTextCursor(cursor); |
797 | } |
798 | |
799 | connect(m_roleEditor, SIGNAL(roleEditingCanceled(QByteArray,QVariant)), |
800 | this, SLOT(slotRoleEditingCanceled(QByteArray,QVariant))); |
801 | connect(m_roleEditor, SIGNAL(roleEditingFinished(QByteArray,QVariant)), |
802 | this, SLOT(slotRoleEditingFinished(QByteArray,QVariant))); |
803 | |
804 | // Adjust the geometry of the editor |
805 | QRectF rect = roleEditingRect(current); |
806 | const int frameWidth = m_roleEditor->frameWidth(); |
807 | rect.adjust(-frameWidth, -frameWidth, frameWidth, frameWidth); |
808 | rect.translate(pos()); |
809 | if (rect.right() > parent->width()) { |
810 | rect.setWidth(parent->width() - rect.left()); |
811 | } |
812 | m_roleEditor->setGeometry(rect.toRect()); |
813 | m_roleEditor->show(); |
814 | m_roleEditor->setFocus(); |
815 | } |
816 | |
817 | void KStandardItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) |
818 | { |
819 | if (m_roleEditor) { |
820 | setEditedRole(QByteArray()); |
821 | Q_ASSERT(!m_roleEditor); |
822 | } |
823 | |
824 | KItemListWidget::resizeEvent(event); |
825 | |
826 | m_dirtyLayout = true; |
827 | } |
828 | |
829 | void KStandardItemListWidget::showEvent(QShowEvent* event) |
830 | { |
831 | KItemListWidget::showEvent(event); |
832 | |
833 | // Listen to changes of the clipboard to mark the item as cut/uncut |
834 | KFileItemClipboard* clipboard = KFileItemClipboard::instance(); |
835 | |
836 | const KUrl itemUrl = data().value("url" ).value<KUrl>(); |
837 | m_isCut = clipboard->isCut(itemUrl); |
838 | |
839 | connect(clipboard, SIGNAL(cutItemsChanged()), |
840 | this, SLOT(slotCutItemsChanged())); |
841 | } |
842 | |
843 | void KStandardItemListWidget::hideEvent(QHideEvent* event) |
844 | { |
845 | disconnect(KFileItemClipboard::instance(), SIGNAL(cutItemsChanged()), |
846 | this, SLOT(slotCutItemsChanged())); |
847 | |
848 | KItemListWidget::hideEvent(event); |
849 | } |
850 | |
851 | void KStandardItemListWidget::slotCutItemsChanged() |
852 | { |
853 | const KUrl itemUrl = data().value("url" ).value<KUrl>(); |
854 | const bool isCut = KFileItemClipboard::instance()->isCut(itemUrl); |
855 | if (m_isCut != isCut) { |
856 | m_isCut = isCut; |
857 | m_pixmap = QPixmap(); |
858 | m_dirtyContent = true; |
859 | update(); |
860 | } |
861 | } |
862 | |
863 | void KStandardItemListWidget::slotRoleEditingCanceled(const QByteArray& role, |
864 | const QVariant& value) |
865 | { |
866 | closeRoleEditor(); |
867 | emit roleEditingCanceled(index(), role, value); |
868 | setEditedRole(QByteArray()); |
869 | } |
870 | |
871 | void KStandardItemListWidget::slotRoleEditingFinished(const QByteArray& role, |
872 | const QVariant& value) |
873 | { |
874 | closeRoleEditor(); |
875 | emit roleEditingFinished(index(), role, value); |
876 | setEditedRole(QByteArray()); |
877 | } |
878 | |
879 | void KStandardItemListWidget::triggerCacheRefreshing() |
880 | { |
881 | if ((!m_dirtyContent && !m_dirtyLayout) || index() < 0) { |
882 | return; |
883 | } |
884 | |
885 | refreshCache(); |
886 | |
887 | const QHash<QByteArray, QVariant> values = data(); |
888 | m_isExpandable = m_supportsItemExpanding && values["isExpandable" ].toBool(); |
889 | m_isHidden = isHidden(); |
890 | m_customizedFont = customizedFont(styleOption().font); |
891 | m_customizedFontMetrics = QFontMetrics(m_customizedFont); |
892 | |
893 | updateExpansionArea(); |
894 | updateTextsCache(); |
895 | updatePixmapCache(); |
896 | |
897 | m_dirtyLayout = false; |
898 | m_dirtyContent = false; |
899 | m_dirtyContentRoles.clear(); |
900 | } |
901 | |
902 | void KStandardItemListWidget::updateExpansionArea() |
903 | { |
904 | if (m_supportsItemExpanding) { |
905 | const QHash<QByteArray, QVariant> values = data(); |
906 | const int expandedParentsCount = values.value("expandedParentsCount" , 0).toInt(); |
907 | if (expandedParentsCount >= 0) { |
908 | const KItemListStyleOption& option = styleOption(); |
909 | const qreal widgetHeight = size().height(); |
910 | const qreal inc = (widgetHeight - option.iconSize) / 2; |
911 | const qreal x = expandedParentsCount * widgetHeight + inc; |
912 | const qreal y = inc; |
913 | m_expansionArea = QRectF(x, y, option.iconSize, option.iconSize); |
914 | return; |
915 | } |
916 | } |
917 | |
918 | m_expansionArea = QRectF(); |
919 | } |
920 | |
921 | void KStandardItemListWidget::updatePixmapCache() |
922 | { |
923 | // Precondition: Requires already updated m_textPos values to calculate |
924 | // the remaining height when the alignment is vertical. |
925 | |
926 | const QSizeF widgetSize = size(); |
927 | const bool iconOnTop = (m_layout == IconsLayout); |
928 | const KItemListStyleOption& option = styleOption(); |
929 | const qreal padding = option.padding; |
930 | |
931 | const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : option.iconSize; |
932 | const int maxIconHeight = option.iconSize; |
933 | |
934 | const QHash<QByteArray, QVariant> values = data(); |
935 | |
936 | bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight); |
937 | if (!updatePixmap && m_dirtyContent) { |
938 | updatePixmap = m_dirtyContentRoles.isEmpty() |
939 | || m_dirtyContentRoles.contains("iconPixmap" ) |
940 | || m_dirtyContentRoles.contains("iconName" ) |
941 | || m_dirtyContentRoles.contains("iconOverlays" ); |
942 | } |
943 | |
944 | if (updatePixmap) { |
945 | m_pixmap = values["iconPixmap" ].value<QPixmap>(); |
946 | if (m_pixmap.isNull()) { |
947 | // Use the icon that fits to the MIME-type |
948 | QString iconName = values["iconName" ].toString(); |
949 | if (iconName.isEmpty()) { |
950 | // The icon-name has not been not resolved by KFileItemModelRolesUpdater, |
951 | // use a generic icon as fallback |
952 | iconName = QLatin1String("unknown" ); |
953 | } |
954 | const QStringList overlays = values["iconOverlays" ].toStringList(); |
955 | m_pixmap = pixmapForIcon(iconName, overlays, maxIconHeight); |
956 | } else if (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight) { |
957 | // A custom pixmap has been applied. Assure that the pixmap |
958 | // is scaled to the maximum available size. |
959 | KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight)); |
960 | } |
961 | |
962 | if (m_isCut) { |
963 | KIconEffect* effect = KIconLoader::global()->iconEffect(); |
964 | m_pixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); |
965 | } |
966 | |
967 | if (m_isHidden) { |
968 | KIconEffect::semiTransparent(m_pixmap); |
969 | } |
970 | |
971 | if (m_layout == IconsLayout && isSelected()) { |
972 | const QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color(); |
973 | QImage image = m_pixmap.toImage(); |
974 | KIconEffect::colorize(image, color, 0.8f); |
975 | m_pixmap = QPixmap::fromImage(image); |
976 | } |
977 | } |
978 | |
979 | if (!m_overlay.isNull()) { |
980 | QPainter painter(&m_pixmap); |
981 | painter.drawPixmap(0, m_pixmap.height() - m_overlay.height(), m_overlay); |
982 | } |
983 | |
984 | int scaledIconSize = 0; |
985 | if (iconOnTop) { |
986 | const TextInfo* textInfo = m_textInfo.value("text" ); |
987 | scaledIconSize = static_cast<int>(textInfo->pos.y() - 2 * padding); |
988 | } else { |
989 | const int textRowsCount = (m_layout == CompactLayout) ? visibleRoles().count() : 1; |
990 | const qreal requiredTextHeight = textRowsCount * m_customizedFontMetrics.height(); |
991 | scaledIconSize = (requiredTextHeight < maxIconHeight) ? |
992 | widgetSize.height() - 2 * padding : maxIconHeight; |
993 | } |
994 | |
995 | const int maxScaledIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : scaledIconSize; |
996 | const int maxScaledIconHeight = scaledIconSize; |
997 | |
998 | m_scaledPixmapSize = m_pixmap.size(); |
999 | m_scaledPixmapSize.scale(maxScaledIconWidth, maxScaledIconHeight, Qt::KeepAspectRatio); |
1000 | |
1001 | if (iconOnTop) { |
1002 | // Center horizontally and align on bottom within the icon-area |
1003 | m_pixmapPos.setX((widgetSize.width() - m_scaledPixmapSize.width()) / 2); |
1004 | m_pixmapPos.setY(padding + scaledIconSize - m_scaledPixmapSize.height()); |
1005 | } else { |
1006 | // Center horizontally and vertically within the icon-area |
1007 | const TextInfo* textInfo = m_textInfo.value("text" ); |
1008 | m_pixmapPos.setX(textInfo->pos.x() - 2 * padding |
1009 | - (scaledIconSize + m_scaledPixmapSize.width()) / 2); |
1010 | m_pixmapPos.setY(padding |
1011 | + (scaledIconSize - m_scaledPixmapSize.height()) / 2); |
1012 | } |
1013 | |
1014 | m_iconRect = QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize)); |
1015 | |
1016 | // Prepare the pixmap that is used when the item gets hovered |
1017 | if (isHovered()) { |
1018 | m_hoverPixmap = m_pixmap; |
1019 | KIconEffect* effect = KIconLoader::global()->iconEffect(); |
1020 | // In the KIconLoader terminology, active = hover. |
1021 | if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) { |
1022 | m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState); |
1023 | } else { |
1024 | m_hoverPixmap = m_pixmap; |
1025 | } |
1026 | } else if (hoverOpacity() <= 0.0) { |
1027 | // No hover animation is ongoing. Clear m_hoverPixmap to save memory. |
1028 | m_hoverPixmap = QPixmap(); |
1029 | } |
1030 | } |
1031 | |
1032 | void KStandardItemListWidget::updateTextsCache() |
1033 | { |
1034 | QTextOption textOption; |
1035 | switch (m_layout) { |
1036 | case IconsLayout: |
1037 | textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
1038 | textOption.setAlignment(Qt::AlignHCenter); |
1039 | break; |
1040 | case CompactLayout: |
1041 | case DetailsLayout: |
1042 | textOption.setAlignment(Qt::AlignLeft); |
1043 | textOption.setWrapMode(QTextOption::NoWrap); |
1044 | break; |
1045 | default: |
1046 | Q_ASSERT(false); |
1047 | break; |
1048 | } |
1049 | |
1050 | qDeleteAll(m_textInfo); |
1051 | m_textInfo.clear(); |
1052 | for (int i = 0; i < m_sortedVisibleRoles.count(); ++i) { |
1053 | TextInfo* textInfo = new TextInfo(); |
1054 | textInfo->staticText.setTextFormat(Qt::PlainText); |
1055 | textInfo->staticText.setPerformanceHint(QStaticText::AggressiveCaching); |
1056 | textInfo->staticText.setTextOption(textOption); |
1057 | m_textInfo.insert(m_sortedVisibleRoles[i], textInfo); |
1058 | } |
1059 | |
1060 | switch (m_layout) { |
1061 | case IconsLayout: updateIconsLayoutTextCache(); break; |
1062 | case CompactLayout: updateCompactLayoutTextCache(); break; |
1063 | case DetailsLayout: updateDetailsLayoutTextCache(); break; |
1064 | default: Q_ASSERT(false); break; |
1065 | } |
1066 | |
1067 | const TextInfo* ratingTextInfo = m_textInfo.value("rating" ); |
1068 | if (ratingTextInfo) { |
1069 | // The text of the rating-role has been set to empty to get |
1070 | // replaced by a rating-image showing the rating as stars. |
1071 | const KItemListStyleOption& option = styleOption(); |
1072 | QSizeF ratingSize = preferredRatingSize(option); |
1073 | |
1074 | const qreal availableWidth = (m_layout == DetailsLayout) |
1075 | ? columnWidth("rating" ) - columnPadding(option) |
1076 | : size().width(); |
1077 | if (ratingSize.width() > availableWidth) { |
1078 | ratingSize.rwidth() = availableWidth; |
1079 | } |
1080 | m_rating = QPixmap(ratingSize.toSize()); |
1081 | m_rating.fill(Qt::transparent); |
1082 | |
1083 | QPainter painter(&m_rating); |
1084 | const QRect rect(0, 0, m_rating.width(), m_rating.height()); |
1085 | const int rating = data().value("rating" ).toInt(); |
1086 | KRatingPainter::paintRating(&painter, rect, Qt::AlignJustify | Qt::AlignVCenter, rating); |
1087 | } else if (!m_rating.isNull()) { |
1088 | m_rating = QPixmap(); |
1089 | } |
1090 | } |
1091 | |
1092 | void KStandardItemListWidget::updateIconsLayoutTextCache() |
1093 | { |
1094 | // +------+ |
1095 | // | Icon | |
1096 | // +------+ |
1097 | // |
1098 | // Name role that |
1099 | // might get wrapped above |
1100 | // several lines. |
1101 | // Additional role 1 |
1102 | // Additional role 2 |
1103 | |
1104 | const QHash<QByteArray, QVariant> values = data(); |
1105 | |
1106 | const KItemListStyleOption& option = styleOption(); |
1107 | const qreal padding = option.padding; |
1108 | const qreal maxWidth = size().width() - 2 * padding; |
1109 | const qreal widgetHeight = size().height(); |
1110 | const qreal lineSpacing = m_customizedFontMetrics.lineSpacing(); |
1111 | |
1112 | // Initialize properties for the "text" role. It will be used as anchor |
1113 | // for initializing the position of the other roles. |
1114 | TextInfo* nameTextInfo = m_textInfo.value("text" ); |
1115 | const QString nameText = KStringHandler::preProcessWrap(values["text" ].toString()); |
1116 | nameTextInfo->staticText.setText(nameText); |
1117 | |
1118 | // Calculate the number of lines required for the name and the required width |
1119 | qreal nameWidth = 0; |
1120 | qreal nameHeight = 0; |
1121 | QTextLine line; |
1122 | |
1123 | QTextLayout layout(nameTextInfo->staticText.text(), m_customizedFont); |
1124 | layout.setTextOption(nameTextInfo->staticText.textOption()); |
1125 | layout.beginLayout(); |
1126 | int nameLineIndex = 0; |
1127 | while ((line = layout.createLine()).isValid()) { |
1128 | line.setLineWidth(maxWidth); |
1129 | nameWidth = qMax(nameWidth, line.naturalTextWidth()); |
1130 | nameHeight += line.height(); |
1131 | |
1132 | ++nameLineIndex; |
1133 | if (nameLineIndex == option.maxTextLines) { |
1134 | // The maximum number of textlines has been reached. If this is |
1135 | // the case provide an elided text if necessary. |
1136 | const int textLength = line.textStart() + line.textLength(); |
1137 | if (textLength < nameText.length()) { |
1138 | // Elide the last line of the text |
1139 | QString lastTextLine = nameText.mid(line.textStart()); |
1140 | lastTextLine = m_customizedFontMetrics.elidedText(lastTextLine, |
1141 | Qt::ElideRight, |
1142 | maxWidth); |
1143 | const QString elidedText = nameText.left(line.textStart()) + lastTextLine; |
1144 | nameTextInfo->staticText.setText(elidedText); |
1145 | |
1146 | const qreal lastLineWidth = m_customizedFontMetrics.boundingRect(lastTextLine).width(); |
1147 | nameWidth = qMax(nameWidth, lastLineWidth); |
1148 | } |
1149 | break; |
1150 | } |
1151 | } |
1152 | layout.endLayout(); |
1153 | |
1154 | // Use one line for each additional information |
1155 | const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0); |
1156 | nameTextInfo->staticText.setTextWidth(maxWidth); |
1157 | nameTextInfo->pos = QPointF(padding, widgetHeight - |
1158 | nameHeight - |
1159 | additionalRolesCount * lineSpacing - |
1160 | padding); |
1161 | m_textRect = QRectF(padding + (maxWidth - nameWidth) / 2, |
1162 | nameTextInfo->pos.y(), |
1163 | nameWidth, |
1164 | nameHeight); |
1165 | |
1166 | // Calculate the position for each additional information |
1167 | qreal y = nameTextInfo->pos.y() + nameHeight; |
1168 | foreach (const QByteArray& role, m_sortedVisibleRoles) { |
1169 | if (role == "text" ) { |
1170 | continue; |
1171 | } |
1172 | |
1173 | const QString text = roleText(role, values); |
1174 | TextInfo* textInfo = m_textInfo.value(role); |
1175 | textInfo->staticText.setText(text); |
1176 | |
1177 | qreal requiredWidth = 0; |
1178 | |
1179 | QTextLayout layout(text, m_customizedFont); |
1180 | QTextOption textOption; |
1181 | textOption.setWrapMode(QTextOption::NoWrap); |
1182 | layout.setTextOption(textOption); |
1183 | |
1184 | layout.beginLayout(); |
1185 | QTextLine textLine = layout.createLine(); |
1186 | if (textLine.isValid()) { |
1187 | textLine.setLineWidth(maxWidth); |
1188 | requiredWidth = textLine.naturalTextWidth(); |
1189 | if (requiredWidth > maxWidth) { |
1190 | const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideRight, maxWidth); |
1191 | textInfo->staticText.setText(elidedText); |
1192 | requiredWidth = m_customizedFontMetrics.width(elidedText); |
1193 | } else if (role == "rating" ) { |
1194 | // Use the width of the rating pixmap, because the rating text is empty. |
1195 | requiredWidth = m_rating.width(); |
1196 | } |
1197 | } |
1198 | layout.endLayout(); |
1199 | |
1200 | textInfo->pos = QPointF(padding, y); |
1201 | textInfo->staticText.setTextWidth(maxWidth); |
1202 | |
1203 | const QRectF textRect(padding + (maxWidth - requiredWidth) / 2, y, requiredWidth, lineSpacing); |
1204 | m_textRect |= textRect; |
1205 | |
1206 | y += lineSpacing; |
1207 | } |
1208 | |
1209 | // Add a padding to the text rectangle |
1210 | m_textRect.adjust(-padding, -padding, padding, padding); |
1211 | } |
1212 | |
1213 | void KStandardItemListWidget::updateCompactLayoutTextCache() |
1214 | { |
1215 | // +------+ Name role |
1216 | // | Icon | Additional role 1 |
1217 | // +------+ Additional role 2 |
1218 | |
1219 | const QHash<QByteArray, QVariant> values = data(); |
1220 | |
1221 | const KItemListStyleOption& option = styleOption(); |
1222 | const qreal widgetHeight = size().height(); |
1223 | const qreal lineSpacing = m_customizedFontMetrics.lineSpacing(); |
1224 | const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * lineSpacing; |
1225 | const int scaledIconSize = (textLinesHeight < option.iconSize) ? widgetHeight - 2 * option.padding : option.iconSize; |
1226 | |
1227 | qreal maximumRequiredTextWidth = 0; |
1228 | const qreal x = option.padding * 3 + scaledIconSize; |
1229 | qreal y = qRound((widgetHeight - textLinesHeight) / 2); |
1230 | const qreal maxWidth = size().width() - x - option.padding; |
1231 | foreach (const QByteArray& role, m_sortedVisibleRoles) { |
1232 | const QString text = roleText(role, values); |
1233 | TextInfo* textInfo = m_textInfo.value(role); |
1234 | textInfo->staticText.setText(text); |
1235 | |
1236 | qreal requiredWidth = m_customizedFontMetrics.width(text); |
1237 | if (requiredWidth > maxWidth) { |
1238 | requiredWidth = maxWidth; |
1239 | const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideRight, maxWidth); |
1240 | textInfo->staticText.setText(elidedText); |
1241 | } |
1242 | |
1243 | textInfo->pos = QPointF(x, y); |
1244 | textInfo->staticText.setTextWidth(maxWidth); |
1245 | |
1246 | maximumRequiredTextWidth = qMax(maximumRequiredTextWidth, requiredWidth); |
1247 | |
1248 | y += lineSpacing; |
1249 | } |
1250 | |
1251 | m_textRect = QRectF(x - 2 * option.padding, 0, maximumRequiredTextWidth + 3 * option.padding, widgetHeight); |
1252 | } |
1253 | |
1254 | void KStandardItemListWidget::updateDetailsLayoutTextCache() |
1255 | { |
1256 | // Precondition: Requires already updated m_expansionArea |
1257 | // to determine the left position. |
1258 | |
1259 | // +------+ |
1260 | // | Icon | Name role Additional role 1 Additional role 2 |
1261 | // +------+ |
1262 | m_textRect = QRectF(); |
1263 | |
1264 | const KItemListStyleOption& option = styleOption(); |
1265 | const QHash<QByteArray, QVariant> values = data(); |
1266 | |
1267 | const qreal widgetHeight = size().height(); |
1268 | const int scaledIconSize = widgetHeight - 2 * option.padding; |
1269 | const int fontHeight = m_customizedFontMetrics.height(); |
1270 | |
1271 | const qreal columnWidthInc = columnPadding(option); |
1272 | qreal firstColumnInc = scaledIconSize; |
1273 | if (m_supportsItemExpanding) { |
1274 | firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2; |
1275 | } else { |
1276 | firstColumnInc += option.padding; |
1277 | } |
1278 | |
1279 | qreal x = firstColumnInc; |
1280 | const qreal y = qMax(qreal(option.padding), (widgetHeight - fontHeight) / 2); |
1281 | |
1282 | foreach (const QByteArray& role, m_sortedVisibleRoles) { |
1283 | QString text = roleText(role, values); |
1284 | |
1285 | // Elide the text in case it does not fit into the available column-width |
1286 | qreal requiredWidth = m_customizedFontMetrics.width(text); |
1287 | const qreal roleWidth = columnWidth(role); |
1288 | qreal availableTextWidth = roleWidth - columnWidthInc; |
1289 | |
1290 | const bool isTextRole = (role == "text" ); |
1291 | if (isTextRole) { |
1292 | availableTextWidth -= firstColumnInc; |
1293 | } |
1294 | |
1295 | if (requiredWidth > availableTextWidth) { |
1296 | text = m_customizedFontMetrics.elidedText(text, Qt::ElideRight, availableTextWidth); |
1297 | requiredWidth = m_customizedFontMetrics.width(text); |
1298 | } |
1299 | |
1300 | TextInfo* textInfo = m_textInfo.value(role); |
1301 | textInfo->staticText.setText(text); |
1302 | textInfo->pos = QPointF(x + columnWidthInc / 2, y); |
1303 | x += roleWidth; |
1304 | |
1305 | if (isTextRole) { |
1306 | const qreal textWidth = option.extendedSelectionRegion |
1307 | ? size().width() - textInfo->pos.x() |
1308 | : requiredWidth + 2 * option.padding; |
1309 | m_textRect = QRectF(textInfo->pos.x() - 2 * option.padding, 0, |
1310 | textWidth + option.padding, size().height()); |
1311 | |
1312 | // The column after the name should always be aligned on the same x-position independent |
1313 | // from the expansion-level shown in the name column |
1314 | x -= firstColumnInc; |
1315 | } else if (isRoleRightAligned(role)) { |
1316 | textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc; |
1317 | } |
1318 | } |
1319 | } |
1320 | |
1321 | void KStandardItemListWidget::updateAdditionalInfoTextColor() |
1322 | { |
1323 | QColor c1; |
1324 | if (m_customTextColor.isValid()) { |
1325 | c1 = m_customTextColor; |
1326 | } else if (isSelected() && m_layout != DetailsLayout) { |
1327 | c1 = styleOption().palette.highlightedText().color(); |
1328 | } else { |
1329 | c1 = styleOption().palette.text().color(); |
1330 | } |
1331 | |
1332 | // For the color of the additional info the inactive text color |
1333 | // is not used as this might lead to unreadable text for some color schemes. Instead |
1334 | // the text color c1 is slightly mixed with the background color. |
1335 | const QColor c2 = styleOption().palette.base().color(); |
1336 | const int p1 = 70; |
1337 | const int p2 = 100 - p1; |
1338 | m_additionalInfoTextColor = QColor((c1.red() * p1 + c2.red() * p2) / 100, |
1339 | (c1.green() * p1 + c2.green() * p2) / 100, |
1340 | (c1.blue() * p1 + c2.blue() * p2) / 100); |
1341 | } |
1342 | |
1343 | void KStandardItemListWidget::drawPixmap(QPainter* painter, const QPixmap& pixmap) |
1344 | { |
1345 | if (m_scaledPixmapSize != pixmap.size()) { |
1346 | QPixmap scaledPixmap = pixmap; |
1347 | KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize); |
1348 | painter->drawPixmap(m_pixmapPos, scaledPixmap); |
1349 | |
1350 | #ifdef KSTANDARDITEMLISTWIDGET_DEBUG |
1351 | painter->setPen(Qt::blue); |
1352 | painter->drawRect(QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize))); |
1353 | #endif |
1354 | } else { |
1355 | painter->drawPixmap(m_pixmapPos, pixmap); |
1356 | } |
1357 | } |
1358 | |
1359 | void KStandardItemListWidget::drawSiblingsInformation(QPainter* painter) |
1360 | { |
1361 | const int siblingSize = size().height(); |
1362 | const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2; |
1363 | QRect siblingRect(x, 0, siblingSize, siblingSize); |
1364 | |
1365 | QStyleOption option; |
1366 | option.palette.setColor(QPalette::Text, option.palette.color(normalTextColorRole())); |
1367 | bool isItemSibling = true; |
1368 | |
1369 | const QBitArray siblings = siblingsInformation(); |
1370 | for (int i = siblings.count() - 1; i >= 0; --i) { |
1371 | option.rect = siblingRect; |
1372 | option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None; |
1373 | |
1374 | if (isItemSibling) { |
1375 | option.state |= QStyle::State_Item; |
1376 | if (m_isExpandable) { |
1377 | option.state |= QStyle::State_Children; |
1378 | } |
1379 | if (data()["isExpanded" ].toBool()) { |
1380 | option.state |= QStyle::State_Open; |
1381 | } |
1382 | isItemSibling = false; |
1383 | } |
1384 | |
1385 | style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter); |
1386 | |
1387 | siblingRect.translate(-siblingRect.width(), 0); |
1388 | } |
1389 | } |
1390 | |
1391 | QRectF KStandardItemListWidget::roleEditingRect(const QByteArray& role) const |
1392 | { |
1393 | const TextInfo* textInfo = m_textInfo.value(role); |
1394 | if (!textInfo) { |
1395 | return QRectF(); |
1396 | } |
1397 | |
1398 | QRectF rect(textInfo->pos, textInfo->staticText.size()); |
1399 | if (m_layout == DetailsLayout) { |
1400 | rect.setWidth(columnWidth(role) - rect.x()); |
1401 | } |
1402 | |
1403 | return rect; |
1404 | } |
1405 | |
1406 | void KStandardItemListWidget::closeRoleEditor() |
1407 | { |
1408 | disconnect(m_roleEditor, SIGNAL(roleEditingCanceled(QByteArray,QVariant)), |
1409 | this, SLOT(slotRoleEditingCanceled(QByteArray,QVariant))); |
1410 | disconnect(m_roleEditor, SIGNAL(roleEditingFinished(QByteArray,QVariant)), |
1411 | this, SLOT(slotRoleEditingFinished(QByteArray,QVariant))); |
1412 | |
1413 | if (m_roleEditor->hasFocus()) { |
1414 | // If the editing was not ended by a FocusOut event, we have |
1415 | // to transfer the keyboard focus back to the KItemListContainer. |
1416 | scene()->views()[0]->parentWidget()->setFocus(); |
1417 | } |
1418 | |
1419 | if (m_oldRoleEditor) { |
1420 | m_oldRoleEditor->deleteLater(); |
1421 | } |
1422 | m_oldRoleEditor = m_roleEditor; |
1423 | m_roleEditor->hide(); |
1424 | m_roleEditor = 0; |
1425 | } |
1426 | |
1427 | QPixmap KStandardItemListWidget::pixmapForIcon(const QString& name, const QStringList& overlays, int size) |
1428 | { |
1429 | const QString key = "KStandardItemListWidget:" % name % ":" % overlays.join(":" ) % ":" % QString::number(size); |
1430 | QPixmap pixmap; |
1431 | |
1432 | if (!QPixmapCache::find(key, pixmap)) { |
1433 | const KIcon icon(name); |
1434 | |
1435 | int requestedSize; |
1436 | if (size <= KIconLoader::SizeSmall) { |
1437 | requestedSize = KIconLoader::SizeSmall; |
1438 | } else if (size <= KIconLoader::SizeSmallMedium) { |
1439 | requestedSize = KIconLoader::SizeSmallMedium; |
1440 | } else if (size <= KIconLoader::SizeMedium) { |
1441 | requestedSize = KIconLoader::SizeMedium; |
1442 | } else if (size <= KIconLoader::SizeLarge) { |
1443 | requestedSize = KIconLoader::SizeLarge; |
1444 | } else if (size <= KIconLoader::SizeHuge) { |
1445 | requestedSize = KIconLoader::SizeHuge; |
1446 | } else if (size <= KIconLoader::SizeEnormous) { |
1447 | requestedSize = KIconLoader::SizeEnormous; |
1448 | } else if (size <= KIconLoader::SizeEnormous * 2) { |
1449 | requestedSize = KIconLoader::SizeEnormous * 2; |
1450 | } else { |
1451 | requestedSize = size; |
1452 | } |
1453 | |
1454 | pixmap = icon.pixmap(requestedSize, requestedSize); |
1455 | if (requestedSize != size) { |
1456 | KPixmapModifier::scale(pixmap, QSize(size, size)); |
1457 | } |
1458 | |
1459 | // Strangely KFileItem::overlays() returns empty string-values, so |
1460 | // we need to check first whether an overlay must be drawn at all. |
1461 | // It is more efficient to do it here, as KIconLoader::drawOverlays() |
1462 | // assumes that an overlay will be drawn and has some additional |
1463 | // setup time. |
1464 | foreach (const QString& overlay, overlays) { |
1465 | if (!overlay.isEmpty()) { |
1466 | // There is at least one overlay, draw all overlays above m_pixmap |
1467 | // and cancel the check |
1468 | KIconLoader::global()->drawOverlays(overlays, pixmap, KIconLoader::Desktop); |
1469 | break; |
1470 | } |
1471 | } |
1472 | |
1473 | QPixmapCache::insert(key, pixmap); |
1474 | } |
1475 | |
1476 | return pixmap; |
1477 | } |
1478 | |
1479 | QSizeF KStandardItemListWidget::preferredRatingSize(const KItemListStyleOption& option) |
1480 | { |
1481 | const qreal height = option.fontMetrics.ascent(); |
1482 | return QSizeF(height * 5, height); |
1483 | } |
1484 | |
1485 | qreal KStandardItemListWidget::columnPadding(const KItemListStyleOption& option) |
1486 | { |
1487 | return option.padding * 6; |
1488 | } |
1489 | |
1490 | #include "kstandarditemlistwidget.moc" |
1491 | |