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