Merge branch 'master' into feat/uuid-shapes-texts-images

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-27 20:48:54 +13:00
31 changed files with 3786 additions and 55 deletions
@@ -0,0 +1,934 @@
/*
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;
}
@@ -0,0 +1,121 @@
/*
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/>.
*/
#ifndef ELEMENTPICKERPOPUP_H
#define ELEMENTPICKERPOPUP_H
#include "elementslocation.h"
#include "../shortcutbarsettings.h"
#include <QFrame>
class ElementsCollectionWidget;
class QLineEdit;
class QListView;
class QStandardItemModel;
class QLabel;
class QAction;
class QHBoxLayout;
class QListWidget;
class QListWidgetItem;
class QSizeGrip;
class QToolButton;
class QVBoxLayout;
/**
@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);
void popUpShortcutBar(const QPoint &global_pos,
ShortcutBarSettings::Context context);
signals:
/// Emitted when the user picks an element; the popup has closed
void elementChosen(const ElementsLocation &location);
protected:
void keyPressEvent(QKeyEvent *event) override;
void closeEvent(QCloseEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void runSearch();
void chooseCurrent();
void showPalette();
void show(const QPoint &global_pos);
void setCommands(const QStringList &ids);
void layoutTiles(int bar_width);
QAction *commandAction(const QString &id) const;
QListWidgetItem *barItem(const QString &id, bool icon_only) const;
void startCustomising();
void fillCustomising(const QStringList &ids);
void finishCustomising(bool save);
void runSymbolSearch();
void setPickerVisible(bool visible);
void keepOnScreen(const QPoint &global_pos);
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;
QWidget *m_commands = nullptr;
QHBoxLayout *m_commands_layout = nullptr;
/// The bar's tiles, commands and elements, in order
QList<QToolButton *> m_tiles;
QVBoxLayout *m_tile_rows = nullptr;
QSizeGrip *m_grip = nullptr;
/// The size grip is being dragged, from this x at this width
bool m_grip_active = false;
int m_grip_press_x = 0;
int m_grip_press_width = 0;
/// Opened as the shortcut bar, as opposed to the plain picker
bool m_bar_mode = false;
ShortcutBarSettings::Context m_context = ShortcutBarSettings::Canvas;
/// Customising the bar in place
bool m_customising = false;
QWidget *m_editor = nullptr;
QListWidget *m_edit_row = nullptr;
QListWidget *m_edit_available = nullptr;
/// Element search, for pinning elements to the bar
QWidget *m_edit_symbols_box = nullptr;
QLineEdit *m_edit_symbols_search = nullptr;
QListWidget *m_edit_symbols = nullptr;
bool m_palette_mode = true;
};
#endif // ELEMENTPICKERPOPUP_H
@@ -35,6 +35,12 @@
#include <QCheckBox>
#include <QDesktopServices>
#include <QSettings>
#include <QShortcut>
#include <QListView>
#include <QStandardItemModel>
#include <QSet>
#include <algorithm>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
@@ -49,6 +55,32 @@
#include <QStatusBar>
#include <QLineEdit>
namespace {
/**
@brief The SearchResultsView class
The flat list of search results. Rows are not backed by the collection
model, so the drag is built here from the path each row carries, with
the same content and pixmap as a drag from the tree.
*/
class SearchResultsView : public QListView
{
public:
using QListView::QListView;
protected:
void startDrag(Qt::DropActions supportedActions) override
{
const QString path =
currentIndex().data(Qt::UserRole + 2).toString();
if (path.isEmpty()) {
QListView::startDrag(supportedActions);
return;
}
ElementsTreeView::execElementDrag(this, ElementsLocation(path));
}
};
}
/**
@brief ElementsCollectionWidget::ElementsCollectionWidget
Default constructor.
@@ -217,8 +249,26 @@ void ElementsCollectionWidget::setUpWidget()
m_tab_widget->addTab(m_tree_view, tr("Collections"));
m_tab_widget->addTab(m_macros_tree_view, tr("Modèles"));
//Flat ranked search results.
//The tree search hides non-matching rows, so hits stay scattered
//through five levels of expanded folders -- searching "diode" leaves
//you scrolling a tree to find them. This shows the same matches as a
//ranked list instead, and takes the tab widget's place while a search
//is active.
m_search_model = new QStandardItemModel(this);
m_search_results = new SearchResultsView(this);
m_search_results->setModel(m_search_model);
m_search_results->setDragDropMode(QAbstractItemView::DragOnly);
m_search_results->setIconSize(QSize(50, 50));
m_search_results->setUniformItemSizes(false);
m_search_results->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_search_results->setContextMenuPolicy(Qt::CustomContextMenu);
m_search_results->setMouseTracking(true);
m_search_results->hide();
m_main_vlayout->addWidget(m_search_field);
m_main_vlayout->addWidget(m_tab_widget);
m_main_vlayout->addWidget(m_search_results);
m_progress_bar = new QProgressBar(this);
m_progress_bar->setFormat(QObject::tr("chargement %p% (%v sur %m)"));
@@ -266,15 +316,7 @@ void ElementsCollectionWidget::setUpConnection()
this, &ElementsCollectionWidget::dirProperties);
connect(m_tree_view, &QTreeView::doubleClicked,
[this](const QModelIndex &index)
{
this->m_index_at_context_menu = index ;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (eci && eci->collectionPath().endsWith(".qetmak")) {
return; // Do nothing on double click for macros
}
this->editElement();
});
[this](const QModelIndex &index) { this->activateIndex(index); });
connect(m_tree_view, &QTreeView::entered,
[this] (const QModelIndex &index) {
@@ -284,19 +326,58 @@ void ElementsCollectionWidget::setUpConnection()
qde->statusBar()->showMessage(eci->localName());
});
//Enter on the highlighted item does the same as a double click, so a
//run of elements can be placed without leaving the keyboard. Bound as
//a shortcut on the view rather than by reimplementing keyPressEvent,
//which would mean subclassing ElementsTreeView for one key.
for (const auto key : {Qt::Key_Return, Qt::Key_Enter}) {
auto *sc = new QShortcut(QKeySequence(key), m_tree_view);
sc->setContext(Qt::WidgetShortcut);
connect(sc, &QShortcut::activated, this, [this]() {
this->activateIndex(m_tree_view->currentIndex());
});
}
//The flat results list carries the collection path directly, so it
//does not go through activateIndex() -- there is no tree index behind
//a row to look an ElementCollectionItem up from.
auto place_from_results = [this](const QModelIndex &index) {
const QString path = index.data(Qt::UserRole + 2).toString();
if (path.isEmpty()) {
return;
}
ElementsLocation location(path);
if (location.exist()) {
emit insertElementRequested(location);
}
};
connect(m_search_results, &QListView::doubleClicked, this, place_from_results);
for (const auto key : {Qt::Key_Return, Qt::Key_Enter}) {
auto *sc = new QShortcut(QKeySequence(key), m_search_results);
sc->setContext(Qt::WidgetShortcut);
connect(sc, &QShortcut::activated, this, [this, place_from_results]() {
place_from_results(m_search_results->currentIndex());
});
}
//Down from the search field moves into the results, so the whole
//type-then-place run happens without touching the mouse.
auto *to_results = new QShortcut(QKeySequence(Qt::Key_Down), m_search_field);
to_results->setContext(Qt::WidgetShortcut);
connect(to_results, &QShortcut::activated, this, [this]() {
if (!m_search_results->isVisible() || !m_search_model->rowCount()) {
return;
}
m_search_results->setFocus();
if (!m_search_results->currentIndex().isValid()) {
m_search_results->setCurrentIndex(m_search_model->index(0, 0));
}
});
connect(m_macros_tree_view, &QTreeView::customContextMenuRequested,
this, &ElementsCollectionWidget::customContextMenu);
connect(m_macros_tree_view, &QTreeView::doubleClicked,
[this](const QModelIndex &index)
{
this->m_index_at_context_menu = index ;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (eci && eci->collectionPath().endsWith(".qetmak")) {
return; // Do nothing on double click for macros
}
this->editElement();
});
[this](const QModelIndex &index) { this->activateIndex(index); });
connect(m_macros_tree_view, &QTreeView::entered,
[this] (const QModelIndex &index) {
@@ -405,6 +486,60 @@ void ElementsCollectionWidget::openDir()
}
/**
@brief ElementsCollectionWidget::activateIndex
What a double click (or Enter) on @a index does.
Historically this opened the element editor, which is the slowest action
available on a symbol you are most likely about to place. Placing is now
the default and editing has moved to the context menu, where it already
was. The old behaviour is preserved behind a preference for anyone who
relies on it.
@param index
*/
void ElementsCollectionWidget::activateIndex(const QModelIndex &index)
{
m_index_at_context_menu = index;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (!eci) {
return;
}
//Macros are placed, never edited, and were already skipped here.
const bool is_macro = eci->collectionPath().endsWith(".qetmak");
QSettings settings;
const bool insert = settings.value(
QStringLiteral("elementscollection/double-click-inserts"), true).toBool();
if (insert) {
insertCurrentElement();
return;
}
if (!is_macro) {
editElement();
}
}
/**
@brief ElementsCollectionWidget::insertCurrentElement
Ask for the current item to be placed on the folio.
*/
void ElementsCollectionWidget::insertCurrentElement()
{
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_index_at_context_menu);
if (!(eci && eci->isElement())) {
return;
}
ElementsLocation location(eci->collectionPath());
if (!location.exist()) {
return;
}
emit insertElementRequested(location);
}
/**
@brief ElementsCollectionWidget::editElement
Edit the element represented by the current selected item
@@ -415,10 +550,19 @@ void ElementsCollectionWidget::editElement()
if ( !(eci && eci->isElement()) ) return;
// Prevent the element editor from opening for macros
if (eci->collectionPath().endsWith(".qetmak")) return;
editLocation(ElementsLocation(eci->collectionPath()));
}
ElementsLocation location(eci->collectionPath());
/**
@brief ElementsCollectionWidget::editLocation
Open the element editor on @a location. Macros have no editor and are
ignored.
@param location
*/
void ElementsCollectionWidget::editLocation(const ElementsLocation &location)
{
// Prevent the element editor from opening for macros
if (!location.exist() || location.path().endsWith(".qetmak")) return;
QETApp *app = QETApp::instance();
app->openElementLocations(QList<ElementsLocation>() << location);
@@ -932,6 +1076,7 @@ void ElementsCollectionWidget::search()
//Reset the search
if (text.isEmpty())
{
clearFlatResults();
QModelIndex current_index = m_tree_view->currentIndex();
m_tree_view->reset();
@@ -958,22 +1103,154 @@ void ElementsCollectionWidget::search()
return;
}
hideCollection(true);
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
QModelIndexList match_index;
for (QString txt : text_list) {
match_index << m_model->match(m_showed_index.isValid()
? m_model->index(0,0,m_showed_index)
: m_model->index(0,0),
Qt::UserRole+1,
QVariant(txt),
-1,
Qt::MatchContains
| Qt::MatchRecursive);
showFlatResults(rankedSearch(text, m_showed_index));
}
/**
@brief ElementsCollectionWidget::rankedSearch
Run the collection search for @a text and return the element hits, best
first. The hits carry no model index, so a list that is not backed by the
collection model can show them too.
@param text : search text, "+" separating terms
@param within : restrict the search to this item's children, or the
whole collection when invalid
@return ranked hits, best first
*/
QVector<ElementSearchHit> ElementsCollectionWidget::rankedSearch(
const QString &text,
const QModelIndex &within)
{
QVector<ElementSearchHit> hits;
if (!m_model || text.size() < 3) {
return hits;
}
for(QModelIndex index : match_index)
showAndExpandItem(index);
const QStringList terms = text.split("+", Qt::SkipEmptyParts);
QModelIndexList matches;
for (const QString &term : terms) {
matches << m_model->match(within.isValid()
? m_model->index(0, 0, within)
: m_model->index(0, 0),
Qt::UserRole+1,
QVariant(term),
-1,
Qt::MatchContains
| Qt::MatchRecursive);
}
const QString needle = terms.value(0).toLower();
QVector<QPair<int, ElementSearchHit>> scored;
QSet<QString> seen;
for (const QModelIndex &index : std::as_const(matches))
{
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (!(eci && eci->isElement())) {
continue;
}
const QString path = eci->collectionPath();
//match() is run once per "+"-separated term, so the same element
//can arrive several times.
if (path.isEmpty() || seen.contains(path)) {
continue;
}
seen.insert(path);
ElementSearchHit hit;
hit.path = path;
hit.name = index.data(Qt::DisplayRole).toString();
hit.icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
//Where it lives, so two similarly-named symbols are tellable apart
QStringList parts;
for (QModelIndex p = index.parent(); p.isValid(); p = p.parent()) {
parts.prepend(p.data(Qt::DisplayRole).toString());
}
hit.folder = parts.join(QStringLiteral(" / "));
const QString hay = index.data(Qt::UserRole + 1).toString().toLower();
scored.append({rankMatch(needle, hit.name, hay), hit});
}
std::stable_sort(scored.begin(), scored.end(),
[](const QPair<int, ElementSearchHit> &a,
const QPair<int, ElementSearchHit> &b) {
return a.first > b.first;
});
hits.reserve(scored.size());
for (const auto &p : std::as_const(scored)) {
hits.append(p.second);
}
return hits;
}
/**
@brief ElementsCollectionWidget::rankMatch
Score a hit so the list can be ordered by how well it matches.
The tree search treats every hit equally, which is fine when they stay in
place but useless in a ranked list: typing "diode" should not put an
element whose *description* mentions diodes above one actually called
"Diode". Name beats element-info field, earlier beats later, shorter beats
longer.
@param needle : lower-cased search text
@param name : the element's display name
@param haystack : the full indexed string (name + every info field)
@return a score, higher is better
*/
int ElementsCollectionWidget::rankMatch(const QString &needle,
const QString &name,
const QString &haystack)
{
const QString n = name.toLower();
int score = 0;
if (n == needle) {
score = 1000;
} else if (n.startsWith(needle)) {
score = 800;
} else if (n.contains(needle)) {
score = 600 - qMin(n.indexOf(needle), 99);
} else if (haystack.contains(needle)) {
//Matched only on an info field -- manufacturer, reference,
//description. Still a real hit, just a weaker one.
score = 300;
}
//Among equally-placed hits prefer the shorter name: "Diode" over
//"Diode Zener bidirectional".
return score - qMin(n.size(), 99);
}
/**
@brief ElementsCollectionWidget::showFlatResults
Replace the tree with a flat list of @a hits, in the order given.
@param hits : from rankedSearch()
*/
void ElementsCollectionWidget::showFlatResults(const QVector<ElementSearchHit> &hits)
{
m_search_model->clear();
for (const ElementSearchHit &hit : hits)
{
auto *item = new QStandardItem(hit.icon, hit.name);
item->setEditable(false);
item->setToolTip(hit.folder);
item->setData(hit.path, Qt::UserRole + 2);
m_search_model->appendRow(item);
}
m_tab_widget->hide();
m_search_results->show();
}
/**
@brief ElementsCollectionWidget::clearFlatResults
Put the tree back when the search field is emptied.
*/
void ElementsCollectionWidget::clearFlatResults()
{
m_search_model->clear();
m_search_results->hide();
m_tab_widget->show();
}
/**
@@ -43,6 +43,22 @@ class ElementsTreeView;
and all action needed to use this widget.
This is the element collection widget used in the diagram editor.
*/
class QListView;
class QStandardItemModel;
/**
@brief One ranked hit from a collection search.
Carries everything a result row needs, so a list that is not backed by
the collection model can show it.
*/
struct ElementSearchHit
{
QString path; ///< collection path, enough to build an ElementsLocation
QString name; ///< display name
QString folder; ///< where it lives, for telling similar names apart
QIcon icon;
};
class ElementsCollectionWidget : public QWidget
{
Q_OBJECT
@@ -56,6 +72,8 @@ class ElementsCollectionWidget : public QWidget
void removeProject (QETProject *project);
void highlightUnusedElement();
void setCurrentLocation(const ElementsLocation &location);
QVector<ElementSearchHit> rankedSearch(const QString &text,
const QModelIndex &within = QModelIndex());
protected:
void leaveEvent(QEvent *event) override;
@@ -86,9 +104,26 @@ class ElementsCollectionWidget : public QWidget
public slots:
void reload();
void loadingFinished();
void insertCurrentElement();
void editLocation(const ElementsLocation &location);
signals:
/**
Emitted when the user asks for an element to be placed on the
current folio. Whoever hosts this widget decides which view
receives it -- the dock is inside a diagram editor, but the
picker popup is not, so the widget must not reach for an
ancestor editor itself.
*/
void insertElementRequested(const ElementsLocation &location);
private:
void locationWasSaved(const ElementsLocation& location);
void activateIndex(const QModelIndex &index);
void showFlatResults(const QVector<ElementSearchHit> &hits);
void clearFlatResults();
static int rankMatch(const QString &needle, const QString &name,
const QString &haystack);
private:
@@ -100,6 +135,9 @@ class ElementsCollectionWidget : public QWidget
ElementsTreeView *m_tree_view;
ElementsTreeView *m_macros_tree_view = nullptr;
QTabWidget *m_tab_widget = nullptr;
/// Flat ranked results, shown in place of the tree while searching
QListView *m_search_results = nullptr;
QStandardItemModel *m_search_model = nullptr;
QVBoxLayout *m_main_vlayout;
QMenu *m_context_menu;
QModelIndex m_index_at_context_menu;
@@ -82,13 +82,27 @@ void ElementsTreeView::startDrag(Qt::DropActions supportedActions)
@param location : location to use for create the content of the QDrag
*/
void ElementsTreeView::startElementDrag(const ElementsLocation &location)
{
execElementDrag(this, location);
}
/**
@brief ElementsTreeView::execElementDrag
Build and run the QDrag for @a location, from @a source.
Static so that a view which is not an ElementsTreeView -- the flat list
of search results -- starts exactly the same drag as the tree.
@param source : the widget the drag starts from
@param location : location to use for create the content of the QDrag
*/
void ElementsTreeView::execElementDrag(QWidget *source,
const ElementsLocation &location)
{
if (! location.exist()) return;
#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
QDrag* drag = new QDrag(this);
QDrag* drag = new QDrag(source);
#else
QScopedPointer<QDrag> drag(new QDrag(this));
QScopedPointer<QDrag> drag(new QDrag(source));
#endif
QString location_str = location.toString();
@@ -209,7 +223,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
&elmt_creation_state));
if (elmt_creation_state) { return; }
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), palette()));
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), source->palette()));
QPoint elmt_hotspot(temp_elmt->hotspot());
//Adjust the size of the pixmap if he is too big
@@ -32,6 +32,8 @@ class ElementsTreeView : public QTreeView
{
public:
ElementsTreeView(QWidget *parent = nullptr);
static void execElementDrag(QWidget *source,
const ElementsLocation &location);
protected:
void startDrag(Qt::DropActions supportedActions) override;
+164
View File
@@ -0,0 +1,164 @@
/*
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 "diagramcontexttoolbar.h"
#include <QAction>
#include <QEvent>
#include <QGraphicsOpacityEffect>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QSettings>
#include <QToolButton>
namespace {
/// Fully opaque within this distance of the toolbar, in pixels...
const int opaque_distance = 30;
/// ...fading to hidden at this one
const int hide_distance = 200;
}
/**
@brief DiagramContextToolbar::DiagramContextToolbar
@param viewport : the view's viewport, which it is drawn on and watches
*/
DiagramContextToolbar::DiagramContextToolbar(QWidget *viewport) :
QFrame(viewport)
{
setFrameShape(QFrame::StyledPanel);
setAutoFillBackground(true);
setFocusPolicy(Qt::NoFocus);
m_layout = new QHBoxLayout(this);
m_layout->setContentsMargins(2, 2, 2, 2);
m_layout->setSpacing(1);
m_opacity = new QGraphicsOpacityEffect(this);
setGraphicsEffect(m_opacity);
hide();
viewport->installEventFilter(this);
//Key presses go to the view, not its viewport
if (viewport->parentWidget()) {
viewport->parentWidget()->installEventFilter(this);
}
}
/**
@return whether the toolbar should be shown at all (a preference)
*/
bool DiagramContextToolbar::isEnabled()
{
return QSettings().value(QStringLiteral("diagrameditor/context_toolbar"),
true).toBool();
}
/**
@brief DiagramContextToolbar::showAt
Show @a actions just above and to the right of @a viewport_pos, kept
inside the viewport. Clicking a button triggers the action and leaves
the toolbar up, so a command such as rotate can be clicked again.
*/
void DiagramContextToolbar::showAt(const QPoint &viewport_pos,
const QList<QAction *> &actions)
{
while (QLayoutItem *item = m_layout->takeAt(0)) {
delete item->widget();
delete item;
}
for (QAction *action : actions.mid(0, max_actions))
{
auto *button = new QToolButton(this);
button->setAutoRaise(true);
button->setFocusPolicy(Qt::NoFocus);
button->setIconSize(QSize(20, 20));
const QString text = action->text().remove(QLatin1Char('&'));
if (action->icon().isNull()) {
button->setText(text);
} else {
button->setIcon(action->icon());
}
button->setToolTip(text);
button->setEnabled(action->isEnabled());
connect(button, &QToolButton::clicked, action, &QAction::trigger);
m_layout->addWidget(button);
}
if (actions.isEmpty()) {
hide();
return;
}
adjustSize();
const QRect area = parentWidget()->rect();
QPoint pos = viewport_pos + QPoint(16, -height() - 16);
pos.setX(qBound(area.left(), pos.x(), area.right() - width()));
pos.setY(qBound(area.top(), pos.y(), area.bottom() - height()));
move(pos);
m_opacity->setOpacity(1.0);
show();
raise();
}
/**
@brief DiagramContextToolbar::followMouse
Fade with the mouse's distance from the toolbar, and hide past
hide_distance.
*/
void DiagramContextToolbar::followMouse(const QPoint &viewport_pos)
{
const QRect r = geometry();
const int dx = qMax(qMax(r.left() - viewport_pos.x(), 0), viewport_pos.x() - r.right());
const int dy = qMax(qMax(r.top() - viewport_pos.y(), 0), viewport_pos.y() - r.bottom());
const int distance = qMax(dx, dy);
if (distance >= hide_distance) {
hide();
return;
}
const qreal fade = qreal(distance - opaque_distance)
/ (hide_distance - opaque_distance);
m_opacity->setOpacity(qBound(0.15, 1.0 - fade, 1.0));
}
/**
@brief DiagramContextToolbar::eventFilter
Watch the viewport and the view while shown: follow the mouse, and hide
on a press elsewhere, a wheel turn or a key press.
*/
bool DiagramContextToolbar::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched)
if (!isVisible()) {
return false;
}
switch (event->type())
{
case QEvent::MouseMove:
followMouse(static_cast<QMouseEvent *>(event)->position().toPoint());
break;
case QEvent::MouseButtonPress:
case QEvent::Wheel:
case QEvent::KeyPress:
case QEvent::Leave:
hide();
break;
default:
break;
}
return false;
}
+60
View File
@@ -0,0 +1,60 @@
/*
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/>.
*/
#ifndef DIAGRAMCONTEXTTOOLBAR_H
#define DIAGRAMCONTEXTTOOLBAR_H
#include <QFrame>
#include <QList>
class QAction;
class QGraphicsOpacityEffect;
class QHBoxLayout;
/**
@brief A small row of commands shown beside the cursor after a click
selects something on a folio, as SolidWorks does.
It lives on the view's viewport, so it never takes the focus or opens a
window. It fades as the mouse moves away and hides once the mouse is far
enough, on a click elsewhere, a wheel turn or a key press. The commands
are the shortcut bar's for the selection (ShortcutBarSettings).
*/
class DiagramContextToolbar : public QFrame
{
Q_OBJECT
public:
explicit DiagramContextToolbar(QWidget *viewport);
void showAt(const QPoint &viewport_pos, const QList<QAction *> &actions);
static bool isEnabled();
/// At most this many commands, so it stays small
static const int max_actions = 8;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void followMouse(const QPoint &viewport_pos);
QHBoxLayout *m_layout = nullptr;
QGraphicsOpacityEffect *m_opacity = nullptr;
};
#endif // DIAGRAMCONTEXTTOOLBAR_H
+185
View File
@@ -0,0 +1,185 @@
/*
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 "diagramgestureoverlay.h"
#include <QAction>
#include <QPainter>
#include <QPainterPath>
#include <QSettings>
#include <QtMath>
namespace {
const int outer_radius = 84;
/// Releasing closer than this to the centre cancels
const int inner_radius = 22;
const int icon_radius = 56;
const int icon_size = 22;
/// Wider than the ring, so a long command name under it is not cut
const int overlay_width = 280;
}
/**
@brief DiagramGestureOverlay::DiagramGestureOverlay
@param viewport : the view's viewport, which it is drawn on. It lets the
mouse through, so the view keeps receiving the drag.
*/
DiagramGestureOverlay::DiagramGestureOverlay(QWidget *viewport) :
QWidget(viewport)
{
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_TranslucentBackground);
setFixedSize(overlay_width, 2 * outer_radius + 2 + 24);
hide();
}
/**
@return whether right-drag gestures are on (a preference)
*/
bool DiagramGestureOverlay::isEnabled()
{
return QSettings().value(QStringLiteral("diagrameditor/mouse_gestures"),
true).toBool();
}
/**
@brief DiagramGestureOverlay::showAt
Show the ring centred on @a center with @a actions, at most eight,
clockwise from the top.
*/
void DiagramGestureOverlay::showAt(const QPoint &center,
const QList<QAction *> &actions)
{
m_center = center;
m_actions = actions.mid(0, sectors);
m_active = -1;
move(center - QPoint(overlay_width / 2, outer_radius + 1));
show();
raise();
}
/**
@brief DiagramGestureOverlay::sectorAt
@return the sector in the direction of @a viewport_pos from the centre,
0 at the top then clockwise, or -1 within the cancel radius
*/
int DiagramGestureOverlay::sectorAt(const QPoint &viewport_pos) const
{
const QPoint d = viewport_pos - m_center;
if (qHypot(d.x(), d.y()) < inner_radius) {
return -1;
}
//Angle clockwise from the top, in degrees
qreal angle = qRadiansToDegrees(qAtan2(d.x(), -d.y()));
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / sectors))) % sectors;
}
/**
@return the command in the direction of @a viewport_pos, or nullptr when
there is none there or it is disabled
*/
QAction *DiagramGestureOverlay::actionAt(const QPoint &viewport_pos) const
{
const int sector = sectorAt(viewport_pos);
if (sector < 0 || sector >= m_actions.count()) {
return nullptr;
}
QAction *action = m_actions.at(sector);
return action->isEnabled() ? action : nullptr;
}
void DiagramGestureOverlay::setPointer(const QPoint &viewport_pos)
{
const int sector = sectorAt(viewport_pos);
if (sector != m_active) {
m_active = sector;
update();
}
}
void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c(overlay_width / 2, outer_radius + 1);
const qreal step = 360.0 / sectors;
QColor base = palette().color(QPalette::Window);
base.setAlpha(225);
painter.setPen(palette().color(QPalette::Mid));
painter.setBrush(base);
painter.drawEllipse(c, outer_radius, outer_radius);
//Highlight the sector the mouse points at, if it holds a command
if (m_active >= 0 && m_active < m_actions.count()
&& m_actions.at(m_active)->isEnabled())
{
QPainterPath wedge;
wedge.moveTo(c);
//Qt angles run counter-clockwise from 3 o'clock
const qreal start = 90 - m_active * step - step / 2;
wedge.arcTo(QRectF(c.x() - outer_radius, c.y() - outer_radius,
2 * outer_radius, 2 * outer_radius), start, step);
wedge.closeSubpath();
painter.setPen(Qt::NoPen);
painter.setBrush(palette().color(QPalette::Highlight));
painter.drawPath(wedge);
}
painter.setPen(palette().color(QPalette::Mid));
painter.setBrush(palette().color(QPalette::Window));
painter.drawEllipse(c, inner_radius, inner_radius);
for (int i = 0 ; i < m_actions.count() ; ++i)
{
QAction *action = m_actions.at(i);
const qreal a = qDegreesToRadians(i * step);
const QPointF p(c.x() + icon_radius * qSin(a), c.y() - icon_radius * qCos(a));
const QRect r(int(p.x()) - icon_size / 2, int(p.y()) - icon_size / 2,
icon_size, icon_size);
const QIcon::Mode mode = action->isEnabled() ? QIcon::Normal : QIcon::Disabled;
if (!action->icon().isNull()) {
action->icon().paint(&painter, r, Qt::AlignCenter, mode);
} else {
painter.setPen(palette().color(action->isEnabled() ? QPalette::Active : QPalette::Disabled,
QPalette::WindowText));
painter.drawText(r.adjusted(-12, 0, 12, 0), Qt::AlignCenter,
action->text().remove(QLatin1Char('&')).left(3));
}
}
//Name of the highlighted command, under the ring
if (m_active >= 0 && m_active < m_actions.count())
{
const QString name = m_actions.at(m_active)->text().remove(QLatin1Char('&'));
const QRect label(0, 2 * outer_radius + 4, width(), 20);
QFont f = font();
f.setBold(true);
painter.setFont(f);
const QRect text = painter.fontMetrics().boundingRect(label, Qt::AlignCenter, name)
.adjusted(-6, -2, 6, 2);
painter.setPen(Qt::NoPen);
painter.setBrush(base);
painter.drawRoundedRect(text, 4, 4);
painter.setPen(palette().color(QPalette::WindowText));
painter.drawText(label, Qt::AlignCenter, name);
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
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/>.
*/
#ifndef DIAGRAMGESTUREOVERLAY_H
#define DIAGRAMGESTUREOVERLAY_H
#include <QList>
#include <QWidget>
class QAction;
/**
@brief The ring shown while right-dragging on a folio: up to eight
commands around the point where the button went down, the one in the
mouse's direction highlighted. Releasing runs it, releasing near the
centre cancels -- SolidWorks' mouse gestures.
Commands are placed clockwise from the top. The ring only draws; the
view tracks the mouse and asks sectorAt() on release.
*/
class DiagramGestureOverlay : public QWidget
{
Q_OBJECT
public:
explicit DiagramGestureOverlay(QWidget *viewport);
void showAt(const QPoint &center, const QList<QAction *> &actions);
void setPointer(const QPoint &viewport_pos);
int sectorAt(const QPoint &viewport_pos) const;
QAction *actionAt(const QPoint &viewport_pos) const;
static bool isEnabled();
static const int sectors = 8;
protected:
void paintEvent(QPaintEvent *event) override;
private:
QPoint m_center;
QList<QAction *> m_actions;
int m_active = -1;
};
#endif // DIAGRAMGESTUREOVERLAY_H
+353 -2
View File
@@ -38,6 +38,10 @@
#include "utils/conductorcreator.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "diagram.h"
#include "diagramcontexttoolbar.h"
#include "diagramgestureoverlay.h"
#include "shortcutbarsettings.h"
#include "shortcutmanager.h"
#include "ElementsCollection/xmlelementcollection.h"
#include "NameList/nameslist.h"
#include "elementdialog.h"
@@ -45,6 +49,7 @@
#include <QDropEvent>
#include <QPainter>
#include <QPointer>
#include <QSet>
#include <algorithm>
/**
@@ -98,6 +103,9 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_create_template = new QAction(tr("Créer un template", "context menu action"), this);
connect(m_create_template, &QAction::triggered, this, &DiagramView::createTemplateFromSelection);
//Filled each time the context menu opens, see updateFolioReportMenu()
m_folio_report_menu = new QMenu(tr("Renvoi de folio"), this);
//setup three separators, to be use in context menu
for(int i=0 ; i<3 ; ++i)
{
@@ -105,6 +113,14 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_separators.last()->setSeparator(true);
}
m_context_toolbar = new DiagramContextToolbar(viewport());
m_gesture_overlay = new DiagramGestureOverlay(viewport());
connect(m_diagram, &QGraphicsScene::selectionChanged, this, [this]() {
if (m_diagram->selectedItems().isEmpty()) {
m_context_toolbar->hide();
}
});
connect(m_diagram, &Diagram::showDiagram, this, &DiagramView::showDiagram);
connect(m_diagram, &QGraphicsScene::sceneRectChanged, this, &DiagramView::adjustSceneRect);
connect(&(m_diagram -> border_and_titleblock), &BorderTitleBlock::informationChanged, this, &DiagramView::updateWindowTitle);
@@ -234,14 +250,72 @@ void DiagramView::handleElementDrop(QDropEvent *event)
QPointF drop_pos;
drop_pos = mapToScene(event->position().toPoint());
startElementPlacement(location, drop_pos);
}
/**
@brief DiagramView::startElementPlacement
Enter the interactive placement mode for @a location, with the pending
element starting at @a scene_pos.
This is the mode where the element follows the cursor on the grid, a left
click drops a copy, Space rotates it and the element stays loaded so a run
of identical symbols can be placed with successive clicks.
Split out of handleElementDrop() so that placement is reachable without a
drag: the mode itself was always general, it simply had no caller other
than the end of a drop.
@param location : the element or macro to place
@param scene_pos : where the pending element first appears, in scene
coordinates
@return true if the placement mode was entered
*/
bool DiagramView::startElementPlacement(const ElementsLocation &location,
const QPointF &scene_pos)
{
if (!diagram() || !(location.isElement() && location.exist())) {
return false;
}
if (diagram()->isReadOnly()) {
return false;
}
if (location.path().endsWith(".qetmak")) {
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), drop_pos));
diagram()->setEventInterface(
new DiagramEventAddMacro(location, diagram(), scene_pos));
} else {
diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), drop_pos));
//DiagramEventAddElement takes a non-const reference, so it needs
//an lvalue it may modify. Copying keeps the caller's location
//untouched -- QETDiagramEditor stores the same one for
//"insert last element".
ElementsLocation loc(location);
diagram()->setEventInterface(
new DiagramEventAddElement(loc, diagram(), scene_pos));
emit elementPlacementStarted(location);
}
//Set focus to the view to get event
this->setFocus();
return true;
}
/**
@brief DiagramView::defaultPlacementPos
@return where a pending element should appear when placement was not
started by a drop, so there is no cursor position to use.
The cursor is used when it is over the view -- picking up a placement where
the user is already looking -- and the centre of the visible area
otherwise.
*/
QPointF DiagramView::defaultPlacementPos() const
{
const QPoint local = mapFromGlobal(QCursor::pos());
if (viewport() && viewport()->rect().contains(local)) {
return mapToScene(local);
}
return mapToScene(viewport() ? viewport()->rect().center()
: rect().center());
}
/**
@@ -316,6 +390,7 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
*/
void DiagramView::setVisualisationMode()
{
m_ctrl_shift_panning = false;
setDragMode(ScrollHandDrag);
applyReadOnly();
setInteractive(false);
@@ -327,6 +402,7 @@ void DiagramView::setVisualisationMode()
*/
void DiagramView::setSelectionMode()
{
m_ctrl_shift_panning = false;
setDragMode(RubberBandDrag);
setInteractive(true);
applyReadOnly();
@@ -557,6 +633,12 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
{
e->ignore();
//Ctrl+Shift panning whose key release was missed (see
//focusOutEvent()): a click without them ends it
if (m_ctrl_shift_panning && !isCtrlShifting(e)) {
setSelectionMode();
}
if (m_fresh_focus_in)
{
switchToVisualisationModeIfNeeded(e);
@@ -565,6 +647,50 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
if (e->button() == Qt::LeftButton) {
m_press_pos = e->position().toPoint();
}
//Right button: a click opens the context menu on release, a drag is
//a gesture (DiagramGestureOverlay). Left alone while a text is edited.
m_swallow_native_menu = false;
m_gesture_over_tool = false;
if (e->button() == Qt::RightButton
&& DiagramGestureOverlay::isEnabled()
&& !m_diagram->focusItem())
{
m_gesture_tracking = true;
m_gesture_origin = e->position().toPoint();
m_context_toolbar->hide();
//A tool is running, often one a gesture just started. A right
//click still goes to it -- it cancels or finishes the tool -- so
//the press carries on to the scene. A drag ends the tool and
//shows the ring (see mouseMoveEvent).
if (m_diagram->eventInterfaceIsRunning()) {
m_gesture_over_tool = true;
}
else
{
m_swallow_native_menu = true;
//Select what is under the mouse, as the context menu does, so
//a gesture acts on it
if (QGraphicsItem *item = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) {
if (!item->isSelected()) {
m_diagram->clearSelection();
//Clearing the selection can delete handler items, so
//look the item up again (see contextMenuEvent)
if (QGraphicsItem *again = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) {
again->setSelected(true);
}
}
}
e->accept();
return;
}
}
//Start drag view when hold the middle button
if (e->button() == Qt::MiddleButton)
{
@@ -616,6 +742,31 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
setToolTip(tr("X: %1 Y: %2").arg(e->pos().x()).arg(e->pos().y()));
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
if (m_gesture_tracking)
{
const QPoint pos = e->position().toPoint();
if (!m_gesture_overlay->isVisible()
&& (pos - m_gesture_origin).manhattanLength() > 2 * QApplication::startDragDistance()) {
if (m_gesture_over_tool) {
//The drag is a gesture: end the tool, and ignore the
//platform's right-click menu like any other gesture
m_diagram->clearEventInterface();
m_swallow_native_menu = true;
}
m_gesture_overlay->showAt(m_gesture_origin, selectionCommands());
}
if (m_gesture_overlay->isVisible()) {
m_gesture_overlay->setPointer(pos);
e->accept();
return;
}
//Not a drag yet: a running tool keeps following the mouse
if (!m_gesture_over_tool) {
e->accept();
return;
}
}
// Drag the view
if (e->buttons() == Qt::MiddleButton)
{
@@ -677,6 +828,40 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
{
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
//A plain right click on a running tool falls through: the tool
//handles it, as it always has
if (m_gesture_tracking && e->button() == Qt::RightButton
&& !(m_gesture_over_tool && !m_gesture_overlay->isVisible()))
{
m_gesture_tracking = false;
const QPoint pos = e->position().toPoint();
if (m_gesture_overlay->isVisible())
{
//A gesture: run the command it points at, if any
QAction *action = m_gesture_overlay->actionAt(pos);
m_gesture_overlay->hide();
if (action) {
action->trigger();
}
}
else
{
//A plain right click: the context menu, opened here on
//release on every platform
QContextMenuEvent menu_event(QContextMenuEvent::Mouse, pos,
e->globalPosition().toPoint(),
e->modifiers());
m_menu_from_gesture = true;
contextMenuEvent(&menu_event);
m_menu_from_gesture = false;
}
e->accept();
return;
}
if (e->button() == Qt::RightButton) {
m_gesture_tracking = false;
}
// Stop drag view
if (e->button() == Qt::MiddleButton)
{
@@ -721,7 +906,60 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
e->accept();
}
else
{
QGraphicsView::mouseReleaseEvent(e);
//A click, not a drag: moving items or a rubber band selection
//should not be followed by a toolbar under the mouse.
const QPoint pos = e->position().toPoint();
if (e->button() == Qt::LeftButton
&& (pos - m_press_pos).manhattanLength() < QApplication::startDragDistance()) {
showContextToolbar(pos);
}
}
}
/**
@brief DiagramView::selectionCommands
@return the shortcut bar's commands for the current selection, as this
window's actions
*/
QList<QAction *> DiagramView::selectionCommands() const
{
QList<QAction *> actions;
QETDiagramEditor *qde = diagramEditor();
if (!qde) {
return actions;
}
const auto context = ShortcutBarSettings::contextFor(m_diagram->selectedItems());
for (const QString &id : ShortcutBarSettings::ids(context)) {
if (QAction *action = ShortcutManager::instance().action(id, qde)) {
actions << action;
}
}
return actions;
}
/**
@brief DiagramView::showContextToolbar
After a click that leaves something selected, show the shortcut bar's
commands for that selection beside the cursor. Not while placing or
drawing, nor on a read-only folio, nor when switched off in the
configuration.
@param viewport_pos : where the click was
*/
void DiagramView::showContextToolbar(const QPoint &viewport_pos)
{
const QList<QGraphicsItem *> selection = m_diagram->selectedItems();
QETDiagramEditor *qde = diagramEditor();
if (selection.isEmpty() || !qde
|| m_diagram->isReadOnly() || m_diagram->eventInterfaceIsRunning()
|| !DiagramContextToolbar::isEnabled()) {
m_context_toolbar->hide();
return;
}
m_context_toolbar->showAt(viewport_pos, selectionCommands());
}
/**
@@ -801,6 +1039,23 @@ void DiagramView::focusInEvent(QFocusEvent *e) {
}
}
/**
@brief DiagramView::focusOutEvent
Leave the Ctrl+Shift panning: without the focus, the view will not see
Ctrl or Shift being released, which is what normally ends it. A
Ctrl+Shift shortcut that opens a window (Ctrl+Shift+M, the command
search) left the view panning, ignoring clicks on the folio, on Windows,
where no other key release reached the view to end it.
@param e
*/
void DiagramView::focusOutEvent(QFocusEvent *e)
{
if (m_ctrl_shift_panning) {
setSelectionMode();
}
PaletteGraphicsView::focusOutEvent(e);
}
/**
@brief DiagramView::focusNextPrevChild
By default, QWidget intercepts Tab/Shift+Tab to move keyboard focus to
@@ -854,6 +1109,18 @@ void DiagramView::keyPressEvent(QKeyEvent *e)
focusNextChild();
}
return;
case Qt::Key_Return:
case Qt::Key_Enter:
//Repeat the last drawing or placing command, as SolidWorks
//does. Not while a tool is running or a text has the focus:
//both use Enter themselves.
if (e->modifiers() == Qt::NoModifier
&& !m_diagram->eventInterfaceIsRunning()
&& !m_diagram->focusItem()
&& diagramEditor()->repeatLastCommand()) {
return;
}
break;
case Qt::Key_PageUp:
current_project->changeTabUp();
return;
@@ -1396,6 +1663,7 @@ bool DiagramView::switchToVisualisationModeIfNeeded(QInputEvent *e) {
if (isCtrlShifting(e) && !selectedItemHasFocus()) {
if (dragMode() != QGraphicsView::ScrollHandDrag) {
setVisualisationMode();
m_ctrl_shift_panning = true;
return(true);
}
}
@@ -1497,6 +1765,8 @@ QList<QAction *> DiagramView::contextMenuActions() const
//level down where a stray click cannot reach them.
list << m_paste_here;
list << m_separators.at(0);
list << qde->m_insert_last_element;
list << m_folio_report_menu->menuAction();
list << qde->m_add_item_menu->menuAction();
list << m_separators.at(1);
list << qde->m_edit_diagram_properties;
@@ -1560,6 +1830,14 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
//right-click gets.
const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard;
//With gestures on, a right press is tracked by mousePressEvent and
//the menu opened on release; the platform's own event (sent on press
//on X11, on release on Windows) would open it a second time.
if (!from_keyboard && m_swallow_native_menu && !m_menu_from_gesture) {
e->accept();
return;
}
if (from_keyboard)
{
//Aim at the selection when there is one, so the menu appears
@@ -1608,6 +1886,7 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
{
m_paste_here_pos = menu_pos;
m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram());
updateFolioReportMenu();
}
QList <QAction *> list = contextMenuActions();
@@ -1620,6 +1899,78 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
}
}
/**
@brief DiagramView::updateFolioReportMenu
Fill the "Renvoi de folio" submenu of the context menu with the folio
report elements this project already uses -- the ones in its embedded
collection -- or, when it has none yet, the coming and going arrows of
the common collection. An entry places its element where the context
menu was opened. Left empty, and so hidden, on a read-only diagram.
*/
void DiagramView::updateFolioReportMenu()
{
m_folio_report_menu->clear();
if (m_diagram->isReadOnly()) {
return;
}
QList<ElementsLocation> locations;
QETProject *project = m_diagram->project();
XmlElementCollection *collection =
project ? project->embeddedElementCollection() : nullptr;
if (collection)
{
QSet<QString> names;
const QDomNodeList definitions =
collection->root().elementsByTagName(QStringLiteral("definition"));
for (int i = 0 ; i < definitions.count() ; ++i)
{
const QDomElement definition = definitions.at(i).toElement();
const QString link_type = definition.attribute(QStringLiteral("link_type"));
if (link_type != QLatin1String("next_report")
&& link_type != QLatin1String("previous_report")) {
continue;
}
const ElementsLocation location =
collection->domToLocation(definition.parentNode().toElement());
//A project keeps dated copies of the same element
//(01previous_folio-20140521204742.elmt), so list each name once.
if (!location.exist() || names.contains(location.name())) {
continue;
}
names.insert(location.name());
locations << location;
}
}
if (locations.isEmpty())
{
for (const auto path : {
"common://10_electric/10_allpole/100_folio_referencing/01coming_arrow.elmt",
"common://10_electric/10_allpole/100_folio_referencing/02going_arrow.elmt"})
{
const ElementsLocation location(QString::fromLatin1(path));
if (location.exist()) {
locations << location;
}
}
}
std::sort(locations.begin(), locations.end(),
[](const ElementsLocation &a, const ElementsLocation &b) {
return a.name().localeAwareCompare(b.name()) < 0;
});
for (const ElementsLocation &location : std::as_const(locations))
{
QAction *action = m_folio_report_menu->addAction(location.icon(),
location.name());
connect(action, &QAction::triggered, this, [this, location]() {
startElementPlacement(location, mapToScene(m_paste_here_pos));
});
}
}
/**
* @brief DiagramView::createTemplateFromSelection
* Triggered from the context menu to create a new template (macro) from the current selection.
+30
View File
@@ -27,10 +27,13 @@
class CellRuler;
class Conductor;
class Diagram;
class DiagramContextToolbar;
class DiagramGestureOverlay;
class QETDiagramEditor;
class DVEventInterface;
class QInputEvent;
class QGestureEvent;
class QMenu;
/**
This class provides a widget to render an electric diagram in an editable,
@@ -55,11 +58,27 @@ class DiagramView : public PaletteGraphicsView
QAction *m_paste_here = nullptr;
QAction *m_multi_paste = nullptr;
QAction *m_create_template = nullptr;
QMenu *m_folio_report_menu = nullptr;
QPoint m_paste_here_pos;
QPoint m_press_pos;
DiagramContextToolbar *m_context_toolbar = nullptr;
/// Right-drag gestures: tracking since the right button went down
bool m_gesture_tracking = false;
/// The platform's own right-click menu event is to be ignored:
/// the view opens the menu itself on release
bool m_swallow_native_menu = false;
bool m_menu_from_gesture = false;
/// The right press went to a running tool; a drag ends the tool
bool m_gesture_over_tool = false;
QPoint m_gesture_origin;
DiagramGestureOverlay *m_gesture_overlay = nullptr;
QPoint m_last_mouse_pos = QPoint(-1, -1);
QPointF m_drag_last_pos;
bool m_fresh_focus_in,
m_first_activation = true;
/// True while the view pans because Ctrl+Shift is held, as opposed
/// to the visualisation mode chosen from the toolbar.
bool m_ctrl_shift_panning = false;
QList<QAction *> m_separators;
QPolygonF m_free_rubberband;
bool m_free_rubberbanding = false;
@@ -85,6 +104,10 @@ class DiagramView : public PaletteGraphicsView
/// cursor query (QCursor::pos()/setPos() are silently ignored by
/// several window managers and compositors, Wayland included).
QPoint lastMousePos() const { return m_last_mouse_pos; }
bool startElementPlacement(const ElementsLocation &location,
const QPointF &scene_pos);
QPointF defaultPlacementPos() const;
void setCellRulersShown(bool shown);
void setCellLinesShown(bool shown);
@@ -93,6 +116,7 @@ class DiagramView : public PaletteGraphicsView
void contextMenuEvent(QContextMenuEvent *) override;
void wheelEvent(QWheelEvent *) override;
void focusInEvent(QFocusEvent *) override;
void focusOutEvent(QFocusEvent *) override;
void keyPressEvent(QKeyEvent *) override;
void keyReleaseEvent(QKeyEvent *) override;
bool event(QEvent *) override;
@@ -117,6 +141,7 @@ class DiagramView : public PaletteGraphicsView
private:
void handleElementDrop(QDropEvent *);
void updateFolioReportMenu();
void handleTitleBlockDrop(QDropEvent *);
void handleTextDrop(QDropEvent *);
void scrollOnMovement(QKeyEvent *);
@@ -126,6 +151,8 @@ class DiagramView : public PaletteGraphicsView
bool gestures() const;
void updateCellRulers();
void placeCellRulers();
void showContextToolbar(const QPoint &viewport_pos);
QList<QAction *> selectionCommands() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
@@ -145,6 +172,9 @@ class DiagramView : public PaletteGraphicsView
/// Signal emitted when free rubberband changed.
/// When free rubberband selection ends this signal will be emitted with null value.
void freeRubberBandChanged(QPolygonF polygon);
/// Signal emitted when the placement mode is entered for an
/// element (not a macro), whether from a drop or not.
void elementPlacementStarted(const ElementsLocation &);
public slots:
void setVisualisationMode();
@@ -22,6 +22,8 @@
#include "../qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "../qetgraphicsitem/ViewItem/ui/graphicstablepropertieseditor.h"
#include "../qetgraphicsitem/ViewItem/ui/projectdbmodelpropertieswidget.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/conductortextitem.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
@@ -30,11 +32,13 @@
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../ui/dynamicelementtextitemeditor.h"
#include "../ui/elementpropertieswidget.h"
#include "../ui/conductorpropertieseditorwidget.h"
#include "../ui/imagepropertieswidget.h"
#include "../ui/inditextpropertieswidget.h"
#include "../ui/shapegraphicsitempropertieswidget.h"
#include <QGraphicsItem>
#include <QSettings>
/**
@brief PropertiesEditorFactory::propertiesEditor
@@ -83,6 +87,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
return nullptr;
}
QGraphicsItem *item = items.first();
//Selecting a conductor's text label edits its parent conductor (#500),
//mirroring how double-clicking the label opens the conductor dialog.
if (count_ == 1) {
if (auto *cti = qgraphicsitem_cast<ConductorTextItem *>(item)) {
if (Conductor *parent_cond = cti->parentConductor()) {
items = {parent_cond};
item = parent_cond;
}
}
}
const int type_ = item->type();
//The editor widget can only edit one item
@@ -100,6 +115,28 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
switch (type_)
{
case Conductor::Type: //1001
{
//Off unless enabled in the preferences (General page): selecting
//a conductor then brings up nothing in the dock, as before.
if (!QSettings().value(
QStringLiteral("diagrameditor/conductor_properties_panel"),
false).toBool()) {
return nullptr;
}
//Prototype (#500): single-conductor editing in the dock.
if (count_ > 1) {
return nullptr;
}
auto conductor = static_cast<Conductor*>(item);
if (class_name == ConductorPropertiesEditorWidget::staticMetaObject.className())
{
static_cast<ConductorPropertiesEditorWidget*>(editor)->setConductor(conductor);
return editor;
}
return new ConductorPropertiesEditorWidget(conductor, parent);
}
case Element::Type: //1000
{
if (count_ > 1) {
+2
View File
@@ -40,6 +40,7 @@
#include "ui/aboutqetdialog.h"
#include "ui/configpage/generalconfigurationpage.h"
#include "ui/configpage/shortcutsconfigpage.h"
#include "ui/configpage/shortcutbarconfigpage.h"
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
@@ -2213,6 +2214,7 @@ void QETApp::configureQET()
cd.addPage(new ExportConfigPage());
cd.addPage(new PrintConfigPage());
cd.addPage(new ShortcutsConfigPage());
cd.addPage(new ShortcutBarConfigPage());
#ifdef QET_SPACEMOUSE_SUPPORT
cd.addPage(new SpaceMouseConfigPage());
#endif
+222 -1
View File
@@ -22,6 +22,9 @@
#include <QCoreApplication>
#include <QToolButton>
#include "ElementsCollection/elementscollectionwidget.h"
#include "ElementsCollection/elementpickerpopup.h"
#include "shortcutbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "commandsearchpopup.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
@@ -76,6 +79,7 @@
#include <QDebug>
#include <QDir>
#include <QTimer>
#include <algorithm>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
@@ -229,6 +233,13 @@ void QETDiagramEditor::setUpElementsCollectionWidget()
m_qdw_elmt_collection->setWidget(m_element_collection_widget);
m_element_collection_widget->expandFirstItems();
//The widget does not know which view should receive the element -- it
//is also used by the picker popup, which has no editor ancestor -- so
//the host decides.
connect(m_element_collection_widget,
&ElementsCollectionWidget::insertElementRequested,
this, &QETDiagramEditor::insertElementFromCollection);
addDockWidget(Qt::RightDockWidgetArea, m_qdw_elmt_collection);
}
@@ -809,6 +820,25 @@ void QETDiagramEditor::setUpActions()
ShortcutManager::instance().registerAction(m_rotate_texts, "diagrameditor.rotate_texts", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_Space);
ShortcutManager::instance().registerAction(m_edit_selection, "diagrameditor.edit_selection", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_E);
//Re-enter placement mode with the element placed last. Bare A rather
//than Space: Space already rotates the pending element *inside*
//placement mode (diagrameventaddelement.cpp), and is taken three times
//over in this editor besides. A matches KiCad's add-symbol key and
//reads correctly in the source language ("Ajouter"). ShortcutManager
//makes it a default, not a commitment -- it appears in the Shortcuts
//preference page like every other binding.
m_insert_last_element = new QAction(QET::Icons::ElementNew,
tr("Insérer le dernier élément"), this);
m_insert_last_element->setStatusTip(
tr("Place à nouveau le dernier élément inséré", "status bar tip"));
m_insert_last_element->setData("insert_last_element");
m_insert_last_element->setEnabled(false);
ShortcutManager::instance().registerAction(
m_insert_last_element, "diagrameditor.insert_last_element",
tr("Éditeur de schémas"), Qt::Key_A);
connect(m_insert_last_element, &QAction::triggered,
this, &QETDiagramEditor::insertLastElement);
addAction(m_insert_last_element);
//Type to find and run any command, as SolidWorks' "Search Commands"
//and the command palette of many editors. Ctrl+Shift+P, the key those
//editors use, is taken by the autonumbering dock; M for "menu".
@@ -830,6 +860,51 @@ void QETDiagramEditor::setUpActions()
});
addAction(m_command_search);
//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);
//The picker with a row of commands above it, chosen by what is
//selected -- the SolidWorks "S" shortcut bar. S is unbound in this
//editor.
m_show_shortcut_bar = new QAction(tr("Barre de raccourcis"), this);
m_show_shortcut_bar->setStatusTip(
tr("Ouvre à la position du curseur les commandes utiles pour la sélection, et le sélecteur d'éléments",
"status bar tip"));
ShortcutManager::instance().registerAction(
m_show_shortcut_bar, "diagrameditor.show_shortcut_bar",
tr("Éditeur de schémas"), Qt::Key_S);
connect(m_show_shortcut_bar, &QAction::triggered,
this, &QETDiagramEditor::showShortcutBar);
addAction(m_show_shortcut_bar);
//Enter on the folio repeats it (DiagramView::keyPressEvent). That is
//handled by the view rather than bound here, so Enter keeps working
//in the search fields, the collection tree and every dialog.
m_repeat_last_command = new QAction(tr("Répéter la dernière commande"), this);
m_repeat_last_command->setStatusTip(
tr("Relance le dernier outil de dessin ou la dernière insertion d'élément (Entrée sur le folio)",
"status bar tip"));
m_repeat_last_command->setEnabled(false);
ShortcutManager::instance().registerAction(
m_repeat_last_command, "diagrameditor.repeat_last_command",
tr("Éditeur de schémas"), QKeySequence());
connect(m_repeat_last_command, &QAction::triggered,
this, &QETDiagramEditor::repeatLastCommand);
addAction(m_repeat_last_command);
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"));
@@ -962,7 +1037,8 @@ void QETDiagramEditor::setUpActions()
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
//No default key, but an id: they can then be found by the command
//search and bound in the Shortcuts page, like every other command.
//search, bound in the Shortcuts page and placed on the shortcut bar,
//like every other command.
for (QAction *action : m_add_item_actions_group.actions()) {
ShortcutManager::instance().registerAction(
action, "diagrameditor.add_" + action->data().toString(),
@@ -1121,6 +1197,10 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_paste);
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 -> addAction(m_show_shortcut_bar);
menu_edition -> addAction(m_repeat_last_command);
menu_edition -> addAction(m_command_search);
menu_edition -> addSeparator();
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
@@ -1837,6 +1917,8 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
return;
}
setLastCommand(action);
DiagramEventInterface *diagram_event = nullptr;
if (value == "line")
@@ -2030,6 +2112,11 @@ void QETDiagramEditor::slot_updateActions()
m_zoom_actions_group. setEnabled(opened_diagram);
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_show_shortcut_bar-> setEnabled(opened_diagram && editable_project);
m_repeat_last_command-> setEnabled(opened_diagram && editable_project
&& m_last_command && m_last_command->isEnabled());
m_row_column_actions_group. setEnabled(editable_project);
m_background_color_button-> setEnabled(opened_diagram);
m_draw_grid-> setEnabled(opened_diagram);
@@ -2925,6 +3012,7 @@ void QETDiagramEditor::diagramWasAdded(DiagramView *dv)
&QETDiagramEditor::selectionChanged,
Qt::DirectConnection);
connect(dv, &DiagramView::modeChanged, this, &QETDiagramEditor::slot_updateModeActions);
connect(dv, &DiagramView::elementPlacementStarted, this, &QETDiagramEditor::rememberPlacedElement);
}
/**
@@ -3034,6 +3122,139 @@ void QETDiagramEditor::selectionChanged()
}
/**
@brief QETDiagramEditor::insertElementFromCollection
Place @a location on the current folio using the interactive placement
mode -- the same mode a drag and drop ends in, entered without the drag.
@param location
*/
void QETDiagramEditor::insertElementFromCollection(const ElementsLocation &location)
{
DiagramView *dv = currentDiagramView();
if (dv && dv->startElementPlacement(location, dv->defaultPlacementPos())) {
return;
}
//Nowhere to place it: no folio open, or a read-only project. Do what
//a double click did before it placed, rather than nothing.
m_element_collection_widget->editLocation(location);
}
/**
@brief QETDiagramEditor::rememberPlacedElement
Remember @a location for "insert last element". Connected to every view,
so an element placed by drag and drop counts as well as one placed from
the collection without a drag. Macros never reach here: the view does not
report them, since re-entering their placement mode from a shortcut has
not been thought through.
@param location
*/
void QETDiagramEditor::rememberPlacedElement(const ElementsLocation &location)
{
m_last_inserted_element = location;
m_insert_last_element->setEnabled(true);
setLastCommand(m_insert_last_element);
}
/**
@brief QETDiagramEditor::setLastCommand
Remember @a action as the command Enter repeats, and name it in the
Édition menu entry so it is clear what will happen.
*/
void QETDiagramEditor::setLastCommand(QAction *action)
{
m_last_command = action;
m_repeat_last_command->setText(
action == m_insert_last_element
? tr("Répéter : insérer « %1 »").arg(m_last_inserted_element.name())
: tr("Répéter : %1").arg(action->text().remove(QLatin1Char('&'))));
slot_updateActions();
}
/**
@brief QETDiagramEditor::repeatLastCommand
Run the last drawing or placing command again -- a line, a text field,
the last element -- as Enter does in SolidWorks.
@return true if a command was run
*/
bool QETDiagramEditor::repeatLastCommand()
{
if (!m_last_command || !m_repeat_last_command->isEnabled()) {
return false;
}
//A tool that is still on would be switched off by triggering it
if (m_last_command->isCheckable() && m_last_command->isChecked()) {
return false;
}
m_last_command->trigger();
return true;
}
/**
@brief QETDiagramEditor::insertLastElement
Re-enter placement mode with the element placed most recently, so a run of
the same symbol can be dropped without returning to the collection.
*/
void QETDiagramEditor::insertLastElement()
{
if (!m_last_inserted_element.isElement()
|| !m_last_inserted_element.exist()) {
return;
}
insertElementFromCollection(m_last_inserted_element);
}
/**
@brief QETDiagramEditor::elementPicker
@return the element picker, built on first use: most sessions never open
it, and it holds a list view and a model of its own.
*/
ElementPickerPopup *QETDiagramEditor::elementPicker()
{
if (!m_element_picker)
{
m_element_picker = new ElementPickerPopup(m_element_collection_widget,
this);
connect(m_element_picker, &ElementPickerPopup::elementChosen,
this, &QETDiagramEditor::insertElementFromCollection);
}
return m_element_picker;
}
/**
@brief QETDiagramEditor::showElementPicker
Open the element picker where the mouse is.
*/
void QETDiagramEditor::showElementPicker()
{
if (!currentDiagramView()) {
return;
}
elementPicker()->popUpAt(QCursor::pos());
}
/**
@brief QETDiagramEditor::showShortcutBar
Open the element picker where the mouse is, with a row of commands above
it chosen by what is selected: the folio's commands with nothing
selected, conductor commands when only conductors are, and selection
commands otherwise. The commands are ShortcutManager ids, listed in
ShortcutBarSettings and editable in the configuration dialog.
*/
void QETDiagramEditor::showShortcutBar()
{
DiagramView *dv = currentDiagramView();
if (!dv) {
return;
}
const ShortcutBarSettings::Context context =
ShortcutBarSettings::contextFor(dv->diagram()->selectedItems());
elementPicker()->popUpShortcutBar(QCursor::pos(), context);
}
/**
@brief QETDiagramEditor::generateTerminalBlock
*/
+20 -1
View File
@@ -25,6 +25,7 @@
#include <QCloseEvent>
#include <QDir>
#include <QMdiArea>
#include <QPointer>
#include <QSignalMapper>
#include <QUndoGroup>
@@ -45,6 +46,7 @@ class ElementsLocation;
class RecentFiles;
class DiagramPropertiesEditorDockWidget;
class ElementsCollectionWidget;
class ElementPickerPopup;
class CommandSearchPopup;
class AutoNumberingDockWidget;
class TerminalNumberingDialog;
@@ -128,6 +130,12 @@ class QETDiagramEditor : public QETMainWindow
void slot_updatePasteAction();
void slot_updateWindowsMenu();
void slot_updateAutoNumDock();
void insertElementFromCollection(const ElementsLocation &location);
void insertLastElement();
void rememberPlacedElement(const ElementsLocation &location);
void showElementPicker();
void showShortcutBar();
bool repeatLastCommand();
void generateTerminalBlock();
void setWindowedMode();
void setTabbedMode();
@@ -175,7 +183,8 @@ class QETDiagramEditor : public QETMainWindow
*m_edit_diagram_properties, ///< Show a dialog to edit diagram properties
*m_conductor_reset, ///< Reset paths of selected conductors
*m_cut, ///< Cut selection to clipboard
*m_copy; ///< Copy selection to clipboard
*m_copy, ///< Copy selection to clipboard
*m_insert_last_element = nullptr; ///< Place the last placed element again
QActionGroup
m_row_column_actions_group, /// Action related to add/remove rows/column in diagram
@@ -270,9 +279,19 @@ class QETDiagramEditor : public QETMainWindow
*m_qdw_elmt_collection,
*qdw_undo; /// Dock for the undo list
ElementPickerPopup *elementPicker();
QAction *m_show_element_picker = nullptr;
QAction *m_show_shortcut_bar = nullptr;
QAction *m_repeat_last_command = nullptr;
/// What Enter on the folio repeats
QPointer<QAction> m_last_command;
void setLastCommand(QAction *action);
ElementPickerPopup *m_element_picker = nullptr; ///< Built on first use
QAction *m_command_search = nullptr;
CommandSearchPopup *m_command_search_popup = nullptr; ///< Built on first use
ElementsCollectionWidget *m_element_collection_widget;
/// Last element placed from the collection, for "insert last"
ElementsLocation m_last_inserted_element;
DiagramPropertiesEditorDockWidget *m_selection_properties_editor;
/// Elements panel
+143 -11
View File
@@ -35,6 +35,7 @@
#include <QBuffer>
#include <QFileDialog>
#include <QFileInfo>
#include <QFontMetricsF>
#include <QGraphicsSceneContextMenuEvent>
#include <QImageWriter>
#include <QMenu>
@@ -69,7 +70,7 @@ DiagramImageItem::DiagramImageItem(const QPixmap &pixmap, QetGraphicsItem *paren
// those are a single uniform scale() float, which is exactly why an
// image could never break its own aspect ratio before this class
// gained proper independent scaleX/scaleY.
m_transform.pivot = boundingRect().center();
m_transform.pivot = imageRect().center();
setTransform(m_transform.toMatrix());
setFlags(QGraphicsItem::ItemIsSelectable|QGraphicsItem::ItemIsMovable|QGraphicsItem::ItemSendsGeometryChanges);
setAcceptHoverEvents(true);
@@ -110,6 +111,45 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
painter -> drawRect(boundingRect());
painter -> restore();
}
if (!m_label.isEmpty())
{
// Undo the picture's own scale so the caption keeps the size of
// every other text on the folio whether the picture is shown at
// 5 % or 500 %. Rotation and skew are applied after scale in
// ShapeTransform::toMatrix(), so the caption still turns with it.
const QRectF r = labelRect();
painter -> save();
painter -> translate(r.topLeft());
painter -> scale(1.0 / m_label_scale.x(), 1.0 / m_label_scale.y());
painter -> setFont(m_label_font);
painter -> setPen(Qt::black);
painter -> drawText(QRectF(QPointF(0, 0), m_label_size), Qt::AlignCenter, m_label);
painter -> restore();
}
}
/**
@brief DiagramImageItem::setLabel
Set the caption drawn under the picture. An empty string removes it.
@param label
*/
void DiagramImageItem::setLabel(const QString &label)
{
if (label == m_label)
return;
prepareGeometryChange();
m_label = label;
// Font looked up here, not in paint(): diagramTextsFont() reads
// QSettings, far too slow to do on every repaint.
if (!m_label.isEmpty())
m_label_font = QETApp::diagramTextsFont();
m_label_size = m_label.isEmpty()
? QSizeF()
: QFontMetricsF(m_label_font).size(0, m_label);
updateLabelScale();
emit labelChanged();
}
/**
@@ -278,7 +318,7 @@ void DiagramImageItem::setPivotRaw(const QPointF &newPivot)
void DiagramImageItem::resetPivotToBoundingRectCenter()
{
m_pivotIsCustom = false;
setPivot(boundingRect().center());
setPivot(imageRect().center());
}
namespace {
@@ -1263,6 +1303,11 @@ QVariant DiagramImageItem::itemChange(GraphicsItemChange change, const QVariant
}
refreshInteractionHints();
}
else if (change == ItemTransformChange && !m_label.isEmpty())
{
prepareGeometryChange();
updateLabelScale();
}
else if (change == ItemPositionHasChanged || change == ItemTransformHasChanged)
{
if (!m_deferHandleReposition)
@@ -1308,6 +1353,18 @@ QPixmap DiagramImageItem::computeDisplayPixmap(const QPixmap &base, const QRect
@return a QRectF represent the bounding rectangle
*/
QRectF DiagramImageItem::boundingRect() const
{
if (m_label.isEmpty())
return imageRect();
return imageRect().united(labelRect());
}
/**
@brief DiagramImageItem::imageRect
@return the picture's own rectangle, without its label. Handles, the
pivot and every other piece of geometry work on this one.
*/
QRectF DiagramImageItem::imageRect() const
{
if (!pixmap_.isNull()) {
return (QRectF(pixmap_.rect()));
@@ -1317,6 +1374,34 @@ QRectF DiagramImageItem::boundingRect() const
}
}
/**
@brief DiagramImageItem::labelRect
@return the label's rectangle in item coordinates, centred just under
the picture. Divided by the picture's scale, because paint() draws
the label unscaled.
*/
QRectF DiagramImageItem::labelRect() const
{
const QRectF image = imageRect();
const qreal w = m_label_size.width() / m_label_scale.x();
const qreal h = m_label_size.height() / m_label_scale.y();
const qreal gap = 2.0 / m_label_scale.y();
return QRectF(image.center().x() - w / 2.0, image.bottom() + gap, w, h);
}
/**
@brief DiagramImageItem::updateLabelScale
The label rect depends on the picture's scale, so boundingRect()
changes with it. m_transform is already updated by the time the item
transform changes, so the scale is cached here and only refreshed
after prepareGeometryChange() has recorded the old rect.
*/
void DiagramImageItem::updateLabelScale()
{
const auto safe = [](qreal v) { return qFuzzyIsNull(v) ? 1.0 : qAbs(v); };
m_label_scale = QPointF(safe(m_transform.scaleX), safe(m_transform.scaleY));
}
/**
@brief DiagramImageItem::name
@return the generic name of this item (picture)
@@ -1326,6 +1411,36 @@ QString DiagramImageItem::name() const
return tr("une image");
}
namespace {
// Only PNG bytes read from a file are reused as they are: toXml()
// has always written PNG, and a hand-edited file holding another
// format keeps being converted, exactly as before.
bool isPng(const QByteArray &data)
{
return data.startsWith("\x89PNG\r\n\x1a\n");
}
}
/**
@brief DiagramImageItem::encodedPng
@return pixmap as PNG bytes, encoded only when cache does not already
hold them. QPixmap::cacheKey() changes whenever the pixmap is replaced
or modified, which is what makes this safe without every function
that edits the picture having to remember to invalidate the cache.
*/
const QByteArray &DiagramImageItem::encodedPng(const QPixmap &pixmap, QByteArray &cache, qint64 &cacheKey)
{
if (cacheKey != pixmap.cacheKey())
{
cache.clear();
QBuffer buffer(&cache);
buffer.open(QIODevice::WriteOnly);
pixmap.save(&buffer, "PNG");
cacheKey = pixmap.cacheKey();
}
return cache;
}
/**
@brief DiagramImageItem::fromXml
Load this image from xml element e
@@ -1354,6 +1469,17 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
QPixmap pixmap;
pixmap.loadFromData(array);
setPixmap(pixmap);
// From the first text node only, not e.text(): that also collects the
// text of every child element, so on a cropped or colour-keyed picture
// it carries <image_base>'s bytes too. loadFromData() stops at the end
// of the first PNG and never notices; a cache written back verbatim
// would.
const QByteArray ownArray = QByteArray::fromBase64(image_node.toText().data().toLatin1());
if (isPng(ownArray) && !pixmap_.isNull())
{
m_png_cache = ownArray;
m_png_cache_key = pixmap_.cacheKey();
}
// Falls back to treating the loaded result as its own base, with no
// remembered crop or colours -- correct both for a genuinely plain
@@ -1407,7 +1533,14 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
const QByteArray baseArray = QByteArray::fromBase64(baseElement.text().toLatin1());
QPixmap basePixmap;
if (basePixmap.loadFromData(baseArray))
{
m_base_pixmap = basePixmap;
if (isPng(baseArray))
{
m_base_png_cache = baseArray;
m_base_png_cache_key = m_base_pixmap.cacheKey();
}
}
}
// m_crop_rect may still refer to a saved file's base image, not
// pixmap (used as a fallback above only when nothing better is
@@ -1429,7 +1562,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
m_transform.rotation = e.attribute("rotation").toDouble();
m_transform.scaleX = e.attribute("size").toDouble();
m_transform.scaleY = m_transform.scaleX;
m_transform.pivot = boundingRect().center();
m_transform.pivot = imageRect().center();
m_pivotIsCustom = false;
const QDomElement transformElement = e.firstChildElement("transform");
@@ -1450,6 +1583,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
QGraphicsObject::setPos(e.attribute("x").toDouble(), e.attribute("y").toDouble());
setZValue(e.attribute("z", QString::number(this->zValue())).toDouble());
is_movable_ = (e.attribute("is_movable").toInt());
setLabel(e.attribute("label"));
return (true);
}
@@ -1481,12 +1615,13 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const
result.setAttribute("rotation", QString::number(QET::correctAngle(m_transform.rotation)));
result.setAttribute("size", QString::number(m_transform.scaleX));
result.setAttribute("is_movable", bool(is_movable_));
// An attribute, not a child element: fromXml() of every earlier
// version requires the base64 text to be the first child.
if (!m_label.isEmpty())
result.setAttribute("label", m_label);
//write the pixmap in the xml element after he was been transformed to base64
QByteArray array;
QBuffer buffer(&array);
buffer.open(QIODevice::ReadWrite);
pixmap_.save(&buffer, "PNG");
const QByteArray &array = encodedPng(pixmap_, m_png_cache, m_png_cache_key);
QDomText base64 = document.createTextNode(array.toBase64());
result.appendChild(base64);
@@ -1556,10 +1691,7 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const
if (hasCrop || hasColors)
{
QByteArray baseArray;
QBuffer baseBuffer(&baseArray);
baseBuffer.open(QIODevice::ReadWrite);
m_base_pixmap.save(&baseBuffer, "PNG");
const QByteArray &baseArray = encodedPng(m_base_pixmap, m_base_png_cache, m_base_png_cache_key);
QDomElement baseElement = document.createElement("image_base");
baseElement.appendChild(document.createTextNode(baseArray.toBase64()));
result.appendChild(baseElement);
@@ -23,6 +23,7 @@
#include "../ui/imagetransparentcolordialog.h"
#include <QColor>
#include <QFont>
#include <QList>
#include <QUuid>
#include <QStyleOptionGraphicsItem>
@@ -47,6 +48,7 @@ class DiagramImageItem : public QetGraphicsItem {
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
// A second, deliberately non-compensating property on the SAME
// underlying value -- setPivot() (above) intentionally adjusts
// pos() to keep the image visually in place, which is exactly
@@ -127,11 +129,14 @@ class DiagramImageItem : public QetGraphicsItem {
QPointF pivot() const { return m_transform.pivot; }
void setPivot(const QPointF &pivot);
void setPivotRaw(const QPointF &pivot);
QString label() const { return m_label; }
void setLabel(const QString &label);
signals:
void pixmapChanged();
void transformChanged();
void uuidChanged();
void labelChanged();
protected:
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
@@ -152,6 +157,7 @@ class DiagramImageItem : public QetGraphicsItem {
void saveOriginalImageAs();
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
static const QByteArray &encodedPng(const QPixmap &pixmap, QByteArray &cache, qint64 &cacheKey);
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
void toggleHandleMode();
@@ -180,6 +186,9 @@ class DiagramImageItem : public QetGraphicsItem {
static QString hintForHandleRole(HandleRole role);
void showStatusHint(const QString &text) const;
void clearStatusHint() const;
QRectF imageRect() const;
QRectF labelRect() const;
void updateLabelScale();
protected:
QUuid m_uuid = QUuid::createUuid();
@@ -202,6 +211,16 @@ class DiagramImageItem : public QetGraphicsItem {
QPixmap m_base_pixmap;
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
// PNG bytes of pixmap_ and m_base_pixmap as last written, reused by
// toXml() while the pixmap's cacheKey() still matches. PNG encoding
// is the bulk of the time a save spends on a picture, and redoing it
// on every save, autosave and copy of an unchanged picture is pure
// waste. Filled from the file itself on load, so even the first save
// encodes nothing.
mutable QByteArray m_png_cache;
mutable qint64 m_png_cache_key = 0;
mutable QByteArray m_base_png_cache;
mutable qint64 m_base_png_cache_key = 0;
// Independent scaleX/scaleY here is the actual point of this whole
// member: QGraphicsItem::scale() is a single, uniform float, which
@@ -218,6 +237,13 @@ class DiagramImageItem : public QetGraphicsItem {
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
ShapeTransform m_original_transform;
bool m_deferHandleReposition = false; // see setPivot()'s comment
// Optional caption drawn centred under the picture (issue #349).
// Empty by default, and then neither saved nor painted, so a picture
// without one costs exactly what it did before.
QString m_label;
QFont m_label_font;
QSizeF m_label_size; // in scene units, measured once in setLabel()
QPointF m_label_scale{1.0, 1.0}; // scale the label rect was last computed for -- see updateLabelScale()
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
};
#endif
+208
View File
@@ -0,0 +1,208 @@
/*
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 "shortcutbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "shortcutmanager.h"
#include <QCoreApplication>
#include <algorithm>
#include <QSettings>
#include <algorithm>
namespace {
QString settingsKey(ShortcutBarSettings::Context context)
{
switch (context)
{
case ShortcutBarSettings::Canvas:
return QStringLiteral("diagrameditor/shortcut_bar/canvas");
case ShortcutBarSettings::Selection:
return QStringLiteral("diagrameditor/shortcut_bar/selection");
case ShortcutBarSettings::Conductor:
return QStringLiteral("diagrameditor/shortcut_bar/conductor");
}
return QString();
}
}
/**
@return every context, in the order they are shown to the user
*/
QList<ShortcutBarSettings::Context> ShortcutBarSettings::contexts()
{
return {Canvas, Selection, Conductor};
}
/**
@return the context for @a selection: Canvas when empty, Conductor when
it holds only conductors, Selection otherwise
*/
ShortcutBarSettings::Context ShortcutBarSettings::contextFor(
const QList<QGraphicsItem *> &selection)
{
if (selection.isEmpty()) {
return Canvas;
}
const bool only_conductors = std::all_of(
selection.cbegin(), selection.cend(),
[](QGraphicsItem *item) { return item->type() == Conductor::Type; });
return only_conductors ? Conductor : Selection;
}
/**
@return the name of @a context, for the configuration page
*/
QString ShortcutBarSettings::title(Context context)
{
switch (context)
{
case Canvas:
return QCoreApplication::translate("ShortcutBarSettings", "Folio, rien de sélectionné");
case Selection:
return QCoreApplication::translate("ShortcutBarSettings", "Éléments sélectionnés");
case Conductor:
return QCoreApplication::translate("ShortcutBarSettings", "Conducteurs sélectionnés");
}
return QString();
}
/**
@return the ids to show for @a context: the user's list if they saved
one, the defaults otherwise. A saved empty list stays empty.
*/
QStringList ShortcutBarSettings::ids(Context context)
{
QSettings settings;
const QString key = settingsKey(context);
if (!settings.contains(key)) {
return defaultIds(context);
}
return settings.value(key).toStringList();
}
/**
@return the commands a new user sees for @a context
*/
QStringList ShortcutBarSettings::defaultIds(Context context)
{
switch (context)
{
case Canvas:
return {QStringLiteral("diagrameditor.insert_last_element"),
QStringLiteral("diagrameditor.show_element_picker"),
QStringLiteral("diagrameditor.add_text"),
QStringLiteral("diagrameditor.add_line"),
QStringLiteral("diagrameditor.add_rectangle"),
QStringLiteral("diagrameditor.add_terminal_strip"),
QStringLiteral("diagrameditor.paste"),
QStringLiteral("diagrameditor.edit_diagram_properties")};
case Selection:
return {QStringLiteral("diagrameditor.rotate_selection"),
QStringLiteral("diagrameditor.rotate_texts"),
QStringLiteral("diagrameditor.edit_selection"),
QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.cut"),
QStringLiteral("diagrameditor.delete_selection")};
case Conductor:
return {QStringLiteral("diagrameditor.conductor_reset"),
QStringLiteral("diagrameditor.edit_selection"),
QStringLiteral("diagrameditor.delete_selection")};
}
return {};
}
/**
@brief ShortcutBarSettings::setIds
Save @a ids for @a context. Saving the defaults removes the key, so a
later change of defaults still reaches this user.
*/
void ShortcutBarSettings::setIds(Context context, const QStringList &ids)
{
QSettings settings;
if (ids == defaultIds(context)) {
settings.remove(settingsKey(context));
} else {
settings.setValue(settingsKey(context), ids);
}
}
/**
@return every command that can go on the bar: those the diagram editor
registered with ShortcutManager, except the one that opens the bar.
*/
QStringList ShortcutBarSettings::availableIds()
{
QStringList ids;
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts())
{
if (info.id.startsWith(QLatin1String("diagrameditor."))
&& info.id != QLatin1String("diagrameditor.show_shortcut_bar")) {
ids << info.id;
}
}
return ids;
}
/**
@return true if @a id is a pinned element's collection path rather than
a command id. Command ids never contain "://".
*/
bool ShortcutBarSettings::isElement(const QString &id)
{
return id.contains(QLatin1String("://"));
}
/**
@return true if the row for @a context holds at least one element
*/
bool ShortcutBarSettings::hasElements(Context context)
{
const QStringList list = ids(context);
return std::any_of(list.cbegin(), list.cend(), &ShortcutBarSettings::isElement);
}
/**
@return the width the user gave the bar with its size grip, or 0 when
they never did: the tiles then stay on one row
*/
int ShortcutBarSettings::barWidth()
{
return QSettings().value(QStringLiteral("diagrameditor/shortcut_bar/width"), 0).toInt();
}
void ShortcutBarSettings::setBarWidth(int width)
{
QSettings().setValue(QStringLiteral("diagrameditor/shortcut_bar/width"), width);
}
/**
@return the size the user left the customising window at, or an invalid
size when it was never opened
*/
QSize ShortcutBarSettings::editorSize()
{
return QSettings().value(QStringLiteral("diagrameditor/shortcut_bar/editor_size")).toSize();
}
void ShortcutBarSettings::setEditorSize(const QSize &size)
{
QSettings().setValue(QStringLiteral("diagrameditor/shortcut_bar/editor_size"), size);
}
+65
View File
@@ -0,0 +1,65 @@
/*
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/>.
*/
#ifndef SHORTCUTBARSETTINGS_H
#define SHORTCUTBARSETTINGS_H
#include <QList>
class QGraphicsItem;
#include <QString>
#include <QStringList>
#include <QSize>
/**
@brief The commands shown on the diagram editor's shortcut bar.
The bar opens at the cursor and shows a different row of commands
depending on what is selected. Each row is a list of ShortcutManager ids,
so any registered command can go on it and the bar needs no command list
of its own. Stored in QSettings, one key per context; a context the user
never changed uses the defaults below.
A row can also hold elements the user pinned, by collection path
("common://…", "custom://…", "company://…"), mixed in with the commands.
Clicking one places that element.
*/
class ShortcutBarSettings
{
public:
enum Context {
Canvas, ///< nothing selected
Selection, ///< elements, texts or shapes selected
Conductor ///< only conductors selected
};
static QList<Context> contexts();
static Context contextFor(const QList<QGraphicsItem *> &selection);
static QString title(Context context);
static QStringList ids(Context context);
static QStringList defaultIds(Context context);
static void setIds(Context context, const QStringList &ids);
static QStringList availableIds();
static bool isElement(const QString &id);
static bool hasElements(Context context);
static int barWidth();
static void setBarWidth(int width);
static QSize editorSize();
static void setEditorSize(const QSize &size);
};
#endif // SHORTCUTBARSETTINGS_H
@@ -0,0 +1,287 @@
/*
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 "conductorpropertieseditorwidget.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../qetgraphicsitem/conductor.h"
#include "conductorpropertieswidget.h"
#include "../qtextorientationspinboxwidget.h"
#include <KColorButton>
#include <QAbstractButton>
#include <QAbstractSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QGroupBox>
#include <QLineEdit>
#include <QScrollArea>
#include <QSettings>
#include <QSizePolicy>
#include <QSlider>
#include <QVBoxLayout>
/**
@brief ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget
@param conductor : conductor to edit
@param parent : parent widget
*/
ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget(
Conductor *conductor, QWidget *parent) :
PropertiesEditorWidget(parent),
m_cpw(new ConductorPropertiesWidget(this))
{
// The conductor widget is dialog-sized (min width ~600px), far too wide for
// a dock. Host it in a scroll area with a small minimum width so the dock
// can be dragged to any width (a scrollbar appears when narrower than the
// content). QET already persists dock geometry across restarts via
// QETDiagramEditor save/restoreState, so the chosen width is remembered.
// "Apply to all conductors of the potential": same semantics as the modal
// dialog's checkbox (ConductorPropertiesDialog::applyAll), pinned at the top
// of the panel so it stays visible above the scrolling tabs (#500). Unlike
// the dialog the choice is persisted, so a user who always wants it on (or
// off) sets it once. It is a child of this editor, not of m_cpw, so it is
// deliberately outside the live-edit signal wiring in connectChangeSignals()
// (toggling it must not push an edit, only change how the next edit applies).
// Reuse the modal dialog's exact wording for consistency (and so the
// existing translation applies).
m_apply_all_cb = new QCheckBox(
tr("Appliquer les propriétés à l'ensemble des conducteurs de ce potentiel"),
this);
m_apply_all_cb->setChecked(QSettings().value(
QStringLiteral("diagrameditor/conductor_apply_all"), true).toBool());
connect(m_apply_all_cb, &QCheckBox::toggled, this, [](bool on) {
QSettings().setValue(
QStringLiteral("diagrameditor/conductor_apply_all"), on);
});
auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(m_cpw);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_apply_all_cb);
layout->addWidget(scroll);
setMinimumWidth(120);
// Expand vertically to fill the dock like the other editors do (otherwise
// the panel sits at its small size hint with empty space below it, #500),
// while keeping a minimum height so it stays usable when the dock is short.
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
setMinimumHeight(200);
setDisabled(true);
setConductor(conductor);
}
ConductorPropertiesEditorWidget::~ConductorPropertiesEditorWidget()
{}
/**
@brief ConductorPropertiesEditorWidget::setConductor
Set (or change) the conductor whose properties are edited.
@param conductor
*/
void ConductorPropertiesEditorWidget::setConductor(Conductor *conductor)
{
if (!conductor) return;
if (m_conductor && m_conductor != conductor)
disconnect(m_conductor, &Conductor::propertiesChange,
this, &ConductorPropertiesEditorWidget::updateUi);
m_conductor = conductor;
//Keep the dock in sync when the conductor is edited elsewhere (e.g. the
//modal "Edit conductor" dialog); otherwise a stale snapshot would be
//written back on the next apply() and overwrite that change (issue #500).
connect(m_conductor, &Conductor::propertiesChange,
this, &ConductorPropertiesEditorWidget::updateUi, Qt::UniqueConnection);
setEnabled(true);
updateUi();
}
/**
@brief ConductorPropertiesEditorWidget::apply
Push the edit onto the diagram's undo stack.
*/
void ConductorPropertiesEditorWidget::apply()
{
// Ignore the field-change signals emitted while the widget is being loaded
// programmatically (updateUi/reset): mid-load the widget holds a partial
// state that must not be committed onto the conductor.
if (m_updating) return;
if (!m_conductor || !m_conductor->diagram()) return;
if (QUndoCommand *undo = associatedUndo())
m_conductor->diagram()->undoStack().push(undo);
m_initial = m_conductor->properties();
}
/**
@brief ConductorPropertiesEditorWidget::setLiveEdit
In live-edit mode (how the dock uses every editor), each field change is
applied immediately instead of via an explicit apply() call. Without this
override the base class is a no-op and edits in the dock were never applied
(issue #500).
@param live_edit true to enable live edit
@return always true
*/
bool ConductorPropertiesEditorWidget::setLiveEdit(bool live_edit)
{
if (m_live_edit == live_edit) return true;
m_live_edit = live_edit;
if (m_live_edit) connectChangeSignals();
else disconnectChangeSignals();
return true;
}
/**
@brief ConductorPropertiesEditorWidget::connectChangeSignals
Wire every editable control of the hosted ConductorPropertiesWidget to
apply(). Commit-style signals (editingFinished / activated / toggled /
sliderReleased) are used rather than per-keystroke ones so each edit yields
a single, clean undo step. Loading the widget programmatically (updateUi)
also fires some of these, but apply() is a no-op then because
associatedUndo() returns nullptr when the properties are unchanged.
*/
void ConductorPropertiesEditorWidget::connectChangeSignals()
{
if (!m_cpw) return;
const auto add = [this](QMetaObject::Connection c) {
m_live_connections << c;
};
for (auto *w : m_cpw->findChildren<QLineEdit *>())
add(connect(w, &QLineEdit::editingFinished,
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<QAbstractSpinBox *>())
add(connect(w, &QAbstractSpinBox::editingFinished,
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<QComboBox *>())
add(connect(w, QOverload<int>::of(&QComboBox::activated),
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<QSlider *>())
add(connect(w, &QSlider::sliderReleased,
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<KColorButton *>())
add(connect(w, &KColorButton::changed,
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<QTextOrientationSpinBoxWidget *>())
add(connect(w, QOverload<>::of(&QTextOrientationSpinBoxWidget::editingFinished),
this, &ConductorPropertiesEditorWidget::apply));
// Checkboxes and the checkable group boxes (single/multi wire, bicolor…).
for (auto *w : m_cpw->findChildren<QAbstractButton *>())
if (w->isCheckable())
add(connect(w, &QAbstractButton::toggled,
this, &ConductorPropertiesEditorWidget::apply));
for (auto *w : m_cpw->findChildren<QGroupBox *>())
if (w->isCheckable())
add(connect(w, &QGroupBox::toggled,
this, &ConductorPropertiesEditorWidget::apply));
}
/**
@brief ConductorPropertiesEditorWidget::disconnectChangeSignals
Tear down the live-edit connections made by connectChangeSignals().
*/
void ConductorPropertiesEditorWidget::disconnectChangeSignals()
{
for (const QMetaObject::Connection &c : m_live_connections)
disconnect(c);
m_live_connections.clear();
}
/**
@brief ConductorPropertiesEditorWidget::reset
Discard the in-progress edit, restoring the conductor's current properties.
*/
void ConductorPropertiesEditorWidget::reset()
{
if (!m_conductor) return;
m_updating = true;
m_cpw->setProperties(m_initial);
m_updating = false;
}
/**
@brief ConductorPropertiesEditorWidget::updateUi
Reload the widget from the conductor (e.g. when the selection changes).
*/
void ConductorPropertiesEditorWidget::updateUi()
{
if (!m_conductor) return;
m_updating = true;
m_initial = m_conductor->properties();
m_cpw->setProperties(m_initial);
m_updating = false;
}
/**
@brief ConductorPropertiesEditorWidget::associatedUndo
@return the edit as a QPropertyUndoCommand, or nullptr if unchanged.
When "apply to all" is ticked, every conductor on the same potential is
updated in the same undo step (one undo reverts them all), exactly as the
modal dialog does (ConductorPropertiesDialog::PropertiesDialog). Otherwise
only the selected conductor is changed.
*/
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
{
if (!m_conductor) return nullptr;
const ConductorProperties new_properties = m_cpw->properties();
if (new_properties == m_conductor->properties()) return nullptr;
QVariant old_value, new_value;
old_value.setValue(m_conductor->properties());
new_value.setValue(new_properties);
auto *undo = new QPropertyUndoCommand(
m_conductor, "properties", old_value, new_value);
undo->setText(tr("Modifier les propriétés d'un conducteur", "undo caption"));
// Propagate to every conductor on the same potential, as the modal dialog
// does: each related conductor becomes a child command of the same undo
// step, set to the same target properties.
if (m_apply_all_cb && m_apply_all_cb->isChecked())
{
const auto potential = m_conductor->relatedPotentialConductors();
if (!potential.isEmpty())
{
undo->setText(tr("Modifier les propriétés de plusieurs conducteurs",
"undo caption"));
for (Conductor *potential_conductor : potential)
{
QVariant old_v;
old_v.setValue(potential_conductor->properties());
new QPropertyUndoCommand(
potential_conductor, "properties", old_v, new_value, undo);
}
}
}
return undo;
}
/**
@brief ConductorPropertiesEditorWidget::title
@return the panel title.
*/
QString ConductorPropertiesEditorWidget::title() const
{
return tr("Conducteur");
}
@@ -0,0 +1,70 @@
/*
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/>.
*/
#ifndef CONDUCTORPROPERTIESEDITORWIDGET_H
#define CONDUCTORPROPERTIESEDITORWIDGET_H
#include "../PropertiesEditor/propertieseditorwidget.h"
#include "../conductorproperties.h"
#include <QList>
#include <QMetaObject>
class Conductor;
class ConductorPropertiesWidget;
class QCheckBox;
/**
@brief The ConductorPropertiesEditorWidget class
Hosts the existing ConductorPropertiesWidget in the dockable selection-
properties panel, so a selected conductor can be edited in place like the
other item types, instead of only through the modal dialog (issue #500).
A pinned "apply to all conductors of the potential" checkbox (persisted)
mirrors the modal dialog's option to propagate edits to the whole potential.
*/
class ConductorPropertiesEditorWidget : public PropertiesEditorWidget
{
Q_OBJECT
public:
explicit ConductorPropertiesEditorWidget(
Conductor *conductor = nullptr, QWidget *parent = nullptr);
~ConductorPropertiesEditorWidget() override;
void setConductor(Conductor *conductor);
void apply() override;
void reset() override;
void updateUi() override;
QUndoCommand *associatedUndo() const override;
QString title() const override;
bool setLiveEdit(bool live_edit) override;
private:
void connectChangeSignals();
void disconnectChangeSignals();
private:
ConductorPropertiesWidget *m_cpw = nullptr;
QCheckBox *m_apply_all_cb = nullptr;
Conductor *m_conductor = nullptr;
ConductorProperties m_initial;
QList<QMetaObject::Connection> m_live_connections;
bool m_updating = false;
};
#endif // CONDUCTORPROPERTIESEDITORWIDGET_H
@@ -67,6 +67,11 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->grid_startup_cb->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool());
ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
//Stored as "inserts" but presented as "edits", so the default (insert)
//is the unchecked state -- a preference reads better as an opt-out.
ui->m_collection_dblclick_edits->setChecked(!settings.value("elementscollection/double-click-inserts", true).toBool());
ui->m_context_toolbar_cb->setChecked(settings.value("diagrameditor/context_toolbar", true).toBool());
ui->m_mouse_gestures_cb->setChecked(settings.value("diagrameditor/mouse_gestures", true).toBool());
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt());
for (const qreal divisor : TextGrid::divisors)
@@ -97,6 +102,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
else
ui->m_use_windows_mode_rb->setChecked(true);
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
ui->m_conductor_properties_panel->setChecked(settings.value("diagrameditor/conductor_properties_panel", false).toBool());
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
@@ -290,10 +296,14 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("diagrameditor/viewmode", view_mode) ;
settings.setValue("diagrameditor/highlight-integrated-elements", ui->m_highlight_integrated_elements->isChecked());
settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked());
settings.setValue("diagrameditor/conductor_properties_panel", ui->m_conductor_properties_panel->isChecked());
settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value());
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked());
settings.setValue("elementscollection/double-click-inserts", !ui->m_collection_dblclick_edits->isChecked());
settings.setValue("diagrameditor/context_toolbar", ui->m_context_toolbar_cb->isChecked());
settings.setValue("diagrameditor/mouse_gestures", ui->m_mouse_gestures_cb->isChecked());
//Grid step and key navigation
settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value());
settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value());
@@ -63,6 +63,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_conductor_properties_panel">
<property name="text">
<string>Afficher les propriétés d'un conducteur sélectionné dans le panneau Propriétés de la sélection</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
@@ -77,6 +84,36 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_collection_dblclick_edits">
<property name="toolTip">
<string>Par défaut, un double-clic insère l'élément sur le folio ; l'édition reste accessible par le menu contextuel.</string>
</property>
<property name="text">
<string>Double-cliquer dans la collection ouvre l'éditeur d'élément au lieu de l'insérer</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_context_toolbar_cb">
<property name="toolTip">
<string>Après un clic qui sélectionne un élément ou un conducteur, les commandes de la barre de raccourcis apparaissent près du curseur et s'effacent quand la souris s'éloigne.</string>
</property>
<property name="text">
<string>Afficher les commandes près de la sélection</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_mouse_gestures_cb">
<property name="toolTip">
<string>Maintenir le bouton droit et glisser dans une direction lance une commande de la barre de raccourcis. Un simple clic droit ouvre toujours le menu contextuel, au relâchement du bouton.</string>
</property>
<property name="text">
<string>Gestes de la souris avec le bouton droit</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="guides_startup_cb">
<property name="text">
@@ -1175,6 +1212,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<tabstop>m_use_system_color_cb</tabstop>
<tabstop>m_use_gesture_trackpad</tabstop>
<tabstop>m_zoom_out_beyond_folio</tabstop>
<tabstop>m_conductor_properties_panel</tabstop>
<tabstop>m_use_windows_mode_rb</tabstop>
<tabstop>m_use_tab_mode_rb</tabstop>
<tabstop>m_save_label_paste</tabstop>
@@ -0,0 +1,240 @@
/*
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 "shortcutbarconfigpage.h"
#include "../../ElementsCollection/elementslocation.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QAction>
#include <QComboBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
/**
@brief ShortcutBarConfigPage::ShortcutBarConfigPage
@param parent
*/
ShortcutBarConfigPage::ShortcutBarConfigPage(QWidget *parent) :
ConfigPage(parent)
{
const QStringList available = ShortcutBarSettings::availableIds();
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts()) {
if (available.contains(info.id)) {
m_descriptions.insert(info.id, info.description);
}
}
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_pending.insert(c, ShortcutBarSettings::ids(c));
}
auto *explanation = new QLabel(
tr("La barre de raccourcis s'ouvre à la position du curseur "
"(touche S par défaut). Elle montre les commandes choisies "
"ici selon ce qui est sélectionné, puis le sélecteur "
"d'éléments."), this);
explanation->setWordWrap(true);
m_context = new QComboBox(this);
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_context->addItem(ShortcutBarSettings::title(c), c);
}
m_available = new QListWidget(this);
m_available->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_available->setSortingEnabled(true);
m_chosen = new QListWidget(this);
m_chosen->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_chosen->setDragDropMode(QAbstractItemView::InternalMove);
auto *add = new QPushButton(tr("Ajouter →"), this);
auto *remove = new QPushButton(tr("← Retirer"), this);
auto *up = new QPushButton(tr("Monter"), this);
auto *down = new QPushButton(tr("Descendre"), this);
auto *reset = new QPushButton(tr("Valeurs par défaut"), this);
auto *buttons = new QVBoxLayout();
buttons->addStretch();
buttons->addWidget(add);
buttons->addWidget(remove);
buttons->addSpacing(12);
buttons->addWidget(up);
buttons->addWidget(down);
buttons->addStretch();
auto *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Commandes disponibles"), this), 0, 0);
grid->addWidget(new QLabel(tr("Dans la barre, dans l'ordre"), this), 0, 2);
grid->addWidget(m_available, 1, 0);
grid->addLayout(buttons, 1, 1);
grid->addWidget(m_chosen, 1, 2);
auto *context_row = new QHBoxLayout();
context_row->addWidget(new QLabel(tr("Contexte :"), this));
context_row->addWidget(m_context, 1);
context_row->addWidget(reset);
auto *layout = new QVBoxLayout(this);
layout->addWidget(explanation);
layout->addLayout(context_row);
layout->addLayout(grid);
connect(m_context, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
storeContext();
showContext();
});
connect(add, &QPushButton::clicked, this, &ShortcutBarConfigPage::addSelected);
connect(remove, &QPushButton::clicked, this, &ShortcutBarConfigPage::removeSelected);
connect(up, &QPushButton::clicked, this, [this]() { moveSelected(-1); });
connect(down, &QPushButton::clicked, this, [this]() { moveSelected(1); });
connect(reset, &QPushButton::clicked, this, &ShortcutBarConfigPage::resetContext);
connect(m_available, &QListWidget::itemDoubleClicked, this, &ShortcutBarConfigPage::addSelected);
connect(m_chosen, &QListWidget::itemDoubleClicked, this, &ShortcutBarConfigPage::removeSelected);
showContext();
}
/**
@brief ShortcutBarConfigPage::applyConf
Save every context's list.
*/
void ShortcutBarConfigPage::applyConf()
{
storeContext();
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
ShortcutBarSettings::setIds(c, m_pending.value(c));
}
}
QString ShortcutBarConfigPage::title() const
{
return tr("Barre de raccourcis", "configuration page title");
}
QIcon ShortcutBarConfigPage::icon() const
{
return QET::Icons::ConfigureShortcuts;
}
/**
@brief ShortcutBarConfigPage::showContext
Fill both lists for the context chosen in the combo box.
*/
void ShortcutBarConfigPage::showContext()
{
m_shown = static_cast<ShortcutBarSettings::Context>(
m_context->currentData().toInt());
const QStringList chosen = m_pending.value(m_shown);
m_available->clear();
m_chosen->clear();
for (const QString &id : chosen) {
appendItem(m_chosen, id);
}
for (auto it = m_descriptions.cbegin(); it != m_descriptions.cend(); ++it) {
if (!chosen.contains(it.key())) {
appendItem(m_available, it.key());
}
}
}
/**
@brief ShortcutBarConfigPage::storeContext
Keep the shown context's list, in the order on screen.
*/
void ShortcutBarConfigPage::storeContext()
{
QStringList ids;
for (int i = 0 ; i < m_chosen->count() ; ++i) {
ids << m_chosen->item(i)->data(Qt::UserRole).toString();
}
m_pending.insert(m_shown, ids);
}
void ShortcutBarConfigPage::addSelected()
{
for (QListWidgetItem *item : m_available->selectedItems()) {
m_chosen->addItem(m_available->takeItem(m_available->row(item)));
}
}
void ShortcutBarConfigPage::removeSelected()
{
for (QListWidgetItem *item : m_chosen->selectedItems()) {
QListWidgetItem *taken = m_chosen->takeItem(m_chosen->row(item));
//A removed element is not a command to list; it is pinned
//again from the bar's own customising window
if (ShortcutBarSettings::isElement(taken->data(Qt::UserRole).toString())) {
delete taken;
} else {
m_available->addItem(taken);
}
}
}
/**
@brief ShortcutBarConfigPage::moveSelected
Move the selected command @a step rows, keeping it selected.
*/
void ShortcutBarConfigPage::moveSelected(int step)
{
const int row = m_chosen->currentRow();
const int target = row + step;
if (row < 0 || target < 0 || target >= m_chosen->count()) {
return;
}
QListWidgetItem *item = m_chosen->takeItem(row);
m_chosen->insertItem(target, item);
m_chosen->setCurrentRow(target);
}
void ShortcutBarConfigPage::resetContext()
{
m_pending.insert(m_shown, ShortcutBarSettings::defaultIds(m_shown));
showContext();
}
/**
@brief ShortcutBarConfigPage::appendItem
Add @a id to @a list with the command's text and icon, or a pinned
element's name and icon. An id no live action carries (a command from a
build without it) is still listed, by its id, so saving does not
silently drop it.
*/
void ShortcutBarConfigPage::appendItem(QListWidget *list, const QString &id)
{
QString text = m_descriptions.value(id, id);
QIcon icon;
if (ShortcutBarSettings::isElement(id)) {
const ElementsLocation location(id);
if (location.exist()) {
text = location.name();
icon = location.icon();
}
} else if (QAction *action = ShortcutManager::instance().action(id, nullptr)) {
icon = action->icon();
}
auto *item = new QListWidgetItem(icon, text);
item->setData(Qt::UserRole, id);
list->addItem(item);
}
@@ -0,0 +1,64 @@
/*
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/>.
*/
#ifndef SHORTCUTBARCONFIGPAGE_H
#define SHORTCUTBARCONFIGPAGE_H
#include "configpage.h"
#include "../../shortcutbarsettings.h"
#include <QHash>
class QComboBox;
class QListWidget;
/**
@brief The ShortcutBarConfigPage class
Choose which commands the diagram editor's shortcut bar shows, and in
what order, for each selection context. Any command registered with
ShortcutManager by the diagram editor can be added. Changes are kept
per context while the dialog is open and saved by applyConf().
*/
class ShortcutBarConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit ShortcutBarConfigPage(QWidget *parent = nullptr);
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private:
void showContext();
void storeContext();
void addSelected();
void removeSelected();
void moveSelected(int step);
void resetContext();
void appendItem(QListWidget *list, const QString &id);
QComboBox *m_context;
QListWidget *m_available;
QListWidget *m_chosen;
ShortcutBarSettings::Context m_shown = ShortcutBarSettings::Canvas;
QHash<int, QStringList> m_pending;
QHash<QString, QString> m_descriptions;
};
#endif // SHORTCUTBARCONFIGPAGE_H
+24
View File
@@ -64,16 +64,21 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image)
this->setEnabled(true);
if (m_image == image) return;
if (m_image)
{
disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
disconnect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
}
m_image = image;
connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
connect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
m_movable = image->isMovable();
m_scaleX = m_image->scaleFactorX();
m_scaleY = m_image->scaleFactorY();
m_rotation = m_image->rotationAngle();
m_skewX = m_image->skewX();
m_skewY = m_image->skewY();
m_label = m_image->label();
updateUi();
}
@@ -101,6 +106,7 @@ void ImagePropertiesWidget::apply()
m_rotation = m_image->rotationAngle();
m_skewX = m_image->skewX();
m_skewY = m_image->skewY();
m_label = m_image->label();
}
/**
@@ -116,6 +122,7 @@ void ImagePropertiesWidget::reset()
m_image->setRotationAngle(m_rotation);
m_image->setSkewX(m_skewX);
m_image->setSkewY(m_skewY);
m_image->setLabel(m_label);
m_image->setMovable(m_movable);
updateUi();
}
@@ -138,6 +145,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
connect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
}
else
{
@@ -146,6 +154,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
disconnect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
}
return true;
@@ -191,6 +200,19 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
chain("skewX", m_skewX, newSkewX, tr("Modifier l'inclinaison d'une image"));
chain("skewY", m_skewY, newSkewY, tr("Modifier l'inclinaison d'une image"));
// Not through chain(): a string cannot be animated.
const QString newLabel = ui->m_label_le->text();
if (newLabel != m_label)
{
if (undo)
new QPropertyUndoCommand(m_image, "label", m_label, newLabel, undo);
else
{
undo = new QPropertyUndoCommand(m_image, "label", m_label, newLabel);
undo->setText(tr("Modifier le libellé d'une image"));
}
}
return undo;
}
@@ -221,6 +243,8 @@ void ImagePropertiesWidget::updateUi()
ui->m_skew_x_sb->setValue(m_image->skewX());
ui->m_skew_y_sb->setValue(m_image->skewY());
ui->m_lock_pos_cb->setChecked(!m_image->isMovable());
if (ui->m_label_le->text() != m_image->label())
ui->m_label_le->setText(m_image->label());
}
/**
+1
View File
@@ -70,6 +70,7 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
qreal m_rotation;
qreal m_skewX;
qreal m_skewY;
QString m_label;
// Guards the width/height spinboxes' mutual updates when
// "Conserver les proportions" is checked, so setting one
// programmatically in response to the other doesn't re-trigger
+16 -2
View File
@@ -152,14 +152,28 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="6" column="0">
<widget class="QLabel" name="label_label">
<property name="text">
<string>Libellé</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QLineEdit" name="m_label_le">
<property name="toolTip">
<string>Texte affiché sous l'image. Il suit l'image quand elle est déplacée, copiée ou tournée.</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="m_lock_pos_cb">
<property name="text">
<string>Verrouiller la position</string>
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>