mainwindow.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  1. # -*- coding: utf-8 -*-
  2. #
  3. # AWL simulator - GUI main window
  4. #
  5. # Copyright 2012-2022 Michael Buesch <m@bues.ch>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. from __future__ import division, absolute_import, print_function, unicode_literals
  22. #from awlsim.common.cython_support cimport * #@cy
  23. from awlsim.common.compat import *
  24. import sys
  25. import os
  26. from awlsim.gui.util import *
  27. from awlsim.gui.editmdiarea import *
  28. from awlsim.gui.projecttreewidget import *
  29. from awlsim.gui.cpuwidget import *
  30. from awlsim.gui.guiconfig import *
  31. from awlsim.gui.cpuconfig import *
  32. from awlsim.gui.linkconfig import *
  33. from awlsim.gui.hwmodconfig import *
  34. from awlsim.gui.icons import *
  35. from awlsim.gui.templatedialog import *
  36. from awlsim.gui.library import *
  37. from awlsim.gui.runstate import *
  38. from awlsim.gui.toolbars import *
  39. from awlsim.gui.validatorsched import *
  40. class LoadProgressDialog(QDialog):
  41. def __init__(self, parent=None):
  42. QDialog.__init__(self, parent)
  43. self.setLayout(QGridLayout())
  44. self.setContentsMargins(30, 30, 30, 30)
  45. self.setWindowModality(Qt.ApplicationModal)
  46. self.__isShown = False
  47. self.__icon = QLabel(self)
  48. self.layout().addWidget(self.__icon, 0, 0)
  49. self.layout().setColumnMinimumWidth(1, 60)
  50. self.__text = QLabel(self)
  51. self.layout().addWidget(self.__text, 0, 2)
  52. def closeEvent(self, ev):
  53. if self.__isShown:
  54. ev.ignore()
  55. return
  56. QDialog.closeEvent(self, ev)
  57. def showProgress(self):
  58. self.__isShown = True
  59. self.show()
  60. QApplication.processEvents(QEventLoop.ExcludeUserInputEvents, 50)
  61. def hideProgress(self):
  62. self.__isShown = False
  63. self.hide()
  64. QApplication.processEvents(QEventLoop.ExcludeUserInputEvents, 50)
  65. def setGuiRunState(self, guiRunState):
  66. if guiRunState.state == GuiRunState.STATE_LOAD:
  67. self.setWindowTitle("Awlsim - Downloading...")
  68. self.__icon.setPixmap(getIcon("download").pixmap(64, 64))
  69. self.__text.setText("Downloading project to CPU.\n\n"
  70. "Please be patient.\n"
  71. "This might take a few seconds.")
  72. self.showProgress()
  73. else:
  74. self.hideProgress()
  75. class CpuDockWidget(QDockWidget):
  76. def __init__(self, mainWidget, parent=None):
  77. QDockWidget.__init__(self, "", parent)
  78. self.mainWidget = mainWidget
  79. self.setObjectName("CpuDockWidget")
  80. self.toggleViewAction().setIcon(getIcon("cpu"))
  81. self.setFeatures(QDockWidget.DockWidgetMovable |
  82. QDockWidget.DockWidgetFloatable |
  83. QDockWidget.DockWidgetClosable)
  84. self.setAllowedAreas(Qt.AllDockWidgetAreas)
  85. self.setWidget(CpuWidget(mainWidget))
  86. self.topLevelChanged.connect(self.__handleTopLevelChange)
  87. self.__handleTopLevelChange(self.isFloating())
  88. @property
  89. def cpuWidget(self):
  90. return self.widget()
  91. def __handleTopLevelChange(self, floating):
  92. prefix = ""
  93. if floating:
  94. prefix = "%s - " % self.mainWidget.mainWindow.TITLE
  95. self.setWindowTitle("%sCPU view" % prefix)
  96. class ProjectTreeDockWidget(QDockWidget):
  97. def __init__(self, mainWidget, parent=None):
  98. QDockWidget.__init__(self, "", parent)
  99. self.mainWidget = mainWidget
  100. self.setObjectName("ProjectTreeDockWidget")
  101. self.toggleViewAction().setIcon(getIcon("doc_edit"))
  102. self.setFeatures(QDockWidget.DockWidgetMovable |
  103. QDockWidget.DockWidgetFloatable)
  104. self.setAllowedAreas(Qt.AllDockWidgetAreas)
  105. self.projectTreeModel = ProjectTreeModel(mainWidget=mainWidget)
  106. self.projectTreeView = ProjectTreeView(model=self.projectTreeModel,
  107. parent=self)
  108. self.setWidget(self.projectTreeView)
  109. self.topLevelChanged.connect(self.__handleTopLevelChange)
  110. self.__handleTopLevelChange(self.isFloating())
  111. def __handleTopLevelChange(self, floating):
  112. prefix = ""
  113. if floating:
  114. prefix = "%s - " % self.mainWidget.mainWindow.TITLE
  115. self.setWindowTitle("%sProject" % prefix)
  116. class MainWidget(QWidget):
  117. # Signal: Project loaded
  118. projectLoaded = Signal(Project)
  119. # Signal: Dirty-status changed
  120. dirtyChanged = Signal(int)
  121. # Signal: Source text focus changed
  122. textFocusChanged = Signal(bool)
  123. # Signal: UndoAvailable state changed
  124. undoAvailableChanged = Signal(bool)
  125. # Signal: RedoAvailable state changed
  126. redoAvailableChanged = Signal(bool)
  127. # Signal: CopyAvailable state changed
  128. copyAvailableChanged = Signal(bool)
  129. # Signal: CutAvailable state changed
  130. cutAvailableChanged = Signal(bool)
  131. # Signal: PasteAvailable state changed
  132. pasteAvailableChanged = Signal(bool)
  133. # Document dirty levels
  134. EnumGen.start
  135. DIRTY_NO = EnumGen.item
  136. DIRTY_SLIGHT = EnumGen.item
  137. DIRTY_FULL = EnumGen.item
  138. EnumGen.end
  139. def __init__(self, mainWindow, parent=None):
  140. QWidget.__init__(self, parent)
  141. self.setLayout(QGridLayout(self))
  142. self.mainWindow = mainWindow
  143. self.simClient = GuiAwlSimClient(mainWindow)
  144. self.editMdiArea = EditMdiArea(self)
  145. self.layout().addWidget(self.editMdiArea, 0, 0)
  146. self.filename = None
  147. self.__dirtyLevel = self.DIRTY_NO
  148. self.__guiRunState = GuiRunState()
  149. self.__insnPerSecond = 0.0
  150. self.__avgCycleTime = 0.0
  151. self.__minCycleTime = 0.0
  152. self.__maxCycleTime = 0.0
  153. self.__padCycleTime = 0.0
  154. self.__updateStatusBar()
  155. self.editMdiArea.sourceChanged.connect(self.somethingChanged)
  156. self.editMdiArea.focusChanged.connect(self.textFocusChanged)
  157. self.editMdiArea.undoAvailableChanged.connect(self.undoAvailableChanged)
  158. self.editMdiArea.redoAvailableChanged.connect(self.redoAvailableChanged)
  159. self.editMdiArea.copyAvailableChanged.connect(self.copyAvailableChanged)
  160. self.editMdiArea.cutAvailableChanged.connect(self.cutAvailableChanged)
  161. self.editMdiArea.pasteAvailableChanged.connect(self.pasteAvailableChanged)
  162. @property
  163. def projectTreeModel(self):
  164. return self.mainWindow.projectTreeModel
  165. def getProject(self):
  166. return self.projectTreeModel.getProject()
  167. def isDirty(self):
  168. return self.__dirtyLevel == self.DIRTY_FULL
  169. def setDirty(self, dirtyLevel=DIRTY_FULL, force=False):
  170. if dirtyLevel != self.__dirtyLevel or force:
  171. if (not force and
  172. self.__dirtyLevel == self.DIRTY_FULL and
  173. dirtyLevel == self.DIRTY_SLIGHT):
  174. # Cannot go from full to slight.
  175. return
  176. self.__dirtyLevel = dirtyLevel
  177. self.dirtyChanged.emit(self.__dirtyLevel)
  178. def somethingChanged(self):
  179. self.setDirty(self.DIRTY_FULL)
  180. def getFilename(self):
  181. return self.filename
  182. def getSimClient(self):
  183. return self.simClient
  184. def getCpuWidget(self):
  185. return self.mainWindow.cpuWidget
  186. def __updateCpuViewConfig(self):
  187. """Update the GuiCpuStateViewSettings objects in the project.
  188. """
  189. settingsList = []
  190. stateMdiArea = self.getCpuWidget().stateMdi
  191. settingsList.append(stateMdiArea.getSettings())
  192. self.getProject().getGuiSettings().setCpuStateViewSettingsList(settingsList)
  193. def newFile(self, filename=None):
  194. if isWinStandalone:
  195. executableName = "awlsim-gui.exe"
  196. else:
  197. executableName = sys.executable
  198. executable = findExecutable(executableName)
  199. if not executable:
  200. QMessageBox.critical(self,
  201. "Failed to find '%s'" % executableName,
  202. "Could not spawn a new instance.\n"
  203. "Failed to find '%s'" % executableName)
  204. return
  205. if isWinStandalone:
  206. argv = [ executable, ]
  207. else:
  208. argv = [ executable, "-m", "awlsim.gui.startup", ]
  209. if filename:
  210. argv.append(filename)
  211. try:
  212. PopenWrapper(argv, env=AwlSimEnv.getEnv())
  213. except OSError as e:
  214. QMessageBox.critical(self,
  215. "Failed to execute '%s'" % executableName,
  216. "Could not spawn a new instance.\n%s"
  217. "Failed to execute '%s'" % (
  218. str(e), executableName))
  219. return
  220. def loadFile(self, filename, newIfNotExist=False):
  221. if self.isDirty():
  222. res = QMessageBox.question(self,
  223. "Unsaved project",
  224. "The current project is modified and contains unsaved changes.\n "
  225. "Do you want to:\n"
  226. "- Save the project, close it and open the new project\n"
  227. "- Open the new project in a new instance or\n"
  228. "- Discard the changes and open the new project\n"
  229. "- Cancel the operation",
  230. QMessageBox.Save | QMessageBox.Discard |\
  231. QMessageBox.Open | QMessageBox.Cancel,
  232. QMessageBox.Open)
  233. if res == QMessageBox.Save:
  234. if not self.save():
  235. return
  236. elif res == QMessageBox.Discard:
  237. pass
  238. elif res == QMessageBox.Open:
  239. self.newFile(filename)
  240. return
  241. elif res == QMessageBox.Cancel:
  242. return
  243. else:
  244. assert(0)
  245. self.getSimClient().action_goOffline()
  246. self.getCpuWidget().stateMdi.reset()
  247. if not os.path.exists(filename) and newIfNotExist:
  248. # The file does not exist. We implicitly create it.
  249. # The actual file will be created when the project is saved.
  250. isNewProject = True
  251. self.editMdiArea.resetArea()
  252. self.projectTreeModel.reset()
  253. else:
  254. isNewProject = False
  255. try:
  256. self.projectTreeModel.loadProjectFile(filename, self)
  257. guiSettings = self.getProject().getGuiSettings()
  258. for viewSettings in guiSettings.getCpuStateViewSettingsList():
  259. # We currently only have one CPU state view.
  260. # If the project has multiple viewSettings, all but the
  261. # last one will be lost.
  262. self.getCpuWidget().stateMdi.loadFromCpuStateViewSettings(viewSettings)
  263. except AwlSimError as e:
  264. QMessageBox.critical(self,
  265. "Failed to load project file", str(e))
  266. return False
  267. self.filename = filename
  268. if isNewProject or not self.getProject().getProjectFile():
  269. self.setDirty(self.DIRTY_FULL, force=True)
  270. else:
  271. self.setDirty(self.DIRTY_NO, force=True)
  272. self.projectLoaded.emit(self.getProject())
  273. return True
  274. def load(self):
  275. fn, fil = QFileDialog.getOpenFileName(self,
  276. "Open project", "",
  277. "Awlsim project or AWL/STL source (*.awlpro *.awl);;"
  278. "Awlsim project (*.awlpro);;"
  279. "AWL source (*.awl);;"
  280. "All files (*)")
  281. if not fn:
  282. return
  283. self.loadFile(fn, newIfNotExist=False)
  284. def saveFile(self, filename):
  285. try:
  286. self.__updateCpuViewConfig()
  287. res = self.projectTreeModel.saveProjectFile(filename, self)
  288. if res == 0: # Failure
  289. return False
  290. elif res < 0: # Force save-as
  291. return self.save(newFile=True)
  292. except AwlSimError as e:
  293. QMessageBox.critical(self,
  294. "Failed to write project file", str(e))
  295. return False
  296. self.filename = filename
  297. self.setDirty(self.DIRTY_NO, force=True)
  298. return True
  299. def save(self, newFile=False):
  300. if newFile or not self.filename:
  301. fn, fil = QFileDialog.getSaveFileName(self,
  302. "Awlsim project save as", "",
  303. "Awlsim project (*.awlpro)",
  304. "*.awlpro")
  305. if not fn:
  306. return
  307. if not fn.endswith(".awlpro"):
  308. fn += ".awlpro"
  309. return self.saveFile(fn)
  310. else:
  311. return self.saveFile(self.filename)
  312. def guiConfig(self):
  313. dlg = GuiConfigDialog(self.getProject(), self)
  314. dlg.settingsChanged.connect(self.somethingChanged)
  315. if dlg.exec_() == dlg.Accepted:
  316. self.editMdiArea.setGuiSettings(self.getProject().getGuiSettings())
  317. def linkConfig(self):
  318. dlg = LinkConfigDialog(self.getProject(), self)
  319. dlg.settingsChanged.connect(self.somethingChanged)
  320. dlg.exec_()
  321. def cpuConfig(self):
  322. dlg = CpuConfigDialog(self.getProject(), self)
  323. dlg.settingsChanged.connect(self.somethingChanged)
  324. dlg.exec_()
  325. def hwmodConfig(self):
  326. dlg = HwmodConfigDialog(self.getProject(), self)
  327. dlg.settingsChanged.connect(self.somethingChanged)
  328. dlg.exec_()
  329. def __pasteAwlText(self, text):
  330. if not self.editMdiArea.paste(text):
  331. QMessageBox.information(self,
  332. "Please select AWL/STL source",
  333. "Can not paste text.\n\n"
  334. "Please move the text cursor to the place "
  335. "in the AWL/STL code where you want to paste to.")
  336. return False
  337. return True
  338. def __pasteSymbol(self, symbolName, address, dataType, comment):
  339. """Paste a symbol into one of the available symbol tables.
  340. symbolName: Symbol name string.
  341. address: Symbol address string.
  342. dataType: Symbol type string.
  343. comment: Symbol comment string.
  344. Returns True, if the symbol has successfully been added.
  345. """
  346. # Parse the symbol.
  347. try:
  348. project = self.getProject()
  349. p = SymTabParser(project.getCpuConf().getConfiguredMnemonics())
  350. symbol = p.parseSym(symbolName, address,
  351. dataType, comment, 0)
  352. except AwlSimError as e:
  353. MessageBox.handleAwlSimError(self,
  354. "Library symbol error", e)
  355. return False
  356. # Try to add the symbol to a symbol table
  357. return self.projectTreeModel.symbolAdd(symbol, parentWidget=self)
  358. def __pasteLibSel(self, libSelection):
  359. """Paste a library selection into the library selection table.
  360. libSelection: The AwlLibEntrySelection() instance.
  361. Returns True, if the selection has successfully been added.
  362. """
  363. return self.projectTreeModel.libSelectionAdd(libSelection,
  364. parentWidget=self)
  365. def insertOB(self):
  366. dlg = TemplateDialog.make_OB(self)
  367. def dialogFinished(result):
  368. if result != QDialog.Accepted:
  369. return
  370. self.__pasteAwlText(Templates.getOB(dlg.getBlockNumber(),
  371. dlg.getVerbose()))
  372. dlg.finished.connect(dialogFinished)
  373. dlg.show()
  374. def insertFC(self):
  375. dlg = TemplateDialog.make_FC(self)
  376. def dialogFinished(result):
  377. if result != QDialog.Accepted:
  378. return
  379. self.__pasteAwlText(Templates.getFC(dlg.getBlockNumber(),
  380. dlg.getVerbose()))
  381. dlg.finished.connect(dialogFinished)
  382. dlg.show()
  383. def insertFB(self):
  384. dlg = TemplateDialog.make_FB(self)
  385. def dialogFinished(result):
  386. if result != QDialog.Accepted:
  387. return
  388. self.__pasteAwlText(Templates.getFB(dlg.getBlockNumber(),
  389. dlg.getVerbose()))
  390. dlg.finished.connect(dialogFinished)
  391. dlg.show()
  392. def insertInstanceDB(self):
  393. dlg = TemplateDialog.make_instanceDB(self)
  394. def dialogFinished(result):
  395. if result != QDialog.Accepted:
  396. return
  397. self.__pasteAwlText(Templates.getInstanceDB(dlg.getBlockNumber(),
  398. dlg.getExtraNumber(),
  399. dlg.getVerbose()))
  400. dlg.finished.connect(dialogFinished)
  401. dlg.show()
  402. def insertGlobalDB(self):
  403. dlg = TemplateDialog.make_globalDB(self)
  404. def dialogFinished(result):
  405. if result != QDialog.Accepted:
  406. return
  407. self.__pasteAwlText(Templates.getGlobalDB(dlg.getBlockNumber(),
  408. dlg.getVerbose()))
  409. dlg.finished.connect(dialogFinished)
  410. dlg.show()
  411. def insertUDT(self):
  412. dlg = TemplateDialog.make_UDT(self)
  413. def dialogFinished(result):
  414. if result != QDialog.Accepted:
  415. return
  416. self.__pasteAwlText(Templates.getUDT(dlg.getBlockNumber(),
  417. dlg.getVerbose()))
  418. dlg.finished.connect(dialogFinished)
  419. dlg.show()
  420. def insertFCcall(self):
  421. dlg = TemplateDialog.make_FCcall(self)
  422. def dialogFinished(result):
  423. if result != QDialog.Accepted:
  424. return
  425. self.__pasteAwlText(Templates.getFCcall(dlg.getBlockNumber(),
  426. dlg.getVerbose()))
  427. dlg.finished.connect(dialogFinished)
  428. dlg.show()
  429. def insertFBcall(self):
  430. dlg = TemplateDialog.make_FBcall(self)
  431. def dialogFinished(result):
  432. if result != QDialog.Accepted:
  433. return
  434. self.__pasteAwlText(Templates.getFBcall(dlg.getBlockNumber(),
  435. dlg.getExtraNumber(),
  436. dlg.getVerbose()))
  437. dlg.finished.connect(dialogFinished)
  438. dlg.show()
  439. def openLibrary(self):
  440. dlg = LibraryDialog(self.getProject(), self)
  441. def dialogFinished(result):
  442. if result != QDialog.Accepted:
  443. return
  444. if dlg.pasteText:
  445. # Paste the code.
  446. if not self.__pasteAwlText(dlg.pasteText):
  447. return
  448. if dlg.pasteSymbol:
  449. # Add a symbol to a symbol table.
  450. symbolName, address, dataType, comment = dlg.pasteSymbol
  451. if not self.__pasteSymbol(symbolName, address,
  452. dataType, comment):
  453. return
  454. if dlg.pasteLibSel:
  455. # Add a library selection to the library table.
  456. if not self.__pasteLibSel(dlg.pasteLibSel):
  457. return
  458. dlg.finished.connect(dialogFinished)
  459. dlg.show()
  460. def undo(self):
  461. self.editMdiArea.undo()
  462. def redo(self):
  463. self.editMdiArea.redo()
  464. def cut(self):
  465. self.editMdiArea.cut()
  466. def copy(self):
  467. self.editMdiArea.copy()
  468. def paste(self):
  469. self.editMdiArea.paste()
  470. def findText(self):
  471. self.editMdiArea.findText()
  472. def findReplaceText(self):
  473. self.editMdiArea.findReplaceText()
  474. def openByIdentHash(self, identHash):
  475. projectTreeModel = self.projectTreeModel
  476. index = projectTreeModel.identHashToIndex(identHash)
  477. if index.isValid():
  478. return projectTreeModel.entryActivate(index, parentWidget=self)
  479. return False
  480. def handleGuiRunStateChange(self, guiRunState):
  481. """CPU RunState changed.
  482. """
  483. self.__guiRunState = guiRunState
  484. self.__updateStatusBar()
  485. self.mainWindow.loadProgressDialog.setGuiRunState(guiRunState)
  486. def handleCpuStats(self, statsMsg):
  487. """Received new AwlSimMessage_CPUSTATS.
  488. """
  489. self.__insnPerSecond = statsMsg.insnPerSecond
  490. self.__avgCycleTime = statsMsg.avgCycleTime
  491. self.__minCycleTime = statsMsg.minCycleTime
  492. self.__maxCycleTime = statsMsg.maxCycleTime
  493. self.__padCycleTime = statsMsg.padCycleTime
  494. self.__updateStatusBar()
  495. def __updateStatusBar(self):
  496. """Update the main window status bar.
  497. """
  498. status = []
  499. if self.__guiRunState == GuiRunState.STATE_OFFLINE:
  500. status.append("CPU: offline")
  501. elif self.__guiRunState == GuiRunState.STATE_ONLINE:
  502. status.append("CPU: online / STOP")
  503. elif self.__guiRunState == GuiRunState.STATE_LOAD:
  504. status.append("CPU: loading")
  505. elif self.__guiRunState == GuiRunState.STATE_RUN:
  506. status.append("CPU: RUN")
  507. elif self.__guiRunState == GuiRunState.STATE_EXCEPTION:
  508. status.append("CPU: EXCEPTION")
  509. if self.__guiRunState == GuiRunState.STATE_RUN:
  510. if self.__insnPerSecond > 0.0:
  511. usPerInsnStr = "%.02f" % ((1.0 / self.__insnPerSecond) * 1000000.0)
  512. status.append("%s stmt/s (%s µs/stmt)" % (
  513. floatToHumanReadable(self.__insnPerSecond),
  514. usPerInsnStr))
  515. if (self.__avgCycleTime > 0.0 and
  516. self.__minCycleTime > 0.0 and
  517. self.__maxCycleTime > 0.0):
  518. avgCycleTimeStr = "%.01f" % (self.__avgCycleTime * 1000.0)
  519. minCycleTimeStr = "%.01f" % (self.__minCycleTime * 1000.0)
  520. maxCycleTimeStr = "%.01f" % (self.__maxCycleTime * 1000.0)
  521. padCycleTimeStr = "%.01f" % (self.__padCycleTime * 1000.0)
  522. status.append("OB1: avg: %s ms min: %s ms max: %s ms padding: %s ms" % (
  523. avgCycleTimeStr,
  524. minCycleTimeStr,
  525. maxCycleTimeStr,
  526. padCycleTimeStr))
  527. statusBar = self.mainWindow.statusBar()
  528. statusBar.showMessage(" -- ".join(status))
  529. class MainWindow(QMainWindow):
  530. TITLE = "Awlsim PLC v%s" % VERSION_STRING
  531. @classmethod
  532. def start(cls,
  533. initialAwlSource = None):
  534. # Set basic qapp-details.
  535. # This is important for QSettings.
  536. QApplication.setOrganizationName("awlsim")
  537. QApplication.setOrganizationDomain(AWLSIM_HOME_DOMAIN)
  538. QApplication.setApplicationName("awlsim-gui")
  539. QApplication.setApplicationVersion(VERSION_STRING)
  540. mainwnd = cls(initialAwlSource)
  541. mainwnd.show()
  542. if initialAwlSource and not mainwnd.mainWidget.isDirty():
  543. # Revert back from DIRTY_SLIGHT to DIRTY_NO.
  544. mainwnd.mainWidget.setDirty(mainwnd.mainWidget.DIRTY_NO,
  545. force=True)
  546. return mainwnd
  547. def __init__(self, awlSource=None, parent=None):
  548. QMainWindow.__init__(self, parent)
  549. self.setWindowIcon(getIcon("cpu"))
  550. self.__profiler = None
  551. self.setStatusBar(QStatusBar(self))
  552. self.mainWidget = MainWidget(self, self)
  553. self.cpuDockWidget = CpuDockWidget(self.mainWidget, self)
  554. self.treeDockWidget = ProjectTreeDockWidget(self.mainWidget, self)
  555. self.setCentralWidget(self.mainWidget)
  556. self.setDockOptions(self.dockOptions() | QMainWindow.AllowTabbedDocks)
  557. self.addDockWidget(Qt.LeftDockWidgetArea, self.treeDockWidget)
  558. self.addDockWidget(Qt.BottomDockWidgetArea, self.cpuDockWidget)
  559. self.tb = QToolBar(self)
  560. self.tb.setObjectName("Main QToolBar")
  561. self.tb.setWindowTitle("Main tool bar")
  562. self.tb.toggleViewAction().setIcon(getIcon("prefs"))
  563. self.tb.addAction(getIcon("new"), "New project",
  564. self.mainWidget.newFile)
  565. self.tb.addAction(getIcon("open"), "Open project",
  566. self.mainWidget.load)
  567. self.tbSaveAct = self.tb.addAction(getIcon("save"), "Save project",
  568. self.mainWidget.save)
  569. self.tb.addSeparator()
  570. self.tbUndoAct = self.tb.addAction(getIcon("undo"), "Undo last edit",
  571. self.mainWidget.undo)
  572. self.tbRedoAct = self.tb.addAction(getIcon("redo"), "Redo",
  573. self.mainWidget.redo)
  574. self.tb.addSeparator()
  575. self.tbCutAct = self.tb.addAction(getIcon("cut"), "Cut",
  576. self.mainWidget.cut)
  577. self.tbCopyAct = self.tb.addAction(getIcon("copy"), "Copy",
  578. self.mainWidget.copy)
  579. self.tbPasteAct = self.tb.addAction(getIcon("paste"), "Paste",
  580. self.mainWidget.paste)
  581. self.tb.addSeparator()
  582. self.tbFindAct = self.tb.addAction(getIcon("find"), "Find...",
  583. self.mainWidget.findText)
  584. self.tbFindReplaceAct = self.tb.addAction(getIcon("findreplace"),
  585. "Find and replace...",
  586. self.mainWidget.findReplaceText)
  587. self.tb.addSeparator()
  588. self.tbLibAct = self.tb.addAction(getIcon("stdlib"), "Standard library",
  589. self.mainWidget.openLibrary)
  590. self.tbLibAct.setToolTip("Standard library.\n"
  591. "(Please click into the AWL/STL source code\n"
  592. "at the place where to paste the library call)")
  593. self.addToolBar(Qt.TopToolBarArea, self.tb)
  594. self.ctrlTb = CpuControlToolBar(self)
  595. self.ctrlTb.toggleViewAction().setIcon(getIcon("prefs"))
  596. self.addToolBar(Qt.LeftToolBarArea, self.ctrlTb)
  597. self.inspectTb = CpuInspectToolBar(self)
  598. self.inspectTb.toggleViewAction().setIcon(getIcon("prefs"))
  599. self.addToolBar(Qt.LeftToolBarArea, self.inspectTb)
  600. self.setMenuBar(QMenuBar(self))
  601. menu = QMenu("&File", self)
  602. menu.addAction(getIcon("new"), "&New project",
  603. self.mainWidget.newFile)
  604. menu.addAction(getIcon("open"), "&Open project...",
  605. self.mainWidget.load)
  606. self.saveAct = menu.addAction(getIcon("save"), "&Save project",
  607. self.mainWidget.save)
  608. menu.addAction(getIcon("save"), "&Save project as...",
  609. lambda: self.mainWidget.save(True))
  610. menu.addSeparator()
  611. menu.addAction(getIcon("exit"), "&Exit...", self.close)
  612. self.menuBar().addMenu(menu)
  613. menu = QMenu("&Edit", self)
  614. self.undoAct = menu.addAction(getIcon("undo"), "&Undo",
  615. self.mainWidget.undo)
  616. self.redoAct = menu.addAction(getIcon("redo"), "&Redo",
  617. self.mainWidget.redo)
  618. menu.addSeparator()
  619. self.cutAct = menu.addAction(getIcon("cut"), "&Cut",
  620. self.mainWidget.cut)
  621. self.copyAct = menu.addAction(getIcon("copy"), "&Copy",
  622. self.mainWidget.copy)
  623. self.pasteAct = menu.addAction(getIcon("paste"), "&Paste",
  624. self.mainWidget.paste)
  625. menu.addSeparator()
  626. self.findAct = menu.addAction(getIcon("find"), "&Find...",
  627. self.mainWidget.findText)
  628. self.findReplaceAct = menu.addAction(getIcon("findreplace"),
  629. "Find and r&eplace...",
  630. self.mainWidget.findReplaceText)
  631. self.menuBar().addMenu(menu)
  632. menu = QMenu("&Library", self)
  633. menu.addAction(getIcon("textsource"), "Insert &OB template...",
  634. self.mainWidget.insertOB)
  635. menu.addAction(getIcon("textsource"), "Insert F&C template...",
  636. self.mainWidget.insertFC)
  637. menu.addAction(getIcon("textsource"), "Insert F&B template...",
  638. self.mainWidget.insertFB)
  639. menu.addAction(getIcon("textsource"), "Insert &instance-DB template...",
  640. self.mainWidget.insertInstanceDB)
  641. menu.addAction(getIcon("textsource"), "Insert &DB template...",
  642. self.mainWidget.insertGlobalDB)
  643. menu.addAction(getIcon("textsource"), "Insert &UDT template...",
  644. self.mainWidget.insertUDT)
  645. menu.addSeparator()
  646. menu.addAction(getIcon("textsource"), "Insert FC C&ALL template...",
  647. self.mainWidget.insertFCcall)
  648. menu.addAction(getIcon("textsource"), "Insert FB CA&LL template...",
  649. self.mainWidget.insertFBcall)
  650. menu.addSeparator()
  651. self.libAct = menu.addAction(getIcon("stdlib"), "&Standard library...",
  652. self.mainWidget.openLibrary)
  653. self.menuBar().addMenu(menu)
  654. menu = QMenu("&CPU", self)
  655. menu.addAction(self.ctrlTb.onlineAction)
  656. menu.addAction(self.ctrlTb.resetAction)
  657. menu.addAction(self.ctrlTb.downloadAction)
  658. menu.addAction(self.ctrlTb.downloadSingleAction)
  659. menu.addAction(self.ctrlTb.runAction)
  660. menu.addAction(self.ctrlTb.diagAction)
  661. menu.addSeparator()
  662. menu.addAction(self.inspectTb.blocksAction)
  663. menu.addAction(self.inspectTb.inputsAction)
  664. menu.addAction(self.inspectTb.outputsAction)
  665. menu.addAction(self.inspectTb.flagsAction)
  666. menu.addAction(self.inspectTb.dbAction)
  667. menu.addAction(self.inspectTb.timerAction)
  668. menu.addAction(self.inspectTb.counterAction)
  669. menu.addAction(self.inspectTb.cpuAction)
  670. menu.addAction(self.inspectTb.lcdAction)
  671. self.menuBar().addMenu(menu)
  672. self.__windowMenu = QMenu("&Window", self)
  673. self.menuBar().addMenu(self.__windowMenu)
  674. self.__windowMenu.aboutToShow.connect(self.__buildWindowMenu)
  675. menu = QMenu("&Help", self)
  676. menu.addAction(getIcon("browser"), "Awlsim &homepage...", self.awlsimHomepage)
  677. menu.addSeparator()
  678. menu.addAction(getIcon("cpu"), "&About...", self.about)
  679. menu.addSeparator()
  680. self.__actProfileStart = menu.addAction(getIcon("enable"),
  681. "Start profiling",
  682. self.profileStart)
  683. self.__actProfileStop = menu.addAction(getIcon("disable"),
  684. "Stop profiling",
  685. self.profileStop)
  686. profEnabled = (AwlSimEnv.getProfileLevel() > 0)
  687. self.__actProfileStart.setVisible(profEnabled)
  688. self.__actProfileStop.setVisible(False)
  689. self.menuBar().addMenu(menu)
  690. self.__loadProgressDialog = LoadProgressDialog(self)
  691. self.__sourceTextHasFocus = False
  692. self.__dirtyChanged(MainWidget.DIRTY_NO)
  693. self.__textFocusChanged(False)
  694. self.__undoAvailableChanged(False)
  695. self.__redoAvailableChanged(False)
  696. self.__copyAvailableChanged(False)
  697. self.__cutAvailableChanged(False)
  698. self.__pasteAvailableChanged(False)
  699. client = self.getSimClient()
  700. self.mainWidget.projectLoaded.connect(self.__handleProjectLoaded)
  701. self.mainWidget.dirtyChanged.connect(self.__dirtyChanged)
  702. self.mainWidget.textFocusChanged.connect(self.__textFocusChanged)
  703. self.mainWidget.undoAvailableChanged.connect(self.__undoAvailableChanged)
  704. self.mainWidget.redoAvailableChanged.connect(self.__redoAvailableChanged)
  705. self.mainWidget.copyAvailableChanged.connect(self.__copyAvailableChanged)
  706. self.mainWidget.cutAvailableChanged.connect(self.__cutAvailableChanged)
  707. self.mainWidget.pasteAvailableChanged.connect(self.__pasteAvailableChanged)
  708. self.cpuDockWidget.toggleViewAction().toggled.connect(self.__cpuDockToggled)
  709. self.inspectTb.connectToCpuWidget(self.cpuWidget)
  710. GuiValidatorSched.get().haveValidationResult.connect(self.projectTreeModel.handleAwlSimError)
  711. self.projectTreeModel.projectContentChanged.connect(self.mainWidget.somethingChanged)
  712. client.haveCpuStats.connect(self.mainWidget.handleCpuStats)
  713. client.haveIdentsMsg.connect(self.projectTreeModel.handleIdentsMsg)
  714. client.haveException.connect(self.projectTreeModel.handleAwlSimError)
  715. client.haveIdentsMsg.connect(self.editMdiArea.handleIdentsMsg)
  716. client.haveInsnDump.connect(self.editMdiArea.handleInsnDump)
  717. client.guiRunState.stateChanged.connect(self.editMdiArea.setGuiRunState)
  718. client.guiRunState.stateChanged.connect(self.projectTreeModel.setGuiRunState)
  719. client.guiRunState.stateChanged.connect(self.mainWidget.handleGuiRunStateChange)
  720. if awlSource:
  721. self.mainWidget.loadFile(awlSource, newIfNotExist=True)
  722. self.__restoreState()
  723. @property
  724. def loadProgressDialog(self):
  725. return self.__loadProgressDialog
  726. @property
  727. def editMdiArea(self):
  728. return self.mainWidget.editMdiArea
  729. @property
  730. def projectTreeModel(self):
  731. return self.treeDockWidget.projectTreeModel
  732. @property
  733. def cpuWidget(self):
  734. return self.cpuDockWidget.cpuWidget
  735. def getProject(self):
  736. return self.projectTreeModel.getProject()
  737. def getSimClient(self):
  738. return self.mainWidget.getSimClient()
  739. def __cpuDockToggled(self, cpuDockEnabled):
  740. action = self.inspectTb.toggleViewAction()
  741. if action.isChecked() != cpuDockEnabled:
  742. action.trigger()
  743. action.setEnabled(cpuDockEnabled)
  744. def __buildWindowMenu(self):
  745. """Rebuild the window menu.
  746. """
  747. menu = self.__windowMenu
  748. menu.clear()
  749. mdiSubWins = self.editMdiArea.subWindowList()
  750. if mdiSubWins:
  751. activeMdiSubWin = self.editMdiArea.activeOpenSubWindow
  752. for mdiSubWin in mdiSubWins:
  753. def activateWin(mdiSubWin=mdiSubWin):
  754. self.editMdiArea.setActiveSubWindow(mdiSubWin)
  755. action = menu.addAction(mdiSubWin.windowIcon(),
  756. mdiSubWin.windowTitle(),
  757. activateWin)
  758. if mdiSubWin is activeMdiSubWin:
  759. font = action.font()
  760. font.setBold(True)
  761. action.setFont(font)
  762. menu.addSeparator()
  763. def closeActive():
  764. w = self.editMdiArea.activeOpenSubWindow
  765. if w:
  766. w.close()
  767. def closeAllExceptActive():
  768. active = self.editMdiArea.activeOpenSubWindow
  769. for w in self.editMdiArea.subWindowList():
  770. if w is not active:
  771. w.close()
  772. def closeAll():
  773. self.editMdiArea.closeAllSubWindows()
  774. action = menu.addAction(getIcon("doc_close"),
  775. "&Close active window",
  776. closeActive)
  777. action.setEnabled(bool(mdiSubWins))
  778. action = menu.addAction(getIcon("doc_close"),
  779. "Close &all except active",
  780. closeAllExceptActive)
  781. action.setEnabled(bool(mdiSubWins))
  782. action = menu.addAction(getIcon("doc_close"),
  783. "Close a&ll",
  784. closeAll)
  785. action.setEnabled(bool(mdiSubWins))
  786. menu.addSeparator()
  787. menu.addAction(self.cpuDockWidget.toggleViewAction())
  788. menu.addSeparator()
  789. for tb in (self.tb, self.ctrlTb, self.inspectTb):
  790. menu.addAction(tb.toggleViewAction())
  791. def __saveState(self):
  792. settings = QSettings()
  793. # Save the main window state
  794. settings.setValue("gui_main_window_state",
  795. self.saveState(VERSION_ID))
  796. # Save the main window geometry
  797. settings.setValue("gui_main_window_geo",
  798. self.saveGeometry())
  799. def __restoreState(self):
  800. settings = QSettings()
  801. # Restore the main window geometry
  802. geo = settings.value("gui_main_window_geo")
  803. if geo:
  804. self.restoreGeometry(geo)
  805. # Restore the main window state
  806. state = settings.value("gui_main_window_state")
  807. if state:
  808. self.restoreState(state, VERSION_ID)
  809. # Only allow inspect tool bar switching, if the CPU dock is available.
  810. cpuDockEn = self.cpuDockWidget.toggleViewAction().isChecked()
  811. self.inspectTb.toggleViewAction().setEnabled(cpuDockEn)
  812. def __dirtyChanged(self, dirtyLevel):
  813. self.saveAct.setEnabled(dirtyLevel != MainWidget.DIRTY_NO)
  814. self.tbSaveAct.setEnabled(dirtyLevel != MainWidget.DIRTY_NO)
  815. filename = self.mainWidget.getFilename()
  816. if filename:
  817. postfix = " -- " + os.path.basename(filename)
  818. if dirtyLevel == MainWidget.DIRTY_FULL:
  819. postfix += "*"
  820. else:
  821. postfix = ""
  822. self.setWindowTitle("%s%s" % (self.TITLE, postfix))
  823. def __handleProjectLoaded(self, project):
  824. self.__updateLibActions()
  825. self.__updateFindActions()
  826. def __updateFindActions(self):
  827. findAvailable = self.editMdiArea.findTextIsAvailable()
  828. self.tbFindAct.setEnabled(findAvailable)
  829. self.findAct.setEnabled(findAvailable)
  830. replaceAvailable = self.editMdiArea.findReplaceTextIsAvailable()
  831. self.tbFindReplaceAct.setEnabled(replaceAvailable)
  832. self.findReplaceAct.setEnabled(replaceAvailable)
  833. def __updateLibActions(self):
  834. # Enable/disable the library toolbar button.
  835. # The menu library button is always available on purpose.
  836. self.tbLibAct.setEnabled(self.editMdiArea.pasteIsAvailable())
  837. def __textFocusChanged(self, textHasFocus):
  838. self.__sourceTextHasFocus = textHasFocus
  839. self.__updateLibActions()
  840. self.__updateFindActions()
  841. def __undoAvailableChanged(self, undoAvailable):
  842. self.undoAct.setEnabled(undoAvailable)
  843. self.tbUndoAct.setEnabled(undoAvailable)
  844. def __redoAvailableChanged(self, redoAvailable):
  845. self.redoAct.setEnabled(redoAvailable)
  846. self.tbRedoAct.setEnabled(redoAvailable)
  847. def __copyAvailableChanged(self, copyAvailable):
  848. self.copyAct.setEnabled(copyAvailable)
  849. self.tbCopyAct.setEnabled(copyAvailable)
  850. def __cutAvailableChanged(self, cutAvailable):
  851. self.cutAct.setEnabled(cutAvailable)
  852. self.tbCutAct.setEnabled(cutAvailable)
  853. def __pasteAvailableChanged(self, pasteAvailable):
  854. self.pasteAct.setEnabled(pasteAvailable)
  855. self.tbPasteAct.setEnabled(pasteAvailable)
  856. self.__updateLibActions()
  857. def closeEvent(self, ev):
  858. if self.mainWidget.isDirty():
  859. res = QMessageBox.question(self,
  860. "Unsaved AWL/STL code",
  861. "The editor contains unsaved AWL/STL code.\n"
  862. "AWL/STL code will be lost by exiting without saving.",
  863. QMessageBox.Discard | QMessageBox.Save | QMessageBox.Cancel,
  864. QMessageBox.Cancel)
  865. if res == QMessageBox.Save:
  866. if not self.mainWidget.save():
  867. ev.ignore()
  868. return
  869. elif res == QMessageBox.Cancel:
  870. ev.ignore()
  871. return
  872. self.__saveState()
  873. self.getSimClient().shutdown()
  874. ev.accept()
  875. QMainWindow.closeEvent(self, ev)
  876. self.profileStop()
  877. def keyPressEvent(self, ev):
  878. if ev.matches(QKeySequence.Save):
  879. self.mainWidget.save(False)
  880. ev.accept()
  881. return
  882. elif ev.matches(QKeySequence.SaveAs):
  883. self.mainWidget.save(True)
  884. ev.accept()
  885. return
  886. QMainWindow.keyPressEvent(self, ev)
  887. def awlsimHomepage(self):
  888. QDesktopServices.openUrl(QUrl(AWLSIM_HOME_URL, QUrl.StrictMode))
  889. def about(self):
  890. QMessageBox.about(self, "About Awlsim PLC",
  891. "Awlsim PLC version %s\n"
  892. "\n"
  893. "Copyright 2012-2024 Michael Büsch <m@bues.ch>\n"
  894. "\n"
  895. "Project home: %s\n"
  896. "\n"
  897. "\n"
  898. "This program is free software; you can redistribute it and/or modify "
  899. "it under the terms of the GNU General Public License as published by "
  900. "the Free Software Foundation; either version 2 of the License, or "
  901. "(at your option) any later version.\n"
  902. "\n"
  903. "This program is distributed in the hope that it will be useful, "
  904. "but WITHOUT ANY WARRANTY; without even the implied warranty of "
  905. "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the "
  906. "GNU General Public License for more details.\n"
  907. "\n"
  908. "You should have received a copy of the GNU General Public License along "
  909. "with this program; if not, write to the Free Software Foundation, Inc., "
  910. "51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA." %\
  911. (VERSION_STRING, AWLSIM_HOME_URL))
  912. def profileStart(self):
  913. if not self.__profiler:
  914. try:
  915. self.__profiler = Profiler()
  916. self.__profiler.start()
  917. except AwlSimError as e:
  918. MessageBox.handleAwlSimError(self,
  919. "Failed to start profiler.", e)
  920. self.__profiler = None
  921. return
  922. self.__actProfileStart.setVisible(False)
  923. self.__actProfileStop.setVisible(True)
  924. def profileStop(self):
  925. if self.__profiler:
  926. self.__profiler.stop()
  927. printInfo("GUI profiler dump:\n" + self.__profiler.getResult())
  928. self.__profiler = None
  929. self.__actProfileStart.setVisible(True)
  930. self.__actProfileStop.setVisible(False)