diff --git a/sources/ElementsCollection/elementscollectionwidget.cpp b/sources/ElementsCollection/elementscollectionwidget.cpp index 3c015c716..511dd0ef8 100644 --- a/sources/ElementsCollection/elementscollectionwidget.cpp +++ b/sources/ElementsCollection/elementscollectionwidget.cpp @@ -35,6 +35,8 @@ #include #include +#include +#include #include #include #include @@ -266,15 +268,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 +278,23 @@ 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()); + }); + } + 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 +403,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 +467,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() << location); diff --git a/sources/ElementsCollection/elementscollectionwidget.h b/sources/ElementsCollection/elementscollectionwidget.h index bf00f37d6..b6997b7a8 100644 --- a/sources/ElementsCollection/elementscollectionwidget.h +++ b/sources/ElementsCollection/elementscollectionwidget.h @@ -86,9 +86,22 @@ 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); private: diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 843b694be..06149836b 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -223,14 +223,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()); } /** diff --git a/sources/diagramview.h b/sources/diagramview.h index eb73432cb..43201fd2e 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -79,6 +79,10 @@ class DiagramView : public PaletteGraphicsView /// 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; + protected: void mouseDoubleClickEvent(QMouseEvent *) override; void contextMenuEvent(QContextMenuEvent *) override; @@ -132,6 +136,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(); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index da26262b2..cc5c546f3 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -226,6 +226,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); } @@ -755,6 +762,26 @@ 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); + 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")); @@ -1038,6 +1065,7 @@ 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 -> addSeparator(); //The same actions the "Ajouter" toolbar holds. They were toolbar-only, //which left them unreachable for anyone working without a mouse: a @@ -1937,6 +1965,7 @@ 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_row_column_actions_group. setEnabled(editable_project); m_background_color_button-> setEnabled(opened_diagram); m_draw_grid-> setEnabled(opened_diagram); @@ -2830,6 +2859,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); } /** @@ -2939,6 +2969,53 @@ 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); +} + +/** + @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::generateTerminalBlock */ diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 71ad56244..622c78c16 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -125,6 +125,9 @@ 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 generateTerminalBlock(); void setWindowedMode(); void setTabbedMode(); @@ -259,6 +262,9 @@ class QETDiagramEditor : public QETMainWindow *qdw_undo; /// Dock for the undo list ElementsCollectionWidget *m_element_collection_widget; + /// Last element placed from the collection, for "insert last" + ElementsLocation m_last_inserted_element; + QAction *m_insert_last_element = nullptr; DiagramPropertiesEditorDockWidget *m_selection_properties_editor; /// Elements panel diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index f5689046c..5f6194410 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -66,6 +66,9 @@ 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->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt()); ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt()); ui->DiagramEditor_xKeyGrid_sb->setValue(settings.value("diagrameditor/key_Xgrid", 10).toInt()); @@ -284,6 +287,7 @@ void GeneralConfigurationPage::applyConf() 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()); //Grid step and key navigation settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value()); settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 030b66877..9d4bd92e9 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -77,6 +77,16 @@ + + + + Par défaut, un double-clic insère l'élément sur le folio ; l'édition reste accessible par le menu contextuel. + + + Double-cliquer dans la collection ouvre l'éditeur d'élément au lieu de l'insérer + + +