# Copyright (C) 2014 Dignity Health
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# NO CLINICAL USE. THE SOFTWARE IS NOT INTENDED FOR COMMERCIAL PURPOSES
# AND SHOULD BE USED ONLY FOR NON-COMMERCIAL RESEARCH PURPOSES. THE
# SOFTWARE MAY NOT IN ANY EVENT BE USED FOR ANY CLINICAL OR DIAGNOSTIC
# PURPOSES. YOU ACKNOWLEDGE AND AGREE THAT THE SOFTWARE IS NOT INTENDED FOR
# USE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITY, INCLUDING BUT NOT
# LIMITED TO LIFE SUPPORT OR EMERGENCY MEDICAL OPERATIONS OR USES. LICENSOR
# MAKES NO WARRANTY AND HAS NO LIABILITY ARISING FROM ANY USE OF THE
# SOFTWARE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITIES.
import sys
import time
import logging
import subprocess
# gpi
from gpi import QtCore, QtGui, VERSION, RELEASE_DATE
from .config import Config
from .console import Tee
from .canvasGraph import GraphWidget
from .cmd import Commands
from .defines import LOGO_PATH, GPI_DOCS_DIR
from . import logger
from .logger import manager
from .widgets import DisplayBox, TextBox, TextEdit
from .sysspecs import Specs
from .update import UpdateWindow
# start logger for this module
log = manager.getLogger(__name__)
[docs]class MainCanvas(QtGui.QMainWindow):
"""
- Implements the canvas QWidgets, contains the main menus and provides user
settings via menu or rc file.
- Anchors the canvas and provides the main menu and status bar.
"""
def __init__(self):
super(MainCanvas, self).__init__()
# useful for tracking number of file handles
#self._report = QtCore.QTimer()
#self._report.setInterval(1000)
#self._report.timeout.connect(Specs.numOpenFiles)
#self._report.start()
# A statusbar widget
self._statusLabel = QtGui.QLabel()
# for copying between canvases
self._copybuffer = None
# TAB WIDGET
self.tabs = QtGui.QTabWidget()
self.tabs.setTabsClosable(True)
self.tabs.setTabPosition(QtGui.QTabWidget.North)
self.tabs.tabCloseRequested.connect(self.closeCanvasTab)
self.tabs.currentChanged.connect(self.tabChange)
self.tabs.setMovable(True)
self.setCentralWidget(self.tabs)
self.tabs.show()
# ADD TAB BUTTON
self.addbutton = QtGui.QPushButton('+')
self.addbutton.clicked[bool].connect(self.addNewCanvasTab)
self.tabs.setCornerWidget(self.addbutton)
# ADD CANVAS TABS
self._canvasCnt = 1
newGraph = GraphWidget("Canvas 1", self)
newGraph._curState.connect(self.updateCanvasStatus)
self.tabs.addTab(newGraph, "Canvas 1")
# possible names for this project
if (time.localtime().tm_mon == 4) and (time.localtime().tm_mday == 1):
titleList = []
titleList += ['Master Control Processor (MCP)']
titleList += ['Code Flow GPI']
titleList += ['Code Shepherd']
titleList += ['Source Flow']
titleList += ['Algorithm Processing Network (APN)']
titleList += ['Visual Algorithm Processor (VAP)']
titleList += ['Data-Analysing Robot Youth Lifeform (D.A.R.Y.L.)']
titleList += ['Heuristically-programmed ALgorithmic computer (H.A.L.)']
titleList += ['Johnny Five']
titleList += ['Visual Programming Paradigm (Vpp)']
titleList += ['Graphical Prototyping Platform (Gpp)']
titleList += ['Node Commander (GPI)']
titleList += ['Algorithm Dominator (GPI)']
titleList += ['ProtoWizard (GPI)']
titleList += ['Algorithm Maestro (GPI)']
titleList += ['Node Guru (GPI)']
titleList += ['Process Master (GPI)']
titleList += ['Prototype Expert (GPI)']
titleList += ['Algorithm Assemblage (GPI)']
titleList += ['High Performance Algorithm Collider (GPI)']
titleList += ['Cluster Flow (GPI)']
titleList += ['Mothra (GPI)']
titleList += ['Assimilator (GPI)']
titleList += ['Algorithm Integrator (GPI)']
titleList += ['Method Mapper (GPI)']
titleList += ['Method Master (GPI)']
titleList += ['Vfunc (Visual Functor)']
from random import choice
self.setWindowTitle(choice(titleList))
else:
self.setWindowTitle('Graphical Programming Interface (GPI)')
# system tray icon (this actually works in Ubuntu)
from .defines import ICON_PATH
self._gpiIcon = QtGui.QIcon(ICON_PATH)
self.setWindowIcon(self._gpiIcon)
#self._trayicon = QtGui.QSystemTrayIcon(self._gpiIcon, parent=self)
#self._trayicon.show()
# don't bother with the menus if the gui is not up
if not Commands.noGUI():
self.createMenus()
best_style = None
if 'Macintosh (aqua)' in list(QtGui.QStyleFactory.keys()):
log.debug("Choosing Mac aqua style.")
best_style = 'Macintosh (aqua)'
elif 'Cleanlooks' in list(QtGui.QStyleFactory.keys()):
log.debug("Choosing Cleanlooks style.")
best_style = 'Cleanlooks'
if best_style:
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create(best_style))
QtGui.QApplication.setPalette(
QtGui.QApplication.style().standardPalette())
# Status Bar
message = "A context menu is available by right-clicking"
self.statusBar().addPermanentWidget(self._statusLabel)
self.statusBar().showMessage(message)
self.updateCanvasStatus()
def setStatusTip(self, msg):
self.statusBar().showMessage(msg)
def updateCanvasStatus(self, curState=None):
'''Modifies the QLabel portion of main window's statusBar.
Shows the current state of the focused canvas.
'''
if curState is None:
if self.tabs.currentWidget():
curState = self.tabs.currentWidget().getCurStateSig()
else:
return # Likely GPI is being closed.
# update the canvas only if the supplied curState is also from the
# current canvas -this needs to be redone
graph = self.tabs.currentWidget()
if graph.title() == curState['title']:
msg = curState['title']+": "+curState['msg'] # base message
if 'walltime' in curState:
msg += ' (Elapsed: '+ str(curState['walltime']) +')'
self._statusLabel.setText(msg) # quickly show this incase mem calc is too long
# only do this calc if in Idle
if curState['msg'] == 'Idle':
# calculate the port-memory usage of the graph
pmem = graph.totalPortMem()
if pmem > 0:
msg += ' ['+graph.totalPortMem_disp(pmem)
if Specs.TOTAL_PHYMEM() == 0:
pct_physmem = ''
msg += pct_physmem
else:
pct_physmem = ', %.*f%s RAM' % (1, float(
100.0 * pmem/ Specs.TOTAL_PHYMEM()), '%')
msg += pct_physmem
msg += ']'
self._statusLabel.setText(msg)
def updateNodeStatus(self, txt):
'''Modifies the QLabel portion of main window's statusBar.
A node's status can be updated textually by either naming steps,
showing step #/total, or with percentages.
'''
self._statusLabel.setText(txt)
def quitConfirmed(self):
'''Make sure an accidental quit doesn't ruin the user's day.
'''
reply = QtGui.QMessageBox.question(self, 'Message',
"Quit without saving?", QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
return True
else:
return False
def addNewCanvasTab(self):
'''Push a new GraphWidget into the tabbar.
'''
self._canvasCnt += 1
title = "Canvas "+str(self._canvasCnt)
newGraph = GraphWidget(title, self)
newGraph._curState.connect(self.updateCanvasStatus)
self.tabs.addTab(newGraph, title)
self.tabs.setCurrentIndex(self.tabs.count()-1)
def tabChange(self, index):
log.debug("tabChange: "+str(index))
self.updateCanvasStatus()
def closeEvent(self, event):
'''close all graphs before shutting down.
'''
if not self.quitConfirmed():
event.ignore()
return
while self.tabs.count():
self.tabs.widget(0).close()
self.tabs.removeTab(0)
event.accept()
def closeCanvasTab(self, index):
'''Leave at least one tab open.
'''
if self.tabs.count() == 1:
return
# delete all nodes before closeing by calling the close procedure
if self.tabs.widget(index).closeGraphWithDialog():
self.tabs.removeTab(index)
def console(self):
log.debug("MainCanvas(): console()")
self.txtbox = TextEdit('Console')
# Redirect stdio
sys.stdout = Tee(sys.stdout)
sys.stderr = Tee(sys.stderr)
# NOTE: this is only good for QThread NOT Multiprocess.
sys.stdout.newStreamTxt.connect(self.consoleWrite)
sys.stderr.newStreamTxt.connect(self.consoleWrite)
# set layout
wdgvbox = QtGui.QVBoxLayout()
wdgvbox.addWidget(self.txtbox)
# set master widget
self.consoleWdg = QtGui.QWidget()
self.consoleWdg.setLayout(wdgvbox)
self.consoleWdg.show()
self.consoleWdg.raise_()
def consoleWrite(self, m):
self.txtbox.wdg.moveCursor(QtGui.QTextCursor.End)
self.txtbox.wdg.insertPlainText(m)
def about(self):
# Display
image = QtGui.QImage(LOGO_PATH)
dispbox = DisplayBox('')
dispbox.set_pixmap(QtGui.QPixmap.fromImage(image))
dispbox.set_scale(0.157)
dispbox.set_interp(True)
# Text
tab = ' '
txt = '<center><font size=4><b>Graphical Programming Interface (GPI)</b></font><br><b><a href=http://gpilab.com>gpilab.com</a></b><br><br>' +\
'Nicholas Zwart<br>Barrow Neurological Institute<br>Phoenix, Arizona' +\
'<br><br>' +\
tab + 'Release: '+ str(VERSION) + ' (' + str(RELEASE_DATE) +')' +\
'<p>'+\
'GPI is a graphical development environment designed for rapid prototyping '+\
'of numeric algorithms.'+\
'</p>'+\
'<p>'+\
'GPI development is sponsored by Philips Healthcare.'+\
'</p></center>'
txtbox = TextBox('')
txtbox.set_val(txt)
txtbox.set_wordwrap(True)
txtbox.set_openExternalLinks(True)
# set layout
wdgvbox = QtGui.QVBoxLayout()
wdgvbox.addWidget(dispbox)
wdgvbox.addWidget(txtbox)
wdgvbox.setStretch(0, 2)
# set master widget
self.aboutWdg = QtGui.QWidget()
self.aboutWdg.setLayout(wdgvbox)
# self.aboutWdg.setSizePolicy(QtGui.QSizePolicy.Minimum, \
# QtGui.QSizePolicy.Preferred)
# self.aboutWdg.setMaximumWidth(420)
self.aboutWdg.show()
self.aboutWdg.raise_()
log.debug(str(dispbox.sizeHint()))
def generateConfigFile(self):
# a place for a user dialog if need be
log.debug("generateConfigFile(): called")
Config.generateConfigFile()
#reply = QtGui.QMessageBox.question(self, 'Message',
# "Overwrite Existing" +
# " Configuration File (" +
# self._configFileName + ")?",
# QtGui.QMessageBox.Yes |
# QtGui.QMessageBox.No,
# QtGui.QMessageBox.No)
#if reply == QtGui.QMessageBox.No:
# log.info("generateConfigFile(): aborted.")
# return
def generateUserLib(self):
log.debug("generateUserLib(): called")
Config.generateUserLib()
graph = self.tabs.currentWidget()
graph.rescanLibrary()
def createNewNode(self):
log.debug("createNewNode(): called")
graph = self.tabs.currentWidget()
graph.getLibrary().showNewNodeListWindow()
def rescanKnownLibs(self):
log.debug("Scanning LIB_DIRS for new nodes and libs.")
graph = self.tabs.currentWidget()
graph.rescanLibrary()
def createMenus(self):
# STYLE
#self.styleMenu = QtGui.QMenu("&Style", self)
#ag = QtGui.QActionGroup(self.styleMenu, exclusive=True)
#for s in QtGui.QStyleFactory.keys(): # get menu items based on keys
# a = ag.addAction(QtGui.QAction(s, self.styleMenu, checkable=True))
# self.styleMenu.addAction(a)
#ag.selected.connect(self.changeStyle)
#self.menuBar().addMenu(self.styleMenu)
# FILE
self.fileMenu = QtGui.QMenu("&File", self)
fileMenu_newTab = QtGui.QAction("New Tab", self, shortcut="Ctrl+T", triggered=self.addNewCanvasTab)
self.fileMenu.addAction(fileMenu_newTab)
self.fileMenu.addAction("Create New Node", self.createNewNode)
self.menuBar().addMenu(self.fileMenu)
# CONFIG
self.configMenu = QtGui.QMenu("&Config", self)
self.configMenu.addAction("Generate Config File (" +
str(Config.configFilePath()) + ")",
self.generateConfigFile)
self.configMenu.addAction("Generate User Library (" +
str(Config.userLibPath()) + ")",
self.generateUserLib)
self.configMenu.addAction("Scan For New Nodes",
self.rescanKnownLibs)
#self.configMenu.addAction("Rescan Config File (" +
# str(Config.configFilePath()) + ")",
# Config.loadConfigFile)
self.menuBar().addMenu(self.configMenu)
# DEBUG
self.debugMenu = QtGui.QMenu("&Debug")
ag = QtGui.QActionGroup(self.debugMenu, exclusive=False)
## logger output sub-menu
self.loggerMenu = self.debugMenu.addMenu("Logger Level")
self._loglevel_debug_act = QtGui.QAction("Debug", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logging.DEBUG))
self._loglevel_info_act = QtGui.QAction("Info", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logging.INFO))
self._loglevel_node_act = QtGui.QAction("Node", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logger.GPINODE))
self._loglevel_warn_act = QtGui.QAction("Warn", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logging.WARNING))
self._loglevel_error_act = QtGui.QAction("Error", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logging.ERROR))
self._loglevel_critical_act = QtGui.QAction("Critical", self, checkable=True,
triggered=lambda: self.setLoggerLevel(logging.CRITICAL))
self.loggerMenuGroup = QtGui.QActionGroup(self)
self.loggerMenuGroup.addAction(self._loglevel_debug_act)
self.loggerMenuGroup.addAction(self._loglevel_info_act)
self.loggerMenuGroup.addAction(self._loglevel_node_act)
self.loggerMenuGroup.addAction(self._loglevel_warn_act)
self.loggerMenuGroup.addAction(self._loglevel_error_act)
self.loggerMenuGroup.addAction(self._loglevel_critical_act)
self.loggerMenu.addAction(self._loglevel_debug_act)
self.loggerMenu.addAction(self._loglevel_info_act)
self.loggerMenu.addAction(self._loglevel_node_act)
self.loggerMenu.addAction(self._loglevel_warn_act)
self.loggerMenu.addAction(self._loglevel_error_act)
self.loggerMenu.addAction(self._loglevel_critical_act)
# initialize the log level -default
if Commands.logLevel():
#self._loglevel_warn_act.setChecked(True)
self.setLoggerLevel(Commands.logLevel())
self.setLoggerLevelMenuCheckbox(Commands.logLevel())
else:
self._loglevel_warn_act.setChecked(True)
self.setLoggerLevel(logging.WARNING)
# console submenu
#a = ag.addAction(QtGui.QAction("Console", self.debugMenu,
# checkable=False))
#self.connect(a, QtCore.SIGNAL("triggered()"), self.console)
#self.debugMenu.addAction(a)
#a = ag.addAction(QtGui.QAction("Debug Info", self.debugMenu, checkable=True))
#self.debugMenu.addAction(a)
#ag.selected.connect(self.debugOptions)
# DEBUG
self.debugMenu.addAction("Print sys.paths", self.printSysPath)
self.debugMenu.addAction("Print sys.modules", self.printSysModules)
self.menuBar().addMenu(self.debugMenu)
# WINDOW
self.windowMenu = QtGui.QMenu("Window", self)
self.windowMenu_closeAct = QtGui.QAction("Close Node Menus (Current Tab)", self, shortcut="Ctrl+X", triggered=self.closeAllNodeMenus)
self.windowMenu.addAction(self.windowMenu_closeAct)
self.menuBar().addMenu(self.windowMenu)
# HELP
self.helpMenu = QtGui.QMenu("&Help", self)
aboutAction = self.helpMenu.addAction("&About")
self.connect(aboutAction, QtCore.SIGNAL("triggered()"), self.about)
self.checkForUpdate = QtGui.QAction("Check For Updates...", self, triggered=self.openUpdater)
self.checkForUpdate.setMenuRole(QtGui.QAction.ApplicationSpecificRole)
self.helpMenu.addAction(self.checkForUpdate)
self.helpMenu_openDocs = QtGui.QAction("Documentation", self, triggered=self.openWebsite)
self.helpMenu.addAction(self.helpMenu_openDocs)
self.helpMenu_openDocs = QtGui.QAction("Examples", self, triggered=self.openExamplesFolder)
self.helpMenu.addAction(self.helpMenu_openDocs)
self.menuBar().addMenu(self.helpMenu)
def openUpdater(self):
self._updateWin = UpdateWindow(dry_run=False)
self._updateWin.show()
self._updateWin.raise_()
# TODO: move this and others like it to a common help-object that can errorcheck.
def openWebsite(self):
if not QtGui.QDesktopServices.openUrl(QtCore.QUrl('http://docs.gpilab.com')):
QtGui.QMessageBox.information(self, 'Documentation',"Documentation can be found at\nhttp://docs.gpilab.com", QtGui.QMessageBox.Close)
def openDocsFolder(self):
if Specs.inOSX():
subprocess.Popen("open " + GPI_DOCS_DIR, shell=True)
elif Specs.inLinux():
subprocess.Popen("xdg-open " + GPI_DOCS_DIR, shell=True)
log.dialog("GPI documentation can be found in: "+GPI_DOCS_DIR)
def openExamplesFolder(self):
if Specs.inOSX():
subprocess.Popen("open " + GPI_DOCS_DIR+'/Examples', shell=True)
elif Specs.inLinux():
subprocess.Popen("xdg-open " + GPI_DOCS_DIR+'/Examples', shell=True)
log.dialog("GPI examples can be found in: "+GPI_DOCS_DIR+'/Examples')
def closeAllNodeMenus(self):
self.tabs.currentWidget().closeAllNodeMenus()
def setLoggerLevel(self, lev):
manager.setLevel(lev)
def setLoggerLevelMenuCheckbox(self, lev):
if lev == logging.DEBUG:
self._loglevel_debug_act.setChecked(True)
if lev == logging.INFO:
self._loglevel_info_act.setChecked(True)
if lev == logger.GPINODE:
self._loglevel_node_act.setChecked(True)
if lev == logging.WARNING:
self._loglevel_warn_act.setChecked(True)
if lev == logging.ERROR:
self._loglevel_error_act.setChecked(True)
if lev == logging.CRITICAL:
self._loglevel_critical_act.setChecked(True)
def printSysPath(self):
print("Current module search path (sys.path):")
for path in sys.path:
print(path)
def printSysModules(self):
print("Current modules loaded (sys.modules):")
for k in sorted(sys.modules.keys()):
v = sys.modules[k]
print((k + " : " + str(v)))
if False:
if k.lower().count('spiral'):
print(("key: " + k + ", " + str(v)))
elif str(v).lower().count('spiral'):
print(("key: " + k + ", " + str(v)))
def changeStyle(self, action):
# UI style
log.debug("MainCanvas(): ChangeStyle called:")
log.debug(str(action.text()))
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create(action.text()))
QtGui.QApplication.setPalette(
QtGui.QApplication.style().standardPalette())
def debugOptions(self, action):
if action.text() == "Debug Info":
self.printSysPath()
#if action.text() == "Console":
# print "MainCanvas(): Console"