mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 04:54:13 +02:00
2807771200
The popup now also takes a folio position before the cell, the way cross references and folio reports write it by default (%f-%l%c): 3-B13 offers "Folio 3 (<title>), case B13", and Enter shows that folio and zooms onto the cell. Case and a leading "p" are ignored, and the separator is optional: 3-b13, 3b13, P3B13 and p3-b13 all work. A plain B13 still means the current folio, and P3 alone is still row P, column 3: a folio always needs a cell after it. A folio position past the last folio, or a cell outside that folio's border, offers nothing. The zoom on another folio is queued, so it runs once the tab switch has been handled. Checked on the ATS example: jumping to 1-C5 from folio 3 gives the same view, pixel for pixel, as C5 typed on folio 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
314 lines
9.2 KiB
C++
314 lines
9.2 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "jumptoelementdialog.h"
|
|
|
|
#include "../diagram.h"
|
|
#include "../diagramview.h"
|
|
#include "../qetproject.h"
|
|
#include "../qetgraphicsitem/element.h"
|
|
|
|
#include <QEvent>
|
|
#include <QKeyEvent>
|
|
#include <QLineEdit>
|
|
#include <QListWidget>
|
|
#include <QRegularExpression>
|
|
#include <QVBoxLayout>
|
|
|
|
/**
|
|
@brief JumpToElementDialog::JumpToElementDialog
|
|
@param diagram : the diagram whose elements can be jumped to
|
|
@param parent
|
|
*/
|
|
JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
|
|
QDialog(parent),
|
|
m_diagram(diagram)
|
|
{
|
|
setWindowTitle(tr("Atteindre un élément", "window title"));
|
|
|
|
m_filter_edit = new QLineEdit(this);
|
|
m_filter_edit->setPlaceholderText(tr("Nom, label ou information de l'élément, ou case (ex. B13 ou 3-B13)…"));
|
|
m_filter_edit->installEventFilter(this);
|
|
|
|
m_result_list = new QListWidget(this);
|
|
m_result_list->setFocusPolicy(Qt::NoFocus);
|
|
|
|
auto *layout = new QVBoxLayout(this);
|
|
layout->addWidget(m_filter_edit);
|
|
layout->addWidget(m_result_list);
|
|
setLayout(layout);
|
|
resize(420, 320);
|
|
|
|
connect(m_filter_edit, &QLineEdit::textChanged, this, &JumpToElementDialog::updateFilteredList);
|
|
connect(m_result_list, &QListWidget::itemActivated, this, &JumpToElementDialog::activateCurrentItem);
|
|
|
|
buildCandidates();
|
|
updateFilteredList(QString());
|
|
m_filter_edit->setFocus();
|
|
}
|
|
|
|
JumpToElementDialog::~JumpToElementDialog()
|
|
{
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::buildCandidates
|
|
Collect every element on m_diagram into m_candidates, along with the
|
|
searchable text used to filter it (its type name, its label, and its
|
|
other element informations).
|
|
*/
|
|
void JumpToElementDialog::buildCandidates()
|
|
{
|
|
m_candidates.clear();
|
|
if (!m_diagram) {
|
|
return;
|
|
}
|
|
|
|
for (auto item : m_diagram->items()) {
|
|
Element *element = qgraphicsitem_cast<Element *>(item);
|
|
if (!element) {
|
|
continue;
|
|
}
|
|
|
|
const QString label = element->elementInformations().value(QStringLiteral("label")).toString();
|
|
const QString name = element->name();
|
|
|
|
Candidate candidate;
|
|
candidate.element = element;
|
|
candidate.label = label;
|
|
candidate.display_text = label.isEmpty() ? name : (label + QStringLiteral(" — ") + name);
|
|
|
|
QStringList search_parts;
|
|
search_parts << label << name;
|
|
const DiagramContext infos = element->elementInformations();
|
|
for (const QString &key : infos.keys()) {
|
|
search_parts << infos.value(key).toString();
|
|
}
|
|
candidate.search_text = search_parts.join(QLatin1Char(' ')).toLower();
|
|
|
|
m_candidates << candidate;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::updateFilteredList
|
|
Refill m_result_list with every candidate whose search text contains
|
|
@a filter_text (case-insensitive), and select the first match.
|
|
@param filter_text
|
|
*/
|
|
void JumpToElementDialog::updateFilteredList(const QString &filter_text)
|
|
{
|
|
m_result_list->clear();
|
|
|
|
const QString needle = filter_text.trimmed().toLower();
|
|
bool exact_label_match = false;
|
|
for (int i = 0; i < m_candidates.size(); ++i) {
|
|
const Candidate &candidate = m_candidates.at(i);
|
|
if (!candidate.element) {
|
|
continue;
|
|
}
|
|
if (!needle.isEmpty() && !candidate.search_text.contains(needle)) {
|
|
continue;
|
|
}
|
|
auto *list_item = new QListWidgetItem(candidate.display_text, m_result_list);
|
|
list_item->setData(Qt::UserRole, i);
|
|
if (candidate.label.compare(needle, Qt::CaseInsensitive) == 0) {
|
|
exact_label_match = true;
|
|
}
|
|
}
|
|
|
|
//A cell of the border, on this folio (ex : B13) or on another one
|
|
//(ex : 3-B13, folio 3, the way cross references write it), comes
|
|
//first, unless an element is labelled exactly like it: Enter keeps
|
|
//jumping to that element.
|
|
if (QListWidgetItem *cell_item = cellItem(needle)) {
|
|
m_result_list->insertItem(exact_label_match ? m_result_list->count() : 0,
|
|
cell_item);
|
|
}
|
|
|
|
if (m_result_list->count() > 0) {
|
|
m_result_list->setCurrentRow(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::activateCurrentItem
|
|
Select the element corresponding to the currently highlighted result
|
|
on the diagram, scroll it into view, and close this dialog.
|
|
*/
|
|
void JumpToElementDialog::activateCurrentItem()
|
|
{
|
|
QListWidgetItem *current = m_result_list->currentItem();
|
|
if (!current || !m_diagram) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
const int index = current->data(Qt::UserRole).toInt();
|
|
if (index == -1) {
|
|
const QList<Diagram *> diagrams = m_diagram->project()
|
|
? m_diagram->project()->diagrams()
|
|
: QList<Diagram *>();
|
|
const int folio = current->data(Qt::UserRole + 2).toInt();
|
|
if (folio >= 0 && folio < diagrams.size()) {
|
|
zoomToCell(diagrams.at(folio), current->data(Qt::UserRole + 1).toRectF());
|
|
}
|
|
accept();
|
|
return;
|
|
}
|
|
if (index < 0 || index >= m_candidates.size()) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
Element *element = m_candidates.at(index).element;
|
|
if (!element) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
m_diagram->clearSelection();
|
|
element->setSelected(true);
|
|
element->ensureVisible();
|
|
accept();
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::cellItem
|
|
@param needle : the typed text, trimmed and lower case
|
|
@return a new list item for the cell \a needle names, on m_diagram
|
|
(ex : b13) or on the folio at a position of the project (ex : 3-b13,
|
|
3b13, p3b13), or nullptr if \a needle names no cell of an existing
|
|
folio. The item holds -1, the cell rect and the folio position.
|
|
*/
|
|
QListWidgetItem *JumpToElementDialog::cellItem(const QString &needle) const
|
|
{
|
|
if (!m_diagram || !m_diagram->project()) {
|
|
return nullptr;
|
|
}
|
|
const QList<Diagram *> diagrams = m_diagram->project()->diagrams();
|
|
|
|
Diagram *diagram = m_diagram;
|
|
QString cell = needle;
|
|
QString text;
|
|
QRectF cell_rect = diagram->border_and_titleblock.cellRect(cell);
|
|
if (cell_rect.isNull()) {
|
|
//A folio position then a cell, like %f-%l%c in cross references
|
|
static const QRegularExpression folio_cell_re(
|
|
QStringLiteral("^p?\\s*(\\d{1,4})\\s*[-/.:]?\\s*([a-z]+\\s*\\d{1,4})$"));
|
|
const QRegularExpressionMatch match = folio_cell_re.match(needle);
|
|
if (!match.hasMatch()) {
|
|
return nullptr;
|
|
}
|
|
const int folio = match.captured(1).toInt();
|
|
if (folio < 1 || folio > diagrams.size()) {
|
|
return nullptr;
|
|
}
|
|
diagram = diagrams.at(folio - 1);
|
|
cell = match.captured(2);
|
|
cell_rect = diagram->border_and_titleblock.cellRect(cell);
|
|
if (cell_rect.isNull()) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
cell = cell.remove(QLatin1Char(' ')).toUpper();
|
|
if (diagram == m_diagram) {
|
|
text = tr("Case %1").arg(cell);
|
|
} else {
|
|
const QString title = diagram->title();
|
|
text = title.isEmpty()
|
|
? tr("Folio %1, case %2").arg(diagrams.indexOf(diagram) + 1).arg(cell)
|
|
: tr("Folio %1 (%2), case %3").arg(diagrams.indexOf(diagram) + 1).arg(title, cell);
|
|
}
|
|
|
|
auto *item = new QListWidgetItem(text);
|
|
item->setData(Qt::UserRole, -1);
|
|
item->setData(Qt::UserRole + 1, cell_rect);
|
|
item->setData(Qt::UserRole + 2, diagrams.indexOf(diagram));
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::zoomToCell
|
|
Show \a diagram and zoom its view on \a cell_rect with one cell of
|
|
context around it.
|
|
@param diagram : the folio holding the cell
|
|
@param cell_rect : the cell, in scene coordinate
|
|
*/
|
|
void JumpToElementDialog::zoomToCell(Diagram *diagram, const QRectF &cell_rect)
|
|
{
|
|
const QRectF rect = cell_rect.adjusted(-cell_rect.width(), -cell_rect.height(),
|
|
cell_rect.width(), cell_rect.height());
|
|
const bool other_folio = diagram != m_diagram;
|
|
if (other_folio) {
|
|
diagram->showMe();
|
|
}
|
|
for (QGraphicsView *view : diagram->views()) {
|
|
if (auto *diagram_view = qobject_cast<DiagramView *>(view)) {
|
|
if (other_folio) {
|
|
//A folio shown for the first time is laid out at
|
|
//its size once the tab switch has been handled
|
|
QMetaObject::invokeMethod(diagram_view, [diagram_view, rect]() {
|
|
diagram_view->zoomToRect(rect);
|
|
}, Qt::QueuedConnection);
|
|
} else {
|
|
diagram_view->zoomToRect(rect);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief JumpToElementDialog::eventFilter
|
|
Redirect Up/Down/Enter/Escape typed in the filter field to the result
|
|
list, so the user never has to leave the text field to navigate or
|
|
confirm a choice.
|
|
*/
|
|
bool JumpToElementDialog::eventFilter(QObject *watched, QEvent *event)
|
|
{
|
|
if (watched == m_filter_edit && event->type() == QEvent::KeyPress) {
|
|
auto *key_event = static_cast<QKeyEvent *>(event);
|
|
switch (key_event->key()) {
|
|
case Qt::Key_Down:
|
|
case Qt::Key_Up: {
|
|
const int row_count = m_result_list->count();
|
|
if (row_count == 0) {
|
|
return true;
|
|
}
|
|
int row = m_result_list->currentRow();
|
|
row = key_event->key() == Qt::Key_Down
|
|
? (row + 1) % row_count
|
|
: (row - 1 + row_count) % row_count;
|
|
m_result_list->setCurrentRow(row);
|
|
return true;
|
|
}
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
activateCurrentItem();
|
|
return true;
|
|
case Qt::Key_Escape:
|
|
reject();
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return QDialog::eventFilter(watched, event);
|
|
}
|