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