diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 1bf68b48d..0a6a14d75 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -451,6 +451,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.h
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.cpp
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.h
+ ${QET_DIR}/sources/ElementsCollection/elementpickerpopup.cpp
+ ${QET_DIR}/sources/ElementsCollection/elementpickerpopup.h
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.h
${QET_DIR}/sources/ElementsCollection/elementslocation.cpp
diff --git a/sources/ElementsCollection/elementpickerpopup.cpp b/sources/ElementsCollection/elementpickerpopup.cpp
new file mode 100644
index 000000000..553aa3d5c
--- /dev/null
+++ b/sources/ElementsCollection/elementpickerpopup.cpp
@@ -0,0 +1,331 @@
+/*
+ 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 .
+*/
+#include "elementpickerpopup.h"
+
+#include "elementscollectionwidget.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../qetapp.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);
+
+ 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);
+
+ layout->addWidget(m_search);
+ layout->addWidget(m_view);
+ layout->addWidget(m_hint);
+
+ //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_search->clear();
+ m_model->clear();
+ showPalette();
+
+ adjustSize();
+ QPoint pos = global_pos;
+
+ //Keep it fully on the screen the cursor is on: opening at the cursor
+ //near a right or bottom edge would otherwise push it off.
+ 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);
+ show();
+ m_search->setFocus();
+}
+
+/**
+ @brief ElementPickerPopup::runSearch
+*/
+void ElementPickerPopup::runSearch()
+{
+ m_model->clear();
+ if (!m_source) {
+ return;
+ }
+
+ if (m_search->text().isEmpty()) {
+ showPalette();
+ return;
+ }
+
+ m_palette_mode = false;
+ m_view->setViewMode(QListView::ListMode);
+ m_view->setGridSize(QSize());
+ m_view->setIconSize(QSize(40, 40));
+
+ const QVector 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:
+ hide();
+ return;
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ chooseCurrent();
+ return;
+ case Qt::Key_Down:
+ case Qt::Key_Up:
+ case Qt::Key_PageDown:
+ case Qt::Key_PageUp:
+ if (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();
+
+ 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;
+}
diff --git a/sources/ElementsCollection/elementpickerpopup.h b/sources/ElementsCollection/elementpickerpopup.h
new file mode 100644
index 000000000..ab5213579
--- /dev/null
+++ b/sources/ElementsCollection/elementpickerpopup.h
@@ -0,0 +1,76 @@
+/*
+ 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 .
+*/
+#ifndef ELEMENTPICKERPOPUP_H
+#define ELEMENTPICKERPOPUP_H
+
+#include "elementslocation.h"
+
+#include
+
+class ElementsCollectionWidget;
+class QLineEdit;
+class QListView;
+class QStandardItemModel;
+class QLabel;
+
+/**
+ @brief A cursor-anchored element picker.
+
+ Opens where the mouse is, focused on its search field: type to filter,
+ Enter to place, Esc to close. It is a recall tool, as opposed to the
+ Collections dock, which is a browsing tool.
+
+ It deliberately does not build its own ElementsCollectionModel. Loading the
+ collection is already the slow part of startup and a second copy would
+ double it, so the picker asks the dock's widget to run the query -- see
+ ElementsCollectionWidget::rankedSearch(). That also means the two always
+ agree on results and ranking.
+*/
+class ElementPickerPopup : public QFrame
+{
+ Q_OBJECT
+
+ public:
+ explicit ElementPickerPopup(ElementsCollectionWidget *source,
+ QWidget *parent = nullptr);
+
+ void popUpAt(const QPoint &global_pos);
+
+ signals:
+ /// Emitted when the user picks an element; the popup has closed
+ void elementChosen(const ElementsLocation &location);
+
+ protected:
+ void keyPressEvent(QKeyEvent *event) override;
+
+ private:
+ void runSearch();
+ void chooseCurrent();
+ void showPalette();
+ int loadPaletteDir(const QString &dir_path, const QString &prefix,
+ int depth);
+
+ ElementsCollectionWidget *m_source = nullptr;
+ QLineEdit *m_search = nullptr;
+ QListView *m_view = nullptr;
+ QStandardItemModel *m_model = nullptr;
+ QLabel *m_hint = nullptr;
+ bool m_palette_mode = true;
+};
+
+#endif // ELEMENTPICKERPOPUP_H
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index 540907430..eaedcfc6e 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -22,6 +22,7 @@
#include
#include
#include "ElementsCollection/elementscollectionwidget.h"
+#include "ElementsCollection/elementpickerpopup.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
#include "conductornumexport.h"
@@ -823,6 +824,22 @@ void QETDiagramEditor::setUpActions()
this, &QETDiagramEditor::insertLastElement);
addAction(m_insert_last_element);
+ //Cursor-anchored picker. Insert is unbound anywhere in the tree and
+ //reads correctly for the action, which keeps A free for the far more
+ //frequent "place the same symbol again".
+ m_show_element_picker = new QAction(QET::Icons::Add,
+ tr("Insérer un élément…"), this);
+ m_show_element_picker->setStatusTip(
+ tr("Ouvre le sélecteur d'éléments à la position du curseur",
+ "status bar tip"));
+ m_show_element_picker->setData("show_element_picker");
+ ShortcutManager::instance().registerAction(
+ m_show_element_picker, "diagrameditor.show_element_picker",
+ tr("Éditeur de schémas"), Qt::Key_Insert);
+ connect(m_show_element_picker, &QAction::triggered,
+ this, &QETDiagramEditor::showElementPicker);
+ addAction(m_show_element_picker);
+
m_delete_selection->setStatusTip( tr("Enlève les éléments sélectionnés du folio", "status bar tip"));
m_rotate_selection->setStatusTip( tr("Pivote les éléments et textes sélectionnés", "status bar tip"));
m_rotate_group_selection->setStatusTip( tr("Pivote la sélection comme un groupe autour de son centre, au lieu de chaque élément sur place", "status bar tip"));
@@ -1108,6 +1125,7 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_duplicate);
menu_edition -> addAction(m_configure_duplicate);
menu_edition -> addAction(m_insert_last_element);
+ menu_edition -> addAction(m_show_element_picker);
menu_edition -> addSeparator();
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
//which left them unreachable for anyone working without a mouse: a
@@ -2016,6 +2034,7 @@ void QETDiagramEditor::slot_updateActions()
m_select_actions_group. setEnabled(opened_diagram);
m_add_item_actions_group. setEnabled(editable_project);
m_insert_last_element-> setEnabled(opened_diagram && editable_project && !m_last_inserted_element.isNull());
+ m_show_element_picker-> setEnabled(opened_diagram && editable_project);
m_row_column_actions_group. setEnabled(editable_project);
m_background_color_button-> setEnabled(opened_diagram);
m_draw_grid-> setEnabled(opened_diagram);
@@ -3067,6 +3086,29 @@ void QETDiagramEditor::insertLastElement()
insertElementFromCollection(m_last_inserted_element);
}
+/**
+ @brief QETDiagramEditor::showElementPicker
+ Open the element picker where the mouse is.
+
+ Built lazily: most sessions of the diagram editor never open it, and it
+ holds a list view and a model of its own.
+*/
+void QETDiagramEditor::showElementPicker()
+{
+ if (!currentDiagramView()) {
+ return;
+ }
+
+ if (!m_element_picker)
+ {
+ m_element_picker = new ElementPickerPopup(m_element_collection_widget,
+ this);
+ connect(m_element_picker, &ElementPickerPopup::elementChosen,
+ this, &QETDiagramEditor::insertElementFromCollection);
+ }
+ m_element_picker->popUpAt(QCursor::pos());
+}
+
/**
@brief QETDiagramEditor::generateTerminalBlock
*/
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index 3cb2659ec..657d56d22 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -45,6 +45,7 @@ class ElementsLocation;
class RecentFiles;
class DiagramPropertiesEditorDockWidget;
class ElementsCollectionWidget;
+class ElementPickerPopup;
class AutoNumberingDockWidget;
class TerminalNumberingDialog;
@@ -130,6 +131,7 @@ class QETDiagramEditor : public QETMainWindow
void insertElementFromCollection(const ElementsLocation &location);
void insertLastElement();
void rememberPlacedElement(const ElementsLocation &location);
+ void showElementPicker();
void generateTerminalBlock();
void setWindowedMode();
void setTabbedMode();
@@ -271,6 +273,8 @@ class QETDiagramEditor : public QETMainWindow
*m_qdw_elmt_collection,
*qdw_undo; /// Dock for the undo list
+ QAction *m_show_element_picker = nullptr;
+ ElementPickerPopup *m_element_picker = nullptr; ///< Built on first use
ElementsCollectionWidget *m_element_collection_widget;
/// Last element placed from the collection, for "insert last"
ElementsLocation m_last_inserted_element;