1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the examples of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:BSD$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** BSD License Usage |
18 | ** Alternatively, you may use this file under the terms of the BSD license |
19 | ** as follows: |
20 | ** |
21 | ** "Redistribution and use in source and binary forms, with or without |
22 | ** modification, are permitted provided that the following conditions are |
23 | ** met: |
24 | ** * Redistributions of source code must retain the above copyright |
25 | ** notice, this list of conditions and the following disclaimer. |
26 | ** * Redistributions in binary form must reproduce the above copyright |
27 | ** notice, this list of conditions and the following disclaimer in |
28 | ** the documentation and/or other materials provided with the |
29 | ** distribution. |
30 | ** * Neither the name of The Qt Company Ltd nor the names of its |
31 | ** contributors may be used to endorse or promote products derived |
32 | ** from this software without specific prior written permission. |
33 | ** |
34 | ** |
35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
46 | ** |
47 | ** $QT_END_LICENSE$ |
48 | ** |
49 | ****************************************************************************/ |
50 | |
51 | #include <QtWidgets> |
52 | |
53 | #include "mainwindow.h" |
54 | |
55 | MainWindow::MainWindow() |
56 | { |
57 | init(); |
58 | setCurrentFile(QString()); |
59 | } |
60 | |
61 | MainWindow::MainWindow(const QString &fileName) |
62 | { |
63 | init(); |
64 | loadFile(fileName); |
65 | } |
66 | |
67 | void MainWindow::closeEvent(QCloseEvent *event) |
68 | { |
69 | if (maybeSave()) { |
70 | writeSettings(); |
71 | event->accept(); |
72 | } else { |
73 | event->ignore(); |
74 | } |
75 | } |
76 | |
77 | void MainWindow::newFile() |
78 | { |
79 | MainWindow *other = new MainWindow; |
80 | other->tile(previous: this); |
81 | other->show(); |
82 | } |
83 | |
84 | void MainWindow::open() |
85 | { |
86 | const QString fileName = QFileDialog::getOpenFileName(parent: this); |
87 | if (!fileName.isEmpty()) |
88 | openFile(fileName); |
89 | } |
90 | |
91 | void MainWindow::openFile(const QString &fileName) |
92 | { |
93 | MainWindow *existing = findMainWindow(fileName); |
94 | if (existing) { |
95 | existing->show(); |
96 | existing->raise(); |
97 | existing->activateWindow(); |
98 | return; |
99 | } |
100 | |
101 | if (isUntitled && textEdit->document()->isEmpty() && !isWindowModified()) { |
102 | loadFile(fileName); |
103 | return; |
104 | } |
105 | |
106 | MainWindow *other = new MainWindow(fileName); |
107 | if (other->isUntitled) { |
108 | delete other; |
109 | return; |
110 | } |
111 | other->tile(previous: this); |
112 | other->show(); |
113 | } |
114 | |
115 | bool MainWindow::save() |
116 | { |
117 | return isUntitled ? saveAs() : saveFile(fileName: curFile); |
118 | } |
119 | |
120 | bool MainWindow::saveAs() |
121 | { |
122 | QString fileName = QFileDialog::getSaveFileName(parent: this, caption: tr(s: "Save As" ), |
123 | dir: curFile); |
124 | if (fileName.isEmpty()) |
125 | return false; |
126 | |
127 | return saveFile(fileName); |
128 | } |
129 | |
130 | void MainWindow::about() |
131 | { |
132 | QMessageBox::about(parent: this, title: tr(s: "About SDI" ), |
133 | text: tr(s: "The <b>SDI</b> example demonstrates how to write single " |
134 | "document interface applications using Qt." )); |
135 | } |
136 | |
137 | void MainWindow::documentWasModified() |
138 | { |
139 | setWindowModified(true); |
140 | } |
141 | |
142 | void MainWindow::init() |
143 | { |
144 | setAttribute(Qt::WA_DeleteOnClose); |
145 | |
146 | isUntitled = true; |
147 | |
148 | textEdit = new QTextEdit; |
149 | setCentralWidget(textEdit); |
150 | |
151 | createActions(); |
152 | createStatusBar(); |
153 | |
154 | readSettings(); |
155 | |
156 | connect(sender: textEdit->document(), signal: &QTextDocument::contentsChanged, |
157 | receiver: this, slot: &MainWindow::documentWasModified); |
158 | |
159 | setUnifiedTitleAndToolBarOnMac(true); |
160 | } |
161 | |
162 | void MainWindow::tile(const QMainWindow *previous) |
163 | { |
164 | if (!previous) |
165 | return; |
166 | int topFrameWidth = previous->geometry().top() - previous->pos().y(); |
167 | if (!topFrameWidth) |
168 | topFrameWidth = 40; |
169 | const QPoint pos = previous->pos() + 2 * QPoint(topFrameWidth, topFrameWidth); |
170 | if (screen()->availableGeometry().contains(p: rect().bottomRight() + pos)) |
171 | move(pos); |
172 | } |
173 | |
174 | //! [implicit tr context] |
175 | void MainWindow::createActions() |
176 | { |
177 | QMenu * = menuBar()->addMenu(title: tr(s: "&File" )); |
178 | //! [implicit tr context] |
179 | QToolBar *fileToolBar = addToolBar(title: tr(s: "File" )); |
180 | |
181 | const QIcon newIcon = QIcon::fromTheme(name: "document-new" , fallback: QIcon(":/images/new.png" )); |
182 | QAction *newAct = new QAction(newIcon, tr(s: "&New" ), this); |
183 | newAct->setShortcuts(QKeySequence::New); |
184 | newAct->setStatusTip(tr(s: "Create a new file" )); |
185 | connect(sender: newAct, signal: &QAction::triggered, receiver: this, slot: &MainWindow::newFile); |
186 | fileMenu->addAction(action: newAct); |
187 | fileToolBar->addAction(action: newAct); |
188 | |
189 | const QIcon openIcon = QIcon::fromTheme(name: "document-open" , fallback: QIcon(":/images/open.png" )); |
190 | QAction *openAct = new QAction(openIcon, tr(s: "&Open..." ), this); |
191 | openAct->setShortcuts(QKeySequence::Open); |
192 | openAct->setStatusTip(tr(s: "Open an existing file" )); |
193 | connect(sender: openAct, signal: &QAction::triggered, receiver: this, slot: &MainWindow::open); |
194 | fileMenu->addAction(action: openAct); |
195 | fileToolBar->addAction(action: openAct); |
196 | |
197 | const QIcon saveIcon = QIcon::fromTheme(name: "document-save" , fallback: QIcon(":/images/save.png" )); |
198 | QAction *saveAct = new QAction(saveIcon, tr(s: "&Save" ), this); |
199 | saveAct->setShortcuts(QKeySequence::Save); |
200 | saveAct->setStatusTip(tr(s: "Save the document to disk" )); |
201 | connect(sender: saveAct, signal: &QAction::triggered, receiver: this, slot: &MainWindow::save); |
202 | fileMenu->addAction(action: saveAct); |
203 | fileToolBar->addAction(action: saveAct); |
204 | |
205 | const QIcon saveAsIcon = QIcon::fromTheme(name: "document-save-as" ); |
206 | QAction *saveAsAct = fileMenu->addAction(actionIcon: saveAsIcon, text: tr(s: "Save &As..." ), object: this, slot: &MainWindow::saveAs); |
207 | saveAsAct->setShortcuts(QKeySequence::SaveAs); |
208 | saveAsAct->setStatusTip(tr(s: "Save the document under a new name" )); |
209 | |
210 | fileMenu->addSeparator(); |
211 | |
212 | QMenu * = fileMenu->addMenu(title: tr(s: "Recent..." )); |
213 | connect(sender: recentMenu, signal: &QMenu::aboutToShow, receiver: this, slot: &MainWindow::updateRecentFileActions); |
214 | recentFileSubMenuAct = recentMenu->menuAction(); |
215 | |
216 | for (int i = 0; i < MaxRecentFiles; ++i) { |
217 | recentFileActs[i] = recentMenu->addAction(text: QString(), object: this, slot: &MainWindow::openRecentFile); |
218 | recentFileActs[i]->setVisible(false); |
219 | } |
220 | |
221 | recentFileSeparator = fileMenu->addSeparator(); |
222 | |
223 | setRecentFilesVisible(MainWindow::hasRecentFiles()); |
224 | |
225 | QAction *closeAct = fileMenu->addAction(text: tr(s: "&Close" ), object: this, slot: &QWidget::close); |
226 | closeAct->setShortcut(tr(s: "Ctrl+W" )); |
227 | closeAct->setStatusTip(tr(s: "Close this window" )); |
228 | |
229 | const QIcon exitIcon = QIcon::fromTheme(name: "application-exit" ); |
230 | QAction *exitAct = fileMenu->addAction(actionIcon: exitIcon, text: tr(s: "E&xit" ), qApp, slot: &QApplication::closeAllWindows); |
231 | exitAct->setShortcuts(QKeySequence::Quit); |
232 | exitAct->setStatusTip(tr(s: "Exit the application" )); |
233 | |
234 | QMenu * = menuBar()->addMenu(title: tr(s: "&Edit" )); |
235 | QToolBar *editToolBar = addToolBar(title: tr(s: "Edit" )); |
236 | |
237 | #ifndef QT_NO_CLIPBOARD |
238 | const QIcon cutIcon = QIcon::fromTheme(name: "edit-cut" , fallback: QIcon(":/images/cut.png" )); |
239 | QAction *cutAct = new QAction(cutIcon, tr(s: "Cu&t" ), this); |
240 | cutAct->setShortcuts(QKeySequence::Cut); |
241 | cutAct->setStatusTip(tr(s: "Cut the current selection's contents to the " |
242 | "clipboard" )); |
243 | connect(sender: cutAct, signal: &QAction::triggered, receiver: textEdit, slot: &QTextEdit::cut); |
244 | editMenu->addAction(action: cutAct); |
245 | editToolBar->addAction(action: cutAct); |
246 | |
247 | const QIcon copyIcon = QIcon::fromTheme(name: "edit-copy" , fallback: QIcon(":/images/copy.png" )); |
248 | QAction *copyAct = new QAction(copyIcon, tr(s: "&Copy" ), this); |
249 | copyAct->setShortcuts(QKeySequence::Copy); |
250 | copyAct->setStatusTip(tr(s: "Copy the current selection's contents to the " |
251 | "clipboard" )); |
252 | connect(sender: copyAct, signal: &QAction::triggered, receiver: textEdit, slot: &QTextEdit::copy); |
253 | editMenu->addAction(action: copyAct); |
254 | editToolBar->addAction(action: copyAct); |
255 | |
256 | const QIcon pasteIcon = QIcon::fromTheme(name: "edit-paste" , fallback: QIcon(":/images/paste.png" )); |
257 | QAction *pasteAct = new QAction(pasteIcon, tr(s: "&Paste" ), this); |
258 | pasteAct->setShortcuts(QKeySequence::Paste); |
259 | pasteAct->setStatusTip(tr(s: "Paste the clipboard's contents into the current " |
260 | "selection" )); |
261 | connect(sender: pasteAct, signal: &QAction::triggered, receiver: textEdit, slot: &QTextEdit::paste); |
262 | editMenu->addAction(action: pasteAct); |
263 | editToolBar->addAction(action: pasteAct); |
264 | |
265 | menuBar()->addSeparator(); |
266 | #endif // !QT_NO_CLIPBOARD |
267 | |
268 | QMenu * = menuBar()->addMenu(title: tr(s: "&Help" )); |
269 | QAction *aboutAct = helpMenu->addAction(text: tr(s: "&About" ), object: this, slot: &MainWindow::about); |
270 | aboutAct->setStatusTip(tr(s: "Show the application's About box" )); |
271 | |
272 | QAction *aboutQtAct = helpMenu->addAction(text: tr(s: "About &Qt" ), qApp, slot: &QApplication::aboutQt); |
273 | aboutQtAct->setStatusTip(tr(s: "Show the Qt library's About box" )); |
274 | |
275 | #ifndef QT_NO_CLIPBOARD |
276 | cutAct->setEnabled(false); |
277 | copyAct->setEnabled(false); |
278 | connect(sender: textEdit, signal: &QTextEdit::copyAvailable, receiver: cutAct, slot: &QAction::setEnabled); |
279 | connect(sender: textEdit, signal: &QTextEdit::copyAvailable, receiver: copyAct, slot: &QAction::setEnabled); |
280 | #endif // !QT_NO_CLIPBOARD |
281 | } |
282 | |
283 | void MainWindow::createStatusBar() |
284 | { |
285 | statusBar()->showMessage(text: tr(s: "Ready" )); |
286 | } |
287 | |
288 | void MainWindow::readSettings() |
289 | { |
290 | QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName()); |
291 | const QByteArray geometry = settings.value(key: "geometry" , defaultValue: QByteArray()).toByteArray(); |
292 | if (geometry.isEmpty()) { |
293 | const QRect availableGeometry = screen()->availableGeometry(); |
294 | resize(w: availableGeometry.width() / 3, h: availableGeometry.height() / 2); |
295 | move(ax: (availableGeometry.width() - width()) / 2, |
296 | ay: (availableGeometry.height() - height()) / 2); |
297 | } else { |
298 | restoreGeometry(geometry); |
299 | } |
300 | } |
301 | |
302 | void MainWindow::writeSettings() |
303 | { |
304 | QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName()); |
305 | settings.setValue(key: "geometry" , value: saveGeometry()); |
306 | } |
307 | |
308 | bool MainWindow::maybeSave() |
309 | { |
310 | if (!textEdit->document()->isModified()) |
311 | return true; |
312 | const QMessageBox::StandardButton ret |
313 | = QMessageBox::warning(parent: this, title: tr(s: "SDI" ), |
314 | text: tr(s: "The document has been modified.\n" |
315 | "Do you want to save your changes?" ), |
316 | buttons: QMessageBox::Save | QMessageBox::Discard |
317 | | QMessageBox::Cancel); |
318 | switch (ret) { |
319 | case QMessageBox::Save: |
320 | return save(); |
321 | case QMessageBox::Cancel: |
322 | return false; |
323 | default: |
324 | break; |
325 | } |
326 | return true; |
327 | } |
328 | |
329 | void MainWindow::loadFile(const QString &fileName) |
330 | { |
331 | |
332 | QFile file(fileName); |
333 | if (!file.open(flags: QFile::ReadOnly | QFile::Text)) { |
334 | QMessageBox::warning(parent: this, title: tr(s: "SDI" ), |
335 | text: tr(s: "Cannot read file %1:\n%2." ) |
336 | .arg(args: QDir::toNativeSeparators(pathName: fileName), args: file.errorString())); |
337 | return; |
338 | } |
339 | |
340 | QTextStream in(&file); |
341 | QGuiApplication::setOverrideCursor(Qt::WaitCursor); |
342 | textEdit->setPlainText(in.readAll()); |
343 | QGuiApplication::restoreOverrideCursor(); |
344 | |
345 | setCurrentFile(fileName); |
346 | statusBar()->showMessage(text: tr(s: "File loaded" ), timeout: 2000); |
347 | } |
348 | |
349 | void MainWindow::setRecentFilesVisible(bool visible) |
350 | { |
351 | recentFileSubMenuAct->setVisible(visible); |
352 | recentFileSeparator->setVisible(visible); |
353 | } |
354 | |
355 | static inline QString recentFilesKey() { return QStringLiteral("recentFileList" ); } |
356 | static inline QString fileKey() { return QStringLiteral("file" ); } |
357 | |
358 | static QStringList readRecentFiles(QSettings &settings) |
359 | { |
360 | QStringList result; |
361 | const int count = settings.beginReadArray(prefix: recentFilesKey()); |
362 | for (int i = 0; i < count; ++i) { |
363 | settings.setArrayIndex(i); |
364 | result.append(t: settings.value(key: fileKey()).toString()); |
365 | } |
366 | settings.endArray(); |
367 | return result; |
368 | } |
369 | |
370 | static void writeRecentFiles(const QStringList &files, QSettings &settings) |
371 | { |
372 | const int count = files.size(); |
373 | settings.beginWriteArray(prefix: recentFilesKey()); |
374 | for (int i = 0; i < count; ++i) { |
375 | settings.setArrayIndex(i); |
376 | settings.setValue(key: fileKey(), value: files.at(i)); |
377 | } |
378 | settings.endArray(); |
379 | } |
380 | |
381 | bool MainWindow::hasRecentFiles() |
382 | { |
383 | QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName()); |
384 | const int count = settings.beginReadArray(prefix: recentFilesKey()); |
385 | settings.endArray(); |
386 | return count > 0; |
387 | } |
388 | |
389 | void MainWindow::prependToRecentFiles(const QString &fileName) |
390 | { |
391 | QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName()); |
392 | |
393 | const QStringList oldRecentFiles = readRecentFiles(settings); |
394 | QStringList recentFiles = oldRecentFiles; |
395 | recentFiles.removeAll(t: fileName); |
396 | recentFiles.prepend(t: fileName); |
397 | if (oldRecentFiles != recentFiles) |
398 | writeRecentFiles(files: recentFiles, settings); |
399 | |
400 | setRecentFilesVisible(!recentFiles.isEmpty()); |
401 | } |
402 | |
403 | void MainWindow::updateRecentFileActions() |
404 | { |
405 | QSettings settings(QCoreApplication::organizationName(), QCoreApplication::applicationName()); |
406 | |
407 | const QStringList recentFiles = readRecentFiles(settings); |
408 | const int count = qMin(a: int(MaxRecentFiles), b: recentFiles.size()); |
409 | int i = 0; |
410 | for ( ; i < count; ++i) { |
411 | const QString fileName = MainWindow::strippedName(fullFileName: recentFiles.at(i)); |
412 | recentFileActs[i]->setText(tr(s: "&%1 %2" ).arg(a: i + 1).arg(a: fileName)); |
413 | recentFileActs[i]->setData(recentFiles.at(i)); |
414 | recentFileActs[i]->setVisible(true); |
415 | } |
416 | for ( ; i < MaxRecentFiles; ++i) |
417 | recentFileActs[i]->setVisible(false); |
418 | } |
419 | |
420 | void MainWindow::openRecentFile() |
421 | { |
422 | if (const QAction *action = qobject_cast<const QAction *>(object: sender())) |
423 | openFile(fileName: action->data().toString()); |
424 | } |
425 | |
426 | bool MainWindow::saveFile(const QString &fileName) |
427 | { |
428 | QString errorMessage; |
429 | |
430 | QGuiApplication::setOverrideCursor(Qt::WaitCursor); |
431 | QSaveFile file(fileName); |
432 | if (file.open(flags: QFile::WriteOnly | QFile::Text)) { |
433 | QTextStream out(&file); |
434 | out << textEdit->toPlainText(); |
435 | if (!file.commit()) { |
436 | errorMessage = tr(s: "Cannot write file %1:\n%2." ) |
437 | .arg(args: QDir::toNativeSeparators(pathName: fileName), args: file.errorString()); |
438 | } |
439 | } else { |
440 | errorMessage = tr(s: "Cannot open file %1 for writing:\n%2." ) |
441 | .arg(args: QDir::toNativeSeparators(pathName: fileName), args: file.errorString()); |
442 | } |
443 | QGuiApplication::restoreOverrideCursor(); |
444 | |
445 | if (!errorMessage.isEmpty()) { |
446 | QMessageBox::warning(parent: this, title: tr(s: "SDI" ), text: errorMessage); |
447 | return false; |
448 | } |
449 | |
450 | setCurrentFile(fileName); |
451 | statusBar()->showMessage(text: tr(s: "File saved" ), timeout: 2000); |
452 | return true; |
453 | } |
454 | |
455 | void MainWindow::setCurrentFile(const QString &fileName) |
456 | { |
457 | static int sequenceNumber = 1; |
458 | |
459 | isUntitled = fileName.isEmpty(); |
460 | if (isUntitled) { |
461 | curFile = tr(s: "document%1.txt" ).arg(a: sequenceNumber++); |
462 | } else { |
463 | curFile = QFileInfo(fileName).canonicalFilePath(); |
464 | } |
465 | |
466 | textEdit->document()->setModified(false); |
467 | setWindowModified(false); |
468 | |
469 | if (!isUntitled && windowFilePath() != curFile) |
470 | MainWindow::prependToRecentFiles(fileName: curFile); |
471 | |
472 | setWindowFilePath(curFile); |
473 | } |
474 | |
475 | QString MainWindow::strippedName(const QString &fullFileName) |
476 | { |
477 | return QFileInfo(fullFileName).fileName(); |
478 | } |
479 | |
480 | MainWindow *MainWindow::findMainWindow(const QString &fileName) const |
481 | { |
482 | QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath(); |
483 | |
484 | const QList<QWidget *> topLevelWidgets = QApplication::topLevelWidgets(); |
485 | for (QWidget *widget : topLevelWidgets) { |
486 | MainWindow *mainWin = qobject_cast<MainWindow *>(object: widget); |
487 | if (mainWin && mainWin->curFile == canonicalFilePath) |
488 | return mainWin; |
489 | } |
490 | |
491 | return nullptr; |
492 | } |
493 | |