Files
qelectrotech-source-mirror/sources/projectview.cpp
T
ispyisail 55886e5e8a Add undo/redo support for folio add, delete, and reorder (#575)
Three new QUndoCommand subclasses (AddDiagramCommand, RemoveDiagramCommand,
MoveDiagramCommand) pushed onto the project's existing (already
project-scoped) undo stack, so folio structure edits are undoable
alongside every item-level edit already on that stack.

- QETProject::addDiagram()/detachDiagram() are the shared attach/detach
  primitives: they mutate the diagram list, connect/disconnect the two
  per-diagram signals set up at add time, and emit diagramAdded/
  diagramRemoved. AddDiagramCommand and RemoveDiagramCommand call these
  (via friend access) for both redo and undo, so a removed diagram is
  parked rather than destroyed -- it's only actually deleted if the
  command itself falls out of undo history while still detached.
- ProjectView reacts to diagramRemoved the same way it already reacted to
  diagramAdded (tearing down/rebuilding the tab), so both directions of
  both commands go through the same reactive path every other diagram
  listener (project database, cross-references, generic panel) already
  relies on.
- MoveDiagramCommand wraps a new ProjectView::setDiagramPosition(), which
  performs the tab move and the project's diagramOrderChanged() list
  reorder synchronously in one step, instead of relying on the queued
  tabMoved connection (needed for interactive drag-and-drop) to catch up
  later -- avoiding a second, redundant reorder from that queued call.
- Multi-folio delete and multi-folio move (QETDiagramEditor::removeDiagrams()
  and the moveDiagram*(QList<Diagram*>) batch slots) wrap their per-diagram
  loop in QUndoStack::beginMacro()/endMacro(), so a multi-select action is
  one undo step, matching current UX.
- Softened the delete confirmation's "this change is irreversible" wording
  now that it no longer is.

Verified headlessly (Xvfb + xdotool + scrot): add/undo/redo, delete/undo/
redo (single and multi-select, single undo step for the batch), and
move/undo/redo all behave correctly against a 7-folio project.
2026-08-01 17:03:39 +12:00

1212 lines
35 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 "projectview.h"
#include "ElementsCollection/xmlelementcollection.h"
#include "autoNum/assignvariables.h"
#include "diagram.h"
#include "diagramview.h"
#include "editor/ui/qetelementeditor.h"
#include "exportdialog.h"
#include "qetapp.h"
#include "qeticons.h"
#include "qetmessagebox.h"
#include "qetproject.h"
#include "titleblock/qettemplateeditor.h"
#include "ui/borderpropertieswidget.h"
#include "ui/conductorpropertieswidget.h"
#include "ui/dialogwaiting.h"
#include "ui/projectpropertiesdialog.h"
#include "ui/titleblockpropertieswidget.h"
#include "undocommand/movediagramcommand.h"
#include "undocommand/removediagramcommand.h"
/**
Constructeur
@param project projet a visualiser
@param parent Widget parent
*/
ProjectView::ProjectView(QETProject *project, QWidget *parent) :
QWidget(parent),
m_project(nullptr)
{
initActions();
initWidgets();
initLayout();
setProject(project);
}
/**
Destructeur
Supprime les DiagramView embarquees
*/
ProjectView::~ProjectView()
{
for (auto dv_ : m_diagram_ids.values())
dv_->deleteLater();
}
/**
@return le projet actuellement visualise par le ProjectView
*/
QETProject *ProjectView::project()
{
return(m_project);
}
/**
@brief ProjectView::setProject
Set the project display by the project view
@param project
*/
void ProjectView::setProject(QETProject *project)
{
if (!m_project)
{
m_project = project;
connect(m_project, &QETProject::projectTitleChanged, this, &ProjectView::updateWindowTitle);
connect(m_project, &QETProject::projectModified, this, &ProjectView::updateWindowTitle);
connect(m_project, &QETProject::readOnlyChanged, this, &ProjectView::adjustReadOnlyState);
connect(m_project, &QETProject::addAutoNumDiagram, [this](){this->project()->addNewDiagram();});
connect(m_project, &QETProject::diagramAdded, [this](QETProject *project, Diagram *diagram) {
Q_UNUSED(project)
this->diagramAdded(diagram);
});
connect(m_project, &QETProject::diagramRemoved, [this](QETProject *project, Diagram *diagram) {
Q_UNUSED(project)
this->diagramRemoved(diagram);
});
adjustReadOnlyState();
loadDiagrams();
}
}
/**
@return la liste des schemas ouverts dans le projet
*/
QList<DiagramView *> ProjectView::diagram_views() const
{
return(m_diagram_view_list);
}
/**
@brief ProjectView::currentDiagram
@return The current active diagram view or nullptr if there isn't diagramView in this project view.
*/
DiagramView *ProjectView::currentDiagram() const
{
int current_tab_index = m_tab -> currentIndex();
if (current_tab_index == -1)
return nullptr;
return(m_diagram_ids[current_tab_index]);
}
/**
Gere la fermeture du schema.
@param qce Le QCloseEvent decrivant l'evenement
*/
void ProjectView::closeEvent(QCloseEvent *qce) {
bool can_close_project = tryClosing();
if (can_close_project) {
qce -> accept();
emit(projectClosed(this));
} else {
qce -> ignore();
}
}
/**
@brief change current diagramview to next folio
*/
void ProjectView::changeTabDown()
{
DiagramView *nextDiagramView = this->nextDiagram();
if (nextDiagramView!=nullptr){
rebuildDiagramsMap();
m_tab -> setCurrentWidget(nextDiagramView);
}
}
/**
@return next folio of current diagramview
*/
DiagramView *ProjectView::nextDiagram()
{
int current_tab_index = m_tab -> currentIndex();
int next_tab_index = current_tab_index + 1; //get next tab index
if (next_tab_index<m_diagram_ids.count()) //if next tab index >= greatest tab the last tab is activated so no need to change tab.
return(m_diagram_ids[next_tab_index]);
else
return nullptr;
}
/**
@brief change current diagramview to previous tab
*/
void ProjectView::changeTabUp()
{
DiagramView *previousDiagramView = this->previousDiagram();
if (previousDiagramView!=nullptr){
rebuildDiagramsMap();
m_tab -> setCurrentWidget(previousDiagramView);
}
}
/**
@return previous folio of current diagramview
*/
DiagramView *ProjectView::previousDiagram()
{
int current_tab_index = m_tab -> currentIndex();
int previous_tab_index = current_tab_index - 1; //get previous tab index
if (previous_tab_index>=0) //if previous tab index = 0 then the first tab is activated so no need to change tab.
return(m_diagram_ids[previous_tab_index]);
else
return nullptr;
}
/**
@brief change current diagramview to last tab
*/
void ProjectView::changeLastTab()
{
DiagramView *lastDiagramView = this->lastDiagram();
m_tab->setCurrentWidget(lastDiagramView);
}
/**
@return last folio of current project
*/
DiagramView *ProjectView::lastDiagram()
{
return(m_diagram_ids.last());
}
/**
@brief change current diagramview to first tab
*/
void ProjectView::changeFirstTab()
{
DiagramView *firstDiagramView = this->firstDiagram();
m_tab->setCurrentWidget(firstDiagramView);
}
/**
@return first folio of current project
*/
DiagramView *ProjectView::firstDiagram()
{
return(m_diagram_ids.first());
}
/**
Cette methode essaye de fermer successivement les editeurs d'element puis
les schemas du projet. L'utilisateur peut refuser de fermer un schema ou un
editeur.
@return true si tout a pu etre ferme, false sinon
@see tryClosingElementEditors()
@see tryClosingDiagrams()
*/
bool ProjectView::tryClosing()
{
if (!m_project) return(true);
// First step: require external editors closing -- users may either cancel
// the whole closing process or save (and therefore add) content into this
// project. Of course, they may also discard them.
if (!tryClosingElementEditors()) {
return(false);
}
// Check how different the current situation is from a brand new, untouched project
if (m_project -> filePath().isEmpty() && !m_project -> projectWasModified()) {
return(true);
}
// Second step: users are presented with a dialog that enables them to
// choose whether they want to:
// - cancel the closing process,
// - discard all modifications,
// - or specify what is to be saved, i.e. they choose whether they wants to
// save/give up/remove diagrams considered as modified.
int user_input = tryClosingDiagrams();
if (user_input == QMessageBox::Cancel) {
return(false); // the closing process was cancelled
} else if (user_input == QMessageBox::Discard) {
return(true); // all modifications were discarded
}
// Check how different the current situation is from a brand new, untouched project (yes , again)
if (m_project -> filePath().isEmpty() && !m_project -> projectWasModified()) {
return(true);
}
if (m_project -> filePath().isEmpty()) {
QString filepath = askUserForFilePath();
if (filepath.isEmpty()) return(false); // users may cancel the closing
}
QETResult result = m_project -> write();
updateWindowTitle();
if (!result.isOk()) emit(errorEncountered(result.errorMessage()));
return(result.isOk());
}
/**
Un projet comporte des elements integres. Cette methode ferme les editeurs
d'elements associes a ce projet. L'utilisateur peut refuser la fermeture
d'un editeur d'element.
@return true si tous les editeurs d'element ont pu etre fermes, false sinon
*/
bool ProjectView::tryClosingElementEditors()
{
if (!m_project) return(true);
/*
La QETApp permet d'acceder rapidement aux editeurs d'element
editant un element du projet.
*/
QList<QETElementEditor *> editors = QETApp::elementEditors(m_project);
foreach(QETElementEditor *editor, editors) {
if (!editor -> close()) return(false);
}
QList<QETTitleBlockTemplateEditor *> template_editors = QETApp::titleBlockTemplateEditors(m_project);
foreach(QETTitleBlockTemplateEditor *template_editor, template_editors) {
if (!template_editor -> close()) return(false);
}
return(true);
}
/**
@brief ProjectView::tryClosingDiagrams
try to close this project, if diagram or project option are changed
a dialog ask if user want to save the modification.
@return the answer of dialog or discard if no change.
*/
int ProjectView::tryClosingDiagrams()
{
if (!m_project) return(QMessageBox::Discard);
if (!project()->projectOptionsWereModified() &&
project()->undoStack()->isClean() &&
!project()->filePath().isEmpty()) {
// nothing was modified, and we have a filepath, i.e. everything was already
// saved, i.e we can close the project right now
return(QMessageBox::Discard);
}
QString title = project()->title();
if (title.isEmpty()) title = "QElectroTech ";
int close_dialog = QMessageBox::question(this, title,
tr("Le projet à été modifié.\n"
"Voulez-vous enregistrer les modifications ?"),
QMessageBox::Save | QMessageBox::Discard
| QMessageBox::Cancel,
QMessageBox::Save);
return(close_dialog);
}
/**
Ask the user to provide a file path in which the currently edited project will
be saved.
@param assign When true, assign the provided filepath to the project through
setFilePath(). Defaults to true.
@return the file path, or an empty string if none were provided
*/
QString ProjectView::askUserForFilePath(bool assign) {
// ask the user for a filepath in order to save the project
QString filepath = QFileDialog::getSaveFileName(
this,
tr("Enregistrer sous", "dialog title"),
m_project -> currentDir() + "/" + tr("sansnom") + ".qet",
tr("Projet QElectroTech (*.qet)", "filetypes allowed when saving a project file")
);
// if no filepath is provided, return an empty string
if (filepath.isEmpty()) return(filepath);
// if the name does not end with the .qet extension and we're _not_ using xdg-desktop-portal, append it
bool usesPortal =
qEnvironmentVariableIsSet("FLATPAK_ID") ||
qEnvironmentVariableIsSet("SNAP_NAME");
if (!filepath.endsWith(".qet", Qt::CaseInsensitive) && !usesPortal) filepath += ".qet";
if (assign) {
// assign the provided filepath to the currently edited project
m_project -> setFilePath(filepath);
}
return(filepath);
}
/**
@return the QETResult object to be returned when it appears this project
view is not associated to any project.
*/
QETResult ProjectView::noProjectResult() const
{
QETResult no_project(tr("aucun projet affiché", "error message"), false);
return(no_project);
}
/**
* @brief ProjectView::removeDiagram
* Remove a diagram (folio) of the project
* @param diagram_view : diagram view to remove
* @param silent : if true, bypasses the confirmation message box
*/
void ProjectView::removeDiagram(DiagramView *diagram_view, bool silent)
{
if (!diagram_view)
return;
if (m_project -> isReadOnly())
return;
if (!m_diagram_ids.values().contains(diagram_view))
return;
if (!silent) {
//Ask confirmation to user.
int answer = QET::QetMessageBox::question(
this,
tr("Supprimer le folio ?", "message box title"),
tr("Êtes-vous sûr de vouloir supprimer ce folio du projet ?", "message box content"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
QMessageBox::No
);
if (answer != QMessageBox::Yes) {
return;
}
}
m_project->undoStack()->push(new RemoveDiagramCommand(m_project, diagram_view->diagram()));
}
/**
@brief ProjectView::diagramRemoved
Slot called when the project emits diagramRemoved (either from a direct,
non-undoable QETProject::removeDiagram() call, or -- the normal UI path
-- from a RemoveDiagramCommand's redo()/undo()). Tears down the tab and
DiagramView for the given diagram; the diagram itself is left untouched,
since by this point it has already been detached (and possibly parked
for undo) by the caller.
@param diagram
*/
void ProjectView::diagramRemoved(Diagram *diagram)
{
DiagramView *diagram_view = findDiagram(diagram);
if (!diagram_view)
return;
int index_to_remove = m_diagram_ids.key(diagram_view);
m_tab->removeTab(index_to_remove);
m_diagram_view_list.removeAll(diagram_view);
rebuildDiagramsMap();
emit(diagramRemoved(diagram_view));
delete diagram_view;
updateAllTabsTitle();
m_project -> setModified(true);
}
/**
* Enleve un schema du ProjectView
* @param diagram Schema a enlever
* @param silent Si vrai, supprime sans demander confirmation
*/
void ProjectView::removeDiagram(Diagram *diagram, bool silent) {
if (!diagram) return;
if (DiagramView *diagram_view = findDiagram(diagram)) {
removeDiagram(diagram_view, silent);
}
}
/**
Active l'onglet adequat pour afficher le schema passe en parametre
@param diagram Schema a afficher
*/
void ProjectView::showDiagram(DiagramView *diagram) {
if (!diagram) return;
m_tab -> setCurrentWidget(diagram);
}
/**
Active l'onglet adequat pour afficher le schema passe en parametre
@param diagram Schema a afficher
*/
void ProjectView::showDiagram(Diagram *diagram) {
if (!diagram) return;
if (DiagramView *diagram_view = findDiagram(diagram)) {
m_tab -> setCurrentWidget(diagram_view);
}
}
/**
Enable the user to edit properties of the current project through a
configuration dialog.
*/
void ProjectView::editProjectProperties()
{
if (!m_project) return;
ProjectPropertiesDialog dialog(m_project, parentWidget());
dialog.exec();
}
/**
Edite les proprietes du schema courant
*/
void ProjectView::editCurrentDiagramProperties()
{
editDiagramProperties(currentDiagram());
}
/**
Edite les proprietes du schema diagram_view
*/
void ProjectView::editDiagramProperties(DiagramView *diagram_view) {
if (!diagram_view) return;
showDiagram(diagram_view);
diagram_view -> editDiagramProperties();
}
/**
Edite les proprietes du schema diagram
*/
void ProjectView::editDiagramProperties(Diagram *diagram) {
editDiagramProperties(findDiagram(diagram));
}
/**
Deplace le schema diagram_view vers le haut / la gauche
*/
void ProjectView::moveDiagramUp(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (!diagram_view_position) {
// le schema est le premier du projet
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position - 1));
}
/**
Deplace le schema diagram vers le haut / la gauche
*/
void ProjectView::moveDiagramUp(Diagram *diagram) {
moveDiagramUp(findDiagram(diagram));
}
/**
Deplace le schema diagram_view vers le bas / la droite
*/
void ProjectView::moveDiagramDown(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (diagram_view_position + 1 == m_diagram_ids.count()) {
// le schema est le dernier du projet
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position + 1));
}
/**
Deplace le schema diagram vers le bas / la droite
*/
void ProjectView::moveDiagramDown(Diagram *diagram) {
moveDiagramDown(findDiagram(diagram));
}
/*
Deplace le schema diagram_view vers le haut / la gauche en position 0
*/
void ProjectView::moveDiagramUpTop(DiagramView *diagram_view)
{
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (!diagram_view_position) {
// le schema est le premier du projet
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), 0));
}
/*
Deplace le schema diagram vers le haut / la gauche en position 0
*/
void ProjectView::moveDiagramUpTop(Diagram *diagram)
{
moveDiagramUpTop(findDiagram(diagram));
}
/**
Deplace le schema diagram_view vers le haut / la gauche x10
*/
void ProjectView::moveDiagramUpx10(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (!diagram_view_position) {
// le schema est le premier du projet
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position - 10));
}
/**
Deplace le schema diagram vers le haut / la gauche x10
*/
void ProjectView::moveDiagramUpx10(Diagram *diagram) {
moveDiagramUpx10(findDiagram(diagram));
}
/**
* @brief ProjectView::moveDiagramUpx100
* Moves the diagram_view up / left x100
* @param diagram_view View to move
*/
void ProjectView::moveDiagramUpx100(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (!diagram_view_position) {
// The diagram is the first of the project
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position - 100));
}
/**
* @brief ProjectView::moveDiagramUpx100
* Moves the diagram up / left x100
* @param diagram Diagram to move
*/
void ProjectView::moveDiagramUpx100(Diagram *diagram) {
moveDiagramUpx100(findDiagram(diagram));
}
/**
* @brief ProjectView::moveDiagramDownx100
* Moves the diagram_view down / right x100
* @param diagram_view View to move
*/
void ProjectView::moveDiagramDownx100(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (diagram_view_position + 1 == m_diagram_ids.count()) {
// The diagram is the last of the project
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position + 100));
}
/**
* @brief ProjectView::moveDiagramDownx100
* Moves the diagram down / right x100
* @param diagram Diagram to move
*/
void ProjectView::moveDiagramDownx100(Diagram *diagram) {
moveDiagramDownx100(findDiagram(diagram));
}
/**
Deplace le schema diagram_view vers le bas / la droite x10
*/
void ProjectView::moveDiagramDownx10(DiagramView *diagram_view) {
if (!diagram_view) return;
int diagram_view_position = m_diagram_ids.key(diagram_view);
if (diagram_view_position + 1 == m_diagram_ids.count()) {
// le schema est le dernier du projet
return;
}
m_project->undoStack()->push(new MoveDiagramCommand(this, diagram_view->diagram(), diagram_view_position + 10));
}
/**
Deplace le schema diagram vers le bas / la droite x10
*/
void ProjectView::moveDiagramDownx10(Diagram *diagram) {
moveDiagramDownx10(findDiagram(diagram));
}
/**
Exporte le schema.
*/
void ProjectView::exportProject()
{
if (!m_project) return;
ExportDialog ed(m_project, parentWidget());
#ifdef Q_OS_MACOS
ed.setWindowFlags(Qt::Sheet);
#endif
ed.exec();
}
/**
Save project properties along with all modified diagrams.
@see filePath()
@see setFilePath()
@return a QETResult object reflecting the situation
*/
QETResult ProjectView::save()
{
return(doSave());
}
/**
Ask users for a filepath in order to save the project.
@param options May be used to specify what should be saved; defaults to
all modified diagrams.
@return a QETResult object reflecting the situation; note that a valid
QETResult object is returned if the operation was cancelled.
*/
QETResult ProjectView::saveAs()
{
if (!m_project) return(noProjectResult());
QString filepath = askUserForFilePath();
if (filepath.isEmpty()) return(QETResult());
return(doSave());
}
/**
Save project content, then write the project file. May
call saveAs if no filepath was provided before.
@return a QETResult object reflecting the situation; note that a valid
QETResult object is returned if the operation was cancelled.
*/
QETResult ProjectView::doSave()
{
if (!m_project) return(noProjectResult());
if (m_project -> filePath().isEmpty()) {
// The project has not been saved to a file yet,
// so save() actually means saveAs().
return(saveAs());
}
// write to file
QETResult result = m_project -> write();
updateWindowTitle();
project()->undoStack()->clear();
return(result);
}
/**
Allow the user to clean the project, which includes:
* deleting unused title block templates
* deleting unused elements
* deleting empty categories
@return an integer value above zero if elements and/or categories were
cleaned.
*/
int ProjectView::cleanProject()
{
if (!m_project) return(0);
// s'assure que le schema n'est pas en lecture seule
if (m_project -> isReadOnly()) {
QET::QetMessageBox::critical(
this,
tr("Projet en lecture seule", "message box title"),
tr("Ce projet est en lecture seule. Il n'est donc pas possible de le nettoyer.", "message box content")
);
return(0);
}
// construit un petit dialogue pour parametrer le nettoyage
QCheckBox *clean_tbt = new QCheckBox(tr("Supprimer les modèles de cartouche inutilisés dans le projet"));
QCheckBox *clean_elements = new QCheckBox(tr("Supprimer les éléments inutilisés dans le projet"));
QCheckBox *clean_categories = new QCheckBox(tr("Supprimer les catégories vides"));
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
clean_tbt -> setChecked(true);
clean_elements -> setChecked(true);
clean_categories -> setChecked(true);
QDialog clean_dialog(parentWidget());
#ifdef Q_OS_MACOS
clean_dialog.setWindowFlags(Qt::Sheet);
#endif
clean_dialog.setWindowTitle(tr("Nettoyer le projet", "window title"));
QVBoxLayout *clean_dialog_layout = new QVBoxLayout();
clean_dialog_layout -> addWidget(clean_tbt);
clean_dialog_layout -> addWidget(clean_elements);
clean_dialog_layout -> addWidget(clean_categories);
clean_dialog_layout -> addWidget(buttons);
clean_dialog.setLayout(clean_dialog_layout);
connect(buttons, SIGNAL(accepted()), &clean_dialog, SLOT(accept()));
connect(buttons, SIGNAL(rejected()), &clean_dialog, SLOT(reject()));
int clean_count = 0;
if (clean_dialog.exec() == QDialog::Accepted)
{
if (clean_tbt -> isChecked()) {
m_project->embeddedTitleBlockTemplatesCollection()->deleteUnusedTitleBlocKTemplates();
}
if (clean_elements->isChecked()) {
m_project->embeddedElementCollection()->cleanUnusedElement();
}
if (clean_categories->isChecked()) {
m_project->embeddedElementCollection()->cleanUnusedDirectory();
}
}
m_project -> setModified(true);
return(clean_count);
}
/**
Initialize actions for this widget.
*/
void ProjectView::initActions()
{
m_add_new_diagram = new QAction(QET::Icons::AddFolio, tr("Ajouter un folio"), this);
connect(m_add_new_diagram, &QAction::triggered, [this](){this->m_project->addNewDiagram();});
m_first_view = new QAction(QET::Icons::ArrowLeftDouble, tr("Revenir au debut du projet"),this);
connect(m_first_view, &QAction::triggered, [this](){this->m_tab->setCurrentWidget(firstDiagram());});
m_end_view = new QAction(QET::Icons::ArrowRightDouble, tr("Aller à la fin du projet"),this);
connect(m_end_view, &QAction::triggered, [this](){this->m_tab->setCurrentWidget(lastDiagram());});
// button to scroll one page left
m_next_view_left = new QAction(QET::Icons::ArrowLeft, tr("go one page left"),this);
connect(m_next_view_left, &QAction::triggered, [this](){this->m_tab->setCurrentWidget(previousDiagram());});
// button to scroll one page right
m_next_view_right = new QAction(QET::Icons::ArrowRight, tr("go one page right"),this);
connect(m_next_view_right, &QAction::triggered, [this](){this->m_tab->setCurrentWidget(nextDiagram());});
}
/**
Initialize child widgets for this widget.
*/
void ProjectView::initWidgets()
{
setObjectName("ProjectView");
setWindowIcon(QET::Icons::ProjectFileGP);
// initialize the "fallback" widget
fallback_widget_ = new QWidget();
fallback_label_ = new QLabel(
tr(
"Ce projet ne contient aucun folio",
"label displayed when a project contains no diagram"
)
);
fallback_label_ -> setAlignment(Qt::AlignVCenter | Qt::AlignHCenter);
// initialize tabs
#ifdef Q_OS_MACOS
m_tab = new WheelEnabledTabBar(this);
#else
m_tab = new QTabWidget(this);
#endif
m_tab -> setMovable(true);
// setting UsesScrollButton ensures that when the tab bar is full, the tabs are scrolled.
m_tab -> setUsesScrollButtons(true);
// disable the internal scroll buttons of the TabWidget, we will use our own buttons.
m_tab->setStyleSheet("QTabBar QToolButton {border-image: ;border-width: 0px}");
m_tab->setStyleSheet("QTabBar::scroller {width: 0px;}");
// add layouts
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
TopRightCorner_Layout->setContentsMargins(0,0,0,0);
// some place left to the 'next_right_view_button' button
TopRightCorner_Layout->insertSpacing(1,10);
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
// add buttons
QToolButton *m_next_right_view_button =new QToolButton;
m_next_right_view_button->setDefaultAction(m_next_view_right);
m_next_right_view_button->setAutoRaise(true);
TopRightCorner_Layout->addWidget(m_next_right_view_button);
QToolButton *m_end_view_button =new QToolButton;
m_end_view_button->setDefaultAction(m_end_view);
m_end_view_button->setAutoRaise(true);
TopRightCorner_Layout->addWidget(m_end_view_button);
QToolButton *add_new_diagram_button = new QToolButton;
add_new_diagram_button -> setDefaultAction(m_add_new_diagram);
add_new_diagram_button -> setAutoRaise(true);
TopRightCorner_Layout->addWidget(add_new_diagram_button);
// some place right to the 'add_new_diagram_button' button
TopRightCorner_Layout->addSpacing(5);
QToolButton *m_first_view_button =new QToolButton;
m_first_view_button->setDefaultAction(m_first_view);
m_first_view_button->setAutoRaise(true);
TopLeftCorner_Layout->addWidget(m_first_view_button);
QToolButton *m_next_left_view_button =new QToolButton;
m_next_left_view_button->setDefaultAction(m_next_view_left);
m_next_left_view_button->setAutoRaise(true);
TopLeftCorner_Layout->addWidget(m_next_left_view_button);
// some place right to the 'first_view_button' button
TopLeftCorner_Layout->addSpacing(10);
// add widgets to tabbar
QWidget *tabwidgetRight=new QWidget(this);
tabwidgetRight->setLayout(TopRightCorner_Layout);
m_tab -> setCornerWidget(tabwidgetRight, Qt::TopRightCorner);
QWidget *tabwidgetLeft=new QWidget(this);
tabwidgetLeft->setLayout(TopLeftCorner_Layout);
m_tab -> setCornerWidget(tabwidgetLeft, Qt::TopLeftCorner);
// manage signals
connect(m_tab, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int)));
connect(m_tab, SIGNAL(tabBarDoubleClicked(int)), this, SLOT(tabDoubleClicked(int)));
connect(m_tab->tabBar(), SIGNAL(tabMoved(int,int)), this, SLOT(tabMoved(int,int)), Qt::QueuedConnection);
fallback_widget_ -> setVisible(false);
m_tab -> setVisible(false);
}
/**
Initialize layout for this widget.
*/
void ProjectView::initLayout()
{
QVBoxLayout *fallback_widget_layout_ = new QVBoxLayout(fallback_widget_);
fallback_widget_layout_ -> addWidget(fallback_label_);
layout_ = new QVBoxLayout(this);
#ifdef Q_OS_MACOS
layout_ -> setContentsMargins(0, 8, 0, 0);
#else
layout_ -> setContentsMargins(0, 0, 0, 0);
#endif
layout_ -> setSpacing(0);
layout_ -> addWidget(fallback_widget_);
layout_ -> addWidget(m_tab);
}
/**
@brief ProjectView::loadDiagrams
Load diagrams of project.
We create a diagram view for each diagram,
and add it to the project view.
*/
void ProjectView::loadDiagrams()
{
if (!m_project) return;
setDisplayFallbackWidget(m_project -> diagrams().isEmpty());
auto dialog = DialogWaiting::instance();
if(dialog)
{
dialog->setTitle( tr("<p align=\"center\">"
"<b>Ouverture du projet en cours...</b><br/>"
"Création des onglets de folio :"
"</p>"));
}
for(auto diagram : m_project->diagrams())
{
if(dialog)
{
dialog->setDetail(diagram->title());
dialog->setProgressBar(dialog->progressBarValue()+1);
}
auto dv = new DiagramView(diagram);
dv->setFrameStyle(QFrame::Plain | QFrame::NoFrame);
auto index = m_project->folioIndex(diagram);
m_tab->insertTab(index, dv, QET::Icons::Diagram, dv->title());
m_diagram_view_list.insert(index, dv);
connect(dv, &DiagramView::showDiagram, this, QOverload<Diagram*>::of(&ProjectView::showDiagram));
connect(dv, &DiagramView::titleChanged, this, &ProjectView::updateTabTitle);
connect(dv, &DiagramView::findElementRequired, this, &ProjectView::findElementRequired);
connect(&dv->diagram()->border_and_titleblock , &BorderTitleBlock::titleBlockFolioChanged, [this, dv]() {this->updateTabTitle(dv);});
}
rebuildDiagramsMap();
updateAllTabsTitle();
m_tab->setCurrentWidget(firstDiagram());
}
/**
@brief ProjectView::updateWindowTitle
Update the project view title
*/
void ProjectView::updateWindowTitle()
{
QString title;
if (m_project) {
title = m_project -> pathNameTitle();
} else {
title = tr("Projet", "window title for a project-less ProjectView");
}
setWindowTitle(title);
}
/**
Effectue les actions necessaires lorsque le projet visualise entre ou sort
du mode lecture seule.
*/
void ProjectView::adjustReadOnlyState()
{
bool editable = !(m_project -> isReadOnly());
// prevent users from moving existing diagrams
m_tab -> setMovable(editable);
// prevent users from adding new diagrams
m_add_new_diagram -> setEnabled(editable);
// on met a jour le titre du widget, qui reflete l'etat de lecture seule
updateWindowTitle();
}
/**
@brief ProjectView::diagramAdded
Slot called when qetproject emit diagramAdded
@param diagram
*/
void ProjectView::diagramAdded(Diagram *diagram)
{
auto dv = new DiagramView(diagram);
auto index = m_project->folioIndex(diagram);
m_tab->insertTab(index, dv, QET::Icons::Diagram, dv->title());
dv->setFrameStyle(QFrame::Plain | QFrame::NoFrame);
m_diagram_view_list.insert(index, dv);
rebuildDiagramsMap();
updateAllTabsTitle();
connect(dv, &DiagramView::showDiagram, this, QOverload<Diagram*>::of(&ProjectView::showDiagram));
connect(dv, &DiagramView::titleChanged, this, &ProjectView::updateTabTitle);
connect(dv, &DiagramView::findElementRequired, this, &ProjectView::findElementRequired);
connect(&dv->diagram()->border_and_titleblock , &BorderTitleBlock::titleBlockFolioChanged, [this, dv]() {this->updateTabTitle(dv);});
// signal diagram view was added
emit(diagramAdded(dv));
m_project->setModified(true);
showDiagram(dv);
}
/**
@brief ProjectView::updateTabTitle
Update the title of the tab which display the diagram view.
@param diagram_view : The diagram view.
*/
void ProjectView::updateTabTitle(DiagramView *diagram_view)
{
int diagram_tab_id = m_diagram_ids.key(diagram_view, -1);
if (diagram_tab_id != -1)
{
QSettings settings;
QString title;
Diagram *diagram = diagram_view->diagram();
if (settings.value("genericpanel/folio", false).toBool())
{
QString formula = diagram->border_and_titleblock.folio();
autonum::sequentialNumbers seq;
title = autonum::AssignVariables::formulaToLabel(
formula,
seq,
diagram);
}
else
title = QString::number(diagram->folioIndex() + 1);
title += " - ";
title += diagram->title();
m_tab->setTabText(diagram_tab_id ,title);
}
}
/**
@brief ProjectView::updateAllTabsTitle
Update all tabs title
*/
void ProjectView::updateAllTabsTitle()
{
for (DiagramView *dv : m_diagram_ids.values())
updateTabTitle(dv);
}
/**
@param from Index de l'onglet avant le deplacement
@param to Index de l'onglet apres le deplacement
*/
void ProjectView::tabMoved(int from, int to)
{
if (!m_project)
return;
m_project->diagramOrderChanged(from, to);
rebuildDiagramsMap();
//Rebuild the title of each diagram in range from - to
for (int i= qMin(from,to) ; i< qMax(from,to)+1 ; ++i)
{
DiagramView *dv = m_diagram_ids.value(i);
updateTabTitle(dv);
}
}
/**
@brief ProjectView::setDiagramPosition
Move \p diagram's tab to \p new_position (out-of-range values are
clamped by the underlying tab bar, same as a direct QTabBar::moveTab()
call). Used by MoveDiagramCommand for both redo and undo.
tabMoved() -- the slot that normally keeps the project's diagram list in
sync with the tab bar -- is connected with Qt::QueuedConnection (needed
so interactive drag-and-drop reordering settles before the model
updates). That queued call would run a second, redundant
diagramOrderChanged() after this method already performed it
synchronously, so that one connection is temporarily dropped around the
move (blocking all of the tab bar's signals instead would also suppress
QTabWidget's own internal tabMoved connection, which is what keeps its
page stack in the same order as the tab bar -- breaking the move
visually).
@param diagram
@param new_position
*/
void ProjectView::setDiagramPosition(Diagram *diagram, int new_position)
{
if (!m_project)
return;
DiagramView *diagram_view = findDiagram(diagram);
if (!diagram_view)
return;
int current_position = m_diagram_ids.key(diagram_view, -1);
if (current_position < 0)
return;
disconnect(m_tab->tabBar(), SIGNAL(tabMoved(int,int)), this, SLOT(tabMoved(int,int)));
m_tab->tabBar()->moveTab(current_position, new_position);
connect(m_tab->tabBar(), SIGNAL(tabMoved(int,int)), this, SLOT(tabMoved(int,int)), Qt::QueuedConnection);
rebuildDiagramsMap();
int actual_new_position = m_tab->indexOf(diagram_view);
if (actual_new_position != current_position)
m_project->diagramOrderChanged(current_position, actual_new_position);
for (int i = qMin(current_position, actual_new_position); i <= qMax(current_position, actual_new_position); ++i)
updateTabTitle(m_diagram_ids.value(i));
}
/**
@param diagram Schema a trouver
@return le DiagramView correspondant au schema passe en parametre, ou 0 si
le schema n'est pas trouve
*/
DiagramView *ProjectView::findDiagram(Diagram *diagram) {
foreach(DiagramView *diagram_view, diagram_views()) {
if (diagram_view -> diagram() == diagram) {
return(diagram_view);
}
}
return(nullptr);
}
/**
Reconstruit la map associant les index des onglets avec les DiagramView
*/
void ProjectView::rebuildDiagramsMap()
{
// vide la map
m_diagram_ids.clear();
foreach(DiagramView *diagram_view, m_diagram_view_list) {
int dv_idx = m_tab -> indexOf(diagram_view);
if (dv_idx == -1) continue;
m_diagram_ids.insert(dv_idx, diagram_view);
}
}
/**
@brief ProjectView::tabChanged
Manage the tab change.
If tab_id == -1 (there is no diagram opened),
we display the fallback widget.
@param tab_id
*/
void ProjectView::tabChanged(int tab_id)
{
if (tab_id == -1)
setDisplayFallbackWidget(true);
else if(m_tab->count() == 1)
setDisplayFallbackWidget(false);
emit(diagramActivated(m_diagram_ids[tab_id]));
if (m_diagram_ids[tab_id] != nullptr)
m_diagram_ids[tab_id]->diagram()->diagramActivated();
//Clear the event interface of the previous diagram
if (DiagramView *dv = m_diagram_ids[m_previous_tab_index])
dv->diagram()->clearEventInterface();
m_previous_tab_index = tab_id;
}
/**
Gere le double-clic sur un onglet : edite les proprietes du schema
@param tab_id Index de l'onglet concerne
*/
void ProjectView::tabDoubleClicked(int tab_id) {
// repere le schema concerne
DiagramView *diagram_view = m_diagram_ids[tab_id];
if (!diagram_view) return;
diagram_view -> editDiagramProperties();
}
/**
@param fallback true pour afficher le widget de fallback, false pour
afficher les onglets.
Le widget de Fallback est le widget affiche lorsque le projet ne comporte
aucun schema.
*/
void ProjectView::setDisplayFallbackWidget(bool fallback) {
fallback_widget_ -> setVisible(fallback);
m_tab -> setVisible(!fallback);
}