1#!/usr/bin/env python
2
3
4#############################################################################
5##
6## Copyright (C) 2013 Riverbank Computing Limited.
7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8## All rights reserved.
9##
10## This file is part of the examples of PyQt.
11##
12## $QT_BEGIN_LICENSE:BSD$
13## You may use this file under the terms of the BSD license as follows:
14##
15## "Redistribution and use in source and binary forms, with or without
16## modification, are permitted provided that the following conditions are
17## met:
18##   * Redistributions of source code must retain the above copyright
19##     notice, this list of conditions and the following disclaimer.
20##   * Redistributions in binary form must reproduce the above copyright
21##     notice, this list of conditions and the following disclaimer in
22##     the documentation and/or other materials provided with the
23##     distribution.
24##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
25##     the names of its contributors may be used to endorse or promote
26##     products derived from this software without specific prior written
27##     permission.
28##
29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
40## $QT_END_LICENSE$
41##
42#############################################################################
43
44
45from PyQt5.QtCore import (QFile, QFileInfo, QPoint, QSettings, QSignalMapper,
46        QSize, QTextStream, Qt)
47from PyQt5.QtGui import QIcon, QKeySequence
48from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QMainWindow,
49        QMdiArea, QMessageBox, QTextEdit, QWidget)
50
51import mdi_rc
52
53
54class MdiChild(QTextEdit):
55    sequenceNumber = 1
56
57    def __init__(self):
58        super(MdiChild, self).__init__()
59
60        self.setAttribute(Qt.WA_DeleteOnClose)
61        self.isUntitled = True
62
63    def newFile(self):
64        self.isUntitled = True
65        self.curFile = "document%d.txt" % MdiChild.sequenceNumber
66        MdiChild.sequenceNumber += 1
67        self.setWindowTitle(self.curFile + '[*]')
68
69        self.document().contentsChanged.connect(self.documentWasModified)
70
71    def loadFile(self, fileName):
72        file = QFile(fileName)
73        if not file.open(QFile.ReadOnly | QFile.Text):
74            QMessageBox.warning(self, "MDI",
75                    "Cannot read file %s:\n%s." % (fileName, file.errorString()))
76            return False
77
78        instr = QTextStream(file)
79        QApplication.setOverrideCursor(Qt.WaitCursor)
80        self.setPlainText(instr.readAll())
81        QApplication.restoreOverrideCursor()
82
83        self.setCurrentFile(fileName)
84
85        self.document().contentsChanged.connect(self.documentWasModified)
86
87        return True
88
89    def save(self):
90        if self.isUntitled:
91            return self.saveAs()
92        else:
93            return self.saveFile(self.curFile)
94
95    def saveAs(self):
96        fileName, _ = QFileDialog.getSaveFileName(self, "Save As", self.curFile)
97        if not fileName:
98            return False
99
100        return self.saveFile(fileName)
101
102    def saveFile(self, fileName):
103        file = QFile(fileName)
104
105        if not file.open(QFile.WriteOnly | QFile.Text):
106            QMessageBox.warning(self, "MDI",
107                    "Cannot write file %s:\n%s." % (fileName, file.errorString()))
108            return False
109
110        outstr = QTextStream(file)
111        QApplication.setOverrideCursor(Qt.WaitCursor)
112        outstr << self.toPlainText()
113        QApplication.restoreOverrideCursor()
114
115        self.setCurrentFile(fileName)
116        return True
117
118    def userFriendlyCurrentFile(self):
119        return self.strippedName(self.curFile)
120
121    def currentFile(self):
122        return self.curFile
123
124    def closeEvent(self, event):
125        if self.maybeSave():
126            event.accept()
127        else:
128            event.ignore()
129
130    def documentWasModified(self):
131        self.setWindowModified(self.document().isModified())
132
133    def maybeSave(self):
134        if self.document().isModified():
135            ret = QMessageBox.warning(self, "MDI",
136                    "'%s' has been modified.\nDo you want to save your "
137                    "changes?" % self.userFriendlyCurrentFile(),
138                    QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
139
140            if ret == QMessageBox.Save:
141                return self.save()
142
143            if ret == QMessageBox.Cancel:
144                return False
145
146        return True
147
148    def setCurrentFile(self, fileName):
149        self.curFile = QFileInfo(fileName).canonicalFilePath()
150        self.isUntitled = False
151        self.document().setModified(False)
152        self.setWindowModified(False)
153        self.setWindowTitle(self.userFriendlyCurrentFile() + "[*]")
154
155    def strippedName(self, fullFileName):
156        return QFileInfo(fullFileName).fileName()
157
158
159class MainWindow(QMainWindow):
160    def __init__(self):
161        super(MainWindow, self).__init__()
162
163        self.mdiArea = QMdiArea()
164        self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
165        self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
166        self.setCentralWidget(self.mdiArea)
167
168        self.mdiArea.subWindowActivated.connect(self.updateMenus)
169        self.windowMapper = QSignalMapper(self)
170        self.windowMapper.mapped[QWidget].connect(self.setActiveSubWindow)
171
172        self.createActions()
173        self.createMenus()
174        self.createToolBars()
175        self.createStatusBar()
176        self.updateMenus()
177
178        self.readSettings()
179
180        self.setWindowTitle("MDI")
181
182    def closeEvent(self, event):
183        self.mdiArea.closeAllSubWindows()
184        if self.mdiArea.currentSubWindow():
185            event.ignore()
186        else:
187            self.writeSettings()
188            event.accept()
189
190    def newFile(self):
191        child = self.createMdiChild()
192        child.newFile()
193        child.show()
194
195    def open(self):
196        fileName, _ = QFileDialog.getOpenFileName(self)
197        if fileName:
198            existing = self.findMdiChild(fileName)
199            if existing:
200                self.mdiArea.setActiveSubWindow(existing)
201                return
202
203            child = self.createMdiChild()
204            if child.loadFile(fileName):
205                self.statusBar().showMessage("File loaded", 2000)
206                child.show()
207            else:
208                child.close()
209
210    def save(self):
211        if self.activeMdiChild() and self.activeMdiChild().save():
212            self.statusBar().showMessage("File saved", 2000)
213
214    def saveAs(self):
215        if self.activeMdiChild() and self.activeMdiChild().saveAs():
216            self.statusBar().showMessage("File saved", 2000)
217
218    def cut(self):
219        if self.activeMdiChild():
220            self.activeMdiChild().cut()
221
222    def copy(self):
223        if self.activeMdiChild():
224            self.activeMdiChild().copy()
225
226    def paste(self):
227        if self.activeMdiChild():
228            self.activeMdiChild().paste()
229
230    def about(self):
231        QMessageBox.about(self, "About MDI",
232                "The <b>MDI</b> example demonstrates how to write multiple "
233                "document interface applications using Qt.")
234
235    def updateMenus(self):
236        hasMdiChild = (self.activeMdiChild() is not None)
237        self.saveAct.setEnabled(hasMdiChild)
238        self.saveAsAct.setEnabled(hasMdiChild)
239        self.pasteAct.setEnabled(hasMdiChild)
240        self.closeAct.setEnabled(hasMdiChild)
241        self.closeAllAct.setEnabled(hasMdiChild)
242        self.tileAct.setEnabled(hasMdiChild)
243        self.cascadeAct.setEnabled(hasMdiChild)
244        self.nextAct.setEnabled(hasMdiChild)
245        self.previousAct.setEnabled(hasMdiChild)
246        self.separatorAct.setVisible(hasMdiChild)
247
248        hasSelection = (self.activeMdiChild() is not None and
249                        self.activeMdiChild().textCursor().hasSelection())
250        self.cutAct.setEnabled(hasSelection)
251        self.copyAct.setEnabled(hasSelection)
252
253    def updateWindowMenu(self):
254        self.windowMenu.clear()
255        self.windowMenu.addAction(self.closeAct)
256        self.windowMenu.addAction(self.closeAllAct)
257        self.windowMenu.addSeparator()
258        self.windowMenu.addAction(self.tileAct)
259        self.windowMenu.addAction(self.cascadeAct)
260        self.windowMenu.addSeparator()
261        self.windowMenu.addAction(self.nextAct)
262        self.windowMenu.addAction(self.previousAct)
263        self.windowMenu.addAction(self.separatorAct)
264
265        windows = self.mdiArea.subWindowList()
266        self.separatorAct.setVisible(len(windows) != 0)
267
268        for i, window in enumerate(windows):
269            child = window.widget()
270
271            text = "%d %s" % (i + 1, child.userFriendlyCurrentFile())
272            if i < 9:
273                text = '&' + text
274
275            action = self.windowMenu.addAction(text)
276            action.setCheckable(True)
277            action.setChecked(child is self.activeMdiChild())
278            action.triggered.connect(self.windowMapper.map)
279            self.windowMapper.setMapping(action, window)
280
281    def createMdiChild(self):
282        child = MdiChild()
283        self.mdiArea.addSubWindow(child)
284
285        child.copyAvailable.connect(self.cutAct.setEnabled)
286        child.copyAvailable.connect(self.copyAct.setEnabled)
287
288        return child
289
290    def createActions(self):
291        self.newAct = QAction(QIcon(':/images/new.png'), "&New", self,
292                shortcut=QKeySequence.New, statusTip="Create a new file",
293                triggered=self.newFile)
294
295        self.openAct = QAction(QIcon(':/images/open.png'), "&Open...", self,
296                shortcut=QKeySequence.Open, statusTip="Open an existing file",
297                triggered=self.open)
298
299        self.saveAct = QAction(QIcon(':/images/save.png'), "&Save", self,
300                shortcut=QKeySequence.Save,
301                statusTip="Save the document to disk", triggered=self.save)
302
303        self.saveAsAct = QAction("Save &As...", self,
304                shortcut=QKeySequence.SaveAs,
305                statusTip="Save the document under a new name",
306                triggered=self.saveAs)
307
308        self.exitAct = QAction("E&xit", self, shortcut=QKeySequence.Quit,
309                statusTip="Exit the application",
310                triggered=QApplication.instance().closeAllWindows)
311
312        self.cutAct = QAction(QIcon(':/images/cut.png'), "Cu&t", self,
313                shortcut=QKeySequence.Cut,
314                statusTip="Cut the current selection's contents to the clipboard",
315                triggered=self.cut)
316
317        self.copyAct = QAction(QIcon(':/images/copy.png'), "&Copy", self,
318                shortcut=QKeySequence.Copy,
319                statusTip="Copy the current selection's contents to the clipboard",
320                triggered=self.copy)
321
322        self.pasteAct = QAction(QIcon(':/images/paste.png'), "&Paste", self,
323                shortcut=QKeySequence.Paste,
324                statusTip="Paste the clipboard's contents into the current selection",
325                triggered=self.paste)
326
327        self.closeAct = QAction("Cl&ose", self,
328                statusTip="Close the active window",
329                triggered=self.mdiArea.closeActiveSubWindow)
330
331        self.closeAllAct = QAction("Close &All", self,
332                statusTip="Close all the windows",
333                triggered=self.mdiArea.closeAllSubWindows)
334
335        self.tileAct = QAction("&Tile", self, statusTip="Tile the windows",
336                triggered=self.mdiArea.tileSubWindows)
337
338        self.cascadeAct = QAction("&Cascade", self,
339                statusTip="Cascade the windows",
340                triggered=self.mdiArea.cascadeSubWindows)
341
342        self.nextAct = QAction("Ne&xt", self, shortcut=QKeySequence.NextChild,
343                statusTip="Move the focus to the next window",
344                triggered=self.mdiArea.activateNextSubWindow)
345
346        self.previousAct = QAction("Pre&vious", self,
347                shortcut=QKeySequence.PreviousChild,
348                statusTip="Move the focus to the previous window",
349                triggered=self.mdiArea.activatePreviousSubWindow)
350
351        self.separatorAct = QAction(self)
352        self.separatorAct.setSeparator(True)
353
354        self.aboutAct = QAction("&About", self,
355                statusTip="Show the application's About box",
356                triggered=self.about)
357
358        self.aboutQtAct = QAction("About &Qt", self,
359                statusTip="Show the Qt library's About box",
360                triggered=QApplication.instance().aboutQt)
361
362    def createMenus(self):
363        self.fileMenu = self.menuBar().addMenu("&File")
364        self.fileMenu.addAction(self.newAct)
365        self.fileMenu.addAction(self.openAct)
366        self.fileMenu.addAction(self.saveAct)
367        self.fileMenu.addAction(self.saveAsAct)
368        self.fileMenu.addSeparator()
369        action = self.fileMenu.addAction("Switch layout direction")
370        action.triggered.connect(self.switchLayoutDirection)
371        self.fileMenu.addAction(self.exitAct)
372
373        self.editMenu = self.menuBar().addMenu("&Edit")
374        self.editMenu.addAction(self.cutAct)
375        self.editMenu.addAction(self.copyAct)
376        self.editMenu.addAction(self.pasteAct)
377
378        self.windowMenu = self.menuBar().addMenu("&Window")
379        self.updateWindowMenu()
380        self.windowMenu.aboutToShow.connect(self.updateWindowMenu)
381
382        self.menuBar().addSeparator()
383
384        self.helpMenu = self.menuBar().addMenu("&Help")
385        self.helpMenu.addAction(self.aboutAct)
386        self.helpMenu.addAction(self.aboutQtAct)
387
388    def createToolBars(self):
389        self.fileToolBar = self.addToolBar("File")
390        self.fileToolBar.addAction(self.newAct)
391        self.fileToolBar.addAction(self.openAct)
392        self.fileToolBar.addAction(self.saveAct)
393
394        self.editToolBar = self.addToolBar("Edit")
395        self.editToolBar.addAction(self.cutAct)
396        self.editToolBar.addAction(self.copyAct)
397        self.editToolBar.addAction(self.pasteAct)
398
399    def createStatusBar(self):
400        self.statusBar().showMessage("Ready")
401
402    def readSettings(self):
403        settings = QSettings('Trolltech', 'MDI Example')
404        pos = settings.value('pos', QPoint(200, 200))
405        size = settings.value('size', QSize(400, 400))
406        self.move(pos)
407        self.resize(size)
408
409    def writeSettings(self):
410        settings = QSettings('Trolltech', 'MDI Example')
411        settings.setValue('pos', self.pos())
412        settings.setValue('size', self.size())
413
414    def activeMdiChild(self):
415        activeSubWindow = self.mdiArea.activeSubWindow()
416        if activeSubWindow:
417            return activeSubWindow.widget()
418        return None
419
420    def findMdiChild(self, fileName):
421        canonicalFilePath = QFileInfo(fileName).canonicalFilePath()
422
423        for window in self.mdiArea.subWindowList():
424            if window.widget().currentFile() == canonicalFilePath:
425                return window
426        return None
427
428    def switchLayoutDirection(self):
429        if self.layoutDirection() == Qt.LeftToRight:
430            QApplication.setLayoutDirection(Qt.RightToLeft)
431        else:
432            QApplication.setLayoutDirection(Qt.LeftToRight)
433
434    def setActiveSubWindow(self, window):
435        if window:
436            self.mdiArea.setActiveSubWindow(window)
437
438
439if __name__ == '__main__':
440
441    import sys
442
443    app = QApplication(sys.argv)
444    mainWin = MainWindow()
445    mainWin.show()
446    sys.exit(app.exec_())
447