#
##
## This file is part of pyFormex 2.0 (Mon Sep 14 12:29:05 CEST 2020)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: http://pyformex.org
## Project page: http://savannah.nongnu.org/projects/pyformex/
## Copyright 2004-2020 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
## Distributed under the GNU General Public License version 3 or later.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU 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 General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see http://www.gnu.org/licenses/.
##
"""Graphical User Interface for pyFormex.
This module contains the main functions responsible for constructing
and starting the pyFormex GUI.
"""
import sys, os
import types
import warnings
import pyformex as pf
from pyformex import Path
from pyformex import process
from pyformex import utils
from pyformex import software
# If we get here, either PySide2, PySide, PyQt5 or PyQt4 are imported
# Check for OpenGL
software.Module.has('pyopengl', fatal=True)
software.Module.has('pil', fatal=True)
from pyformex.gui import (
signals, QtCore, QtGui, QtWidgets, QPixmap,
menu, appMenu,
toolbar, viewport, guifunc, draw, widgets, drawlock, views,
)
from pyformex.gui.menus import File, Settings, Viewport, Globals
from pyformex.opengl import canvas
from pyformex.gui.qtutils import *
[docs]def hasDRI():
"""Check whether the OpenGL canvas has DRI enabled."""
viewport.setOpenGLFormat()
dri = viewport.opengl_format.directRendering()
return dri
################# Message Board ###############
[docs]class Board(QtWidgets.QTextEdit):
"""Message board for displaying read-only plain text messages."""
def __init__(self, parent=None):
"""Construct the Message Board widget."""
super(Board, self).__init__(parent)
self.setReadOnly(True)
self.setAcceptRichText(False)
self.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken)
self.setMinimumSize(24, 24)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
self.cursor = self.textCursor()
font = QtGui.QFont("DejaVu Sans Mono")
#font.setStyle(QtGui.QFont.StyleNormal)
self.setFont(font)
self.stdout = self.stderr = None # redirected streams
## def textColor(self):
## p = self.palette()
## return p.color(QtGui.QPalette.WindowText)
## def setTextColor(self,qcolor):
## print("SET COLOR TO %s" % qcolor)
## p = self.palette()
## p.setColor(QtGui.QPalette.WindowText,QtCore.Qt.red)
## p.setColor(QtGui.QPalette.Window,QtCore.Qt.yellow)
## self.setPalette(p)
[docs] def write(self, s, color='black'):
"""Write a string to the message board.
If a color is specified, the text is shown in the specified
color, but the default board color remains unchanged.
"""
self.setTextColor(QtGui.QColor(color))
# A single blank character seems to be generated by a print
# instruction containing a comma: skip it
if s == ' ':
return
s = s.rstrip('\n')
if len(s) > 0:
self.append(s)
self.cursor.movePosition(QtGui.QTextCursor.End)
self.setTextCursor(self.cursor)
[docs] def save(self, filename):
"""Save the contents of the board to a file"""
fil = open(filename, 'w')
fil.write(self.toPlainText())
fil.close()
def flush(self):
if sys.stdout == self or sys.stderr == self:
self.update()
[docs] def redirect(self, onoff):
"""Redirect standard and error output to this message board"""
if onoff:
# redirect to Message board
sys.stderr.flush()
sys.stdout.flush()
self.stderr = sys.stderr
self.stdout = sys.stdout
sys.stderr = self
sys.stdout = self
else:
# redirect back to previous
if self.stderr:
sys.stderr = self.stderr
if self.stdout:
sys.stdout = self.stdout
self.stderr = None
self.stdout = None
#####################################
################# GUI ###############
#####################################
def toggleAppScript():
pf.debug("Toggle between app and script", pf.DEBUG.APPS)
from pyformex import apps
appname = pf.cfg['curfile']
if utils.is_script(appname):
path = Path(appname).parent
appdir = apps.findAppDir(path)
if appdir:
appname = appname.stem
pkgname = appdir.pkg
appname = "%s.%s" % (pkgname, appname)
pf.GUI.setcurfile(appname)
else:
if pf.warning("This script is not in an application directory.\n\nYou should add the directory path '%s' to the application paths before you can run this file as an application." % path, actions=['Not this time', 'Add this directory now']).startswith('Add'):
#print("Adding directory %s" % path)
#from pyformex.gui.menus.Settings import addAppdir
Settings.addAppdir(path, dircfg='appdirs')
draw.showInfo('Added the path %s' % path)
else:
fn = apps.findAppSource(appname)
if fn.exists():
pf.GUI.setcurfile(fn)
else:
pf.warning("I can not find the source file for this application.")
#########################################################################
## The File watcher ##
######################
[docs]class FileWatcher(QtCore.QFileSystemWatcher):
"""Watch for changes in files and then execute the associated function.
"""
def __init__(self, *args):
QtCore.QFileSystemWatcher.__init__(self, *args)
self.filesWatched = {}
self.dirsWatched = {}
[docs] def addWatch(self, path, func):
"""Watch for changes in file and the execute func.
When the specified file is changed, func is executed.
Parameters:
- `path`: path of the file to be watched.
- `func`: function to be executed.
"""
self.filesWatched[path] = func
self.addPath(path)
self.fileChanged.connect(self.onFileChanged)
[docs] def removeWatch(self, path):
"""Remove the watch for file path"""
try:
del self.filesWatched[path]
self.removeWatch(path)
except Exception:
pass
def onFileChanged(self, path):
print("FileWatcher: file %s has changed" % path)
f = self.filesWatched.get(path, None)
if f:
f(path)
#########################################################################
## The GUI ##
#############
def getMenuData(name):
modname = 'pyformex.gui.menus.'+name
__import__(modname)
module = sys.modules[modname]
return module.MenuData
# try:
# print("Importing %s menu" % name)
# __import__('pyformex.plugins.menus.'+name)
# except Exception:
# print("Could not load menu '%s'" % name)
# module = globals().get(name, None)
# if isinstance(module, types.ModuleType) and hasattr(module, 'MenuData'):
# return module.MenuData
# TODO: THESE FUNCTION SHOULD BECOME app METHODS after we create an app class
[docs]def setAppStyle(style):
"""Set the main application style."""
style = QtWidgets.QStyleFactory().create(style)
print(style)
# Causes segmentation fault
#pf.app.setStyle(style)
[docs]def setAppFont(font):
"""Set the main application font.
font is either a QFont or a string resulting from the
QFont.toString() method
"""
if not isinstance(font, QtGui.QFont):
f = QtGui.QFont()
f.fromString(font)
font = f
pf.app.setFont(font)
[docs]def setAppFontFamily(family):
"""Set the main application font family to the given family."""
font = pf.app.font()
font.setFamily(family)
setAppFont(font)
[docs]def setAppFontSize(size):
"""Set the main application font size to the given point size."""
font = pf.app.font()
font.setPointSize(int(size))
setAppFont(font)
[docs]def setAppearance():
"""Set all the GUI appearance elements.
Sets the GUI appearance from the current configuration values
'gui/style', 'gui/font', 'gui/fontfamily', 'gui/fontsize'.
"""
style = pf.cfg['gui/style']
font = pf.cfg['gui/font']
family = pf.cfg['gui/fontfamily']
size = pf.cfg['gui/fontsize']
# Setting style causes segmentation errors with pyside2
# if style:
# print("DEBUG: set style to %s" % style)
# setAppStyle(style)
if font or family or size:
if not font:
font = pf.app.font()
#print("CURRENT FONT %s" % font)
if family:
#print("DEBUG: set family to %s" % family)
font.setFamily(family)
if size:
#print("DEBUG: set size to %s" % size)
font.setPointSize(size)
setAppFont(font)
[docs]class Gui(QtWidgets.QMainWindow):
"""Implements a GUI for pyformex."""
toolbar_area = {'top': QtCore.Qt.TopToolBarArea,
'bottom': QtCore.Qt.BottomToolBarArea,
'left': QtCore.Qt.LeftToolBarArea,
'right': QtCore.Qt.RightToolBarArea,
}
def __init__(self, windowname, size=(800, 600), pos=(0, 0), bdsize=(0, 0)):
"""Constructs the GUI.
The GUI has a central canvas for drawing, a menubar and a toolbar
on top, and a statusbar at the bottom.
"""
pf.debug('Creating Main Window', pf.DEBUG.GUI)
self.on_exit = []
QtWidgets.QMainWindow.__init__(self)
self.fullscreen = False
self.setWindowTitle(windowname)
# add widgets to the main window
# The status bar
pf.debug('Creating Status Bar', pf.DEBUG.GUI)
self.statusbar = self.statusBar()
#self.statusbar.setSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Minimum)
#self.statusbar.setFixedHeight(32)
#self.statusbar.setContentsMargins(0,0,0,0)
#widgets.addEffect(self.statusbar,color=(255,0,0))
from pyformex.gui.menus import File
self.curproj = widgets.ButtonBox('Project:', [('None', File.openExistingProject)])
self.curfile = widgets.ButtonBox('', [('Script:', toggleAppScript), ('None', File.openScript)])
self.curdir = widgets.ButtonBox('Cwd:', [('None', draw.askDirname)])
self.canPlay = False
self.canEdit = False
# The menu bar
pf.debug('Creating Menu Bar', pf.DEBUG.GUI)
self.menu = menu.MenuBar('TopMenu')
self.setMenuBar(self.menu)
# The toolbar
pf.debug('Creating ToolBar', pf.DEBUG.GUI)
self.toolbar = self.addToolBar('Top ToolBar')
self.editor = None
# Create a box for the central widget
self.box = QtWidgets.QWidget()
self.setCentralWidget(self.box)
self.boxlayout = QtWidgets.QVBoxLayout()
self.boxlayout.setContentsMargins(*pf.cfg['gui/boxmargins'])
self.box.setLayout(self.boxlayout)
#self.box.setFrameStyle(qt.QFrame.Sunken | qt.QFrame.Panel)
#self.box.setLineWidth(2)
# Create a splitter
self.splitter = QtWidgets.QSplitter()
self.boxlayout.addWidget(self.splitter)
self.splitter.setOrientation(QtCore.Qt.Vertical)
self.splitter.show()
# self.central is the central widget of the main window
# self.viewports is its layout, containing multiple viewports
pf.debug('Creating Central Widget', pf.DEBUG.GUI)
self.central = QtWidgets.QWidget()
self.central.autoFillBackground()
#self.central.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken)
self.central.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
self.central.resize(*pf.cfg['gui/size'])
self.centralgrid = QtWidgets.QGridLayout()
if pf.options.canvas:
self.viewports = viewport.MultiCanvas(parent=self.central)
self.centralgrid.addLayout(self.viewports, 0, 0)
self.central.setLayout(self.centralgrid)
self.splitter.addWidget(self.central)
# Create the message board / interpreter
self.board = self.console = None
self.createConsole(pf.cfg['gui/console'])
#self.splitter.setSizes([(800,200),(800,600)])
self.box.setLayout(self.boxlayout)
################# MENU ###########################
pf.debug('Creating Menus: %s' % pf.cfg['gui/menu'], pf.DEBUG.GUI)
menudata = [getMenuData(key) for key in pf.cfg['gui/menu']]
self.menu.insertItems(menudata)
self.menu.show()
# Define Toolbar contents
self.actions = toolbar.addActionButtons(self.toolbar)
# timeout button
toolbar.addTimeoutButton(self.toolbar)
pf.debug('Creating Toolbars', pf.DEBUG.GUI)
self.camerabar = self.updateToolBar('camerabar', 'Camera ToolBar')
self.modebar = self.updateToolBar('modebar', 'RenderMode ToolBar')
self.viewbar = self.updateToolBar('viewbar', 'Views ToolBar')
self.toolbars = [self.toolbar, self.camerabar, self.modebar, self.viewbar]
self.enableToolbars(False)
############### CAMERA menu and toolbar #############
if self.camerabar:
toolbar.addCameraButtons(self.camerabar)
toolbar.addButton(self.camerabar, "Pick to focus", 'focus', draw.pickFocus)
toolbar.addPerspectiveButton(self.camerabar)
############### RENDERMODE menu and toolbar #############
pmenu = self.menu.item('viewport')
mmenu = QtWidgets.QMenu('Render Mode')
modes = ['wireframe', 'smooth', 'smoothwire', 'flat', 'flatwire']
self.modebtns = menu.ActionList(
modes, guifunc.renderMode, menu=mmenu, toolbar=self.modebar)
pmenu.insertMenu(pmenu.item('background color'), mmenu)
mmenu = QtWidgets.QMenu('Wire Mode')
modes = ['none', 'all', 'border', 'feature']
self.wmodebtns = menu.ActionList(
modes, guifunc.wireMode, menu=mmenu, toolbar=None)
pmenu.insertMenu(pmenu.item('background color'), mmenu)
# Add the toggle type buttons
if self.modebar and pf.cfg['gui/wirebutton']:
toolbar.addWireButton(self.modebar)
if self.modebar and pf.cfg['gui/transbutton']:
toolbar.addTransparencyButton(self.modebar)
if self.modebar and pf.cfg['gui/lightbutton']:
toolbar.addLightButton(self.modebar)
if self.modebar and pf.cfg['gui/normalsbutton']:
toolbar.addNormalsButton(self.modebar)
if self.modebar and pf.cfg['gui/shrinkbutton']:
toolbar.addShrinkButton(self.modebar)
if self.modebar:
toolbar.addButton(self.modebar, "Popup dialog to interactively change object rendering", 'objects', Viewport.showObjectDialog)
############### VIEWS menu ################
if pf.cfg['gui/viewmenu']:
if pf.cfg['gui/viewmenu'] == 'main':
parent = self.menu
before = 'help'
else:
parent = self.menu.item('camera')
before = parent.item('---')
self.viewsMenu = menu.Menu('&Views', parent=parent, before=before)
else:
self.viewsMenu = None
# Save front orientation
self.frontview = None
self.setViewButtons(pf.cfg['gui/frontview']
)
## TESTING SAVE CURRENT VIEW ##
self.saved_views = {}
self.saved_views_name = utils.NameSequence('View')
if self.viewsMenu:
name = next(self.saved_views_name)
self.menu.item('camera').addAction('Save View', self.saveView)
# Restore previous pos/size
pf.debug('Restore size/pos', pf.DEBUG.GUI)
self.resize(*size)
self.move(*pos)
self.board.resize(*bdsize)
pf.debug('Set Curdir', pf.DEBUG.GUI)
self.setcurdir()
# redirect standard/error output if option set
#
# TODO: we should redirect it to a buffer and
# wait until GUI shown, then show in board
# else show on stdout/err
#self.board.redirect(pf.cfg['gui/redirect'])
if pf.options.debuglevel:
s = sizeReport(self, 'DEBUG: Main:') + \
sizeReport(self.central, 'DEBUG: Canvas:') + \
sizeReport(self.board, 'DEBUG: Board:')
pf.debug(s, pf.DEBUG.GUI)
# Drawing lock
self.drawwait = pf.cfg['draw/wait']
self.drawlock = drawlock.DrawLock()
# Runall mode register
self.runallmode = False
# Materials and Lights database
self.materials = canvas.createMaterials()
## for m in self.materials:
## print self.materials[m]
# Modeless child dialogs
self.doc_dialog = None
pf.debug('Done initializing GUI', pf.DEBUG.GUI)
# Set up signal/slot connections
self.signals = signals.Signals()
self.signals.FULLSCREEN.connect(self.fullScreen)
self.filewatch = FileWatcher()
# Set up hot keys: hitting the key will emit the corresponding signal
self.hotkey = {
QtCore.Qt.Key_F2: self.signals.SAVE,
# QtCore.Qt.Key_F5: self.signals.FULLSCREEN,
QtCore.Qt.Key_F11: self.signals.FULLSCREEN,
}
[docs] def createConsole(self, config):
"""Create the message board and console
config should be one of the following:
'b' : only board, no console
'bc': a board and separate console
'c' : only console, taking ove the board functions
"""
if 'b' in config:
if self.board is None or self.board == self.console:
self.board = Board()
self.splitter.addWidget(self.board)
if 'c' in config:
if self.console is None:
pf.logger.info("Added experimental console")
from pyformex.gui import pyconsole
self.console = pyconsole.PyConsole(pf.interpreter)
self.console.interpreter.globals = draw.Globals()
self.splitter.addWidget(self.console)
if 'b' not in config:
pf.warning('Running pyFormex without the message board is currently not working correctly yet!')
if self.board is not None and self.board != self.console:
self.board.close()
self.board = self.console
if 'c' not in config:
if self.console is not None:
self.console.close()
self.console = None
pf.console = self.console
[docs] def close_doc_dialog(self):
"""Close the doc_dialog if it is open."""
if self.doc_dialog is not None:
self.doc_dialog.close()
self.doc_dialog = None
[docs] def createView(self, name, angles):
"""Create a new view and add it to the list of predefined views.
This creates a named view with specified angles or, if the name
already exists, changes its angles to the new values.
It adds the view to the views Menu and Toolbar, if these exist and
do not have the name yet.
"""
if name not in self.viewbtns.names():
iconpath = utils.findIcon('userview')
self.viewbtns.add(name, iconpath)
views.setAngles(name, angles)
[docs] def saveView(self, name=None, addtogui=True):
"""Save the current view and optionally create a button for it.
This saves the current viewport ModelView and Projection matrices
under the specified name.
It adds the view to the views Menu and Toolbar, if these exist and
do not have the name yet.
"""
if name is None:
name = next(self.saved_views_name)
self.saved_views[name] = (pf.canvas.camera.modelview, None)
if name not in self.viewbtns.names():
iconpath = utils.findIcon('userview')
self.viewbtns.add(name, iconpath)
[docs] def applyView(self, name):
"""Apply a saved view to the current camera.
"""
m, p = self.saved_views.get(name, (None, None))
if m is not None:
self.viewports.current.camera.setModelview(m)
[docs] def setView(self, view):
"""Change the view of the current GUI viewport, keeping the bbox.
view is the name of one of the defined views.
"""
view = str(view)
if view in self.saved_views:
self.applyView(view)
else:
self.viewports.current.setCamera(angles=view)
self.viewports.current.update()
def updateAppdirs(self):
appMenu.reloadMenu()
def updateToolBars(self):
for t in ['camerabar', 'modebar', 'viewbar']:
self.updateToolBar(t)
def addStatusBarButtons(self):
self.statusbar.addWidget(self.curproj)
self.statusbar.addWidget(self.curfile)
self.statusbar.addWidget(self.curdir)
r = self.statusbar.childrenRect()
self.statusbar.setFixedHeight(r.height()+4)
def addInputBox(self):
self.input = widgets.InputString('Input:', '')
self.statusbar.addWidget(self.input)
def toggleInputBox(self, onoff=None):
if onoff is None:
onoff = self.input.isHidden()
self.input.setVisible(onoff)
def addCoordsTracker(self):
self.coordsbox = widgets.CoordsBox()
self.statusbar.addPermanentWidget(self.coordsbox)
def toggleCoordsTracker(self, onoff=None):
def track(x, y, z):
(X, Y, Z), = pf.canvas.unproject(x, y, z, True)
# print("%s --> %s" % ((x, y, z), (X, Y, Z)))
pf.GUI.coordsbox.setValues([X, Y, Z])
if onoff is None:
onoff = self.coordsbox.isHidden()
if onoff:
func = track
else:
func = None
for vp in self.viewports.all:
vp.trackfunc = func
self.coordsbox.setVisible(onoff)
[docs] def maxCanvasSize(self):
"""Return the maximum canvas size.
The maximum canvas size is the size of the central space in the
main window, occupied by the OpenGL viewports.
"""
return Size(pf.GUI.central)
[docs] def showEditor(self):
"""Start the editor."""
if not hasattr(self, 'editor'):
self.editor = Editor(self, 'Editor')
self.editor.show()
self.editor.setText("Hallo\n")
[docs] def closeEditor(self):
"""Close the editor."""
if hasattr(self, 'editor'):
self.editor.close()
self.editor = None
[docs] def setcurproj(self, project=''):
"""Show the current project name."""
self.curproj.setText(Path(project).name)
[docs] def setcurfile(self, appname):
"""Set the current application or script.
appname is either an application module name or a script file.
"""
is_app = appname != '' and not utils.is_script(appname)
if is_app:
# application
label = 'App:'
name = appname
from pyformex import apps
app = apps.load(appname)
if app is None:
self.canPlay = False
try:
self.canEdit = apps.findAppSource(appname).exists()
except Exception:
self.canEdit = False
else:
self.canPlay = hasattr(app, 'run')
appsource = apps.findAppSource(app)
if appsource:
self.canEdit = apps.findAppSource(app).exists()
else:
print("Could not find source of app '%s'" % app)
self.canEdit = False
else:
# script file
label = 'Script:'
name = Path(appname).name
self.canPlay = self.canEdit = utils.is_pyFormex(appname) or appname.endswith('.pye')
pf.prefcfg['curfile'] = appname
#self.curfile.label.setText(label)
self.curfile.setText(label, 0)
self.curfile.setText(name, 1)
self.enableButtons(self.actions, ['Play', 'Info'], self.canPlay)
self.enableButtons(self.actions, ['Edit'], self.canEdit)
self.enableButtons(self.actions, ['ReRun'], is_app and(self.canEdit or self.canPlay))
self.enableButtons(self.actions, ['Step', 'Continue'], False)
icon = 'ok' if self.canPlay else 'notok'
iconpath = utils.findIcon(icon)
self.curfile.setIcon(QtGui.QIcon(QPixmap(iconpath)), 1)
[docs] def setcurdir(self):
"""Show the current workdir."""
dirname = Path.cwd()
shortname = dirname.name
self.curdir.setText(shortname)
self.curdir.setToolTip(str(dirname))
def setBusy(self, busy=True, force=False):
if busy:
pf.app.setOverrideCursor(QtCore.Qt.WaitCursor)
else:
pf.app.restoreOverrideCursor()
self.processEvents()
[docs] def resetCursor(self):
"""Clear the override cursor stack.
This will reset the application cursor to the initial default.
"""
while pf.app.overrideCursor():
pf.app.restoreOverrideCursor()
self.processEvents()
[docs] def keyPressEvent(self, e):
"""Top level key press event handler.
Events get here if they are not handled by a lower level handler.
Every key press arriving here generates a WAKEUP signal, and if a
dedicated signal for the key was installed in the keypress table,
that signal is emitted too.
Finally, the event is removed.
"""
key = e.key()
pf.debug('Key %s pressed' % key, pf.DEBUG.GUI)
self.signals.WAKEUP.emit()
signal = self.hotkey.get(key, None)
if signal is not None:
signal.emit()
e.ignore()
# TODO: check if this is still needed !
[docs] def XPos(self):
"""Get the main window position from the xwininfo command.
The Qt position does not get updated when
changing the window size from the left.
This substitute function will find the correct position from
the xwininfo command output.
"""
res = xwininfo(self.winId())
ax, ay, rx, ry = [int(res[key]) for key in [
'Absolute upper-left X', 'Absolute upper-left Y',
'Relative upper-left X', 'Relative upper-left Y',
]]
return ax-rx, ay-ry
[docs] def XGeometry(self):
"""Get the main window position and size from the xwininfo command.
"""
res = xwininfo(self.winId())
x, y, w, h = [int(res[key]) for key in [
'Absolute upper-left X', 'Absolute upper-left Y',
'Width', 'Height',
]]
return x, y, w, h
[docs] def writeSettings(self):
"""Store the GUI settings
"""
pf.debug('Store current settings', pf.DEBUG.CONFIG)
# TODO: check if this is still needed
# FIX QT4 BUG
# Make sure QT4 has position right
self.move(*self.XPos())
# store the history and main window size/pos
pf.prefcfg['gui/scripthistory'] = pf.GUI.scripthistory.files
pf.prefcfg['gui/apphistory'] = pf.GUI.apphistory.files
pf.prefcfg.update({'size': Size(pf.GUI),
'pos': Pos(pf.GUI),
'bdsize': Size(pf.GUI.board),
}, name='gui')
# TODO: this can be removed?
[docs] def processEvents(self):
"""Process interactive GUI events."""
if pf.app:
pf.app.processEvents()
[docs] def findDialog(self, name):
"""Find the InputDialog with the specified name.
Returns the list with maching dialogs, possibly empty.
"""
return self.findChildren(widgets.InputDialog, str(name))
[docs] def closeDialog(self, name):
"""Close the InputDialog with the specified name.
Closest all the InputDialogs with the specified caption
owned by the GUI.
"""
for w in self.findDialog(name):
w.close()
# TODO: This should go to a toolbar class
def reloadActionButtons(self):
for b in self.actions:
self.toolbar.removeAction(self.actions[b].defaultAction())
self.actions = toolbar.addActionButtons(self.toolbar)
[docs] def startRun(self):
"""Change the GUI when an app/script starts running.
This method enables/disables the parts of the GUI that should or
should not be available while a script is running
It is called by the application executor.
"""
self.drawlock.allow()
if pf.options.canvas:
pf.canvas.update()
self.enableButtons(self.actions, ['ReRun'], False)
self.enableButtons(self.actions, ['Play', 'Step', 'Continue', 'Stop'], True)
# by default, we run the script in the current GUI viewport
if pf.options.canvas:
pf.canvas = pf.GUI.viewports.current
if pf.GUI.board == pf.GUI.console:
pf.GUI.console.boardmode = True
pf.app.processEvents()
[docs] def stopRun(self):
"""Change the GUI when an app/script stops running.
This method enables/disables the parts of the GUI that should or
should not be available when no script is being executed.
It is called by the application executor when an application stops.
"""
self.drawlock.release()
pf.canvas.update()
self.enableButtons(self.actions, ['Play', 'ReRun'], True)
self.enableButtons(self.actions, ['Step', 'Continue', 'Stop'], False)
# acknowledge viewport switching
pf.canvas = pf.GUI.viewports.current
if pf.GUI.board == pf.GUI.console:
pf.GUI.console.boardmode = False
pf.app.processEvents()
[docs] def cleanup(self):
"""Cleanup the GUI (restore default state)."""
pf.debug('GUI cleanup', pf.DEBUG.GUI)
self.drawlock.release()
pf.canvas.cancel_selection()
pf.canvas.cancel_draw()
draw.clear_canvas()
self.resetCursor()
[docs] def onExit(self, func):
"""Register a function for execution on exit"""
self.on_exit.append(func)
[docs] def closeEvent(self, event):
"""Override the close event handler.
We override the default close event handler for the main
window, to allow the user to cancel the exit, and to save
the latest settings.
"""
#
# DEV: things going wrong during the event handler are hard to debug!
# You can add those things to a function and add the function to a
# menu for testing. At the end of the file helpMenu.py there is an
# example (commented out).
#
from pyformex import script
pf.GUI.cleanup()
if pf.options.gui:
script.force_finish()
if exitDialog():
self.drawlock.free()
dooze = pf.cfg['gui/dooze']
if dooze > 0:
print("Exiting in %s seconds" % dooze)
draw.sleep(dooze)
# force reset redirect
sys.stderr.flush()
sys.stdout.flush()
sys.stderr = sys.__stderr__
sys.stdout = sys.__stdout__
pf.debug("Executing registered exit functions", pf.DEBUG.GUI)
for f in self.on_exit:
pf.debug(f, pf.DEBUG.GUI)
f()
self.writeSettings()
event.accept()
else:
event.ignore()
[docs] def fullScreen(self, onoff=None):
"""Toggle the canvas full screen mode.
Fullscreen mode hides all the components of the main window, except
for the central canvas, maximizes the main window, and removes the
window decorations, thus leaving only the OpenGL canvas on the full
screen. (Currently there is also still a small border remaining.)
This mode is activated by pressing the F5 key. A second F5 press
will revert to normal display mode.
"""
hide = [self.board, self.statusbar, self.menu] + self.toolbars
if self.console:
hide.append(self.console)
if onoff is None:
onoff = not self.fullscreen
if onoff:
# goto fullscreen
for w in hide:
w.hide()
self.boxlayout.setContentsMargins(0, 0, 0, 0)
self.showFullScreen()
else:
# go to normal mode
for w in hide:
w.show()
self.boxlayout.setContentsMargins(*pf.cfg['gui/boxmargins'])
self.showNormal()
self.update()
self.fullscreen = onoff
pf.app.processEvents()
[docs]def exitDialog():
"""Show the exit dialog to the user.
"""
from pyformex.gui.menus import File
confirm = pf.cfg['gui/exitconfirm']
## print "confirm = %s" % confirm
## print "pf.PF.filename = %s" % pf.PF.filename
## print "pf.PF.hits = %s" % pf.PF.hits
if confirm == 'never':
return True
if confirm == 'smart' and (pf.PF.filename is None or pf.PF.hits == 0):
return True
print("Project variable changes: %s" % pf.PF.hits)
print("pyFormex globals: %s" % list(pf.PF.keys()))
save_opts = ['To current project file', 'Under another name', 'Do not save']
res = draw.askItems(
[draw._I('info', itemtype='label', value="You have unsaved global variables. What shall I do?"),
draw._I('save', itemtype='vradio', choices=save_opts, text='Save the current globals'),
draw._I('reopen', pf.cfg['openlastproj'], text="Reopen the project on next startup"),
],
caption='pyFormex exit dialog')
if not res:
# Cancel the exit
return False
save = save_opts.index(res['save'])
if save == 0:
File.saveProject()
elif save == 1:
File.saveAsProject()
if not res['reopen']:
File.closeProject(save=False, clear=False)
return True
[docs]def xwininfo(windowid=None, name=None):
"""Returns the X window info parsed as a dict.
Either the windowid or the window name has to be specified.
"""
cmd = 'xwininfo %s'
if windowid is not None:
args = " -id %s" % windowid
elif name is not None:
args = " -name '%s'" % name
else:
raise ValueError("Either windowid or name have to be specified")
P = process.run(cmd % args)
res = {}
if not P.returncode:
for line in P.stdout.split('\n'):
s = line.split(':')
if len(s) < 2:
s = s[0].strip().split(' ')
if len(s) < 2:
continue
elif len(s) > 2:
if s[0] == 'xwininfo':
s = s[-2:] # remove the xwininfo string
t = s[1].split()
s[1] = t[0] # windowid
name = ' '.join(t[1:]).strip().strip('"')
res['Window name'] = name
if s[0][0] == '-':
s[0] = s[0][1:]
res[s[0].strip()] = s[1].strip()
return res
[docs]def pidofxwin(windowid):
"""Returns the PID of the process that has created the window.
Remark: Not all processes store the PID information in the way
it is retrieved here. In many cases (X over network) the PID can
not be retrieved. However, the intent of this function is just to
find a dangling pyFormex process, and should probably work on
a normal desktop configuration.
"""
import re
#
# We need a new shell here, otherwise we get a 127 exit.
#
P = process.run("xprop -id '%s' _NET_WM_PID" % windowid, shell=True)
m = re.match(r"_NET_WM_PID\(.*\)\s*=\s*(?P<pid>\d+)", P.stdout)
if m:
pid = m.group('pid')
return int(pid)
return None
[docs]def windowExists(windowname):
"""Check if a GUI window with the given name exists.
On X-Window systems, we can use the xwininfo command to find out whether
a window with the specified name exists.
"""
P = process.run('xwininfo -name "%s" > /dev/null 2>&1' % windowname,
shell=True)
return P.returncode == 0
[docs]def findOldProcesses(max=16):
"""Find old pyFormex GUI processes still running.
There is a maximum to the number of processes that can be detected.
16 will suffice largely, because there is no sane reason to open that many
pyFormex GUI's on the same screen.
Returns the next available main window name, and a list of
running pyFormex GUI processes, if any.
"""
windowname = pf.Version()
count = 0
running = []
while count < max:
info = xwininfo(name=windowname)
if info:
name = info['Window name']
windowid = info['Window id']
if name == windowname:
pid = pidofxwin(windowid)
else:
pid = None
# pid control needed for invisible windows on ubuntu
if pid:
running.append((windowid, name, pid))
count += 1
windowname = '%s (%s)' % (pf.Version(), count)
else:
break
else:
break
return windowname, running
[docs]def killProcesses(pids):
"""Kill the processes in the pids list."""
warning = """..
Killing processes
-----------------
I will now try to kill the following processes::
%s
You can choose the signal to be sent to the processes:
- KILL (9)
- TERM (15)
We advice you to first try the TERM(15) signal, and only if that
does not seem to work, use the KILL(9) signal.
""" % pids
actions = ['Cancel the operation', 'KILL(9)', 'TERM(15)']
answer = draw.ask(warning, actions)
if answer == 'TERM(15)':
utils.killProcesses(pids, 15)
elif answer == 'KILL(9)':
utils.killProcesses(pids, 9)
########################
# Main application
########################
[docs]class Application(QtWidgets.QApplication):
"""The interactive Qt application"""
def __init__(self, args):
QtWidgets.QApplication.__init__(self, args)
def currentStyle(self):
return self.style().metaObject().className()[1:-5]
def getStyles(self):
return [str(k) for k in QtWidgets.QStyleFactory().keys()]
[docs]def showSplash():
"""Show the splash screen"""
pf.debug("Loading the splash image", pf.DEBUG.GUI)
splash = None
splash_path = pf.cfg['gui/splash']
if splash_path.exists():
pf.debug('Loading splash %s' % splash_path, pf.DEBUG.GUI)
splashimage = QPixmap(splash_path)
splash = QtWidgets.QSplashScreen(splashimage)
splash.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.SplashScreen)
splash.setFont(QtGui.QFont("Helvetica", 20))
splash.showMessage(pf.Version(), QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop, QtCore.Qt.red)
splash.show()
return splash
[docs]def startGUI(args):
"""Create the Qt application and GUI.
A (possibly empty) list of command line options should be provided.
Qt wil remove the recognized Qt and X11 options.
"""
# This seems to be the only way to make sure the numeric conversion is
# always correct
#
QtCore.QLocale.setDefault(QtCore.QLocale.c())
#
#pf.options.debug = -1
pf.debug("Arguments passed to the QApplication: %s" % args, pf.DEBUG.INFO)
pf.app = Application(args)
#
pf.debug("Arguments left after constructing the QApplication: %s" % args, pf.DEBUG.INFO)
pf.debug("Arguments left after constructing the QApplication: %s" % '\n'.join(pf.app.arguments()), pf.DEBUG.INFO)
#pf.options.debug = 0
# As far as I have been testing this, the args passed to the Qt application are
# NOT acknowledged and neither are they removed!!
pf.debug("Setting application attributes", pf.DEBUG.INFO)
pf.app.setOrganizationName("pyformex.org")
pf.app.setOrganizationDomain("pyformex.org")
pf.app.setApplicationName("pyFormex")
pf.app.setApplicationVersion(pf.__version__)
## pf.settings = QtCore.QSettings("pyformex.org", "pyFormex")
## pf.settings.setValue("testje","testvalue")
# set the appearance
pf.debug("Setting Appearance", pf.DEBUG.GUI)
setAppearance()
# Quit application if last window closed
pf.app.lastWindowClosed.connect(pf.app.quit)
# Set OpenGL format and check if we have DRI
dri = hasDRI()
# Check for existing pyFormex processes
pf.debug("Checking for running pyFormex", pf.DEBUG.INFO)
if pf.X11:
windowname, running = findOldProcesses()
else:
windowname, running = "UNKOWN", []
pf.debug("%s,%s" % (windowname, running), pf.DEBUG.INFO)
while len(running) > 0:
if len(running) >= 16:
print("Too many open pyFormex windows --- bailing out")
return -1
pids = [i[2] for i in running if i[2] is not None]
warning = """..
pyFormex is already running on this screen
------------------------------------------
A main pyFormex window already exists on your screen.
If you really intended to start another instance of pyFormex, you
can just continue now.
The window might however be a leftover from a previously crashed pyFormex
session, in which case you might not even see the window anymore, nor be able
to shut down that running process. In that case, you would better bail out now
and try to fix the problem by killing the related process(es).
If you think you have already killed those processes, you may check it by
rerunning the tests.
"""
actions = ['Really Continue', 'Rerun the tests', 'Bail out and fix the problem']
if pids:
warning += """
I have identified the process(es) by their PID as::
%s
If you trust me enough, you can also have me kill this processes for you.
""" % pids
actions[2:2] = ['Kill the running processes']
if dri:
answer = draw.ask(warning, actions)
else:
warning += """
I have detected that the Direct Rendering Infrastructure
is not activated on your system. Continuing with a second
instance of pyFormex may crash your XWindow system.
You should seriously consider to bail out now!!!
"""
answer = draw.warning(warning, actions)
if answer == 'Really Continue':
break # OK, Go ahead
elif answer == 'Rerun the tests':
windowname, running = findOldProcesses() # try again
elif answer == 'Kill the running processes':
killProcesses(pids)
windowname, running = findOldProcesses() # try again
else:
return -1 # I'm out of here!
splash = showSplash()
# create GUI, show it, run it
pf.debug("Creating the GUI", pf.DEBUG.GUI)
if splash is not None:
splash.showMessage("Creating the GUI");
desktop = pf.app.desktop()
pf.maxsize = Size(desktop.availableGeometry())
size = pf.cfg['gui/size']
pos = pf.cfg['gui/pos']
bdsize = pf.cfg['gui/bdsize']
size = MinSize(size, pf.maxsize)
# Create the GUI
pf.GUI = Gui(windowname,
pf.cfg['gui/size'],
pf.cfg['gui/pos'],
pf.cfg['gui/bdsize'],
)
# # set the appearance
# pf.debug("Setting Appearance", pf.DEBUG.GUI)
# pf.GUI.setAppearance()
# setup the message board
pf.GUI.board.clear()
pf.GUI.board.write("""%s (C) Benedict Verhegghe
pyFormex comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU General Public License, version 3 or later. See Help->License or the file COPYING for details.
""" % pf.fullVersion())
if pf.GUI.console:
pf.GUI.console.showPrompt()
# Set interaction functions
def show_warning(message, category, filename, lineno, file=None, line=None):
"""Replace the default warnings.showwarning
We display the warnings using our interactive warning widget.
This feature can be turned off by setting
cfg['warnings/popup'] = False
"""
message = str(message)
# NOTE: the following will expand our short message identifier
# with the expanded message text from messages.py
full_message = warnings.formatwarning(message, category, filename, lineno, line)
print(full_message)
res, check = draw.showMessage(full_message, level='warning', check="Do not show this warning anymore in this and future sessions")
utils.filterWarning(message, category=category, save=check[0])
if pf.cfg['warnings/popup']:
warnings.showwarning = show_warning
pf.warning = draw.warning
pf.error = draw.error
# setup the canvas
if pf.options.canvas:
if splash is not None:
splash.showMessage("Creating the canvas");
pf.debug("Setting the canvas", pf.DEBUG.GUI)
pf.GUI.processEvents()
pf.GUI.viewports.changeLayout(1)
pf.GUI.viewports.setCurrent(0)
#pf.canvas = pf.GUI.viewports.current
pf.canvas.setRenderMode(pf.cfg['draw/rendermode'])
draw.reset()
# set canvas background
# (does not work before a draw.reset, do not know why)
pf.canvas.setBackground(color=pf.cfg['canvas/bgcolor'],
image=pf.cfg['canvas/bgimage'])
pf.canvas.update()
# setup the status bar
pf.debug("Setup status bar", pf.DEBUG.GUI)
pf.GUI.addInputBox()
pf.GUI.toggleInputBox(False)
if pf.options.canvas:
pf.GUI.addCoordsTracker()
pf.GUI.toggleCoordsTracker(pf.cfg['gui/coordsbox'])
pf.debug("Using window name %s" % pf.GUI.windowTitle(), pf.DEBUG.GUI)
# Script/App menu
if splash is not None:
splash.showMessage("Loading script/app menu")
pf.GUI.scriptmenu = appMenu.createAppMenu(
parent=pf.GUI.menu, before='help', mode='script')
pf.GUI.appmenu = appMenu.createAppMenu(
parent=pf.GUI.menu, before='help')
# Create databases
createDatabases()
# Link them in Globals menu
Globals._init_(pf.GUI.database, pf.GUI.selection['geometry'])
# Plugin menus
if splash is not None:
splash.showMessage("Loading plugins");
from pyformex import plugins
filemenu = pf.GUI.menu.item('file')
pf.gui.plugin_menu = plugins.create_plugin_menu(filemenu, before='---1')
# Load configured plugins, ignore if not found
plugins.loadConfiguredPlugins()
# show current application/file
if splash is not None:
splash.showMessage("Load current application");
appname = pf.cfg['curfile']
pf.GUI.setcurfile(appname)
# Last minute menu modifications can go here
# cleanup
if splash is not None:
splash.showMessage("Set status bar");
pf.GUI.addStatusBarButtons()
pf.debug("Showing the GUI", pf.DEBUG.GUI)
if splash is not None:
splash.showMessage("Show the GUI");
pf.GUI.show()
if splash is not None:
# remove the splash window
splash.finish(pf.GUI)
# redirect standard output to board
# TODO: this should disappear when we have buffered stdout
# and moved this up into GUI init
pf.debug("Redirection", pf.DEBUG.GUI)
if pf.cfg['gui/redirect']:
pf.GUI.board.redirect(True)
pf.debug("Update", pf.DEBUG.GUI)
pf.GUI.update()
if pf.cfg['gui/fortune']:
P = process.run(pf.cfg['fortune'])
if P.returncode == 0:
draw.showInfo(P.stdout)
# display startup warning
if pf.cfg['gui/startup_warning']:
utils.warn(pf.cfg['gui/startup_warning'])
# Enable the toolbars
pf.GUI.enableToolbars()
#pf.app.setQuitOnLastWindowClosed(False)
pf.debug("ProcessEvents", pf.DEBUG.GUI)
pf.app_started = True
pf.GUI.processEvents()
# load last project
#
# TODO
if pf.cfg['openlastproj'] and pf.cfg['curproj']:
fn = Path(pf.cfg['curproj'])
if fn.exists():
proj = File.readProjectFile(fn)
if proj:
File.setProject(proj)
#
pf.debug("GUI Started", pf.DEBUG.GUI)
return 0
[docs]def createDatabases():
"""Create unified database objects for all menus."""
from pyformex.plugins import objects
from pyformex.geometry import Geometry
from pyformex.formex import Formex
from pyformex.mesh import Mesh
from pyformex.trisurface import TriSurface
from pyformex.plugins.curve import PolyLine, BezierSpline
from pyformex.plugins.nurbs import NurbsCurve
pf.GUI.database = objects.Objects()
pf.GUI.drawable = objects.DrawableObjects()
pf.GUI.selection = {
'geometry': objects.DrawableObjects(clas=Geometry),
'formex': objects.DrawableObjects(clas=Formex),
'mesh': objects.DrawableObjects(clas=Mesh),
'surface': objects.DrawableObjects(clas=TriSurface),
'polyline': objects.DrawableObjects(clas=PolyLine),
'nurbs': objects.DrawableObjects(clas=NurbsCurve),
'curve': objects.DrawableObjects(clas=BezierSpline),
}
easter_egg='ZnJvbSBkYXRldGltZSBpbXBvcnQgZGF0ZQp0b2RheSA9IGRhdGUudG9kYXkoKQppZiB0b2RheSA8IGRhdGUoMjAxOSw2LDEpIG9yIHRvZGF5ID4gZGF0ZSgyMDE5LDYsMzApOgogICAgZXhpdCgpCmNsZWFyKCkKdGV4dCA9ICdweUZvcm1leCAqMTUgeWVhcnMqIDotKSAnKjMKRiA9IEZvcm1leCgnNDowMTIzJykucmVwbGljKGxlbih0ZXh0KSkKZGEgPSAwLjAwNQpuPTEwMApyID0gbGlzdChyYW5nZShuKSkKRkEgPSBOb25lCmZvciBpIGluIHIgKyByWzo6LTFdOgogICAgYSA9IGkqZGEKICAgIHRvcnNlID0gbGFtYmRhIHgsIHksIHo6IFt4LCBjb3MoYSp4KSp5LXNpbihhKngpKnosIGNvcyhhKngpKnorc2luKGEqeCkqeV0KICAgIEcgPSBGLm1hcCh0b3JzZSkKICAgIEdBID0gZHJhd1RleHQodGV4dCxzaXplPTQwLHBvcz0oMTAsMjAwKSxncmlkPUcpCiAgICB1bmRyYXcoRkEpCiAgICBGQSA9IEdBCiAgICBzbGVlcCgwLjAxKQphdXRvZ2xvYmFscz1Ob25lCg=='
[docs]def runGUI():
"""Go into interactive mode"""
try:
# Make the workdir the current dir
os.chdir(pf.cfg['workdir'])
pf.debug("Setting workdir to %s" % pf.cfg['workdir'], pf.DEBUG.INFO)
except Exception:
# Save the current dir as workdir
Settings.updateSettings({'workdir': Path.cwd(), '_save_': True})
# correctly display the current workdir
pf.GUI.setcurdir()
pf.interactive = True
if pf.cfg['gui/easter_egg'] and True:
pf.debug("Show easter egg", pf.DEBUG.INFO)
try:
draw.playScript(easter_egg, encoding='egg')
except Exception:
pass
pf.debug("Start main loop", pf.DEBUG.INFO)
res = pf.app.exec_()
pf.debug("Exit main loop with value %s" % res, pf.DEBUG.INFO)
return res
#### End