1 /**
2  * UGENE - Integrated Bioinformatics Tools.
3  * Copyright (C) 2008-2021 UniPro <ugene@unipro.ru>
4  * http://ugene.net
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #include "MoveToObjectMaController.h"
23 
24 #include <QMessageBox>
25 
26 #include <U2Core/AddSequencesToAlignmentTask.h>
27 #include <U2Core/AppContext.h>
28 #include <U2Core/Counter.h>
29 #include <U2Core/DocumentModel.h>
30 #include <U2Core/GObject.h>
31 #include <U2Core/GObjectTypes.h>
32 #include <U2Core/GObjectUtils.h>
33 #include <U2Core/L10n.h>
34 #include <U2Core/MultiTask.h>
35 #include <U2Core/MultipleSequenceAlignmentObject.h>
36 #include <U2Core/U2Mod.h>
37 #include <U2Core/U2OpStatusUtils.h>
38 
39 #include <U2Formats/ExportTasks.h>
40 
41 #include <U2Gui/DialogUtils.h>
42 #include <U2Gui/GUIUtils.h>
43 #include <U2Gui/LastUsedDirHelper.h>
44 #include <U2Gui/OpenViewTask.h>
45 
46 #include <U2View/MSAEditor.h>
47 #include <U2View/MaCollapseModel.h>
48 #include <U2View/MaEditor.h>
49 #include <U2View/MaEditorSelection.h>
50 #include <U2View/MaEditorWgt.h>
51 
52 namespace U2 {
53 
MoveToObjectMaController(MaEditor * maEditor)54 MoveToObjectMaController::MoveToObjectMaController(MaEditor *maEditor)
55     : QObject(maEditor), MaEditorContext(maEditor) {
56     moveSelectionToAnotherObjectAction = new QAction(tr("Move selected rows to another alignment"));
57     moveSelectionToAnotherObjectAction->setObjectName("move_selection_to_another_object");
58     connect(moveSelectionToAnotherObjectAction, &QAction::triggered, this, &MoveToObjectMaController::showMoveSelectedRowsToAnotherObjectMenu);
59 
60     moveSelectionToNewFileAction = new QAction(tr("Create a new alignment"));
61     moveSelectionToNewFileAction->setObjectName("move_selection_to_new_file");
62     connect(moveSelectionToNewFileAction, &QAction::triggered, this, &MoveToObjectMaController::runMoveSelectedRowsToNewFileDialog);
63 
64     connect(editor, &MaEditor::si_updateActions, this, &MoveToObjectMaController::updateActions);
65     connect(editor, &MaEditor::si_buildMenu, this, &MoveToObjectMaController::buildMenu);
66 }
67 
buildMoveSelectionToAnotherObjectMenu() const68 QMenu *MoveToObjectMaController::buildMoveSelectionToAnotherObjectMenu() const {
69     QMenu *menu = new QMenu(moveSelectionToAnotherObjectAction->text());
70     menu->setEnabled(moveSelectionToAnotherObjectAction->isEnabled());
71     CHECK(menu->isEnabled(), menu);
72 
73     menu->addAction(moveSelectionToNewFileAction);
74 
75     QList<GObject *> writableMsaObjects = GObjectUtils::findAllObjects(UOF_LoadedOnly, GObjectTypes::MULTIPLE_SEQUENCE_ALIGNMENT, true);
76     writableMsaObjects.removeOne(maObject);
77     std::stable_sort(writableMsaObjects.begin(), writableMsaObjects.end(), [&](const GObject *o1, const GObject *o2) {
78         return o1->getGObjectName().compare(o2->getGObjectName(), Qt::CaseInsensitive);
79     });
80     menu->addSeparator();
81     if (writableMsaObjects.isEmpty()) {
82         QAction *noObjectsAction = menu->addAction(tr("No other alignment objects in the project"), []() {});
83         noObjectsAction->setObjectName("no_other_objects_item");
84         noObjectsAction->setEnabled(false);
85     }
86     QIcon objectMenuIcon(":core/images/msa.png");
87     for (const GObject *object : qAsConst(writableMsaObjects)) {
88         GObjectReference reference(object);
89         QString fileName = object->getDocument()->getURL().fileName();
90         QString menuItemText = object->getGObjectName() + " [" + fileName + "] ";
91         QAction *action = menu->addAction(objectMenuIcon, menuItemText, [this, reference]() {
92             GCounter::increment("MoveSelectedMsaRowsToNewObject");
93             GObject *referenceObject = GObjectUtils::selectObjectByReference(reference, UOF_LoadedOnly);
94             CHECK_EXT(referenceObject != nullptr, QMessageBox::critical(ui, L10N::errorTitle(), L10N::errorObjectNotFound(reference.objName)), );
95             CHECK_EXT(!referenceObject->isStateLocked(), QMessageBox::critical(ui, L10N::errorTitle(), L10N::errorObjectIsReadOnly(reference.objName)), );
96 
97             auto targetMsaObject = qobject_cast<MultipleSequenceAlignmentObject *>(referenceObject);
98             CHECK_EXT(targetMsaObject != nullptr, QMessageBox::critical(ui, L10N::errorTitle(), L10N::nullPointerError(reference.objName)), );
99 
100             QList<int> selectedViewRowIndexes = getSelection().getSelectedRowIndexes();
101             QList<int> selectedMaRowIndexes = collapseModel->getMaRowIndexesByViewRowIndexes(selectedViewRowIndexes, true);
102             QList<qint64> rowIdsToRemove = maObject->getRowIdsByRowIndexes(selectedMaRowIndexes);
103             CHECK_EXT(!rowIdsToRemove.isEmpty(), QMessageBox::critical(ui, L10N::errorTitle(), L10N::internalError()), );
104             QList<DNASequence> sequencesWithGapsToMove;
105             for (int maRowIndex : qAsConst(selectedMaRowIndexes)) {
106                 MultipleAlignmentRow row = maObject->getRow(maRowIndex);
107                 QByteArray sequenceWithGaps = row->getSequenceWithGaps(true, false);
108                 sequencesWithGapsToMove << DNASequence(row->getName(), sequenceWithGaps, maObject->getAlphabet());
109             }
110             auto addRowsTask = new AddSequenceObjectsToAlignmentTask(targetMsaObject, sequencesWithGapsToMove, -1, true);
111             auto removeRowsTask = new RemoveRowsFromMaObjectTask(editor, rowIdsToRemove);
112             AppContext::getTaskScheduler()->registerTopLevelTask(new MultiTask(tr("Move rows to another alignment"), {addRowsTask, removeRowsTask}));
113         });
114         action->setObjectName(fileName);  // For UI testing.
115     }
116     return menu;
117 }
118 
showMoveSelectedRowsToAnotherObjectMenu()119 void MoveToObjectMaController::showMoveSelectedRowsToAnotherObjectMenu() {
120     QScopedPointer<QMenu> menu(buildMoveSelectionToAnotherObjectMenu());
121     menu->exec(QCursor::pos());
122 }
123 
updateActions()124 void MoveToObjectMaController::updateActions() {
125     int countOfSelectedRows = getSelection().getCountOfSelectedRows();
126     bool isMoveOk = !maObject->isStateLocked() && countOfSelectedRows > 0 && countOfSelectedRows < maObject->getNumRows();
127     moveSelectionToAnotherObjectAction->setEnabled(isMoveOk);
128     moveSelectionToNewFileAction->setEnabled(isMoveOk);
129 }
130 
buildMenu(GObjectView *,QMenu * menu,const QString & menuType)131 void MoveToObjectMaController::buildMenu(GObjectView *, QMenu *menu, const QString &menuType) {
132     CHECK(menuType == MsaEditorMenuType::CONTEXT, );
133     QMenu *exportMenu = GUIUtils::findSubMenu(menu, MSAE_MENU_EXPORT);
134     SAFE_POINT(exportMenu != nullptr, "exportMenu is null", );
135     QAction *menuAction = exportMenu->addMenu(buildMoveSelectionToAnotherObjectMenu());
136     menuAction->setObjectName(moveSelectionToAnotherObjectAction->objectName());
137 }
138 
runMoveSelectedRowsToNewFileDialog()139 void MoveToObjectMaController::runMoveSelectedRowsToNewFileDialog() {
140     GCounter::increment("MoveSelectedMsaRowsToNewFile");
141 
142     // Get the file name to move rows to first.
143     LastUsedDirHelper lod;
144     DocumentFormatConstraints formatConstraints;
145     formatConstraints.supportedObjectTypes << GObjectTypes::MULTIPLE_SEQUENCE_ALIGNMENT;
146     QString filter = DialogUtils::prepareDocumentsFileFilter(formatConstraints, false);
147     QString selectedFilter = DialogUtils::prepareDocumentsFileFilter(BaseDocumentFormats::CLUSTAL_ALN, false);
148     lod.url = U2FileDialog::getSaveFileName(ui, tr("Select a new file to move selected rows"), lod, filter, &selectedFilter);
149     CHECK(!lod.url.isEmpty(), );
150 
151     QString url = lod.url;
152     QFileInfo urlInfo(url);
153     QString fileExtension = urlInfo.suffix();
154     DocumentFormatRegistry *formatRegistry = AppContext::getDocumentFormatRegistry();
155     DocumentFormat *format = formatRegistry->selectFormatByFileExtension(fileExtension);
156     if (format == nullptr) {
157         format = formatRegistry->getFormatById(BaseDocumentFormats::CLUSTAL_ALN);
158     }
159     QStringList extensions = format->getSupportedDocumentFileExtensions();
160     if (!extensions.isEmpty() && !extensions.contains(fileExtension)) {
161         url += "." + extensions.first();
162     }
163 
164     // Create a sub-alignment from moved rows.
165     QList<int> selectedViewRowIndexes = getSelection().getSelectedRowIndexes();
166     QList<int> selectedMaRowIndexes = collapseModel->getMaRowIndexesByViewRowIndexes(selectedViewRowIndexes, true);
167     QList<qint64> rowIdsToRemove = maObject->getRowIdsByRowIndexes(selectedMaRowIndexes);
168     SAFE_POINT(!rowIdsToRemove.isEmpty(), "rowIdsToRemove is empty", );
169 
170     MultipleSequenceAlignment msaToExport;
171     msaToExport->setName(urlInfo.baseName());
172     msaToExport->setAlphabet(maObject->getAlphabet());
173     for (int maRowIndex : qAsConst(selectedMaRowIndexes)) {
174         const MultipleAlignmentRow &row = maObject->getRow(maRowIndex);
175         msaToExport->addRow(row->getName(), row->getSequenceWithGaps(true, true));
176     }
177 
178     // Run 2 tasks: first create a new document, next remove moved rows from the original document.
179     auto createNewMsaTask = new AddDocumentAndOpenViewTask(new ExportAlignmentTask(msaToExport, url, format->getFormatId()));
180     auto removeRowsTask = new RemoveRowsFromMaObjectTask(editor, rowIdsToRemove);
181     auto task = new MultiTask(tr("Export alignment rows to a new file"), {createNewMsaTask, removeRowsTask});
182     AppContext::getTaskScheduler()->registerTopLevelTask(task);
183 }
184 
185 /************************************************************************/
186 /* RemoveRowsFromMsaObjectTask */
187 /************************************************************************/
RemoveRowsFromMaObjectTask(MaEditor * _maEditor,const QList<qint64> & _rowIds)188 RemoveRowsFromMaObjectTask::RemoveRowsFromMaObjectTask(MaEditor *_maEditor, const QList<qint64> &_rowIds)
189     : Task(tr("Remove rows from alignment"), TaskFlag_RunInMainThread), maEditor(_maEditor), rowIds(_rowIds) {
190 }
191 
run()192 void RemoveRowsFromMaObjectTask::run() {
193     CHECK(!maEditor.isNull(), );  // The editor may be closed while the task in the queue.
194 
195     MultipleAlignmentObject *maObject = maEditor->getMaObject();
196     CHECK_EXT(rowIds.size() < maObject->getNumRows(), setError(tr("Can't remove all rows from the alignment")), );
197     U2UseCommonUserModStep userModStep(maObject->getEntityRef(), stateInfo);
198     CHECK_OP(stateInfo, );
199 
200     maObject->removeRowsById(rowIds);
201     // If not cleared explicitly another row is auto-selected and the result may be misinterpret like not all rows were moved.
202     maEditor->getSelectionController()->clearSelection();
203 }
204 
205 }  // namespace U2
206