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 "MDIManagerImpl.h"
23 
24 #include <QAction>
25 #include <QHBoxLayout>
26 #include <QMenu>
27 #include <QSet>
28 #include <QShortcut>
29 #include <QToolBar>
30 
31 #include <U2Core/AppContext.h>
32 #include <U2Core/AppSettings.h>
33 #include <U2Core/Log.h>
34 #include <U2Core/Settings.h>
35 #include <U2Core/U2SafePoints.h>
36 #include <U2Core/UserApplicationsSettings.h>
37 
38 namespace U2 {
39 
40 #define SETTINGS_DIR QString("main_window/mdi/")
41 
getWindowName(MDIItem * mdiItem)42 static QString getWindowName(MDIItem *mdiItem) {
43     if (mdiItem == nullptr) {
44         return "<no window>";
45     }
46     return mdiItem->w->windowTitle();
47 }
48 
~MWMDIManagerImpl()49 MWMDIManagerImpl::~MWMDIManagerImpl() {
50 }
51 
prepareGUI()52 void MWMDIManagerImpl::prepareGUI() {
53     mdiContentOwner = nullptr;
54 
55     connect(mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow *)), SLOT(sl_onSubWindowActivated(QMdiSubWindow *)));
56 
57     windowMapper = new QSignalMapper(this);
58     connect(windowMapper, SIGNAL(mapped(QWidget *)), this, SLOT(sl_setActiveSubWindow(QWidget *)));
59 
60     // prepare Window menu
61     closeAct = new QAction(tr("Close active view"), this);
62     closeAct->setObjectName("Close active view");
63     closeAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W));
64     closeAct->setShortcutContext(Qt::WidgetWithChildrenShortcut);
65     closeAct->setStatusTip(tr("Close active view"));
66     connect(closeAct, SIGNAL(triggered()), mdiArea, SLOT(closeActiveSubWindow()));
67 
68     closeAllAct = new QAction(tr("Close all windows"), this);
69     closeAllAct->setObjectName("Close all windows");
70     closeAllAct->setStatusTip(tr("Close all windows"));
71     connect(closeAllAct, SIGNAL(triggered()), mdiArea, SLOT(closeAllSubWindows()));
72 
73     windowLayout = new QMenu(tr("Window layout"));
74     windowLayout->setObjectName("Window layout");
75     windowLayout->setStatusTip(tr("Window layout"));
76 
77     bool tabbedLayout = AppContext::getAppSettings()->getUserAppsSettings()->tabbedWindowLayout();
78     multipleDocsAct = new QAction(tr("Multiple documents"), this);
79     multipleDocsAct->setCheckable(true);
80     multipleDocsAct->setChecked(!tabbedLayout);
81     connect(multipleDocsAct, SIGNAL(triggered()), SLOT(sl_setWindowLayoutToMultiDoc()));
82 
83     tabbedDocsAct = new QAction(tr("Tabbed documents"), this);
84     tabbedDocsAct->setCheckable(true);
85     tabbedDocsAct->setChecked(tabbedLayout);
86     connect(tabbedDocsAct, SIGNAL(triggered()), SLOT(sl_setWindowLayoutToTabbed()));
87 
88     tileAct = new QAction(QIcon(":ugene/images/window_tile.png"), tr("Tile windows"), this);
89     tileAct->setObjectName("Tile windows");
90     tileAct->setStatusTip(tr("Tile windows"));
91     connect(tileAct, SIGNAL(triggered()), mdiArea, SLOT(tileSubWindows()));
92 
93     cascadeAct = new QAction(QIcon(":ugene/images/window_cascade.png"), tr("Cascade windows"), this);
94     cascadeAct->setObjectName("Cascade windows");
95     cascadeAct->setStatusTip(tr("Cascade windows"));
96     connect(cascadeAct, SIGNAL(triggered()), mdiArea, SLOT(cascadeSubWindows()));
97 
98 #ifdef Q_OS_DARWIN
99     QKeySequence nextActKeySequence(Qt::CTRL + (Qt::Key)'`');
100     QKeySequence nextActKeySequenceAdditional(Qt::META + Qt::Key_Tab);
101     QKeySequence prevActKeySequence(Qt::CTRL + Qt::SHIFT + (Qt::Key)'`');
102     QKeySequence prevActKeySequenceAdditional(Qt::META + Qt::SHIFT + Qt::Key_Tab);
103 
104     QShortcut *additionalNextShortcut = new QShortcut(nextActKeySequenceAdditional, mdiArea);
105     connect(additionalNextShortcut, SIGNAL(activated()), mdiArea, SLOT(activateNextSubWindow()));
106 
107     QShortcut *additionalPrevShortcut = new QShortcut(prevActKeySequenceAdditional, mdiArea);
108     connect(additionalPrevShortcut, SIGNAL(activated()), mdiArea, SLOT(activatePreviousSubWindow()));
109 
110 #else
111     QKeySequence nextActKeySequence(Qt::CTRL + Qt::Key_Tab);
112     QKeySequence prevActKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab);
113 #endif
114 
115     nextAct = new QAction(QIcon(":ugene/images/window_next.png"), tr("Next window"), this);
116     nextAct->setObjectName("Next window");
117     nextAct->setStatusTip(tr("Next window"));
118     nextAct->setShortcut(nextActKeySequence);
119     connect(nextAct, SIGNAL(triggered()), mdiArea, SLOT(activateNextSubWindow()));
120 
121     previousAct = new QAction(QIcon(":ugene/images/window_prev.png"), tr("Previous window"), this);
122     previousAct->setObjectName("Previous window");
123     previousAct->setStatusTip(tr("Previous window"));
124     previousAct->setShortcut(prevActKeySequence);
125     connect(previousAct, SIGNAL(triggered()), mdiArea, SLOT(activatePreviousSubWindow()));
126 
127     separatorAct = new QAction("-", this);
128     separatorAct->setSeparator(true);
129 
130     defaultIsMaximized = AppContext::getSettings()->getValue(SETTINGS_DIR + "maximized", true).toBool();
131 
132     QMenu *windowMenu = mw->getTopLevelMenu(MWMENU_WINDOW);
133     connect(windowMenu, SIGNAL(aboutToShow()), this, SLOT(sl_updateWindowMenu()));
134 
135     updateState();
136     clearMDIContent(true);
137 
138     sl_updateWindowLayout();
139     connect(AppContext::getAppSettings()->getUserAppsSettings(), SIGNAL(si_windowLayoutChanged()), SLOT(sl_updateWindowLayout()));
140 }
141 
eventFilter(QObject * obj,QEvent * event)142 bool MWMDIManagerImpl::eventFilter(QObject *obj, QEvent *event) {
143     QEvent::Type t = event->type();
144     if (t == QEvent::Close) {
145         QMdiSubWindow *qw = qobject_cast<QMdiSubWindow *>(obj);
146         MDIItem *item = getMDIItem(qw);
147         if (item != nullptr) {
148             uiLog.trace(QString("Processing close window request for '%1'").arg(getWindowName(item)));
149 
150             // check if user really wants to close the window, ignore event if not
151             if (!onCloseEvent(item->w)) {
152                 uiLog.trace(QString("Ignoring close window request for '%1'").arg(getWindowName(item)));
153                 event->ignore();
154                 return true;
155             }
156 
157             // here we sure that window will be closed
158             emit si_windowClosing(item->w);
159 
160             if (item == mdiContentOwner) {  // if 'current' window is closed -> clear MDI
161                 clearMDIContent(true);
162             }
163             items.removeAll(item);
164             delete item;
165             updateState();
166         }
167     } else if (t == QEvent::WindowStateChange) {
168         QMdiSubWindow *qw = qobject_cast<QMdiSubWindow *>(obj);
169         defaultIsMaximized = qw->isMaximized();
170     }
171     return QObject::eventFilter(obj, event);
172 }
173 
updateState()174 void MWMDIManagerImpl::updateState() {
175     updateActions();
176     sl_updateWindowMenu();
177 
178     AppContext::getSettings()->setValue(SETTINGS_DIR + "maximized", defaultIsMaximized);
179 }
180 
updateActions()181 void MWMDIManagerImpl::updateActions() {
182     bool hasMDIWindows = !items.empty();
183 
184     closeAct->setEnabled(hasMDIWindows);
185     closeAllAct->setEnabled(hasMDIWindows);
186     tileAct->setEnabled(hasMDIWindows);
187     cascadeAct->setEnabled(hasMDIWindows);
188     nextAct->setEnabled(hasMDIWindows);
189     previousAct->setEnabled(hasMDIWindows);
190     separatorAct->setVisible(hasMDIWindows);
191 }
192 
sl_updateWindowMenu()193 void MWMDIManagerImpl::sl_updateWindowMenu() {
194     QMenu *windowMenu = mw->getTopLevelMenu(MWMENU_WINDOW);
195     windowMenu->clear();  // TODO: avoid cleaning 3rd party actions
196     windowMenu->addMenu(windowLayout);
197     windowLayout->addAction(multipleDocsAct);
198     windowLayout->addAction(tabbedDocsAct);
199     windowMenu->addAction(closeAct);
200     windowMenu->addAction(closeAllAct);
201 
202     if (mdiArea->viewMode() == QMdiArea::SubWindowView) {
203         windowMenu->addSeparator();
204         windowMenu->addAction(tileAct);
205         windowMenu->addAction(cascadeAct);
206     }
207     windowMenu->addSeparator();
208     windowMenu->addAction(nextAct);
209     windowMenu->addAction(previousAct);
210     windowMenu->addAction(separatorAct);
211 
212     separatorAct->setVisible(!items.isEmpty());
213 
214     MDIItem *currentItem = getCurrentMDIItem();
215     for (int i = 0; i < items.size(); ++i) {
216         MDIItem *item = items.at(i);
217         QString text;
218         if (i < 9) {
219             text = QString("&%1 %2").arg(i + 1).arg(item->w->windowTitle());
220         } else {
221             text = QString("%1 %2").arg(i + 1).arg(item->w->windowTitle());
222         }
223         QAction *action = windowMenu->addAction(text);
224         action->setCheckable(true);
225         action->setChecked(item == currentItem);
226         connect(action, SIGNAL(triggered()), windowMapper, SLOT(map()));
227         windowMapper->setMapping(action, item->qw);
228     }
229 }
230 
getCurrentMDIItem() const231 MDIItem *MWMDIManagerImpl::getCurrentMDIItem() const {
232     QMdiSubWindow *currentSubWindow = mdiArea->currentSubWindow();
233     if (currentSubWindow != nullptr) {
234         return getMDIItem(currentSubWindow);
235     }
236     return nullptr;
237 }
238 
addMDIWindow(MWMDIWindow * w)239 void MWMDIManagerImpl::addMDIWindow(MWMDIWindow *w) {
240     bool contains = getWindowById(w->getId()) != nullptr;
241     if (contains) {
242         assert(0);  // must never happen
243         return;
244     }
245     w->setParent(mdiArea);
246     QMdiSubWindow *qw = mdiArea->addSubWindow(w);
247     qw->setWindowTitle(w->windowTitle());
248     QIcon icon = w->windowIcon();
249     if (icon.isNull()) {
250         icon = QIcon(":/ugene/images/ugene_16.png");
251     }
252     qw->setWindowIcon(icon);
253     // qw->setAttribute(Qt::WA_NativeWindow);
254     MDIItem *i = new MDIItem(w, qw);
255     items.append(i);
256     qw->installEventFilter(this);
257 
258     uiLog.trace(QString("Adding window: '%1'").arg(w->windowTitle()));
259 
260     updateState();
261 
262     emit si_windowAdded(w);
263 
264     if (items.count() == 1 && defaultIsMaximized) {
265         qw->showMaximized();
266     } else {
267         qw->show();
268     }
269     qw->raise();
270 }
271 
getWindows() const272 QList<MWMDIWindow *> MWMDIManagerImpl::getWindows() const {
273     QList<MWMDIWindow *> res;
274     foreach (MDIItem *i, items) {
275         res.append(i->w);
276     }
277     return res;
278 }
279 
closeMDIWindow(MWMDIWindow * w)280 bool MWMDIManagerImpl::closeMDIWindow(MWMDIWindow *w) {
281     MDIItem *i = getMDIItem(w);
282     if (nullptr == i)
283         return false;
284     return i->qw->close();
285 }
286 
getWindowById(int id) const287 MWMDIWindow *MWMDIManagerImpl::getWindowById(int id) const {
288     MDIItem *i = getMDIItem(id);
289     return i == nullptr ? nullptr : i->w;
290 }
291 
getMDIItem(int id) const292 MDIItem *MWMDIManagerImpl::getMDIItem(int id) const {
293     foreach (MDIItem *i, items) {
294         if (i->w->getId() == id) {
295             return i;
296         }
297     }
298     return nullptr;
299 }
300 
getMDIItem(MWMDIWindow * w) const301 MDIItem *MWMDIManagerImpl::getMDIItem(MWMDIWindow *w) const {
302     foreach (MDIItem *i, items) {
303         if (i->w == w) {
304             return i;
305         }
306     }
307     return nullptr;
308 }
309 
getMDIItem(QMdiSubWindow * qw) const310 MDIItem *MWMDIManagerImpl::getMDIItem(QMdiSubWindow *qw) const {
311     foreach (MDIItem *item, items) {
312         if (item->qw == qw) {
313             return item;
314         }
315     }
316     return nullptr;
317 }
318 
activateWindow(MWMDIWindow * w)319 void MWMDIManagerImpl::activateWindow(MWMDIWindow *w) {
320     MDIItem *i = getMDIItem(w);
321     if (i == nullptr) {
322         return;
323     }
324     AppContext::setActiveWindowName(w->windowTitle());
325     mdiArea->setActiveSubWindow(i->qw);
326     updateState();
327 }
328 
sl_setActiveSubWindow(QWidget * w)329 void MWMDIManagerImpl::sl_setActiveSubWindow(QWidget *w) {
330     if (!w) {
331         return;
332     }
333     mdiArea->setActiveSubWindow(qobject_cast<QMdiSubWindow *>(w));
334 }
335 
clearMDIContent(bool addCloseAction)336 void MWMDIManagerImpl::clearMDIContent(bool addCloseAction) {
337     // clear toolbar
338     mdiContentOwner = nullptr;
339 
340     QToolBar *tb = mw->getToolbar(MWTOOLBAR_ACTIVEMDI);
341     QMenu *m = mw->getTopLevelMenu(MWMENU_ACTIONS);
342 
343     // delete submenus inserted to toolbar and menu
344     // todo: provide a flag to enable/disable this behavior for MDI window
345     QList<QAction *> allMDIActions = tb->actions() + m->actions();
346     QSet<QMenu *> toDel;
347     foreach (QAction *ma, allMDIActions) {
348         QMenu *am = ma->menu();
349         if (am != nullptr) {
350             toDel.insert(am);
351         }
352     }
353 
354     tb->clear();
355     m->clear();
356 
357     foreach (QMenu *mtd, toDel) {
358         delete mtd;
359     }
360 
361     if (addCloseAction) {
362         m->addAction(closeAct);
363     }
364 }
365 
onWindowsSwitched(QMdiSubWindow * deactivated,MWMDIWindow * activated)366 void MWMDIManagerImpl::onWindowsSwitched(QMdiSubWindow *deactivated, MWMDIWindow *activated) {
367     MDIItem *deItem = getMDIItem(deactivated);
368     if ((nullptr != deItem) && (nullptr != deItem->w)) {
369         emit si_windowDeactivated(deItem->w);
370     }
371     emit si_windowActivated(activated);
372 }
373 
sl_onSubWindowActivated(QMdiSubWindow * w)374 void MWMDIManagerImpl::sl_onSubWindowActivated(QMdiSubWindow *w) {
375     // Details: sub-window is activated and deactivated
376     //  1) every time user switches MDI windows
377     //  2) every time user switches applications
378     //  Here we update mdi content only for 1 case and trying to avoid 2 case
379 
380     QMdiSubWindow *currentWindow = mdiArea->currentSubWindow();
381     if (w == nullptr && currentWindow != nullptr) {  // simple deactivation, current window is not changed
382         uiLog.trace(QString("Window deactivation, no MDI context switch, window: '%1'").arg(getWindowName(mdiContentOwner)));
383         assert(getMDIItem(currentWindow) == mdiContentOwner);
384         emit si_windowActivated(nullptr);
385         return;
386     }
387     if (mdiContentOwner != nullptr && mdiContentOwner->qw == w) {  // simple activation, current window is not changed
388         uiLog.trace(QString("Window activation, no MDI context switch, window: '%1'").arg(getWindowName(mdiContentOwner)));
389         emit si_windowActivated(mdiContentOwner->w);
390         return;
391     }
392     if (w == nullptr) {  // currentWindow is NULL here, mdiContentOwner & it's content cleaned in eventFilter(CloseEvent)
393         uiLog.trace(QString("Closing active window"));
394         clearMDIContent(true);  // UGENE-4987 workaround. It must be 'false' here
395         emit si_windowActivated(nullptr);
396         return;
397     }
398     MDIItem *mdiItem = getMDIItem(w);
399     SAFE_POINT(mdiItem != nullptr, "The window does not belong to the MDI manager!", );
400     uiLog.trace(QString("Switching active MDI window from '%1' to '%2'").arg(getWindowName(mdiContentOwner)).arg(getWindowName(mdiItem)));
401     // clear old windows menu/tb content
402     clearMDIContent(false);
403 
404 #ifdef Q_OS_DARWIN
405     // A workaround for UGENE-6315 (QTBUG-67895): background widgets are drawn over the foreground widget on macOS with the native style
406     if (QMdiArea::TabbedView == mdiArea->viewMode()) {
407         foreach (MDIItem *item, items) {
408             if (item != mdiItem && nullptr != item && nullptr != item->w && nullptr != item->qw) {
409                 item->w->hide();
410                 item->qw->setWindowFlags(item->qw->windowFlags() | Qt::FramelessWindowHint);
411             }
412         }
413 
414         if (nullptr != mdiItem && nullptr != mdiItem->w && nullptr != mdiItem->qw) {
415             mdiItem->qw->setWindowFlags(mdiItem->qw->windowFlags() & (~Qt::FramelessWindowHint));
416             mdiItem->w->show();
417         }
418     }
419 #endif
420 
421     // add new content to menu/tb
422     QToolBar *tb = mw->getToolbar(MWTOOLBAR_ACTIVEMDI);
423     mdiContentOwner = mdiItem;
424     SAFE_POINT(mdiContentOwner->w != nullptr, "Incorrect MDI window is detected!", );
425     mdiContentOwner->w->setupMDIToolbar(tb);
426 
427     QMenu *m = mw->getTopLevelMenu(MWMENU_ACTIONS);
428     SAFE_POINT(mdiContentOwner->w != nullptr, "Incorrect menu is detected!", );
429     mdiContentOwner->w->setupViewMenu(m);
430     m->addAction(closeAct);
431     onWindowsSwitched(w, mdiItem->w);
432 }
433 
getActiveWindow() const434 MWMDIWindow *MWMDIManagerImpl::getActiveWindow() const {
435     MDIItem *i = getCurrentMDIItem();
436     if (i == nullptr) {
437         return nullptr;
438     }
439     return i->w;
440 }
441 
sl_updateWindowLayout()442 void MWMDIManagerImpl::sl_updateWindowLayout() {
443     bool tabbed = AppContext::getAppSettings()->getUserAppsSettings()->tabbedWindowLayout();
444     mdiArea->setViewMode(tabbed ? QMdiArea::TabbedView : QMdiArea::SubWindowView);
445     multipleDocsAct->setChecked(!tabbed);
446     tabbedDocsAct->setChecked(tabbed);
447 }
448 
sl_setWindowLayoutToMultiDoc()449 void MWMDIManagerImpl::sl_setWindowLayoutToMultiDoc() {
450     UserAppsSettings *st = AppContext::getAppSettings()->getUserAppsSettings();
451     st->setTabbedWindowLayout(false);
452     sl_updateWindowLayout();
453 }
454 
sl_setWindowLayoutToTabbed()455 void MWMDIManagerImpl::sl_setWindowLayoutToTabbed() {
456     UserAppsSettings *st = AppContext::getAppSettings()->getUserAppsSettings();
457     st->setTabbedWindowLayout(true);
458     sl_updateWindowLayout();
459 }
460 
461 }  // namespace U2
462