mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 13:24:14 +02:00
47e5a8072b
After customising, the bar now opens at the cursor like every other time, instead of where the customising window was. A size grip in the bar's corner sets its width. The tiles are laid out in rows that wrap at that width, so a bar with many commands and pinned elements becomes a block rather than one long strip; the height follows the rows, and the width is saved. Until the user sets one, the tiles stay on one row, as before. The grip is driven by hand: QSizeGrip asks the window manager to resize, and on X11 a popup is not managed, so nothing would happen. The hint line wraps rather than being cut off at narrow widths. The customising window remembers the size it was left at. Discussion #1033. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
935 lines
29 KiB
C++
935 lines
29 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "elementpickerpopup.h"
|
|
|
|
#include "elementscollectionwidget.h"
|
|
|
|
#include <QAction>
|
|
#include <QGuiApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QListWidget>
|
|
#include <QMenu>
|
|
#include <QPushButton>
|
|
#include <QToolButton>
|
|
#include <QCloseEvent>
|
|
#include <QCursor>
|
|
#include <QKeyEvent>
|
|
#include <QMouseEvent>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QListView>
|
|
#include <QScreen>
|
|
#include <QSizeGrip>
|
|
#include <QStandardItemModel>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QSettings>
|
|
|
|
#include <limits>
|
|
|
|
#include "../qetapp.h"
|
|
#include "../shortcutmanager.h"
|
|
#include "elementslocation.h"
|
|
|
|
//The palette is read from disk each time the picker opens. It is meant as
|
|
//a shortlist, and a large custom collection would otherwise make opening
|
|
//slow and the grid unusable.
|
|
static const int max_palette_entries = 60;
|
|
|
|
/**
|
|
@brief ElementPickerPopup::ElementPickerPopup
|
|
@param source : the collection widget whose model the search runs against
|
|
@param parent
|
|
*/
|
|
ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source,
|
|
QWidget *parent) :
|
|
QFrame(parent, Qt::Popup),
|
|
m_source(source)
|
|
{
|
|
setFrameShape(QFrame::StyledPanel);
|
|
setFrameShadow(QFrame::Raised);
|
|
setMinimumWidth(340);
|
|
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(6, 6, 6, 6);
|
|
layout->setSpacing(4);
|
|
|
|
//Command row, shown when the picker is opened as the shortcut bar
|
|
m_commands = new QWidget(this);
|
|
m_commands_layout = new QHBoxLayout(m_commands);
|
|
m_commands_layout->setContentsMargins(0, 0, 0, 0);
|
|
m_commands_layout->setSpacing(2);
|
|
m_commands->hide();
|
|
//SolidWorks: right-click the bar to customise it
|
|
m_commands->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_commands, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu;
|
|
menu.addAction(tr("Personnaliser la barre…"), this, &ElementPickerPopup::startCustomising);
|
|
menu.exec(m_commands->mapToGlobal(pos));
|
|
});
|
|
|
|
//Customising, shown instead of everything else. Two lists that
|
|
//commands are dragged between: the bar, left to right, and the
|
|
//commands that are not on it.
|
|
m_editor = new QWidget(this);
|
|
auto *editor_layout = new QVBoxLayout(m_editor);
|
|
editor_layout->setContentsMargins(0, 0, 0, 0);
|
|
auto *help = new QLabel(
|
|
tr("Glissez les commandes et les éléments dans la barre, hors de la "
|
|
"barre, ou d'une place à l'autre. Un double-clic fait passer une "
|
|
"commande ou un élément d'une liste à l'autre."), m_editor);
|
|
help->setWordWrap(true);
|
|
m_edit_row = new QListWidget(m_editor);
|
|
m_edit_row->setFlow(QListView::LeftToRight);
|
|
m_edit_row->setWrapping(false);
|
|
m_edit_row->setIconSize(QSize(24, 24));
|
|
m_edit_row->setFixedHeight(44);
|
|
m_edit_available = new QListWidget(m_editor);
|
|
m_edit_available->setIconSize(QSize(20, 20));
|
|
m_edit_available->setMinimumHeight(220);
|
|
for (QListWidget *list : {m_edit_row, m_edit_available}) {
|
|
list->setDragDropMode(QAbstractItemView::DragDrop);
|
|
list->setDefaultDropAction(Qt::MoveAction);
|
|
list->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
}
|
|
//A drop copies the item as it was in the other list, so a command
|
|
//dragged onto the bar would keep its text and one dragged off would
|
|
//have none. Set the text for the list it has landed in.
|
|
//Deferred: a drop inserts the row first and copies the item's data
|
|
//into it afterwards.
|
|
auto relabel = [this](QListWidget *list, bool icon_only) {
|
|
return [this, list, icon_only]() {
|
|
QTimer::singleShot(0, this, [list, icon_only]() {
|
|
for (int i = 0 ; i < list->count() ; ++i) {
|
|
QListWidgetItem *item = list->item(i);
|
|
item->setText(icon_only && !item->icon().isNull()
|
|
? QString() : item->toolTip());
|
|
}
|
|
});
|
|
};
|
|
};
|
|
connect(m_edit_row->model(), &QAbstractItemModel::rowsInserted,
|
|
this, relabel(m_edit_row, true));
|
|
connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted,
|
|
this, relabel(m_edit_available, false));
|
|
//An element dragged off the bar onto the commands is removed from
|
|
//the bar, not listed as a command
|
|
connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted, this, [this]() {
|
|
QTimer::singleShot(0, this, [this]() {
|
|
for (int i = m_edit_available->count() - 1 ; i >= 0 ; --i) {
|
|
if (ShortcutBarSettings::isElement(
|
|
m_edit_available->item(i)->data(Qt::UserRole).toString())) {
|
|
delete m_edit_available->takeItem(i);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
connect(m_edit_row, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
|
|
const QString id = item->data(Qt::UserRole).toString();
|
|
delete item;
|
|
if (!ShortcutBarSettings::isElement(id)) {
|
|
m_edit_available->addItem(barItem(id, false));
|
|
}
|
|
});
|
|
connect(m_edit_available, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
|
|
const QString id = item->data(Qt::UserRole).toString();
|
|
delete item;
|
|
m_edit_row->addItem(barItem(id, true));
|
|
});
|
|
|
|
//Elements to pin: the same ranked search as the picker, dragged
|
|
//onto the bar. Copied, not moved, so a result can be pinned and
|
|
//still be seen in the list.
|
|
m_edit_symbols_box = new QWidget(m_editor);
|
|
auto *symbols_layout = new QVBoxLayout(m_edit_symbols_box);
|
|
symbols_layout->setContentsMargins(0, 0, 0, 0);
|
|
m_edit_symbols_search = new QLineEdit(m_edit_symbols_box);
|
|
m_edit_symbols_search->setPlaceholderText(tr("Rechercher un élément…"));
|
|
m_edit_symbols_search->setClearButtonEnabled(true);
|
|
m_edit_symbols = new QListWidget(m_edit_symbols_box);
|
|
m_edit_symbols->setIconSize(QSize(32, 32));
|
|
m_edit_symbols->setMinimumHeight(220);
|
|
m_edit_symbols->setDragDropMode(QAbstractItemView::DragOnly);
|
|
m_edit_symbols->setDefaultDropAction(Qt::CopyAction);
|
|
m_edit_symbols->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
symbols_layout->addWidget(new QLabel(tr("Éléments :"), m_edit_symbols_box));
|
|
symbols_layout->addWidget(m_edit_symbols_search);
|
|
symbols_layout->addWidget(m_edit_symbols);
|
|
|
|
auto *symbols_timer = new QTimer(this);
|
|
symbols_timer->setSingleShot(true);
|
|
symbols_timer->setInterval(300);
|
|
connect(m_edit_symbols_search, &QLineEdit::textChanged, this,
|
|
[symbols_timer]() { symbols_timer->start(); });
|
|
connect(symbols_timer, &QTimer::timeout, this, &ElementPickerPopup::runSymbolSearch);
|
|
connect(m_edit_symbols, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
|
|
m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true));
|
|
});
|
|
auto *defaults = new QPushButton(tr("Valeurs par défaut"), m_editor);
|
|
auto *cancel = new QPushButton(tr("Annuler"), m_editor);
|
|
auto *done = new QPushButton(tr("Terminé"), m_editor);
|
|
done->setDefault(true);
|
|
connect(defaults, &QPushButton::clicked, this, [this]() {
|
|
fillCustomising(ShortcutBarSettings::defaultIds(m_context));
|
|
});
|
|
connect(cancel, &QPushButton::clicked, this, [this]() { finishCustomising(false); });
|
|
connect(done, &QPushButton::clicked, this, [this]() { finishCustomising(true); });
|
|
auto *editor_buttons = new QHBoxLayout();
|
|
editor_buttons->addWidget(defaults);
|
|
editor_buttons->addStretch();
|
|
editor_buttons->addWidget(cancel);
|
|
editor_buttons->addWidget(done);
|
|
editor_layout->addWidget(help);
|
|
editor_layout->addWidget(new QLabel(tr("Dans la barre :"), m_editor));
|
|
editor_layout->addWidget(m_edit_row);
|
|
auto *lists = new QHBoxLayout();
|
|
auto *commands_column = new QVBoxLayout();
|
|
commands_column->addWidget(new QLabel(tr("Autres commandes :"), m_editor));
|
|
commands_column->addWidget(m_edit_available);
|
|
lists->addLayout(commands_column);
|
|
lists->addWidget(m_edit_symbols_box);
|
|
editor_layout->addLayout(lists);
|
|
editor_layout->addLayout(editor_buttons);
|
|
m_editor->hide();
|
|
|
|
m_search = new QLineEdit(this);
|
|
m_search->setPlaceholderText(tr("Rechercher un élément…"));
|
|
m_search->setClearButtonEnabled(true);
|
|
|
|
m_model = new QStandardItemModel(this);
|
|
m_view = new QListView(this);
|
|
m_view->setModel(m_model);
|
|
m_view->setIconSize(QSize(40, 40));
|
|
m_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
|
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
m_view->setMinimumHeight(260);
|
|
|
|
m_hint = new QLabel(tr("Entrée pour insérer · Échap pour fermer"), this);
|
|
m_hint->setEnabled(false);
|
|
//Wrapped rather than cut off when the bar is made narrow
|
|
m_hint->setWordWrap(true);
|
|
|
|
//The bar's width is the user's: dragging the grip wraps the tiles
|
|
//onto more rows, and the width is kept for next time
|
|
m_grip = new QSizeGrip(this);
|
|
m_grip->setToolTip(tr("Glisser pour changer la largeur de la barre"));
|
|
m_grip->installEventFilter(this);
|
|
auto *hint_row = new QHBoxLayout();
|
|
hint_row->addWidget(m_hint, 1);
|
|
hint_row->addWidget(m_grip, 0, Qt::AlignBottom | Qt::AlignRight);
|
|
|
|
layout->addWidget(m_commands);
|
|
layout->addWidget(m_search);
|
|
layout->addWidget(m_view);
|
|
layout->addLayout(hint_row);
|
|
layout->addWidget(m_editor);
|
|
|
|
//Search as you type, after a short idle: shorter than the dock's
|
|
//500 ms, since the popup is opened and closed in a few seconds, but
|
|
//the collection is large enough that every keystroke would show.
|
|
auto *timer = new QTimer(this);
|
|
timer->setSingleShot(true);
|
|
timer->setInterval(300);
|
|
connect(m_search, &QLineEdit::textChanged, this,
|
|
[timer]() { timer->start(); });
|
|
connect(timer, &QTimer::timeout, this, &ElementPickerPopup::runSearch);
|
|
|
|
connect(m_view, &QListView::doubleClicked, this,
|
|
[this](const QModelIndex &) { chooseCurrent(); });
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::popUpAt
|
|
Show the picker at @a global_pos, kept on screen, with the search field
|
|
focused and any previous query cleared.
|
|
@param global_pos
|
|
*/
|
|
void ElementPickerPopup::popUpAt(const QPoint &global_pos)
|
|
{
|
|
m_bar_mode = false;
|
|
setCommands({});
|
|
show(global_pos);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::popUpShortcutBar
|
|
Show the picker at @a global_pos with the shortcut bar's commands for
|
|
@a context above it.
|
|
@param global_pos
|
|
@param context
|
|
*/
|
|
void ElementPickerPopup::popUpShortcutBar(const QPoint &global_pos,
|
|
ShortcutBarSettings::Context context)
|
|
{
|
|
m_bar_mode = true;
|
|
m_context = context;
|
|
setCommands(ShortcutBarSettings::ids(context));
|
|
show(global_pos);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::commandAction
|
|
@return the action registered under @a id by the window this popup
|
|
belongs to, or nullptr
|
|
*/
|
|
QAction *ElementPickerPopup::commandAction(const QString &id) const
|
|
{
|
|
return ShortcutManager::instance().action(id, parentWidget());
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::show
|
|
Reset the search and show at @a global_pos, kept on screen.
|
|
*/
|
|
void ElementPickerPopup::show(const QPoint &global_pos)
|
|
{
|
|
m_search->clear();
|
|
m_model->clear();
|
|
showPalette();
|
|
|
|
keepOnScreen(global_pos);
|
|
QFrame::show();
|
|
m_search->setFocus();
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::keepOnScreen
|
|
Fit the popup to its contents and move it to @a global_pos, kept fully on
|
|
the screen that point is on: opening at the cursor near a right or bottom
|
|
edge would otherwise push it off.
|
|
*/
|
|
void ElementPickerPopup::keepOnScreen(const QPoint &global_pos)
|
|
{
|
|
adjustSize();
|
|
//The sizes the user gave the customising window and the bar
|
|
if (m_customising) {
|
|
const QSize size = ShortcutBarSettings::editorSize();
|
|
if (size.isValid()) {
|
|
resize(size.expandedTo(minimumSizeHint()));
|
|
}
|
|
} else if (m_bar_mode && ShortcutBarSettings::barWidth() > 0) {
|
|
resize(qMax(ShortcutBarSettings::barWidth(), minimumSizeHint().width()),
|
|
height());
|
|
}
|
|
QPoint pos = global_pos;
|
|
|
|
if (QScreen *screen = QGuiApplication::screenAt(global_pos)) {
|
|
const QRect avail = screen->availableGeometry();
|
|
pos.setX(qBound(avail.left(),
|
|
pos.x(), avail.right() - width()));
|
|
pos.setY(qBound(avail.top(),
|
|
pos.y(), avail.bottom() - height()));
|
|
}
|
|
|
|
move(pos);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::setCommands
|
|
Show @a ids as a row of buttons above the search field, or hide the row
|
|
when there are none. A disabled command keeps its place, greyed out, so
|
|
the row looks the same every time for a given selection. An id no live
|
|
action carries, or a pinned element that no longer exists, is skipped.
|
|
|
|
Clicking a button closes the picker first, then triggers the action: a
|
|
command such as "add a line" starts a mode on the folio, which needs the
|
|
focus the popup holds. A pinned element is placed, as if chosen from the
|
|
picker.
|
|
@param ids : command ids and pinned elements' collection paths
|
|
*/
|
|
void ElementPickerPopup::setCommands(const QStringList &ids)
|
|
{
|
|
while (QLayoutItem *item = m_commands_layout->takeAt(0)) {
|
|
delete item->widget();
|
|
delete item;
|
|
}
|
|
qDeleteAll(m_tiles);
|
|
m_tiles.clear();
|
|
m_tile_rows = new QVBoxLayout();
|
|
m_tile_rows->setSpacing(2);
|
|
m_commands_layout->addLayout(m_tile_rows);
|
|
|
|
for (const QString &id : ids)
|
|
{
|
|
if (ShortcutBarSettings::isElement(id))
|
|
{
|
|
const ElementsLocation location(id);
|
|
if (!location.exist()) {
|
|
continue;
|
|
}
|
|
auto *button = new QToolButton(m_commands);
|
|
button->setAutoRaise(true);
|
|
button->setIconSize(QSize(24, 24));
|
|
button->setIcon(location.icon());
|
|
button->setToolTip(location.name());
|
|
button->setFocusPolicy(Qt::NoFocus);
|
|
connect(button, &QToolButton::clicked, this, [this, location]() {
|
|
hide();
|
|
emit elementChosen(location);
|
|
});
|
|
m_tiles << button;
|
|
continue;
|
|
}
|
|
|
|
QAction *action = commandAction(id);
|
|
if (!action) {
|
|
continue;
|
|
}
|
|
auto *button = new QToolButton(m_commands);
|
|
button->setAutoRaise(true);
|
|
button->setIconSize(QSize(24, 24));
|
|
const QString text = action->text().remove(QLatin1Char('&'));
|
|
if (action->icon().isNull()) {
|
|
button->setText(text);
|
|
button->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
|
} else {
|
|
button->setIcon(action->icon());
|
|
}
|
|
const QKeySequence key = action->shortcut();
|
|
button->setToolTip(key.isEmpty()
|
|
? text
|
|
: QStringLiteral("%1 (%2)").arg(
|
|
text, key.toString(QKeySequence::NativeText)));
|
|
button->setEnabled(action->isEnabled());
|
|
button->setFocusPolicy(Qt::NoFocus);
|
|
connect(button, &QToolButton::clicked, this, [this, action]() {
|
|
hide();
|
|
action->trigger();
|
|
});
|
|
m_tiles << button;
|
|
}
|
|
m_commands_layout->addStretch();
|
|
if (m_bar_mode)
|
|
{
|
|
auto *customise = new QToolButton(m_commands);
|
|
customise->setAutoRaise(true);
|
|
customise->setText(QStringLiteral("…"));
|
|
customise->setToolTip(tr("Personnaliser la barre…"));
|
|
customise->setFocusPolicy(Qt::NoFocus);
|
|
connect(customise, &QToolButton::clicked, this, &ElementPickerPopup::startCustomising);
|
|
m_commands_layout->addWidget(customise, 0, Qt::AlignTop);
|
|
}
|
|
layoutTiles(ShortcutBarSettings::barWidth());
|
|
m_grip->setVisible(m_bar_mode);
|
|
//An empty bar still shows, so it can be customised back
|
|
m_commands->setVisible(m_bar_mode || !ids.isEmpty());
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::layoutTiles
|
|
Put the bar's tiles in rows no wider than the bar at @a bar_width, in
|
|
their order, as many per row as fit. A @a bar_width of 0 or less, the
|
|
width the user never set, keeps them on one row.
|
|
*/
|
|
void ElementPickerPopup::layoutTiles(int bar_width)
|
|
{
|
|
if (!m_tile_rows) {
|
|
return;
|
|
}
|
|
//Emptying a row layout leaves its buttons alone; they go into the
|
|
//new rows below
|
|
while (QLayoutItem *item = m_tile_rows->takeAt(0)) {
|
|
delete item->layout();
|
|
}
|
|
|
|
//Room left beside the tiles: the popup's margins and the "…" button
|
|
const int limit = bar_width > 0
|
|
? bar_width - 12 - 30
|
|
: std::numeric_limits<int>::max();
|
|
QHBoxLayout *row = nullptr;
|
|
int used = 0;
|
|
for (QToolButton *tile : std::as_const(m_tiles))
|
|
{
|
|
const int w = tile->sizeHint().width() + m_tile_rows->spacing();
|
|
if (!row || (used + w > limit && used > 0)) {
|
|
row = new QHBoxLayout();
|
|
row->setSpacing(2);
|
|
row->addStretch();
|
|
m_tile_rows->addLayout(row);
|
|
used = 0;
|
|
}
|
|
//Before the stretch, which stays last so the row is left-aligned
|
|
row->insertWidget(row->count() - 1, tile);
|
|
used += w;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::eventFilter
|
|
Drive the size grip by hand. QSizeGrip asks the window manager to do the
|
|
resize, and a popup is a window the window manager does not manage, so
|
|
on X11 nothing would happen. Only the width follows the mouse: the tiles
|
|
re-flow into rows as it changes, the height follows the rows, and the
|
|
width is saved when the grip is let go.
|
|
*/
|
|
bool ElementPickerPopup::eventFilter(QObject *watched, QEvent *event)
|
|
{
|
|
if (watched != m_grip) {
|
|
return QFrame::eventFilter(watched, event);
|
|
}
|
|
switch (event->type())
|
|
{
|
|
case QEvent::MouseButtonPress: {
|
|
auto *me = static_cast<QMouseEvent *>(event);
|
|
if (me->button() != Qt::LeftButton) {
|
|
break;
|
|
}
|
|
m_grip_active = true;
|
|
m_grip_press_x = me->globalPosition().toPoint().x();
|
|
m_grip_press_width = width();
|
|
return true;
|
|
}
|
|
case QEvent::MouseMove: {
|
|
if (!m_grip_active) {
|
|
break;
|
|
}
|
|
auto *me = static_cast<QMouseEvent *>(event);
|
|
const int w = qMax(minimumWidth(), m_grip_press_width
|
|
+ me->globalPosition().toPoint().x() - m_grip_press_x);
|
|
layoutTiles(w);
|
|
layout()->activate();
|
|
resize(w, sizeHint().height());
|
|
return true;
|
|
}
|
|
case QEvent::MouseButtonRelease:
|
|
if (!m_grip_active) {
|
|
break;
|
|
}
|
|
m_grip_active = false;
|
|
ShortcutBarSettings::setBarWidth(width());
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return QFrame::eventFilter(watched, event);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::barItem
|
|
@return a list item for @a id, a command or a pinned element: icon only,
|
|
for the bar row, or icon and text, for the other lists. The id is in
|
|
UserRole.
|
|
*/
|
|
QListWidgetItem *ElementPickerPopup::barItem(const QString &id, bool icon_only) const
|
|
{
|
|
QString text = id;
|
|
QIcon icon;
|
|
if (ShortcutBarSettings::isElement(id)) {
|
|
const ElementsLocation location(id);
|
|
if (location.exist()) {
|
|
text = location.name();
|
|
icon = location.icon();
|
|
}
|
|
} else if (QAction *action = commandAction(id)) {
|
|
text = action->text().remove(QLatin1Char('&'));
|
|
icon = action->icon();
|
|
}
|
|
auto *item = new QListWidgetItem();
|
|
item->setData(Qt::UserRole, id);
|
|
item->setToolTip(text);
|
|
if (!icon.isNull()) {
|
|
item->setIcon(icon);
|
|
}
|
|
if (!icon_only || item->icon().isNull()) {
|
|
item->setText(text);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::startCustomising
|
|
Turn the bar into a small window holding the two lists. A Qt::Popup
|
|
closes as soon as the mouse is pressed outside it and keeps the mouse
|
|
grab during a drag, so the window is re-shown as a tool window for the
|
|
time of the edit.
|
|
*/
|
|
void ElementPickerPopup::startCustomising()
|
|
{
|
|
const QPoint where = pos();
|
|
m_customising = true;
|
|
hide();
|
|
setWindowFlags(Qt::Tool);
|
|
setWindowTitle(tr("Personnaliser la barre de raccourcis : %1")
|
|
.arg(ShortcutBarSettings::title(m_context)));
|
|
|
|
fillCustomising(ShortcutBarSettings::ids(m_context));
|
|
m_commands->hide();
|
|
setPickerVisible(false);
|
|
m_grip->hide();
|
|
//Elements are pinned to the empty-folio bar only: with something
|
|
//selected, the bar is for acting on it, not for adding more
|
|
m_edit_symbols_search->clear();
|
|
m_edit_symbols->clear();
|
|
m_edit_symbols_box->setVisible(m_context == ShortcutBarSettings::Canvas);
|
|
m_editor->show();
|
|
|
|
keepOnScreen(where);
|
|
QFrame::show();
|
|
activateWindow();
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::fillCustomising
|
|
Put @a ids on the bar row, and every other available command in the
|
|
list below it.
|
|
*/
|
|
void ElementPickerPopup::fillCustomising(const QStringList &ids)
|
|
{
|
|
m_edit_row->clear();
|
|
m_edit_available->clear();
|
|
for (const QString &id : ids) {
|
|
m_edit_row->addItem(barItem(id, true));
|
|
}
|
|
for (const QString &id : ShortcutBarSettings::availableIds()) {
|
|
if (!ids.contains(id)) {
|
|
m_edit_available->addItem(barItem(id, false));
|
|
}
|
|
}
|
|
m_edit_available->sortItems();
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::runSymbolSearch
|
|
Fill the customising window's element list with the ranked hits for its
|
|
search field. Elements embedded in a project are left out: their path
|
|
names the project as it is loaded now, so it would not find them again
|
|
in a later session.
|
|
*/
|
|
void ElementPickerPopup::runSymbolSearch()
|
|
{
|
|
m_edit_symbols->clear();
|
|
if (!m_source) {
|
|
return;
|
|
}
|
|
const QVector<ElementSearchHit> hits =
|
|
m_source->rankedSearch(m_edit_symbols_search->text());
|
|
for (const ElementSearchHit &hit : hits)
|
|
{
|
|
if (ElementsLocation(hit.path).isProject()) {
|
|
continue;
|
|
}
|
|
auto *item = new QListWidgetItem(hit.icon, hit.name);
|
|
item->setData(Qt::UserRole, hit.path);
|
|
item->setToolTip(QStringLiteral("%1\n%2").arg(hit.name, hit.folder));
|
|
m_edit_symbols->addItem(item);
|
|
if (m_edit_symbols->count() >= max_palette_entries) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::finishCustomising
|
|
Leave the edit, saving the bar row when @a save, and show the bar again
|
|
at the cursor so the result can be seen and used straight away. The
|
|
window's size is kept either way.
|
|
*/
|
|
void ElementPickerPopup::finishCustomising(bool save)
|
|
{
|
|
if (save)
|
|
{
|
|
QStringList ids;
|
|
for (int i = 0 ; i < m_edit_row->count() ; ++i) {
|
|
ids << m_edit_row->item(i)->data(Qt::UserRole).toString();
|
|
}
|
|
ShortcutBarSettings::setIds(m_context, ids);
|
|
}
|
|
|
|
ShortcutBarSettings::setEditorSize(size());
|
|
m_customising = false;
|
|
hide();
|
|
m_editor->hide();
|
|
setPickerVisible(true);
|
|
setWindowFlags(Qt::Popup);
|
|
setWindowTitle(QString());
|
|
//At the cursor, like any other opening of the bar
|
|
popUpShortcutBar(QCursor::pos(), m_context);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::closeEvent
|
|
Closing the customising window from its title bar cancels the edit,
|
|
like the Annuler button, rather than leaving the bar half-edited.
|
|
*/
|
|
void ElementPickerPopup::closeEvent(QCloseEvent *event)
|
|
{
|
|
if (m_customising) {
|
|
event->ignore();
|
|
finishCustomising(false);
|
|
return;
|
|
}
|
|
QFrame::closeEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::setPickerVisible
|
|
Show or hide the search field, the results and the hint line.
|
|
*/
|
|
void ElementPickerPopup::setPickerVisible(bool visible)
|
|
{
|
|
m_search->setVisible(visible);
|
|
m_view->setVisible(visible);
|
|
m_hint->setVisible(visible);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::runSearch
|
|
*/
|
|
void ElementPickerPopup::runSearch()
|
|
{
|
|
m_model->clear();
|
|
if (!m_source) {
|
|
return;
|
|
}
|
|
|
|
if (m_search->text().isEmpty()) {
|
|
const bool was_hidden = m_view->isHidden();
|
|
showPalette();
|
|
if (was_hidden != m_view->isHidden()) {
|
|
keepOnScreen(pos());
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (m_view->isHidden()) {
|
|
m_view->show();
|
|
keepOnScreen(pos());
|
|
}
|
|
m_palette_mode = false;
|
|
m_view->setViewMode(QListView::ListMode);
|
|
m_view->setGridSize(QSize());
|
|
m_view->setIconSize(QSize(40, 40));
|
|
|
|
const QVector<ElementSearchHit> hits = m_source->rankedSearch(m_search->text());
|
|
for (const ElementSearchHit &hit : hits)
|
|
{
|
|
auto *item = new QStandardItem(hit.name);
|
|
item->setIcon(hit.icon);
|
|
item->setEditable(false);
|
|
item->setToolTip(hit.folder);
|
|
item->setData(hit.path, Qt::UserRole + 2);
|
|
m_model->appendRow(item);
|
|
}
|
|
|
|
//Preselect the best hit so Enter works straight from the search field
|
|
//without arrowing down first -- that is the whole point of the popup.
|
|
if (m_model->rowCount()) {
|
|
m_view->setCurrentIndex(m_model->index(0, 0));
|
|
}
|
|
m_hint->setText(hits.isEmpty()
|
|
? tr("Aucun résultat")
|
|
: tr("Entrée pour insérer · Échap pour fermer"));
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::chooseCurrent
|
|
Emit the highlighted element and close.
|
|
*/
|
|
void ElementPickerPopup::chooseCurrent()
|
|
{
|
|
const QModelIndex index = m_view->currentIndex();
|
|
if (!index.isValid()) {
|
|
return;
|
|
}
|
|
const QString path = index.data(Qt::UserRole + 2).toString();
|
|
if (path.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
ElementsLocation location(path);
|
|
if (!location.exist()) {
|
|
return;
|
|
}
|
|
|
|
//Close before emitting: placement mode wants the focus, and a popup
|
|
//still up would keep the grab.
|
|
hide();
|
|
emit elementChosen(location);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::keyPressEvent
|
|
Drive the list from the search field, so the hands never leave the
|
|
keyboard: Up/Down move the selection, Enter places, Esc closes.
|
|
@param event
|
|
*/
|
|
void ElementPickerPopup::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
switch (event->key())
|
|
{
|
|
case Qt::Key_Escape:
|
|
if (m_customising) {
|
|
finishCustomising(false);
|
|
} else {
|
|
hide();
|
|
}
|
|
return;
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
if (m_customising && m_edit_symbols_search->hasFocus()) {
|
|
//Enter in the element search pins the best hit rather
|
|
//than closing the window. Searched now, not on the
|
|
//timer, so a quick Enter after typing gets this text.
|
|
runSymbolSearch();
|
|
if (QListWidgetItem *item = m_edit_symbols->item(0)) {
|
|
m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true));
|
|
}
|
|
} else if (m_customising) {
|
|
finishCustomising(true);
|
|
} else {
|
|
chooseCurrent();
|
|
}
|
|
return;
|
|
case Qt::Key_Down:
|
|
case Qt::Key_Up:
|
|
case Qt::Key_PageDown:
|
|
case Qt::Key_PageUp:
|
|
if (!m_customising && m_model->rowCount()) {
|
|
//Forwarded rather than focus-switched, so typing carries
|
|
//on going to the search field.
|
|
QCoreApplication::sendEvent(m_view, event);
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
QFrame::keyPressEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::showPalette
|
|
With an empty search field, show the quick palette as an icon grid.
|
|
|
|
The palette is a folder, not a config file. QET already has a user-owned,
|
|
drag-populated, icon-rendering collection -- so a palette is just a
|
|
directory whose subfolders are categories and whose contents are entries.
|
|
That gives set-up (drag into the folder), ordering (the 01_/02_ filename
|
|
convention the shipped collection already uses), team sharing
|
|
(companyElementsDir) and version control for free, with no schema, parser,
|
|
merge rules or "reset to defaults" to build or maintain.
|
|
|
|
Only the path lives in QSettings, defaulting to the user's custom
|
|
collection.
|
|
*/
|
|
void ElementPickerPopup::showPalette()
|
|
{
|
|
m_palette_mode = true;
|
|
m_model->clear();
|
|
|
|
//Elements pinned to the bar replace the folder grid: the bar already
|
|
//shows the user's shortlist, and a second one below it is noise.
|
|
//The search still works; the list comes back as soon as one types.
|
|
if (m_bar_mode && ShortcutBarSettings::hasElements(m_context)) {
|
|
m_view->hide();
|
|
m_hint->setText(tr("Tapez pour rechercher un élément · Échap pour fermer"));
|
|
return;
|
|
}
|
|
m_view->show();
|
|
|
|
m_view->setViewMode(QListView::IconMode);
|
|
m_view->setIconSize(QSize(48, 48));
|
|
m_view->setGridSize(QSize(92, 84));
|
|
m_view->setResizeMode(QListView::Adjust);
|
|
m_view->setWordWrap(true);
|
|
m_view->setMovement(QListView::Static);
|
|
|
|
QSettings settings;
|
|
const QString path = settings.value(
|
|
QStringLiteral("elementscollection/palette-path"),
|
|
QETApp::customElementsDir()).toString();
|
|
|
|
const int count = loadPaletteDir(path, QString(), 0);
|
|
|
|
if (!count) {
|
|
m_hint->setText(
|
|
tr("Palette vide — glissez des éléments dans votre collection "
|
|
"personnelle, ou tapez pour rechercher"));
|
|
} else {
|
|
m_hint->setText(tr("Entrée pour insérer · Échap pour fermer"));
|
|
m_view->setCurrentIndex(m_model->index(0, 0));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief ElementPickerPopup::loadPaletteDir
|
|
Add every .elmt under @a dir_path to the grid, recursing into subfolders.
|
|
@param dir_path
|
|
@param prefix : folder path so far, shown as the entry's tooltip
|
|
@param depth : recursion guard -- a palette is a shortlist, and the whole
|
|
shipped collection would not be usable as a grid anyway
|
|
@return how many entries were added
|
|
*/
|
|
int ElementPickerPopup::loadPaletteDir(const QString &dir_path,
|
|
const QString &prefix, int depth)
|
|
{
|
|
if (depth > 3 || m_model->rowCount() >= max_palette_entries) {
|
|
return 0;
|
|
}
|
|
QDir dir(dir_path);
|
|
if (!dir.exists()) {
|
|
return 0;
|
|
}
|
|
|
|
int added = 0;
|
|
//Sorted by name, which is what makes the 01_/02_ filename convention
|
|
//work as the ordering mechanism.
|
|
const QFileInfoList entries = dir.entryInfoList(
|
|
QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
|
|
|
for (const QFileInfo &info : entries)
|
|
{
|
|
if (m_model->rowCount() >= max_palette_entries) {
|
|
break;
|
|
}
|
|
if (info.isDir()) {
|
|
added += loadPaletteDir(info.absoluteFilePath(),
|
|
prefix.isEmpty()
|
|
? info.fileName()
|
|
: prefix + QStringLiteral(" / ")
|
|
+ info.fileName(),
|
|
depth + 1);
|
|
continue;
|
|
}
|
|
if (info.suffix().compare(QStringLiteral("elmt"),
|
|
Qt::CaseInsensitive) != 0) {
|
|
continue;
|
|
}
|
|
|
|
ElementsLocation location(info.absoluteFilePath());
|
|
if (!location.exist()) {
|
|
continue;
|
|
}
|
|
|
|
auto *item = new QStandardItem(location.name().isEmpty()
|
|
? info.completeBaseName()
|
|
: location.name());
|
|
item->setIcon(location.icon());
|
|
item->setEditable(false);
|
|
item->setToolTip(prefix.isEmpty() ? info.completeBaseName() : prefix);
|
|
item->setData(info.absoluteFilePath(), Qt::UserRole + 2);
|
|
item->setTextAlignment(Qt::AlignHCenter | Qt::AlignTop);
|
|
m_model->appendRow(item);
|
|
++added;
|
|
}
|
|
return added;
|
|
}
|