1/*****************************************************************************
2 * Copyright (C) 2006 by Peter Penz <peter.penz@gmx.at> *
3 * Copyright (C) 2006 by Aaron J. Seigo <aseigo@kde.org> *
4 * *
5 * This library is free software; you can redistribute it and/or *
6 * modify it under the terms of the GNU Library General Public *
7 * License as published by the Free Software Foundation; either *
8 * version 2 of the License, or (at your option) any later version. *
9 * *
10 * This library 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 GNU *
13 * Library General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU Library General Public License *
16 * along with this library; see the file COPYING.LIB. If not, write to *
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, *
18 * Boston, MA 02110-1301, USA. *
19 *****************************************************************************/
20
21#include "kurlnavigatorbutton_p.h"
22
23#include "kurlnavigator.h"
24#include "kurlnavigatormenu_p.h"
25#include "kdirsortfilterproxymodel.h"
26
27#include <kio/job.h>
28#include <kio/jobclasses.h>
29#include <kglobalsettings.h>
30#include <klocale.h>
31#include <kstringhandler.h>
32
33#include <QtCore/QTimer>
34#include <QtGui/QPainter>
35#include <QtGui/QKeyEvent>
36#include <QtGui/QStyleOption>
37
38namespace KDEPrivate
39{
40
41QPointer<KUrlNavigatorMenu> KUrlNavigatorButton::m_subDirsMenu;
42
43KUrlNavigatorButton::KUrlNavigatorButton(const KUrl& url, QWidget* parent) :
44 KUrlNavigatorButtonBase(parent),
45 m_hoverArrow(false),
46 m_pendingTextChange(false),
47 m_replaceButton(false),
48 m_showMnemonic(false),
49 m_wheelSteps(0),
50 m_url(url),
51 m_subDir(),
52 m_openSubDirsTimer(0),
53 m_subDirsJob(0)
54{
55 setAcceptDrops(true);
56 setUrl(url);
57 setMouseTracking(true);
58
59 m_openSubDirsTimer = new QTimer(this);
60 m_openSubDirsTimer->setSingleShot(true);
61 m_openSubDirsTimer->setInterval(300);
62 connect(m_openSubDirsTimer, SIGNAL(timeout()), this, SLOT(startSubDirsJob()));
63
64 connect(this, SIGNAL(pressed()), this, SLOT(requestSubDirs()));
65}
66
67KUrlNavigatorButton::~KUrlNavigatorButton()
68{
69}
70
71void KUrlNavigatorButton::setUrl(const KUrl& url)
72{
73 m_url = url;
74
75 bool startTextResolving = !m_url.isLocalFile();
76 if (startTextResolving) {
77 // Doing a text-resolving with KIO::stat() for all non-local
78 // URLs leads to problems for protocols where a limit is given for
79 // the number of parallel connections. A black-list
80 // is given where KIO::stat() should not be used:
81 static QSet<QString> protocols;
82 if (protocols.isEmpty()) {
83 protocols << "fish" << "ftp" << "nfs" << "sftp" << "smb" << "webdav";
84 }
85 startTextResolving = !protocols.contains(m_url.protocol());
86 }
87
88 if (startTextResolving) {
89 m_pendingTextChange = true;
90 KIO::StatJob* job = KIO::stat(m_url, KIO::HideProgressInfo);
91 connect(job, SIGNAL(result(KJob*)),
92 this, SLOT(statFinished(KJob*)));
93 emit startedTextResolving();
94 } else {
95 setText(m_url.fileName().replace('&', "&&"));
96 }
97}
98
99KUrl KUrlNavigatorButton::url() const
100{
101 return m_url;
102}
103
104void KUrlNavigatorButton::setText(const QString& text)
105{
106 QString adjustedText = text;
107 if (adjustedText.isEmpty()) {
108 adjustedText = m_url.protocol();
109 }
110 // Assure that the button always consists of one line
111 adjustedText.remove(QLatin1Char('\n'));
112
113 KUrlNavigatorButtonBase::setText(adjustedText);
114 updateMinimumWidth();
115
116 // Assure that statFinished() does not overwrite a text that has been
117 // set by a client of the URL navigator button
118 m_pendingTextChange = false;
119}
120
121void KUrlNavigatorButton::setActiveSubDirectory(const QString& subDir)
122{
123 m_subDir = subDir;
124
125 // We use a different (bold) font on active, so the size hint changes
126 updateGeometry();
127 update();
128}
129
130QString KUrlNavigatorButton::activeSubDirectory() const
131{
132 return m_subDir;
133}
134
135QSize KUrlNavigatorButton::sizeHint() const
136{
137 QFont adjustedFont(font());
138 adjustedFont.setBold(m_subDir.isEmpty());
139 // the minimum size is textWidth + arrowWidth() + 2 * BorderWidth; for the
140 // preferred size we add the BorderWidth 2 times again for having an uncluttered look
141 const int width = QFontMetrics(adjustedFont).width(plainText()) + arrowWidth() + 4 * BorderWidth;
142 return QSize(width, KUrlNavigatorButtonBase::sizeHint().height());
143}
144
145void KUrlNavigatorButton::setShowMnemonic(bool show)
146{
147 if (m_showMnemonic != show) {
148 m_showMnemonic = show;
149 update();
150 }
151}
152
153bool KUrlNavigatorButton::showMnemonic() const
154{
155 return m_showMnemonic;
156}
157
158void KUrlNavigatorButton::paintEvent(QPaintEvent* event)
159{
160 Q_UNUSED(event);
161
162 QPainter painter(this);
163
164 QFont adjustedFont(font());
165 adjustedFont.setBold(m_subDir.isEmpty());
166 painter.setFont(adjustedFont);
167
168 int buttonWidth = width();
169 int preferredWidth = sizeHint().width();
170 if (preferredWidth < minimumWidth()) {
171 preferredWidth = minimumWidth();
172 }
173 if (buttonWidth > preferredWidth) {
174 buttonWidth = preferredWidth;
175 }
176 const int buttonHeight = height();
177
178 const QColor fgColor = foregroundColor();
179 drawHoverBackground(&painter);
180
181 int textLeft = 0;
182 int textWidth = buttonWidth;
183
184 const bool leftToRight = (layoutDirection() == Qt::LeftToRight);
185
186 if (!m_subDir.isEmpty()) {
187 // draw arrow
188 const int arrowSize = arrowWidth();
189 const int arrowX = leftToRight ? (buttonWidth - arrowSize) - BorderWidth : BorderWidth;
190 const int arrowY = (buttonHeight - arrowSize) / 2;
191
192 QStyleOption option;
193 option.initFrom(this);
194 option.rect = QRect(arrowX, arrowY, arrowSize, arrowSize);
195 option.palette = palette();
196 option.palette.setColor(QPalette::Text, fgColor);
197 option.palette.setColor(QPalette::WindowText, fgColor);
198 option.palette.setColor(QPalette::ButtonText, fgColor);
199
200 if (m_hoverArrow) {
201 // highlight the background of the arrow to indicate that the directories
202 // popup can be opened by a mouse click
203 QColor hoverColor = palette().color(QPalette::HighlightedText);
204 hoverColor.setAlpha(96);
205 painter.setPen(Qt::NoPen);
206 painter.setBrush(hoverColor);
207
208 int hoverX = arrowX;
209 if (!leftToRight) {
210 hoverX -= BorderWidth;
211 }
212 painter.drawRect(QRect(hoverX, 0, arrowSize + BorderWidth, buttonHeight));
213 }
214
215 if (leftToRight) {
216 style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &option, &painter, this);
217 } else {
218 style()->drawPrimitive(QStyle::PE_IndicatorArrowLeft, &option, &painter, this);
219 textLeft += arrowSize + 2 * BorderWidth;
220 }
221
222 textWidth -= arrowSize + 2 * BorderWidth;
223 }
224
225 painter.setPen(fgColor);
226 const bool clipped = isTextClipped();
227 const QRect textRect(textLeft, 0, textWidth, buttonHeight);
228 if (clipped) {
229 QColor bgColor = fgColor;
230 bgColor.setAlpha(0);
231 QLinearGradient gradient(textRect.topLeft(), textRect.topRight());
232 if (leftToRight) {
233 gradient.setColorAt(0.8, fgColor);
234 gradient.setColorAt(1.0, bgColor);
235 } else {
236 gradient.setColorAt(0.0, bgColor);
237 gradient.setColorAt(0.2, fgColor);
238 }
239
240 QPen pen;
241 pen.setBrush(QBrush(gradient));
242 painter.setPen(pen);
243 }
244
245 int textFlags = clipped ? Qt::AlignVCenter : Qt::AlignCenter;
246 if (m_showMnemonic) {
247 textFlags |= Qt::TextShowMnemonic;
248 painter.drawText(textRect, textFlags, text());
249 } else {
250 painter.drawText(textRect, textFlags, plainText());
251 }
252}
253
254void KUrlNavigatorButton::enterEvent(QEvent* event)
255{
256 KUrlNavigatorButtonBase::enterEvent(event);
257
258 // if the text is clipped due to a small window width, the text should
259 // be shown as tooltip
260 if (isTextClipped()) {
261 setToolTip(plainText());
262 }
263}
264
265void KUrlNavigatorButton::leaveEvent(QEvent* event)
266{
267 KUrlNavigatorButtonBase::leaveEvent(event);
268 setToolTip(QString());
269
270 if (m_hoverArrow) {
271 m_hoverArrow = false;
272 update();
273 }
274}
275
276void KUrlNavigatorButton::keyPressEvent(QKeyEvent* event)
277{
278 switch (event->key()) {
279 case Qt::Key_Enter:
280 case Qt::Key_Return:
281 emit clicked(m_url, Qt::LeftButton);
282 break;
283 case Qt::Key_Down:
284 case Qt::Key_Space:
285 startSubDirsJob();
286 break;
287 default:
288 KUrlNavigatorButtonBase::keyPressEvent(event);
289 }
290}
291
292void KUrlNavigatorButton::dropEvent(QDropEvent* event)
293{
294 const KUrl::List urls = KUrl::List::fromMimeData(event->mimeData());
295 if (!urls.isEmpty()) {
296 setDisplayHintEnabled(DraggedHint, true);
297
298 emit urlsDropped(m_url, event);
299
300 setDisplayHintEnabled(DraggedHint, false);
301 update();
302 }
303}
304
305void KUrlNavigatorButton::dragEnterEvent(QDragEnterEvent* event)
306{
307 if (event->mimeData()->hasUrls()) {
308 setDisplayHintEnabled(DraggedHint, true);
309 event->acceptProposedAction();
310
311 update();
312 }
313}
314
315void KUrlNavigatorButton::dragMoveEvent(QDragMoveEvent* event)
316{
317 QRect rect = event->answerRect();
318 if (isAboveArrow(rect.center().x())) {
319 m_hoverArrow = true;
320 update();
321
322 if (m_subDirsMenu == 0) {
323 requestSubDirs();
324 } else if (m_subDirsMenu->parent() != this) {
325 m_subDirsMenu->close();
326 m_subDirsMenu->deleteLater();
327 m_subDirsMenu = 0;
328
329 requestSubDirs();
330 }
331 } else {
332 if (m_openSubDirsTimer->isActive()) {
333 cancelSubDirsRequest();
334 }
335 delete m_subDirsMenu;
336 m_subDirsMenu = 0;
337 m_hoverArrow = false;
338 update();
339 }
340}
341
342void KUrlNavigatorButton::dragLeaveEvent(QDragLeaveEvent* event)
343{
344 KUrlNavigatorButtonBase::dragLeaveEvent(event);
345
346 m_hoverArrow = false;
347 setDisplayHintEnabled(DraggedHint, false);
348 update();
349}
350
351void KUrlNavigatorButton::mousePressEvent(QMouseEvent* event)
352{
353 if (isAboveArrow(event->x()) && (event->button() == Qt::LeftButton)) {
354 // the mouse is pressed above the [>] button
355 startSubDirsJob();
356 }
357 KUrlNavigatorButtonBase::mousePressEvent(event);
358}
359
360void KUrlNavigatorButton::mouseReleaseEvent(QMouseEvent* event)
361{
362 if (!isAboveArrow(event->x()) || (event->button() != Qt::LeftButton)) {
363 // the mouse has been released above the text area and not
364 // above the [>] button
365 emit clicked(m_url, event->button());
366 cancelSubDirsRequest();
367 }
368 KUrlNavigatorButtonBase::mouseReleaseEvent(event);
369}
370
371void KUrlNavigatorButton::mouseMoveEvent(QMouseEvent* event)
372{
373 KUrlNavigatorButtonBase::mouseMoveEvent(event);
374
375 const bool hoverArrow = isAboveArrow(event->x());
376 if (hoverArrow != m_hoverArrow) {
377 m_hoverArrow = hoverArrow;
378 update();
379 }
380}
381
382void KUrlNavigatorButton::wheelEvent(QWheelEvent* event)
383{
384 if (event->orientation() == Qt::Vertical) {
385 m_wheelSteps = event->delta() / 120;
386 m_replaceButton = true;
387 startSubDirsJob();
388 }
389
390 KUrlNavigatorButtonBase::wheelEvent(event);
391}
392
393void KUrlNavigatorButton::requestSubDirs()
394{
395 if (!m_openSubDirsTimer->isActive() && (m_subDirsJob == 0)) {
396 m_openSubDirsTimer->start();
397 }
398}
399
400void KUrlNavigatorButton::startSubDirsJob()
401{
402 if (m_subDirsJob != 0) {
403 return;
404 }
405
406 const KUrl url = m_replaceButton ? m_url.upUrl() : m_url;
407 m_subDirsJob = KIO::listDir(url, KIO::HideProgressInfo, false /*no hidden files*/);
408 m_subDirs.clear(); // just to be ++safe
409
410 connect(m_subDirsJob, SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)),
411 this, SLOT(addEntriesToSubDirs(KIO::Job*,KIO::UDSEntryList)));
412
413 if (m_replaceButton) {
414 connect(m_subDirsJob, SIGNAL(result(KJob*)), this, SLOT(replaceButton(KJob*)));
415 } else {
416 connect(m_subDirsJob, SIGNAL(result(KJob*)), this, SLOT(openSubDirsMenu(KJob*)));
417 }
418}
419
420void KUrlNavigatorButton::addEntriesToSubDirs(KIO::Job* job, const KIO::UDSEntryList& entries)
421{
422 Q_ASSERT(job == m_subDirsJob);
423 Q_UNUSED(job);
424
425 foreach (const KIO::UDSEntry& entry, entries) {
426 if (entry.isDir()) {
427 const QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
428 QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
429 if (displayName.isEmpty()) {
430 displayName = name;
431 }
432 if ((name != QLatin1String(".")) && (name != QLatin1String(".."))) {
433 m_subDirs.append(qMakePair(name, displayName));
434 }
435 }
436 }
437}
438
439void KUrlNavigatorButton::urlsDropped(QAction* action, QDropEvent* event)
440{
441 const int result = action->data().toInt();
442 KUrl url = m_url;
443 url.addPath(m_subDirs.at(result).first);
444 urlsDropped(url, event);
445}
446
447void KUrlNavigatorButton::slotMenuActionClicked(QAction* action)
448{
449 const int result = action->data().toInt();
450 KUrl url = m_url;
451 url.addPath(m_subDirs.at(result).first);
452 emit clicked(url, Qt::MidButton);
453}
454
455void KUrlNavigatorButton::statFinished(KJob* job)
456{
457 if (m_pendingTextChange) {
458 m_pendingTextChange = false;
459
460 const KIO::UDSEntry entry = static_cast<KIO::StatJob*>(job)->statResult();
461 QString name = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
462 if (name.isEmpty()) {
463 name = m_url.fileName();
464 }
465 setText(name);
466
467 emit finishedTextResolving();
468 }
469}
470
471/**
472 * Helper function for openSubDirsMenu
473 */
474static bool naturalLessThan(const QPair<QString, QString>& s1, const QPair<QString, QString>& s2)
475{
476 return KStringHandler::naturalCompare(s1.first, s2.first, Qt::CaseInsensitive) < 0;
477}
478
479void KUrlNavigatorButton::openSubDirsMenu(KJob* job)
480{
481 Q_ASSERT(job == m_subDirsJob);
482 m_subDirsJob = 0;
483
484 if (job->error() || m_subDirs.isEmpty()) {
485 // clear listing
486 return;
487 }
488
489 qSort(m_subDirs.begin(), m_subDirs.end(), naturalLessThan);
490 setDisplayHintEnabled(PopupActiveHint, true);
491 update(); // ensure the button is drawn highlighted
492
493 if (m_subDirsMenu != 0) {
494 m_subDirsMenu->close();
495 m_subDirsMenu->deleteLater();
496 m_subDirsMenu = 0;
497 }
498
499 m_subDirsMenu = new KUrlNavigatorMenu(this);
500 initMenu(m_subDirsMenu, 0);
501
502 const bool leftToRight = (layoutDirection() == Qt::LeftToRight);
503 const int popupX = leftToRight ? width() - arrowWidth() - BorderWidth : 0;
504 const QPoint popupPos = parentWidget()->mapToGlobal(geometry().bottomLeft() + QPoint(popupX, 0));
505
506 QPointer<QObject> guard(this);
507
508 const QAction* action = m_subDirsMenu->exec(popupPos);
509
510 // If 'this' has been deleted in the menu's nested event loop, we have to return
511 // immediatedely because any access to a member variable might cause a crash.
512 if (!guard) {
513 return;
514 }
515
516 if (action != 0) {
517 const int result = action->data().toInt();
518 KUrl url = m_url;
519 url.addPath(m_subDirs[result].first);
520 emit clicked(url, Qt::LeftButton);
521 }
522
523 m_subDirs.clear();
524 delete m_subDirsMenu;
525 m_subDirsMenu = 0;
526
527 setDisplayHintEnabled(PopupActiveHint, false);
528}
529
530void KUrlNavigatorButton::replaceButton(KJob* job)
531{
532 Q_ASSERT(job == m_subDirsJob);
533 m_subDirsJob = 0;
534 m_replaceButton = false;
535
536 if (job->error() || m_subDirs.isEmpty()) {
537 return;
538 }
539
540 qSort(m_subDirs.begin(), m_subDirs.end(), naturalLessThan);
541
542 // Get index of the directory that is shown currently in the button
543 const QString currentDir = m_url.fileName();
544 int currentIndex = 0;
545 const int subDirsCount = m_subDirs.count();
546 while (currentIndex < subDirsCount) {
547 if (m_subDirs[currentIndex].first == currentDir) {
548 break;
549 }
550 ++currentIndex;
551 }
552
553 // Adjust the index by respecting the wheel steps and
554 // trigger a replacing of the button content
555 int targetIndex = currentIndex - m_wheelSteps;
556 if (targetIndex < 0) {
557 targetIndex = 0;
558 } else if (targetIndex >= subDirsCount) {
559 targetIndex = subDirsCount - 1;
560 }
561
562 KUrl url = m_url.upUrl();
563 url.addPath(m_subDirs[targetIndex].first);
564
565 emit clicked(url, Qt::LeftButton);
566
567 m_subDirs.clear();
568}
569
570void KUrlNavigatorButton::cancelSubDirsRequest()
571{
572 m_openSubDirsTimer->stop();
573 if (m_subDirsJob != 0) {
574 m_subDirsJob->kill();
575 m_subDirsJob = 0;
576 }
577}
578
579QString KUrlNavigatorButton::plainText() const
580{
581 // Replace all "&&" by '&' and remove all single
582 // '&' characters
583 const QString source = text();
584 const int sourceLength = source.length();
585
586 QString dest;
587 dest.reserve(sourceLength);
588
589 int sourceIndex = 0;
590 int destIndex = 0;
591 while (sourceIndex < sourceLength) {
592 if (source.at(sourceIndex) == QLatin1Char('&')) {
593 ++sourceIndex;
594 if (sourceIndex >= sourceLength) {
595 break;
596 }
597 }
598 dest[destIndex] = source.at(sourceIndex);
599 ++sourceIndex;
600 ++destIndex;
601 }
602
603 return dest;
604}
605
606int KUrlNavigatorButton::arrowWidth() const
607{
608 // if there isn't arrow then return 0
609 int width = 0;
610 if (!m_subDir.isEmpty()) {
611 width = height() / 2;
612 if (width < 4) {
613 width = 4;
614 }
615 }
616
617 return width;
618}
619
620bool KUrlNavigatorButton::isAboveArrow(int x) const
621{
622 const bool leftToRight = (layoutDirection() == Qt::LeftToRight);
623 return leftToRight ? (x >= width() - arrowWidth()) : (x < arrowWidth());
624}
625
626bool KUrlNavigatorButton::isTextClipped() const
627{
628 int availableWidth = width() - 2 * BorderWidth;
629 if (!m_subDir.isEmpty()) {
630 availableWidth -= arrowWidth() - BorderWidth;
631 }
632
633 QFont adjustedFont(font());
634 adjustedFont.setBold(m_subDir.isEmpty());
635 return QFontMetrics(adjustedFont).width(plainText()) >= availableWidth;
636}
637
638void KUrlNavigatorButton::updateMinimumWidth()
639{
640 const int oldMinWidth = minimumWidth();
641
642 int minWidth = sizeHint().width();
643 if (minWidth < 40) {
644 minWidth = 40;
645 }
646 else if (minWidth > 150) {
647 // don't let an overlong path name waste all the URL navigator space
648 minWidth = 150;
649 }
650 if (oldMinWidth != minWidth) {
651 setMinimumWidth(minWidth);
652 }
653}
654
655void KUrlNavigatorButton::initMenu(KUrlNavigatorMenu* menu, int startIndex)
656{
657 connect(menu, SIGNAL(middleMouseButtonClicked(QAction*)),
658 this, SLOT(slotMenuActionClicked(QAction*)));
659 connect(menu, SIGNAL(urlsDropped(QAction*,QDropEvent*)),
660 this, SLOT(urlsDropped(QAction*,QDropEvent*)));
661
662 menu->setLayoutDirection(Qt::LeftToRight);
663
664 const int maxIndex = startIndex + 30; // Don't show more than 30 items in a menu
665 const int lastIndex = qMin(m_subDirs.count() - 1, maxIndex);
666 for (int i = startIndex; i <= lastIndex; ++i) {
667 const QString subDirName = m_subDirs[i].first;
668 const QString subDirDisplayName = m_subDirs[i].second;
669 QString text = KStringHandler::csqueeze(subDirDisplayName, 60);
670 text.replace(QLatin1Char('&'), QLatin1String("&&"));
671 QAction* action = new QAction(text, this);
672 if (m_subDir == subDirName) {
673 QFont font(action->font());
674 font.setBold(true);
675 action->setFont(font);
676 }
677 action->setData(i);
678 menu->addAction(action);
679 }
680 if (m_subDirs.count() > maxIndex) {
681 // If too much items are shown, move them into a sub menu
682 menu->addSeparator();
683 KUrlNavigatorMenu* subDirsMenu = new KUrlNavigatorMenu(menu);
684 subDirsMenu->setTitle(i18nc("@action:inmenu", "More"));
685 initMenu(subDirsMenu, maxIndex);
686 menu->addMenu(subDirsMenu);
687 }
688}
689
690} // namespace KDEPrivate
691
692#include "kurlnavigatorbutton_p.moc"
693