| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | #include "kcommandbar.h" |
| 7 | #include "kcommandbarmodel_p.h" |
| 8 | #include "kconfigwidgets_debug.h" |
| 9 | |
| 10 | #include <QAction> |
| 11 | #include <QCoreApplication> |
| 12 | #include <QGraphicsOpacityEffect> |
| 13 | #include <QHeaderView> |
| 14 | #include <QKeyEvent> |
| 15 | #include <QLabel> |
| 16 | #include <QLineEdit> |
| 17 | #include <QMainWindow> |
| 18 | #include <QMenu> |
| 19 | #include <QPainter> |
| 20 | #include <QPointer> |
| 21 | #include <QScreen> |
| 22 | #include <QSortFilterProxyModel> |
| 23 | #include <QStatusBar> |
| 24 | #include <QStyledItemDelegate> |
| 25 | #include <QTextLayout> |
| 26 | #include <QToolBar> |
| 27 | #include <QTreeView> |
| 28 | #include <QVBoxLayout> |
| 29 | |
| 30 | #include <KConfigGroup> |
| 31 | #include <KFuzzyMatcher> |
| 32 | #include <KLocalizedString> |
| 33 | #include <KSharedConfig> |
| 34 | |
| 35 | static QRect getCommandBarBoundingRect(KCommandBar *commandBar) |
| 36 | { |
| 37 | QWidget *parentWidget = commandBar->parentWidget(); |
| 38 | Q_ASSERT(parentWidget); |
| 39 | |
| 40 | const QMainWindow *mainWindow = qobject_cast<const QMainWindow *>(object: parentWidget); |
| 41 | if (!mainWindow) { |
| 42 | return parentWidget->geometry(); |
| 43 | } |
| 44 | |
| 45 | QRect boundingRect = mainWindow->contentsRect(); |
| 46 | |
| 47 | // exclude the menu bar from the bounding rect |
| 48 | if (const QWidget * = mainWindow->menuWidget()) { |
| 49 | if (!menuWidget->isHidden()) { |
| 50 | boundingRect.setTop(boundingRect.top() + menuWidget->height()); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // exclude the status bar from the bounding rect |
| 55 | if (const QStatusBar *statusBar = mainWindow->findChild<QStatusBar *>()) { |
| 56 | if (!statusBar->isHidden()) { |
| 57 | boundingRect.setBottom(boundingRect.bottom() - statusBar->height()); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // exclude any undocked toolbar from the bounding rect |
| 62 | const QList<QToolBar *> toolBars = mainWindow->findChildren<QToolBar *>(); |
| 63 | for (QToolBar *toolBar : toolBars) { |
| 64 | if (toolBar->isHidden() || toolBar->isFloating()) { |
| 65 | continue; |
| 66 | } |
| 67 | |
| 68 | switch (mainWindow->toolBarArea(toolbar: toolBar)) { |
| 69 | case Qt::TopToolBarArea: |
| 70 | boundingRect.setTop(std::max(a: boundingRect.top(), b: toolBar->geometry().bottom())); |
| 71 | break; |
| 72 | case Qt::RightToolBarArea: |
| 73 | boundingRect.setRight(std::min(a: boundingRect.right(), b: toolBar->geometry().left())); |
| 74 | break; |
| 75 | case Qt::BottomToolBarArea: |
| 76 | boundingRect.setBottom(std::min(a: boundingRect.bottom(), b: toolBar->geometry().top())); |
| 77 | break; |
| 78 | case Qt::LeftToolBarArea: |
| 79 | boundingRect.setLeft(std::max(a: boundingRect.left(), b: toolBar->geometry().right())); |
| 80 | break; |
| 81 | default: |
| 82 | break; |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | return boundingRect; |
| 87 | } |
| 88 | |
| 89 | // BEGIN CommandBarFilterModel |
| 90 | class CommandBarFilterModel final : public QSortFilterProxyModel |
| 91 | { |
| 92 | Q_OBJECT |
| 93 | public: |
| 94 | CommandBarFilterModel(QObject *parent = nullptr) |
| 95 | : QSortFilterProxyModel(parent) |
| 96 | { |
| 97 | connect(sender: this, signal: &CommandBarFilterModel::modelAboutToBeReset, context: this, slot: [this]() { |
| 98 | m_hasActionsWithIcons = false; |
| 99 | }); |
| 100 | } |
| 101 | |
| 102 | bool hasActionsWithIcons() const |
| 103 | { |
| 104 | return m_hasActionsWithIcons; |
| 105 | } |
| 106 | |
| 107 | Q_SLOT void setFilterString(const QString &string) |
| 108 | { |
| 109 | // MUST reset the model here, we want to repopulate |
| 110 | // invalidateFilter() will not work here |
| 111 | beginResetModel(); |
| 112 | m_pattern = string; |
| 113 | endResetModel(); |
| 114 | } |
| 115 | |
| 116 | protected: |
| 117 | bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override |
| 118 | { |
| 119 | const int scoreLeft = sourceLeft.data(arole: KCommandBarModel::Score).toInt(); |
| 120 | const int scoreRight = sourceRight.data(arole: KCommandBarModel::Score).toInt(); |
| 121 | if (scoreLeft == scoreRight) { |
| 122 | const QString textLeft = sourceLeft.data().toString(); |
| 123 | const QString textRight = sourceRight.data().toString(); |
| 124 | |
| 125 | return textRight.localeAwareCompare(s: textLeft) < 0; |
| 126 | } |
| 127 | |
| 128 | return scoreLeft < scoreRight; |
| 129 | } |
| 130 | |
| 131 | bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override |
| 132 | { |
| 133 | const QModelIndex index = sourceModel()->index(row: sourceRow, column: 0, parent: sourceParent); |
| 134 | |
| 135 | bool accept = false; |
| 136 | if (m_pattern.isEmpty()) { |
| 137 | accept = true; |
| 138 | } else { |
| 139 | const QString row = index.data(arole: Qt::DisplayRole).toString(); |
| 140 | KFuzzyMatcher::Result resAction = KFuzzyMatcher::match(pattern: m_pattern, str: row); |
| 141 | sourceModel()->setData(index, value: resAction.score, role: KCommandBarModel::Score); |
| 142 | accept = resAction.matched; |
| 143 | } |
| 144 | |
| 145 | if (accept && !m_hasActionsWithIcons) { |
| 146 | m_hasActionsWithIcons |= !index.data(arole: Qt::DecorationRole).isNull(); |
| 147 | } |
| 148 | |
| 149 | return accept; |
| 150 | } |
| 151 | |
| 152 | private: |
| 153 | QString m_pattern; |
| 154 | mutable bool m_hasActionsWithIcons = false; |
| 155 | }; |
| 156 | // END CommandBarFilterModel |
| 157 | |
| 158 | static QColor getTextColor(const QStyleOptionViewItem &option) |
| 159 | { |
| 160 | QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; |
| 161 | if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) { |
| 162 | cg = QPalette::Inactive; |
| 163 | } |
| 164 | |
| 165 | if (option.state & QStyle::State_Selected) { |
| 166 | return option.palette.color(cg, cr: QPalette::HighlightedText); |
| 167 | } |
| 168 | return option.palette.color(cg, cr: QPalette::Text); |
| 169 | } |
| 170 | |
| 171 | class CommandBarStyleDelegate final : public QStyledItemDelegate |
| 172 | { |
| 173 | Q_OBJECT |
| 174 | public: |
| 175 | CommandBarStyleDelegate(QObject *parent = nullptr) |
| 176 | : QStyledItemDelegate(parent) |
| 177 | { |
| 178 | } |
| 179 | |
| 180 | /* |
| 181 | * Paints a single item's text |
| 182 | */ |
| 183 | static void |
| 184 | paintItemText(QPainter *p, const QString &textt, const QRect &rect, const QStyleOptionViewItem &options, QList<QTextLayout::FormatRange> formats) |
| 185 | { |
| 186 | QString text = options.fontMetrics.elidedText(text: textt, mode: Qt::ElideRight, width: rect.width()); |
| 187 | |
| 188 | // set formats and font |
| 189 | QTextLayout textLayout(text, options.font); |
| 190 | formats.append(other: textLayout.formats()); |
| 191 | textLayout.setFormats(formats); |
| 192 | |
| 193 | // set alignment, rtls etc |
| 194 | QTextOption textOption; |
| 195 | textOption.setTextDirection(options.direction); |
| 196 | textOption.setAlignment(QStyle::visualAlignment(direction: options.direction, alignment: options.displayAlignment)); |
| 197 | textLayout.setTextOption(textOption); |
| 198 | |
| 199 | // layout the text |
| 200 | textLayout.beginLayout(); |
| 201 | |
| 202 | QTextLine line = textLayout.createLine(); |
| 203 | if (!line.isValid()) { |
| 204 | return; |
| 205 | } |
| 206 | |
| 207 | const int lineWidth = rect.width(); |
| 208 | line.setLineWidth(lineWidth); |
| 209 | line.setPosition(QPointF(0, 0)); |
| 210 | |
| 211 | textLayout.endLayout(); |
| 212 | |
| 213 | /* |
| 214 | * get "Y" so that we can properly V-Center align the text in row |
| 215 | */ |
| 216 | const int y = QStyle::alignedRect(direction: Qt::LeftToRight, alignment: Qt::AlignVCenter, size: textLayout.boundingRect().size().toSize(), rectangle: rect).y(); |
| 217 | |
| 218 | // draw the text |
| 219 | const QPointF pos(rect.x(), y); |
| 220 | textLayout.draw(p, pos); |
| 221 | } |
| 222 | |
| 223 | void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override |
| 224 | { |
| 225 | painter->save(); |
| 226 | |
| 227 | /* |
| 228 | * Draw everything, (widget, icon etc) except the text |
| 229 | */ |
| 230 | QStyleOptionViewItem option = opt; |
| 231 | initStyleOption(option: &option, index); |
| 232 | const bool isSelected = option.state & QStyle::State_Selected; |
| 233 | |
| 234 | option.text.clear(); // clear old text |
| 235 | QStyle *style = option.widget->style(); |
| 236 | style->drawControl(element: QStyle::CE_ItemViewItem, opt: &option, p: painter, w: option.widget); |
| 237 | |
| 238 | const int hMargin = style->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: &option, widget: option.widget); |
| 239 | |
| 240 | QRect textRect = option.rect; |
| 241 | |
| 242 | const CommandBarFilterModel *model = static_cast<const CommandBarFilterModel *>(index.model()); |
| 243 | if (model->hasActionsWithIcons()) { |
| 244 | const int iconWidth = option.decorationSize.width() + (hMargin * 2); |
| 245 | if (option.direction == Qt::RightToLeft) { |
| 246 | textRect.adjust(dx1: 0, dy1: 0, dx2: -iconWidth, dy2: 0); |
| 247 | } else { |
| 248 | textRect.adjust(dx1: iconWidth, dy1: 0, dx2: 0, dy2: 0); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | QList<QTextLayout::FormatRange> formats; |
| 253 | const QString original = index.data().toString(); |
| 254 | if (!isSelected) { |
| 255 | // omit custom text coloring if item is selected because the contrast ratio between the custom text colors |
| 256 | // and highlight background is often bad, especially if highlighted text has inverted text color |
| 257 | QStringView str = original; |
| 258 | int componentIdx = original.indexOf(ch: QLatin1Char(':')); |
| 259 | int actionNameStart = 0; |
| 260 | if (componentIdx > 0) { |
| 261 | actionNameStart = componentIdx + 2; |
| 262 | // + 2 because there is a space after colon |
| 263 | str = str.mid(pos: actionNameStart); |
| 264 | |
| 265 | QTextCharFormat gray; |
| 266 | gray.setForeground(option.palette.placeholderText()); |
| 267 | formats.append(t: {.start: 0, .length: componentIdx, .format: gray}); |
| 268 | } |
| 269 | |
| 270 | /* |
| 271 | * Highlight matches from fuzzy matcher |
| 272 | */ |
| 273 | const auto fmtRanges = KFuzzyMatcher::matchedRanges(pattern: m_filterString, str); |
| 274 | QTextCharFormat f; |
| 275 | f.setForeground(option.palette.link()); |
| 276 | formats.reserve(asize: formats.size() + fmtRanges.size()); |
| 277 | std::transform(first: fmtRanges.begin(), last: fmtRanges.end(), result: std::back_inserter(x&: formats), unary_op: [f, actionNameStart](const KFuzzyMatcher::Range &fr) { |
| 278 | return QTextLayout::FormatRange{.start: fr.start + actionNameStart, .length: fr.length, .format: f}; |
| 279 | }); |
| 280 | } |
| 281 | |
| 282 | textRect.adjust(dx1: hMargin, dy1: 0, dx2: -hMargin, dy2: 0); |
| 283 | painter->setPen(getTextColor(option)); |
| 284 | paintItemText(p: painter, textt: original, rect: textRect, options: option, formats: std::move(formats)); |
| 285 | |
| 286 | painter->restore(); |
| 287 | } |
| 288 | |
| 289 | public Q_SLOTS: |
| 290 | void setFilterString(const QString &text) |
| 291 | { |
| 292 | m_filterString = text; |
| 293 | } |
| 294 | |
| 295 | private: |
| 296 | QString m_filterString; |
| 297 | }; |
| 298 | |
| 299 | class ShortcutStyleDelegate final : public QStyledItemDelegate |
| 300 | { |
| 301 | Q_OBJECT |
| 302 | public: |
| 303 | ShortcutStyleDelegate(QObject *parent = nullptr) |
| 304 | : QStyledItemDelegate(parent) |
| 305 | { |
| 306 | } |
| 307 | |
| 308 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override |
| 309 | { |
| 310 | // draw background |
| 311 | option.widget->style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &option, p: painter); |
| 312 | |
| 313 | const QString shortcutString = index.data().toString(); |
| 314 | if (shortcutString.isEmpty()) { |
| 315 | return; |
| 316 | } |
| 317 | |
| 318 | const ShortcutSegments shortcutSegments = splitShortcut(shortcut: shortcutString); |
| 319 | if (shortcutSegments.isEmpty()) { |
| 320 | return; |
| 321 | } |
| 322 | |
| 323 | struct Button { |
| 324 | int textWidth; |
| 325 | QString text; |
| 326 | }; |
| 327 | |
| 328 | // compute the width of each shortcut segment |
| 329 | QList<Button> btns; |
| 330 | btns.reserve(asize: shortcutSegments.count()); |
| 331 | const int hMargin = horizontalMargin(option); |
| 332 | for (const QString &text : shortcutSegments) { |
| 333 | int textWidth = option.fontMetrics.horizontalAdvance(text); |
| 334 | textWidth += 2 * hMargin; |
| 335 | btns.append(t: {.textWidth: textWidth, .text: text}); |
| 336 | } |
| 337 | |
| 338 | int textHeight = option.fontMetrics.lineSpacing(); |
| 339 | // this happens on gnome so we manually decrease the height a bit |
| 340 | if (textHeight == option.rect.height()) { |
| 341 | textHeight -= 4; |
| 342 | } |
| 343 | |
| 344 | const int y = option.rect.y() + (option.rect.height() - textHeight) / 2; |
| 345 | int x; |
| 346 | if (option.direction == Qt::RightToLeft) { |
| 347 | x = option.rect.x() + hMargin; |
| 348 | } else { |
| 349 | x = option.rect.right() - shortcutDrawingWidth(option, shortcutSegments, hMargin) - hMargin; |
| 350 | } |
| 351 | |
| 352 | const QColor keyTextColor = option.palette.buttonText().color(); |
| 353 | const QColor textColor = getTextColor(option); |
| 354 | painter->save(); |
| 355 | painter->setRenderHint(hint: QPainter::Antialiasing); |
| 356 | for (int i = 0, n = btns.count(); i < n; ++i) { |
| 357 | const Button &button = btns.at(i); |
| 358 | |
| 359 | QRect outputRect(x, y, button.textWidth, textHeight); |
| 360 | |
| 361 | // an even element indicates that it is a key |
| 362 | if (i % 2 == 0) { |
| 363 | painter->save(); |
| 364 | painter->setPen(Qt::NoPen); |
| 365 | |
| 366 | // draw rounded rect shadow |
| 367 | auto shadowRect = outputRect.translated(dx: 0, dy: 1); |
| 368 | painter->setBrush(option.palette.shadow()); |
| 369 | painter->drawRoundedRect(rect: shadowRect, xRadius: 3.0, yRadius: 3.0); |
| 370 | |
| 371 | // draw rounded rect itself |
| 372 | painter->setBrush(option.palette.window()); |
| 373 | painter->drawRoundedRect(rect: outputRect, xRadius: 3.0, yRadius: 3.0); |
| 374 | |
| 375 | painter->restore(); |
| 376 | } |
| 377 | |
| 378 | // draw shortcut segment |
| 379 | painter->setPen((i % 2 == 0) ? keyTextColor : textColor); |
| 380 | painter->drawText(r: outputRect, flags: Qt::AlignCenter, text: button.text); |
| 381 | |
| 382 | x += outputRect.width(); |
| 383 | } |
| 384 | |
| 385 | painter->restore(); |
| 386 | } |
| 387 | |
| 388 | QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override |
| 389 | { |
| 390 | if (index.isValid() && index.column() == KCommandBarModel::Column_Shortcut) { |
| 391 | const QString shortcut = index.data().toString(); |
| 392 | if (!shortcut.isEmpty()) { |
| 393 | const ShortcutSegments shortcutSegments = splitShortcut(shortcut); |
| 394 | if (!shortcutSegments.isEmpty()) { |
| 395 | const int hMargin = horizontalMargin(option); |
| 396 | int width = shortcutDrawingWidth(option, shortcutSegments, hMargin); |
| 397 | |
| 398 | // add left and right margins |
| 399 | width += 2 * hMargin; |
| 400 | |
| 401 | return QSize(width, 0); |
| 402 | } |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | return QStyledItemDelegate::sizeHint(option, index); |
| 407 | } |
| 408 | |
| 409 | private: |
| 410 | using ShortcutSegments = QStringList; |
| 411 | |
| 412 | // split shortcut into segments i.e. will return |
| 413 | // ["Ctrl", "+", "A", ", ", "Ctrl", "+", "K"] for "Ctrl+A, Ctrl+K" |
| 414 | // twice as fast as using regular expressions |
| 415 | static ShortcutSegments splitShortcut(const QString &shortcut) |
| 416 | { |
| 417 | ShortcutSegments segments; |
| 418 | if (!shortcut.isEmpty()) { |
| 419 | const int shortcutLength = shortcut.length(); |
| 420 | int start = 0; |
| 421 | for (int i = 0; i < shortcutLength; ++i) { |
| 422 | const QChar c = shortcut.at(i); |
| 423 | if (c == QLatin1Char('+')) { |
| 424 | if (i > start) { |
| 425 | segments << shortcut.mid(position: start, n: i - start); |
| 426 | } |
| 427 | segments << shortcut.at(i); |
| 428 | start = i + 1; |
| 429 | } else if (c == QLatin1Char(',')) { |
| 430 | if (i > start) { |
| 431 | segments << shortcut.mid(position: start, n: i - start); |
| 432 | start = i; |
| 433 | } |
| 434 | const int j = i + 1; |
| 435 | if (j < shortcutLength && shortcut.at(i: j) == QLatin1Char(' ')) { |
| 436 | segments << shortcut.mid(position: start, n: j - start + 1); |
| 437 | i = j; |
| 438 | } else { |
| 439 | segments << shortcut.at(i); |
| 440 | } |
| 441 | start = i + 1; |
| 442 | } |
| 443 | } |
| 444 | if (start < shortcutLength) { |
| 445 | segments << shortcut.mid(position: start); |
| 446 | } |
| 447 | |
| 448 | // check we have successfully parsed the shortcut |
| 449 | if (segments.isEmpty()) { |
| 450 | qCWarning(KCONFIG_WIDGETS_LOG) << "Splitting shortcut failed" << shortcut; |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | return segments; |
| 455 | } |
| 456 | |
| 457 | // returns the width needed to draw the shortcut |
| 458 | static int shortcutDrawingWidth(const QStyleOptionViewItem &option, const ShortcutSegments &shortcutSegments, int hMargin) |
| 459 | { |
| 460 | int width = 0; |
| 461 | if (!shortcutSegments.isEmpty()) { |
| 462 | width = option.fontMetrics.horizontalAdvance(shortcutSegments.join(sep: QString())); |
| 463 | |
| 464 | // add left and right margins for each segment |
| 465 | width += shortcutSegments.count() * 2 * hMargin; |
| 466 | } |
| 467 | |
| 468 | return width; |
| 469 | } |
| 470 | |
| 471 | int horizontalMargin(const QStyleOptionViewItem &option) const |
| 472 | { |
| 473 | return option.widget->style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: &option) + 2; |
| 474 | } |
| 475 | }; |
| 476 | |
| 477 | // BEGIN KCommandBarPrivate |
| 478 | class KCommandBarPrivate |
| 479 | { |
| 480 | public: |
| 481 | QTreeView m_treeView; |
| 482 | QLineEdit m_lineEdit; |
| 483 | KCommandBarModel m_model; |
| 484 | CommandBarFilterModel m_proxyModel; |
| 485 | |
| 486 | /* |
| 487 | * selects first item in treeview |
| 488 | */ |
| 489 | void reselectFirst() |
| 490 | { |
| 491 | const QModelIndex index = m_proxyModel.index(row: 0, column: 0); |
| 492 | m_treeView.setCurrentIndex(index); |
| 493 | } |
| 494 | |
| 495 | /* |
| 496 | * blocks signals before clearing line edit to ensure |
| 497 | * we don't trigger filtering / sorting |
| 498 | */ |
| 499 | void clearLineEdit() |
| 500 | { |
| 501 | const QSignalBlocker blocker(m_lineEdit); |
| 502 | m_lineEdit.clear(); |
| 503 | } |
| 504 | |
| 505 | void slotReturnPressed(KCommandBar *q); |
| 506 | |
| 507 | void setLastUsedActions(); |
| 508 | |
| 509 | QStringList lastUsedActions() const; |
| 510 | }; |
| 511 | |
| 512 | void KCommandBarPrivate::slotReturnPressed(KCommandBar *q) |
| 513 | { |
| 514 | auto act = m_proxyModel.data(index: m_treeView.currentIndex(), role: Qt::UserRole).value<QAction *>(); |
| 515 | if (act) { |
| 516 | // if the action is a menu, we take all its actions |
| 517 | // and reload our dialog with these instead. |
| 518 | if (auto = act->menu()) { |
| 519 | auto = menu->actions(); |
| 520 | KCommandBar::ActionGroup ag; |
| 521 | |
| 522 | // if there are no actions, trigger load actions |
| 523 | // this happens with some menus that are loaded on demand |
| 524 | if (menuActions.size() == 0) { |
| 525 | Q_EMIT menu->aboutToShow(); |
| 526 | ag.actions = menu->actions(); |
| 527 | } |
| 528 | |
| 529 | QString groupName = KLocalizedString::removeAcceleratorMarker(label: act->text()); |
| 530 | ag.name = groupName; |
| 531 | |
| 532 | m_model.refresh(actionGroups: {ag}); |
| 533 | reselectFirst(); |
| 534 | /* |
| 535 | * We want the "textChanged" signal here |
| 536 | * so that proxy model triggers filtering again |
| 537 | * so don't use d->clearLineEdit() |
| 538 | */ |
| 539 | m_lineEdit.clear(); |
| 540 | return; |
| 541 | } else { |
| 542 | m_model.actionTriggered(name: act->text()); |
| 543 | q->hide(); |
| 544 | act->trigger(); |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | clearLineEdit(); |
| 549 | q->hide(); |
| 550 | q->deleteLater(); |
| 551 | } |
| 552 | |
| 553 | void KCommandBarPrivate::setLastUsedActions() |
| 554 | { |
| 555 | auto cfg = KSharedConfig::openStateConfig(); |
| 556 | KConfigGroup cg(cfg, QStringLiteral("General" )); |
| 557 | |
| 558 | QStringList actionNames = cg.readEntry(QStringLiteral("CommandBarLastUsedActions" ), aDefault: QStringList()); |
| 559 | |
| 560 | return m_model.setLastUsedActions(actionNames); |
| 561 | } |
| 562 | |
| 563 | QStringList KCommandBarPrivate::lastUsedActions() const |
| 564 | { |
| 565 | return m_model.lastUsedActions(); |
| 566 | } |
| 567 | // END KCommandBarPrivate |
| 568 | |
| 569 | // BEGIN KCommandBar |
| 570 | KCommandBar::KCommandBar(QWidget *parent) |
| 571 | : QFrame(parent) |
| 572 | , d(new KCommandBarPrivate) |
| 573 | { |
| 574 | setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); |
| 575 | setProperty(name: "_breeze_force_frame" , value: true); |
| 576 | |
| 577 | QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(this); |
| 578 | e->setColor(palette().color(cr: QPalette::Dark)); |
| 579 | e->setOffset(dx: 0, dy: 4); |
| 580 | e->setBlurRadius(48); |
| 581 | setGraphicsEffect(e); |
| 582 | |
| 583 | QVBoxLayout *layout = new QVBoxLayout(); |
| 584 | layout->setSpacing(0); |
| 585 | layout->setContentsMargins(QMargins()); |
| 586 | setLayout(layout); |
| 587 | |
| 588 | layout->addWidget(&d->m_lineEdit); |
| 589 | d->m_lineEdit.setClearButtonEnabled(true); |
| 590 | d->m_lineEdit.addAction(icon: QIcon::fromTheme(QStringLiteral("search" )), position: QLineEdit::LeadingPosition); |
| 591 | d->m_lineEdit.setFrame(false); |
| 592 | d->m_lineEdit.setTextMargins(QMargins() + style()->pixelMetric(metric: QStyle::PM_ButtonMargin)); |
| 593 | setFocusProxy(&d->m_lineEdit); |
| 594 | |
| 595 | layout->addWidget(&d->m_treeView); |
| 596 | d->m_treeView.setTextElideMode(Qt::ElideLeft); |
| 597 | d->m_treeView.setUniformRowHeights(true); |
| 598 | d->m_treeView.setProperty(name: "_breeze_borders_sides" , value: QVariant::fromValue(value: QFlags(Qt::TopEdge))); |
| 599 | |
| 600 | CommandBarStyleDelegate *delegate = new CommandBarStyleDelegate(this); |
| 601 | ShortcutStyleDelegate *del = new ShortcutStyleDelegate(this); |
| 602 | d->m_treeView.setItemDelegateForColumn(column: KCommandBarModel::Column_Command, delegate); |
| 603 | d->m_treeView.setItemDelegateForColumn(column: KCommandBarModel::Column_Shortcut, delegate: del); |
| 604 | |
| 605 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::returnPressed, context: this, slot: [this]() { |
| 606 | d->slotReturnPressed(q: this); |
| 607 | }); |
| 608 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: &d->m_proxyModel, slot: &CommandBarFilterModel::setFilterString); |
| 609 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: delegate, slot: &CommandBarStyleDelegate::setFilterString); |
| 610 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this]() { |
| 611 | d->m_treeView.viewport()->update(); |
| 612 | d->reselectFirst(); |
| 613 | }); |
| 614 | connect(sender: &d->m_treeView, signal: &QTreeView::clicked, context: this, slot: [this]() { |
| 615 | d->slotReturnPressed(q: this); |
| 616 | }); |
| 617 | |
| 618 | d->m_proxyModel.setSourceModel(&d->m_model); |
| 619 | d->m_treeView.setSortingEnabled(true); |
| 620 | d->m_treeView.setModel(&d->m_proxyModel); |
| 621 | |
| 622 | d->m_treeView.header()->setMinimumSectionSize(0); |
| 623 | d->m_treeView.header()->setStretchLastSection(false); |
| 624 | d->m_treeView.header()->setSectionResizeMode(logicalIndex: KCommandBarModel::Column_Command, mode: QHeaderView::Stretch); |
| 625 | d->m_treeView.header()->setSectionResizeMode(logicalIndex: KCommandBarModel::Column_Shortcut, mode: QHeaderView::ResizeToContents); |
| 626 | |
| 627 | parent->installEventFilter(filterObj: this); |
| 628 | d->m_treeView.installEventFilter(filterObj: this); |
| 629 | d->m_lineEdit.installEventFilter(filterObj: this); |
| 630 | |
| 631 | d->m_treeView.setHeaderHidden(true); |
| 632 | d->m_treeView.setRootIsDecorated(false); |
| 633 | d->m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| 634 | d->m_treeView.setSelectionMode(QTreeView::SingleSelection); |
| 635 | |
| 636 | QLabel *placeholderLabel = new QLabel; |
| 637 | placeholderLabel->setAlignment(Qt::AlignCenter); |
| 638 | placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction); |
| 639 | placeholderLabel->setWordWrap(true); |
| 640 | // To match the size of a level 2 Heading/KTitleWidget |
| 641 | QFont placeholderLabelFont = placeholderLabel->font(); |
| 642 | placeholderLabelFont.setPointSize(qRound(d: placeholderLabelFont.pointSize() * 1.3)); |
| 643 | placeholderLabel->setFont(placeholderLabelFont); |
| 644 | // Match opacity of QML placeholder label component |
| 645 | QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(placeholderLabel); |
| 646 | opacityEffect->setOpacity(0.5); |
| 647 | placeholderLabel->setGraphicsEffect(opacityEffect); |
| 648 | |
| 649 | QHBoxLayout *placeholderLayout = new QHBoxLayout; |
| 650 | placeholderLayout->addWidget(placeholderLabel); |
| 651 | d->m_treeView.setLayout(placeholderLayout); |
| 652 | |
| 653 | connect(sender: &d->m_proxyModel, signal: &CommandBarFilterModel::modelReset, context: this, slot: [this, placeholderLabel]() { |
| 654 | if (d->m_proxyModel.rowCount() > 0) { |
| 655 | placeholderLabel->hide(); |
| 656 | } else { |
| 657 | if (d->m_model.rowCount() == 0) { |
| 658 | placeholderLabel->setText(i18n("No commands to display" )); |
| 659 | } else { |
| 660 | placeholderLabel->setText(i18n("No commands matching the filter" )); |
| 661 | } |
| 662 | placeholderLabel->show(); |
| 663 | } |
| 664 | }); |
| 665 | |
| 666 | setHidden(true); |
| 667 | |
| 668 | // Migrate last used action config to new location |
| 669 | KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General" )); |
| 670 | if (cg.hasKey(key: "CommandBarLastUsedActions" )) { |
| 671 | const QStringList actionNames = cg.readEntry(key: "CommandBarLastUsedActions" , aDefault: QStringList()); |
| 672 | |
| 673 | KConfigGroup stateCg(KSharedConfig::openStateConfig(), QStringLiteral("General" )); |
| 674 | stateCg.writeEntry(QStringLiteral("CommandBarLastUsedActions" ), value: actionNames); |
| 675 | |
| 676 | cg.deleteEntry(QStringLiteral("CommandBarLastUsedActions" )); |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | /* |
| 681 | * Destructor defined here to make unique_ptr work |
| 682 | */ |
| 683 | KCommandBar::~KCommandBar() |
| 684 | { |
| 685 | auto lastUsedActions = d->lastUsedActions(); |
| 686 | auto cfg = KSharedConfig::openStateConfig(); |
| 687 | KConfigGroup cg(cfg, QStringLiteral("General" )); |
| 688 | cg.writeEntry(key: "CommandBarLastUsedActions" , value: lastUsedActions); |
| 689 | |
| 690 | // Explicitly remove installed event filters of children of d-pointer |
| 691 | // class, otherwise while KCommandBar is being torn down, an event could |
| 692 | // fire and the eventFilter() accesses d, which would cause a crash |
| 693 | // bug 452527 |
| 694 | d->m_treeView.removeEventFilter(obj: this); |
| 695 | d->m_lineEdit.removeEventFilter(obj: this); |
| 696 | } |
| 697 | |
| 698 | void KCommandBar::setActions(const QList<ActionGroup> &actions) |
| 699 | { |
| 700 | // First set last used actions in the model |
| 701 | d->setLastUsedActions(); |
| 702 | |
| 703 | d->m_model.refresh(actionGroups: actions); |
| 704 | d->reselectFirst(); |
| 705 | |
| 706 | show(); |
| 707 | setFocus(); |
| 708 | } |
| 709 | |
| 710 | void KCommandBar::show() |
| 711 | { |
| 712 | const QRect boundingRect = getCommandBarBoundingRect(commandBar: this); |
| 713 | |
| 714 | static constexpr int minWidth = 500; |
| 715 | const int maxWidth = boundingRect.width(); |
| 716 | const int preferredWidth = maxWidth / 2.4; |
| 717 | |
| 718 | static constexpr int minHeight = 250; |
| 719 | const int maxHeight = boundingRect.height(); |
| 720 | const int preferredHeight = maxHeight / 2; |
| 721 | |
| 722 | const QSize size{std::min(a: maxWidth, b: std::max(a: preferredWidth, b: minWidth)), std::min(a: maxHeight, b: std::max(a: preferredHeight, b: minHeight))}; |
| 723 | |
| 724 | setFixedSize(size); |
| 725 | |
| 726 | // set the position to the top-center of the parent |
| 727 | // just below the menubar/toolbar (if any) |
| 728 | const QPoint position{boundingRect.center().x() - size.width() / 2, boundingRect.y() + 6}; |
| 729 | move(position); |
| 730 | |
| 731 | QWidget::show(); |
| 732 | } |
| 733 | |
| 734 | bool KCommandBar::eventFilter(QObject *obj, QEvent *event) |
| 735 | { |
| 736 | if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { |
| 737 | QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); |
| 738 | if (obj == &d->m_lineEdit) { |
| 739 | const int key = keyEvent->key(); |
| 740 | const bool forward2list = (key == Qt::Key_Up) || (key == Qt::Key_Down) || (key == Qt::Key_PageUp) || (key == Qt::Key_PageDown); |
| 741 | if (forward2list) { |
| 742 | QCoreApplication::sendEvent(receiver: &d->m_treeView, event); |
| 743 | return true; |
| 744 | } |
| 745 | } else if (obj == &d->m_treeView) { |
| 746 | const int key = keyEvent->key(); |
| 747 | const bool forward2input = (key != Qt::Key_Up) && (key != Qt::Key_Down) && (key != Qt::Key_PageUp) && (key != Qt::Key_PageDown) |
| 748 | && (key != Qt::Key_Tab) && (key != Qt::Key_Backtab); |
| 749 | if (forward2input) { |
| 750 | QCoreApplication::sendEvent(receiver: &d->m_lineEdit, event); |
| 751 | return true; |
| 752 | } |
| 753 | } |
| 754 | |
| 755 | if (keyEvent->key() == Qt::Key_Escape) { |
| 756 | if (event->type() == QEvent::ShortcutOverride) { |
| 757 | // Override the shortcut. We will get KeyPress event for Key_Escape |
| 758 | // later and then hide |
| 759 | return true; |
| 760 | } |
| 761 | hide(); |
| 762 | deleteLater(); |
| 763 | return true; |
| 764 | } |
| 765 | } |
| 766 | |
| 767 | // hide on focus out, if neither input field nor list have focus! |
| 768 | else if (event->type() == QEvent::FocusOut && isVisible() && !(d->m_lineEdit.hasFocus() || d->m_treeView.hasFocus())) { |
| 769 | d->clearLineEdit(); |
| 770 | deleteLater(); |
| 771 | hide(); |
| 772 | return true; |
| 773 | } |
| 774 | |
| 775 | // handle resizing |
| 776 | if (parent() == obj && event->type() == QEvent::Resize) { |
| 777 | show(); |
| 778 | } |
| 779 | |
| 780 | return QWidget::eventFilter(watched: obj, event); |
| 781 | } |
| 782 | // END KCommandBar |
| 783 | |
| 784 | #include "kcommandbar.moc" |
| 785 | #include "moc_kcommandbar.cpp" |
| 786 | |