#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## 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/.
##
"""Multiple OpenGL viewports in a Qt widget.
This module provides the MultiCanvas class which allows for multiple
OpenGL viewports in a single Qt OpenGL widget.
"""
import pyformex as pf
from pyformex import arraytools as at
from pyformex import utils
from pyformex.gui import QtWidgets
from . import toolbar
if not pf.sphinx:
if pf.options.mgl:
from opengl3.mglcanvas import *
else:
from .qtcanvas import *
###########################################################################
##### Multiple Viewports #######
[docs]@utils.pzf_register
class MultiCanvas(QtWidgets.QGridLayout):
"""An OpenGL canvas with multiple viewports.
The MultiCanvas implements a central QT widget containing one or more
QtCanvas widgets.
"""
def __init__(self, parent=None):
"""Initialize the multicanvas."""
QtWidgets.QGridLayout.__init__(self)
self.setContentsMargins(0, 0, 0, 0)
self.all = []
self.links = {}
self.current = None
self.ncols = 2
self.rowwise = True
self.pos = None
self.rstretch = None
self.cstretch = None
self.parent = parent
def __getitem__(self, i):
return self.all[i]
[docs] def changeLayout(self, nvps=None, ncols=None, nrows=None, pos=None, rstretch=None, cstretch=None, reset=False):
"""Change the lay-out of the viewports on the OpenGL widget.
nvps: number of viewports
ncols: number of columns
nrows: number of rows
pos: list holding the position and span of each viewport
[[row,col,rowspan,colspan],...]
rstretch: list holding the stretch factor for each row
cstretch: list holding the stretch factor for each column
(rows/columns with a higher stretch factor take more of the
available space)
Each of this parameters is optional.
If a number of viewports is given, viewports will be added
or removed to match the requested number.
By default they are laid out rowwise over two columns.
If ncols is an int, viewports are laid out rowwise over ncols
columns and nrows is ignored. If ncols is None and nrows is an int,
viewports are laid out columnwise over nrows rows. Alternatively,
the pos argument can be used to specify the layout of the viewports.
"""
# add or remove viewports to match the requested number
if at.isInt(nvps):
while len(self.all) > (0 if reset else nvps):
self.removeView()
while len(self.all) < nvps:
self.addView()
# get the new layout definition
if at.isInt(ncols):
rowwise = True
pos = None
elif at.isInt(nrows):
ncols = nrows
rowwise = False
pos = None
elif isinstance(pos, list) and len(pos) == len(self.all):
ncols = None
rowwise = None
else:
return
# remove the viewport widgets
for w in self.all:
self.removeWidget(w)
# assign the new layout arguments
self.ncols = ncols
self.rowwise = rowwise
self.pos = pos
self.rstretch = rstretch
self.cstretch = cstretch
# add the viewport widgets
for w in self.all:
self.showWidget(w)
[docs] def newView(self, shared=True, settings=None):
"""Create a new viewport.
If shared is True, and the MultiCanvas already has one or more
viewports, the new viewport will share display lists and textures
with the first viewport. Since pyFormex is not using display
lists (anymore) and textures are needed to display text, the value
defaults to True, and all viewports will share the same textures,
unless a viewport is created with a specified value for shared:
it can either be another viewport to share textures with, or a
value False or None to not share textures with any viewport. In the
latter case you will not be able to use text display, unless you
initialize the textures yourself.
settings: can be a legal CanvasSettings to initialize the viewport.
Default is to copy settings of the current viewport.
Returns the created viewport, which is an instance of QtCanvas.
"""
if not isinstance(shared, QtCanvas):
if shared and len(self.all) > 0:
shared = self.all[0]
else:
shared = None
# Now shared should be a QtCanvas or None
pf.debug("New viewport sharing textures with %s" % shared, pf.DEBUG.DRAW)
if settings is None:
try:
settings = self.current.settings
except Exception:
settings = {}
#pf.debug("Create new viewport with settings:\n%s"%settings, pf.DEBUG.CANVAS)
##
## BEWARE: shared should be positional, settings should be keyword !
canv = QtCanvas(self.parent, shared, settings=settings)
#print(canv.settings)
return(canv)
[docs] def addView(self):
"""Add a new viewport to the widget"""
canv = self.newView()
if len(self.all) > 0:
# copy default settings from previous
canv.resetDefaults(self.all[-1].settings)
self.all.append(canv)
self.showWidget(canv)
canv.initializeGL() # Initialize OpenGL context and camera
self.setCurrent(canv)
[docs] def setCurrent(self, canv):
"""Make the specified viewport the current one.
canv can be either a viewport or viewport number.
"""
if at.isInt(canv) and canv in range(len(self.all)):
canv = self.all[canv]
if canv == self.current:
pass
elif canv in self.all:
if self.current:
self.current.focus = False
self.current.updateGL()
self.current = canv
# Only show focus if more than 1
self.current.focus = len(pf.GUI.viewports.all) > 1 and pf.cfg['gui/showfocus']
self.current.updateGL()
toolbar.updateViewportButtons(self.current)
pf.canvas = self.current
[docs] def viewIndex(self, view):
"""Return the index of the specified view"""
return self.all.index(view)
[docs] def currentView(self):
"""Return the index of the current view"""
return self.all.index(self.current)
[docs] def removeView(self):
"""Remove the last view"""
if len(self.all) > 1:
w = self.all.pop()
lastnr = len(self.all)
if lastnr in self.links:
del self.links[lastnr]
if self.pos is not None:
self.pos = self.pos[:-1]
if self.current == w:
self.setCurrent(self.all[-1])
self.removeWidget(w)
w.close()
# Remove focus rectangle if only one left
if len(self.all) == 1:
self.current.focus = False
# set the stretch factors
pos = [self.getItemPosition(self.indexOf(w)) for w in self.all]
if self.rstretch is not None:
row = max([p[0]+p[2] for p in pos])
for i in range(row, len(self.rstretch)):
self.setRowStretch(i, 0)
self.rstretch = self.rstretch[:row]
if self.cstretch is not None:
col = max([p[1]+p[3] for p in pos])
for i in range(col, len(self.cstretch)):
self.setColumnStretch(i, 0)
self.cstretch = self.cstretch[:col]
## def setCamera(self,bbox,view):
## self.current.setCamera(bbox,view)
def updateAll(self):
pf.debug("UPDATING ALL VIEWPORTS", pf.DEBUG.GUI)
for v in self.all:
v.update()
pf.app.processEvents()
def printSettings(self):
for i, v in enumerate(self.all):
print("""
## VIEWPORTS ##
Viewport %s; Current:%s; Settings:
%s
""" % (i, v == self.current, v.settings))
def linkScene(self, vp, to):
vp0 = self.all[to]
vp1 = self.all[vp]
vp1.scene = vp0.scene
vp0.zoomAll()
vp1.zoomAll()
vp0.updateGL()
vp1.updateGL()
pf.app.processEvents()
# TODO: We should probably limit linking to the changelayout case
[docs] def link(self, vp, to):
"""Link viewport vp to to"""
nvps = len(self.all)
vp = checkInt(vp, 0, nvps-1)
to = checkInt(to, 0, nvps-1)
# resolve links
while to != vp and to in self.links:
to = self.links[to]
if to == vp:
raise ValueError("Can not link viewport to itself")
print("Link viewport %s to %s" % (vp, to))
self.links[vp] = to
tovp = self.all[to]
oldvp = self.all[vp]
utils.warn('warn_viewport_linking')
newvp = self.newView(shared=True)
self.all[vp] = newvp
self.removeWidget(oldvp)
oldvp.close()
self.showWidget(newvp)
newvp.scene = tovp.scene
#newvp.scene.actors = to.scene.actors
newvp.show()
newvp.setCamera()
newvp.redrawAll()
#newvp.updateGL()
pf.app.processEvents()
[docs] def settings(self):
"""Return the full configuration needed to restore the MultiCanvas"""
d = {
'gui_size': qtutils.Size(pf.GUI),
'central_size': qtutils.Size(pf.GUI.central),
'nvps': len(self.all),
'rowwise': self.rowwise,
'ncols': self.ncols,
'current': self.viewIndex(self.current),
# 'canvas': self.viewIndex(pf.canvas),
}
for i, vp in enumerate(self.all):
d[f'canvas{i}'] = dict(vp.settings)
d[f'camera{i}'] = vp.camera.settings()
return d
[docs] def loadConfig(self, config):
"""Reset the viewports size, layout and cameras from a dict"""
size = config['gui_size']
if size:
pf.GUI.resize(*size)
size = config['central_size']
if size:
pf.GUI.central.resize(*size)
nvps = config['nvps']
rowwise = config['rowwise']
ncols = config['ncols']
nrows = None
if not rowwise:
ncols, nrows = nrows, ncols
self.changeLayout(nvps = nvps, ncols=ncols, nrows=nrows)
for i in range(len(self.all)):
Ci = config[f'canvas{i}']
canvas = self.all[i]
canvas.settings.update(Ci)
Ci = config[f'camera{i}']
canvas.camera.loadConfig(Ci)
current = config['current']
if current:
self.setCurrent(current)
self.update()
###################
## PZF interface ##
def pzf_dict(self):
return { 'kargs:p': self.settings() }
@classmethod
def pzf_load(clas, **kargs):
return kargs
## DEPRECATED ##
# Deprecated old canvas/camera saving format 2023-01
# TODO: remove after 2023-08
[docs] @utils.deprecated('camera_save')
def save(self, filename):
"""Save the canvas settings to file"""
d = self.settings()
open(filename, 'w').write(f"{d!r}\n")
[docs] @utils.deprecated('camera_save')
def load(self, filename):
"""Load the canvas settings from file"""
f = open(filename, 'r')
t = f.read()
d = eval(t)
self.loadConfig(d)
# End