mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 09:14:12 +02:00
Merge branch 'master' into feat/uuid-shapes-texts-images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¢er,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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 ¢er, 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
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user