| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2006 Peter Penz <peter.penz@gmx.at> |
| 3 | SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 6 | */ |
| 7 | |
| 8 | #include "kurlnavigatorbutton_p.h" |
| 9 | |
| 10 | #include "../utils_p.h" |
| 11 | #include "kurlnavigator.h" |
| 12 | #include "kurlnavigatormenu_p.h" |
| 13 | #include <kio/listjob.h> |
| 14 | #include <kio/statjob.h> |
| 15 | |
| 16 | #include <KLocalizedString> |
| 17 | #include <KStringHandler> |
| 18 | |
| 19 | #include <QCollator> |
| 20 | #include <QKeyEvent> |
| 21 | #include <QMimeData> |
| 22 | #include <QPainter> |
| 23 | #include <QStyleOption> |
| 24 | #include <QTimer> |
| 25 | |
| 26 | namespace KDEPrivate |
| 27 | { |
| 28 | QPointer<KUrlNavigatorMenu> KUrlNavigatorButton::; |
| 29 | |
| 30 | KUrlNavigatorButton::KUrlNavigatorButton(const QUrl &url, KUrlNavigator *parent) |
| 31 | : KUrlNavigatorButtonBase(parent) |
| 32 | , m_hoverOverArrow(false) |
| 33 | , m_hoverOverButton(false) |
| 34 | , m_pendingTextChange(false) |
| 35 | , m_replaceButton(false) |
| 36 | , m_showMnemonic(false) |
| 37 | , m_drawSeparator(true) |
| 38 | , m_wheelSteps(0) |
| 39 | , m_url(url) |
| 40 | , m_subDir() |
| 41 | , m_openSubDirsTimer(nullptr) |
| 42 | , m_subDirsJob(nullptr) |
| 43 | , m_padding(5) |
| 44 | { |
| 45 | setAcceptDrops(true); |
| 46 | setUrl(url); |
| 47 | setMouseTracking(true); |
| 48 | |
| 49 | m_openSubDirsTimer = new QTimer(this); |
| 50 | m_openSubDirsTimer->setSingleShot(true); |
| 51 | m_openSubDirsTimer->setInterval(300); |
| 52 | connect(sender: m_openSubDirsTimer, signal: &QTimer::timeout, context: this, slot: &KUrlNavigatorButton::startSubDirsJob); |
| 53 | |
| 54 | connect(sender: this, signal: &QAbstractButton::pressed, context: this, slot: &KUrlNavigatorButton::requestSubDirs); |
| 55 | } |
| 56 | |
| 57 | KUrlNavigatorButton::~KUrlNavigatorButton() |
| 58 | { |
| 59 | } |
| 60 | |
| 61 | void KUrlNavigatorButton::setUrl(const QUrl &url) |
| 62 | { |
| 63 | m_url = url; |
| 64 | |
| 65 | // Doing a text-resolving with KIO::stat() for all non-local |
| 66 | // URLs leads to problems for protocols where a limit is given for |
| 67 | // the number of parallel connections. A black-list |
| 68 | // is given where KIO::stat() should not be used: |
| 69 | static const QSet<QString> protocolBlacklist = QSet<QString>{ |
| 70 | QStringLiteral("nfs" ), |
| 71 | QStringLiteral("fish" ), |
| 72 | QStringLiteral("ftp" ), |
| 73 | QStringLiteral("sftp" ), |
| 74 | QStringLiteral("smb" ), |
| 75 | QStringLiteral("webdav" ), |
| 76 | QStringLiteral("mtp" ), |
| 77 | }; |
| 78 | |
| 79 | const bool startTextResolving = m_url.isValid() && !m_url.isLocalFile() && !protocolBlacklist.contains(value: m_url.scheme()); |
| 80 | |
| 81 | if (startTextResolving) { |
| 82 | m_pendingTextChange = true; |
| 83 | KIO::StatJob *job = KIO::stat(url: m_url, flags: KIO::HideProgressInfo); |
| 84 | connect(sender: job, signal: &KJob::result, context: this, slot: &KUrlNavigatorButton::statFinished); |
| 85 | Q_EMIT startedTextResolving(); |
| 86 | } else { |
| 87 | setText(m_url.fileName().replace(c: QLatin1Char('&'), after: QLatin1String("&&" ))); |
| 88 | } |
| 89 | setIcon(QIcon::fromTheme(name: KIO::iconNameForUrl(url))); |
| 90 | } |
| 91 | |
| 92 | QUrl KUrlNavigatorButton::url() const |
| 93 | { |
| 94 | return m_url; |
| 95 | } |
| 96 | |
| 97 | void KUrlNavigatorButton::setText(const QString &text) |
| 98 | { |
| 99 | QString adjustedText = text; |
| 100 | if (adjustedText.isEmpty()) { |
| 101 | adjustedText = m_url.scheme(); |
| 102 | } |
| 103 | // Assure that the button always consists of one line |
| 104 | adjustedText.remove(c: QLatin1Char('\n')); |
| 105 | |
| 106 | KUrlNavigatorButtonBase::setText(adjustedText); |
| 107 | updateMinimumWidth(); |
| 108 | |
| 109 | // Assure that statFinished() does not overwrite a text that has been |
| 110 | // set by a client of the URL navigator button |
| 111 | m_pendingTextChange = false; |
| 112 | } |
| 113 | |
| 114 | void KUrlNavigatorButton::setActiveSubDirectory(const QString &subDir) |
| 115 | { |
| 116 | m_subDir = subDir; |
| 117 | |
| 118 | // We use a different (bold) font on active, so the size hint changes |
| 119 | updateGeometry(); |
| 120 | update(); |
| 121 | } |
| 122 | |
| 123 | QString KUrlNavigatorButton::activeSubDirectory() const |
| 124 | { |
| 125 | return m_subDir; |
| 126 | } |
| 127 | |
| 128 | QSize KUrlNavigatorButton::sizeHint() const |
| 129 | { |
| 130 | QFont adjustedFont(font()); |
| 131 | adjustedFont.setBold(m_subDir.isEmpty()); |
| 132 | // preferred width is textWidth, iconWidth and padding combined |
| 133 | // add extra padding in end to make sure the space between divider and button is consistent |
| 134 | // the first padding is used between icon and text, second in the end of text |
| 135 | const int width = m_padding + textWidth() + arrowWidth() + m_padding; |
| 136 | return QSize(width, KUrlNavigatorButtonBase::sizeHint().height()); |
| 137 | } |
| 138 | |
| 139 | void KUrlNavigatorButton::setShowMnemonic(bool show) |
| 140 | { |
| 141 | if (m_showMnemonic != show) { |
| 142 | m_showMnemonic = show; |
| 143 | update(); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | bool KUrlNavigatorButton::showMnemonic() const |
| 148 | { |
| 149 | return m_showMnemonic; |
| 150 | } |
| 151 | |
| 152 | void KUrlNavigatorButton::setDrawSeparator(bool draw) |
| 153 | { |
| 154 | if (m_drawSeparator != draw) { |
| 155 | m_drawSeparator = draw; |
| 156 | update(); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | bool KUrlNavigatorButton::drawSeparator() const |
| 161 | { |
| 162 | return m_drawSeparator; |
| 163 | } |
| 164 | |
| 165 | void KUrlNavigatorButton::paintEvent(QPaintEvent *event) |
| 166 | { |
| 167 | Q_UNUSED(event); |
| 168 | |
| 169 | QPainter painter(this); |
| 170 | |
| 171 | QFont adjustedFont(font()); |
| 172 | adjustedFont.setBold(m_subDir.isEmpty()); |
| 173 | painter.setFont(adjustedFont); |
| 174 | |
| 175 | int buttonWidth = width(); |
| 176 | int arrowWidth = KUrlNavigatorButton::arrowWidth(); |
| 177 | |
| 178 | int preferredWidth = sizeHint().width(); |
| 179 | if (preferredWidth < minimumWidth()) { |
| 180 | preferredWidth = minimumWidth(); |
| 181 | } |
| 182 | if (buttonWidth > preferredWidth) { |
| 183 | buttonWidth = preferredWidth; |
| 184 | } |
| 185 | const int buttonHeight = height(); |
| 186 | const QColor fgColor = foregroundColor(); |
| 187 | const bool leftToRight = (layoutDirection() == Qt::LeftToRight); |
| 188 | |
| 189 | // Prepare sizes for icon |
| 190 | QRect textRect; |
| 191 | const int textRectWidth = buttonWidth - arrowWidth - m_padding; |
| 192 | if (leftToRight) { |
| 193 | textRect = QRect(m_padding, 0, textRectWidth, buttonHeight); |
| 194 | } else { |
| 195 | // If no separator is drawn, we can start writing text from 0 |
| 196 | textRect = QRect(m_drawSeparator ? arrowWidth : 0, 0, textRectWidth, buttonHeight); |
| 197 | } |
| 198 | |
| 199 | drawHoverBackground(painter: &painter); |
| 200 | |
| 201 | // Draw gradient overlay if text is clipped |
| 202 | painter.setPen(fgColor); |
| 203 | const bool clipped = isTextClipped(); |
| 204 | if (clipped) { |
| 205 | QColor bgColor = fgColor; |
| 206 | bgColor.setAlpha(0); |
| 207 | QLinearGradient gradient(textRect.topLeft(), textRect.topRight()); |
| 208 | if (leftToRight) { |
| 209 | gradient.setFinalStop(QPoint(gradient.finalStop().x() - m_padding, gradient.finalStop().y())); |
| 210 | gradient.setColorAt(pos: 0.8, color: fgColor); |
| 211 | gradient.setColorAt(pos: 1.0, color: bgColor); |
| 212 | } else { |
| 213 | gradient.setStart(QPoint(gradient.start().x() + m_padding, gradient.start().y())); |
| 214 | gradient.setColorAt(pos: 0.0, color: bgColor); |
| 215 | gradient.setColorAt(pos: 0.2, color: fgColor); |
| 216 | } |
| 217 | |
| 218 | QPen pen; |
| 219 | pen.setBrush(QBrush(gradient)); |
| 220 | painter.setPen(pen); |
| 221 | } |
| 222 | |
| 223 | // Draw folder name |
| 224 | int textFlags = Qt::AlignVCenter; |
| 225 | if (m_showMnemonic) { |
| 226 | textFlags |= Qt::TextShowMnemonic; |
| 227 | painter.drawText(r: textRect, flags: textFlags, text: text()); |
| 228 | } else { |
| 229 | painter.drawText(r: textRect, flags: textFlags, text: plainText()); |
| 230 | } |
| 231 | |
| 232 | // Draw separator arrow |
| 233 | if (m_drawSeparator) { |
| 234 | QStyleOption option; |
| 235 | option.initFrom(w: this); |
| 236 | option.palette = palette(); |
| 237 | option.palette.setColor(acr: QPalette::Text, acolor: fgColor); |
| 238 | option.palette.setColor(acr: QPalette::WindowText, acolor: fgColor); |
| 239 | option.palette.setColor(acr: QPalette::ButtonText, acolor: fgColor); |
| 240 | |
| 241 | if (leftToRight) { |
| 242 | option.rect = QRect(textRect.right(), 0, arrowWidth, buttonHeight); |
| 243 | } else { |
| 244 | // Separator is the first item in RtL mode |
| 245 | option.rect = QRect(0, 0, arrowWidth, buttonHeight); |
| 246 | } |
| 247 | |
| 248 | if (!m_hoverOverArrow) { |
| 249 | option.state = QStyle::State_None; |
| 250 | } |
| 251 | style()->drawPrimitive(pe: leftToRight ? QStyle::PE_IndicatorArrowRight : QStyle::PE_IndicatorArrowLeft, opt: &option, p: &painter, w: this); |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | void KUrlNavigatorButton::enterEvent(QEnterEvent *event) |
| 256 | { |
| 257 | KUrlNavigatorButtonBase::enterEvent(event); |
| 258 | |
| 259 | // if the text is clipped due to a small window width, the text should |
| 260 | // be shown as tooltip |
| 261 | if (isTextClipped()) { |
| 262 | setToolTip(plainText()); |
| 263 | } |
| 264 | if (!m_hoverOverButton) { |
| 265 | m_hoverOverButton = true; |
| 266 | update(); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | void KUrlNavigatorButton::leaveEvent(QEvent *event) |
| 271 | { |
| 272 | KUrlNavigatorButtonBase::leaveEvent(event); |
| 273 | setToolTip(QString()); |
| 274 | |
| 275 | if (m_hoverOverArrow) { |
| 276 | m_hoverOverArrow = false; |
| 277 | update(); |
| 278 | } |
| 279 | if (m_hoverOverButton) { |
| 280 | m_hoverOverButton = false; |
| 281 | update(); |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | void KUrlNavigatorButton::keyPressEvent(QKeyEvent *event) |
| 286 | { |
| 287 | switch (event->key()) { |
| 288 | case Qt::Key_Enter: |
| 289 | case Qt::Key_Return: |
| 290 | Q_EMIT navigatorButtonActivated(url: m_url, button: Qt::LeftButton, modifiers: event->modifiers()); |
| 291 | break; |
| 292 | case Qt::Key_Down: |
| 293 | case Qt::Key_Space: |
| 294 | startSubDirsJob(); |
| 295 | break; |
| 296 | default: |
| 297 | KUrlNavigatorButtonBase::keyPressEvent(event); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | void KUrlNavigatorButton::dropEvent(QDropEvent *event) |
| 302 | { |
| 303 | if (event->mimeData()->hasUrls()) { |
| 304 | setDisplayHintEnabled(hint: DraggedHint, enable: true); |
| 305 | |
| 306 | Q_EMIT urlsDroppedOnNavButton(destination: m_url, event); |
| 307 | |
| 308 | setDisplayHintEnabled(hint: DraggedHint, enable: false); |
| 309 | update(); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | void KUrlNavigatorButton::dragEnterEvent(QDragEnterEvent *event) |
| 314 | { |
| 315 | if (event->mimeData()->hasUrls()) { |
| 316 | setDisplayHintEnabled(hint: DraggedHint, enable: true); |
| 317 | event->acceptProposedAction(); |
| 318 | |
| 319 | update(); |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | void KUrlNavigatorButton::dragMoveEvent(QDragMoveEvent *event) |
| 324 | { |
| 325 | QRect rect = event->answerRect(); |
| 326 | |
| 327 | if (isAboveSeparator(x: rect.center().x())) { |
| 328 | m_hoverOverArrow = true; |
| 329 | update(); |
| 330 | |
| 331 | if (m_subDirsMenu == nullptr) { |
| 332 | requestSubDirs(); |
| 333 | } else if (m_subDirsMenu->parent() != this) { |
| 334 | m_subDirsMenu->close(); |
| 335 | m_subDirsMenu->deleteLater(); |
| 336 | m_subDirsMenu = nullptr; |
| 337 | |
| 338 | requestSubDirs(); |
| 339 | } |
| 340 | } else { |
| 341 | if (m_openSubDirsTimer->isActive()) { |
| 342 | cancelSubDirsRequest(); |
| 343 | } |
| 344 | if (m_subDirsMenu) { |
| 345 | m_subDirsMenu->deleteLater(); |
| 346 | m_subDirsMenu = nullptr; |
| 347 | } |
| 348 | m_hoverOverArrow = false; |
| 349 | update(); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | void KUrlNavigatorButton::dragLeaveEvent(QDragLeaveEvent *event) |
| 354 | { |
| 355 | KUrlNavigatorButtonBase::dragLeaveEvent(event); |
| 356 | |
| 357 | m_hoverOverArrow = false; |
| 358 | setDisplayHintEnabled(hint: DraggedHint, enable: false); |
| 359 | update(); |
| 360 | } |
| 361 | |
| 362 | void KUrlNavigatorButton::mousePressEvent(QMouseEvent *event) |
| 363 | { |
| 364 | if (isAboveSeparator(x: qRound(d: event->position().x())) && (event->button() == Qt::LeftButton)) { |
| 365 | // the mouse is pressed above the folder button |
| 366 | startSubDirsJob(); |
| 367 | } |
| 368 | KUrlNavigatorButtonBase::mousePressEvent(e: event); |
| 369 | } |
| 370 | |
| 371 | void KUrlNavigatorButton::mouseReleaseEvent(QMouseEvent *event) |
| 372 | { |
| 373 | if (!isAboveSeparator(x: qRound(d: event->position().x())) || (event->button() != Qt::LeftButton)) { |
| 374 | // the mouse has been released above the text area and not |
| 375 | // above the folder button |
| 376 | Q_EMIT navigatorButtonActivated(url: m_url, button: event->button(), modifiers: event->modifiers()); |
| 377 | cancelSubDirsRequest(); |
| 378 | } |
| 379 | KUrlNavigatorButtonBase::mouseReleaseEvent(e: event); |
| 380 | } |
| 381 | |
| 382 | void KUrlNavigatorButton::mouseMoveEvent(QMouseEvent *event) |
| 383 | { |
| 384 | KUrlNavigatorButtonBase::mouseMoveEvent(event); |
| 385 | |
| 386 | const bool hoverOverIcon = isAboveSeparator(x: qRound(d: event->position().x())); |
| 387 | if (hoverOverIcon != m_hoverOverArrow) { |
| 388 | m_hoverOverArrow = hoverOverIcon; |
| 389 | update(); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | void KUrlNavigatorButton::wheelEvent(QWheelEvent *event) |
| 394 | { |
| 395 | if (event->angleDelta().y() != 0) { |
| 396 | m_wheelSteps = event->angleDelta().y() / 120; |
| 397 | m_replaceButton = true; |
| 398 | startSubDirsJob(); |
| 399 | } |
| 400 | |
| 401 | KUrlNavigatorButtonBase::wheelEvent(event); |
| 402 | } |
| 403 | |
| 404 | void KUrlNavigatorButton::requestSubDirs() |
| 405 | { |
| 406 | if (!m_openSubDirsTimer->isActive() && (m_subDirsJob == nullptr)) { |
| 407 | m_openSubDirsTimer->start(); |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | void KUrlNavigatorButton::startSubDirsJob() |
| 412 | { |
| 413 | if (m_subDirsJob != nullptr) { |
| 414 | return; |
| 415 | } |
| 416 | |
| 417 | const QUrl url = m_replaceButton ? KIO::upUrl(url: m_url) : m_url; |
| 418 | const KUrlNavigator *urlNavigator = qobject_cast<KUrlNavigator *>(object: parent()); |
| 419 | Q_ASSERT(urlNavigator); |
| 420 | m_subDirsJob = |
| 421 | KIO::listDir(url, flags: KIO::HideProgressInfo, listFlags: urlNavigator->showHiddenFolders() ? KIO::ListJob::ListFlag::IncludeHidden : KIO::ListJob::ListFlags{}); |
| 422 | m_subDirs.clear(); // just to be ++safe |
| 423 | |
| 424 | connect(sender: m_subDirsJob, signal: &KIO::ListJob::entries, context: this, slot: &KUrlNavigatorButton::addEntriesToSubDirs); |
| 425 | |
| 426 | if (m_replaceButton) { |
| 427 | connect(sender: m_subDirsJob, signal: &KJob::result, context: this, slot: &KUrlNavigatorButton::replaceButton); |
| 428 | } else { |
| 429 | connect(sender: m_subDirsJob, signal: &KJob::result, context: this, slot: &KUrlNavigatorButton::openSubDirsMenu); |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | void KUrlNavigatorButton::addEntriesToSubDirs(KIO::Job *job, const KIO::UDSEntryList &entries) |
| 434 | { |
| 435 | Q_ASSERT(job == m_subDirsJob); |
| 436 | Q_UNUSED(job); |
| 437 | |
| 438 | for (const KIO::UDSEntry &entry : entries) { |
| 439 | if (entry.isDir()) { |
| 440 | const QString name = entry.stringValue(field: KIO::UDSEntry::UDS_NAME); |
| 441 | QString displayName = entry.stringValue(field: KIO::UDSEntry::UDS_DISPLAY_NAME); |
| 442 | if (displayName.isEmpty()) { |
| 443 | displayName = name; |
| 444 | } |
| 445 | if (name != QLatin1String("." ) && name != QLatin1String(".." )) { |
| 446 | m_subDirs.push_back(x: {.name: name, .displayName: displayName}); |
| 447 | } |
| 448 | } |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | void KUrlNavigatorButton::slotUrlsDropped(QAction *action, QDropEvent *event) |
| 453 | { |
| 454 | const int result = action->data().toInt(); |
| 455 | QUrl url(m_url); |
| 456 | url.setPath(path: Utils::concatPaths(path1: url.path(), path2: m_subDirs.at(n: result).name)); |
| 457 | Q_EMIT urlsDroppedOnNavButton(destination: url, event); |
| 458 | } |
| 459 | |
| 460 | void KUrlNavigatorButton::(QAction *action, Qt::MouseButton button) |
| 461 | { |
| 462 | const int result = action->data().toInt(); |
| 463 | QUrl url(m_url); |
| 464 | url.setPath(path: Utils::concatPaths(path1: url.path(), path2: m_subDirs.at(n: result).name)); |
| 465 | Q_EMIT navigatorButtonActivated(url, button, modifiers: Qt::NoModifier); |
| 466 | } |
| 467 | |
| 468 | void KUrlNavigatorButton::statFinished(KJob *job) |
| 469 | { |
| 470 | const KIO::UDSEntry entry = static_cast<KIO::StatJob *>(job)->statResult(); |
| 471 | |
| 472 | if (m_pendingTextChange) { |
| 473 | m_pendingTextChange = false; |
| 474 | |
| 475 | QString name = entry.stringValue(field: KIO::UDSEntry::UDS_DISPLAY_NAME); |
| 476 | if (name.isEmpty()) { |
| 477 | name = m_url.fileName(); |
| 478 | } |
| 479 | setText(name); |
| 480 | |
| 481 | Q_EMIT finishedTextResolving(); |
| 482 | } |
| 483 | |
| 484 | const QString iconName = entry.stringValue(field: KIO::UDSEntry::UDS_ICON_NAME); |
| 485 | if (!iconName.isEmpty()) { |
| 486 | setIcon(QIcon::fromTheme(name: iconName)); |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | /* |
| 491 | * Helper struct for sorting folder names |
| 492 | */ |
| 493 | struct FolderNameNaturalLessThan { |
| 494 | FolderNameNaturalLessThan(bool sortHiddenLast) |
| 495 | : m_sortHiddenLast(sortHiddenLast) |
| 496 | { |
| 497 | m_collator.setCaseSensitivity(Qt::CaseInsensitive); |
| 498 | m_collator.setNumericMode(true); |
| 499 | } |
| 500 | |
| 501 | bool operator()(const KUrlNavigatorButton::SubDirInfo &a, const KUrlNavigatorButton::SubDirInfo &b) |
| 502 | { |
| 503 | if (m_sortHiddenLast) { |
| 504 | const bool isHiddenA = a.name.startsWith(c: QLatin1Char('.')); |
| 505 | const bool isHiddenB = b.name.startsWith(c: QLatin1Char('.')); |
| 506 | if (isHiddenA && !isHiddenB) { |
| 507 | return false; |
| 508 | } |
| 509 | if (!isHiddenA && isHiddenB) { |
| 510 | return true; |
| 511 | } |
| 512 | } |
| 513 | return m_collator.compare(s1: a.name, s2: b.name) < 0; |
| 514 | } |
| 515 | |
| 516 | private: |
| 517 | QCollator m_collator; |
| 518 | bool m_sortHiddenLast; |
| 519 | }; |
| 520 | |
| 521 | void KUrlNavigatorButton::(KJob *job) |
| 522 | { |
| 523 | Q_ASSERT(job == m_subDirsJob); |
| 524 | m_subDirsJob = nullptr; |
| 525 | |
| 526 | if (job->error() || m_subDirs.empty()) { |
| 527 | // clear listing |
| 528 | return; |
| 529 | } |
| 530 | |
| 531 | const KUrlNavigator *urlNavigator = qobject_cast<KUrlNavigator *>(object: parent()); |
| 532 | Q_ASSERT(urlNavigator); |
| 533 | FolderNameNaturalLessThan less(urlNavigator->showHiddenFolders() && urlNavigator->sortHiddenFoldersLast()); |
| 534 | std::sort(first: m_subDirs.begin(), last: m_subDirs.end(), comp: less); |
| 535 | setDisplayHintEnabled(hint: PopupActiveHint, enable: true); |
| 536 | update(); // ensure the button is drawn highlighted |
| 537 | |
| 538 | if (m_subDirsMenu != nullptr) { |
| 539 | m_subDirsMenu->close(); |
| 540 | m_subDirsMenu->deleteLater(); |
| 541 | m_subDirsMenu = nullptr; |
| 542 | } |
| 543 | |
| 544 | m_subDirsMenu = new KUrlNavigatorMenu(this); |
| 545 | initMenu(menu: m_subDirsMenu, startIndex: 0); |
| 546 | |
| 547 | const bool leftToRight = (layoutDirection() == Qt::LeftToRight); |
| 548 | const int = leftToRight ? width() - arrowWidth() : 0; |
| 549 | const QPoint = parentWidget()->mapToGlobal(geometry().bottomLeft() + QPoint(popupX, 0)); |
| 550 | |
| 551 | QPointer<QObject> guard(this); |
| 552 | |
| 553 | m_subDirsMenu->exec(pos: popupPos); |
| 554 | |
| 555 | // If 'this' has been deleted in the menu's nested event loop, we have to return |
| 556 | // immediately because any access to a member variable might cause a crash. |
| 557 | if (!guard) { |
| 558 | return; |
| 559 | } |
| 560 | |
| 561 | m_subDirs.clear(); |
| 562 | delete m_subDirsMenu; |
| 563 | m_subDirsMenu = nullptr; |
| 564 | |
| 565 | setDisplayHintEnabled(hint: PopupActiveHint, enable: false); |
| 566 | } |
| 567 | |
| 568 | void KUrlNavigatorButton::replaceButton(KJob *job) |
| 569 | { |
| 570 | Q_ASSERT(job == m_subDirsJob); |
| 571 | m_subDirsJob = nullptr; |
| 572 | m_replaceButton = false; |
| 573 | |
| 574 | if (job->error() || m_subDirs.empty()) { |
| 575 | return; |
| 576 | } |
| 577 | |
| 578 | const KUrlNavigator *urlNavigator = qobject_cast<KUrlNavigator *>(object: parent()); |
| 579 | Q_ASSERT(urlNavigator); |
| 580 | FolderNameNaturalLessThan less(urlNavigator->showHiddenFolders() && urlNavigator->sortHiddenFoldersLast()); |
| 581 | std::sort(first: m_subDirs.begin(), last: m_subDirs.end(), comp: less); |
| 582 | |
| 583 | // Get index of the directory that is shown currently in the button |
| 584 | const QString currentDir = m_url.fileName(); |
| 585 | int currentIndex = 0; |
| 586 | const int subDirsCount = m_subDirs.size(); |
| 587 | while (currentIndex < subDirsCount) { |
| 588 | if (m_subDirs[currentIndex].name == currentDir) { |
| 589 | break; |
| 590 | } |
| 591 | ++currentIndex; |
| 592 | } |
| 593 | |
| 594 | // Adjust the index by respecting the wheel steps and |
| 595 | // trigger a replacing of the button content |
| 596 | int targetIndex = currentIndex - m_wheelSteps; |
| 597 | if (targetIndex < 0) { |
| 598 | targetIndex = 0; |
| 599 | } else if (targetIndex >= subDirsCount) { |
| 600 | targetIndex = subDirsCount - 1; |
| 601 | } |
| 602 | |
| 603 | QUrl url(KIO::upUrl(url: m_url)); |
| 604 | url.setPath(path: Utils::concatPaths(path1: url.path(), path2: m_subDirs[targetIndex].name)); |
| 605 | Q_EMIT navigatorButtonActivated(url, button: Qt::LeftButton, modifiers: Qt::NoModifier); |
| 606 | |
| 607 | m_subDirs.clear(); |
| 608 | } |
| 609 | |
| 610 | void KUrlNavigatorButton::cancelSubDirsRequest() |
| 611 | { |
| 612 | m_openSubDirsTimer->stop(); |
| 613 | if (m_subDirsJob != nullptr) { |
| 614 | m_subDirsJob->kill(); |
| 615 | m_subDirsJob = nullptr; |
| 616 | } |
| 617 | } |
| 618 | |
| 619 | QString KUrlNavigatorButton::plainText() const |
| 620 | { |
| 621 | // Replace all "&&" by '&' and remove all single |
| 622 | // '&' characters |
| 623 | const QString source = text(); |
| 624 | const int sourceLength = source.length(); |
| 625 | |
| 626 | QString dest; |
| 627 | dest.resize(size: sourceLength); |
| 628 | |
| 629 | int sourceIndex = 0; |
| 630 | int destIndex = 0; |
| 631 | while (sourceIndex < sourceLength) { |
| 632 | if (source.at(i: sourceIndex) == QLatin1Char('&')) { |
| 633 | ++sourceIndex; |
| 634 | if (sourceIndex >= sourceLength) { |
| 635 | break; |
| 636 | } |
| 637 | } |
| 638 | dest[destIndex] = source.at(i: sourceIndex); |
| 639 | ++sourceIndex; |
| 640 | ++destIndex; |
| 641 | } |
| 642 | |
| 643 | dest.resize(size: destIndex); |
| 644 | |
| 645 | return dest; |
| 646 | } |
| 647 | |
| 648 | int KUrlNavigatorButton::arrowWidth() const |
| 649 | { |
| 650 | // if there isn't arrow then return 0 |
| 651 | int width = 0; |
| 652 | if (!m_subDir.isEmpty()) { |
| 653 | width = height() / 2; |
| 654 | if (width < 4) { |
| 655 | width = 4; |
| 656 | } |
| 657 | } |
| 658 | |
| 659 | return width; |
| 660 | } |
| 661 | |
| 662 | int KUrlNavigatorButton::textWidth() const |
| 663 | { |
| 664 | QFont adjustedFont(font()); |
| 665 | adjustedFont.setBold(m_subDir.isEmpty()); |
| 666 | return QFontMetrics(adjustedFont).size(flags: Qt::TextSingleLine, str: plainText()).width(); |
| 667 | } |
| 668 | |
| 669 | bool KUrlNavigatorButton::isAboveSeparator(int x) const |
| 670 | { |
| 671 | const bool leftToRight = (layoutDirection() == Qt::LeftToRight); |
| 672 | return leftToRight ? (x >= width() - arrowWidth()) : (x < arrowWidth() + m_padding); |
| 673 | } |
| 674 | |
| 675 | bool KUrlNavigatorButton::isTextClipped() const |
| 676 | { |
| 677 | // Ignore padding when resizing, so text doesnt go under it |
| 678 | int availableWidth = width() - arrowWidth() - m_padding; |
| 679 | |
| 680 | return textWidth() >= availableWidth; |
| 681 | } |
| 682 | |
| 683 | void KUrlNavigatorButton::updateMinimumWidth() |
| 684 | { |
| 685 | const int oldMinWidth = minimumWidth(); |
| 686 | |
| 687 | int minWidth = sizeHint().width(); |
| 688 | if (minWidth < 10) { |
| 689 | minWidth = 10; |
| 690 | } else if (minWidth > 150) { |
| 691 | // don't let an overlong path name waste all the URL navigator space |
| 692 | minWidth = 150; |
| 693 | } |
| 694 | if (oldMinWidth != minWidth) { |
| 695 | setMinimumWidth(minWidth); |
| 696 | } |
| 697 | } |
| 698 | |
| 699 | void KUrlNavigatorButton::(KUrlNavigatorMenu *, int startIndex) |
| 700 | { |
| 701 | connect(sender: menu, signal: &KUrlNavigatorMenu::mouseButtonClicked, context: this, slot: &KUrlNavigatorButton::slotMenuActionClicked); |
| 702 | connect(sender: menu, signal: &KUrlNavigatorMenu::urlsDropped, context: this, slot: &KUrlNavigatorButton::slotUrlsDropped); |
| 703 | |
| 704 | // So that triggering a menu item with the keyboard works |
| 705 | connect(sender: menu, signal: &QMenu::triggered, context: this, slot: [this](QAction *act) { |
| 706 | slotMenuActionClicked(action: act, button: Qt::LeftButton); |
| 707 | }); |
| 708 | |
| 709 | menu->setLayoutDirection(Qt::LeftToRight); |
| 710 | |
| 711 | const int maxIndex = startIndex + 30; // Don't show more than 30 items in a menu |
| 712 | const int = m_subDirs.size(); |
| 713 | const int lastIndex = std::min(a: subDirsSize - 1, b: maxIndex); |
| 714 | for (int i = startIndex; i <= lastIndex; ++i) { |
| 715 | const auto &[subDirName, subDirDisplayName] = m_subDirs[i]; |
| 716 | QString text = KStringHandler::csqueeze(str: subDirDisplayName, maxlen: 60); |
| 717 | text.replace(c: QLatin1Char('&'), after: QLatin1String("&&" )); |
| 718 | QAction *action = new QAction(text, this); |
| 719 | if (m_subDir == subDirName) { |
| 720 | QFont font(action->font()); |
| 721 | font.setBold(true); |
| 722 | action->setFont(font); |
| 723 | } |
| 724 | action->setData(i); |
| 725 | menu->addAction(action); |
| 726 | } |
| 727 | if (subDirsSize > maxIndex) { |
| 728 | // If too much items are shown, move them into a sub menu |
| 729 | menu->addSeparator(); |
| 730 | KUrlNavigatorMenu * = new KUrlNavigatorMenu(menu); |
| 731 | subDirsMenu->setTitle(i18nc("@action:inmenu" , "More" )); |
| 732 | initMenu(menu: subDirsMenu, startIndex: maxIndex); |
| 733 | menu->addMenu(menu: subDirsMenu); |
| 734 | } |
| 735 | } |
| 736 | |
| 737 | } // namespace KDEPrivate |
| 738 | |
| 739 | #include "moc_kurlnavigatorbutton_p.cpp" |
| 740 | |