Related
In my project there is a window for editing files. Each file has some properties that the user can change with several checkboxes. Also each file has an icon.
Now it is implemented through QScrollArea in which widgets are added. It looks like this:
Now I want to add folder support for files. I would like to support drag n drop and so on.
I thought it would be a good idea in my case to use QTreeView.
The widget for the files has already been drawn and it suits me completely:
I haven't worked with QT's model/view framework before. after a couple of days of trying to draw something acceptable, I broke my brain. That's what I did:
import sys
from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtCore import *
from scanner.models import *
from gui.styles.settings import *
from gui.utils import setup_fonts
class BaseFileItem(QStandardItem):
def __init__(self, parent=None):
super(BaseFileItem, self).__init__(parent)
self.setEditable(False)
class TitleItem(QStandardItem):
file_type = 'title'
class FolderItem(QStandardItem):
file_type = 'folder'
class PreviewItem(BaseFileItem):
file_type = 'preview'
class FileItem(BaseFileItem):
file_type = 'file'
class Model(QStandardItemModel):
def supportedDropActions(self):
return Qt.MoveAction
class Delegate(QStyledItemDelegate):
checkbox_width = 50
checkboxes = [
'main',
'preview',
'use',
]
def sizeHint(self, option, index):
match index.data(1002):
case 'title':
height = 32
case 'folder':
height = 24
case _:
height = 48
return QSize(height, height)
def paint(self, painter, option, index):
super(Delegate, self).paint(painter, option, index)
match index.data(1002):
case 'title':
item_type = 'label'
case 'folder':
return
case _:
item_type = 'checkbox'
for column, name in enumerate(self.checkboxes):
self.add_checkbox_item(painter, option, column, name, item_type)
def add_checkbox_item(self, painter, option, column, text, item_type):
rect = option.rect
if item_type == 'checkbox':
item = QStyleOptionButton()
ce = QStyle.CE_CheckBox
else:
item = QStyleOptionHeader()
item.text = text
ce = QStyle.CE_HeaderLabel
item.item_type = item_type
x, y = rect.left() + rect.width() - (column + 1) * self.checkbox_width, rect.top()
item.rect = QRect(x, y, self.checkbox_width, rect.height())
QApplication.style().drawControl(ce, item, painter)
class AssetEditFilesView(QWidget):
rows = ['Asset files', 'Previews', 'Candidates']
def __init__(self, files, parent=None):
self.files = files
super(AssetEditFilesView, self).__init__(parent)
self.setContentsMargins(24, 24, 24, 24)
self.model = Model()
self.tree = QTreeView(self)
self.tree.setContentsMargins(4, 4, 4, 4)
self.tree.setItemDelegate(Delegate(self.tree))
self.tree.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tree.setSortingEnabled(True)
self.tree.setDragEnabled(True)
self.tree.setAcceptDrops(True)
self.tree.setDropIndicatorShown(True)
self.tree.header().hide()
self.tree.setModel(self.model)
self.import_data(files)
self.tree.expandAll()
layout = QVBoxLayout(self)
layout.addWidget(self.tree)
def import_data(self, data):
self.model.setRowCount(0)
for group, files in zip(self.rows, self.prepare_files(data)):
if files:
self.add_files_to_group(group, files)
def add_files_to_group(self, group, files):
files = sorted(files, key=lambda x: str(x.data().asset_path))
data = [f.data().asset_path.parts for f in files]
max_len = max([len(x) for x in data])
prepared = [x for x in zip(*[[y[i] if i < len(y) else None for i in range(max_len)] for y in data])]
structure = self.create_structure(prepared, files)
root = self.add_file_row(self.model.invisibleRootItem(), group, TitleItem)
for folder, items in structure.items():
file_model = PreviewItem if group == 'Previews' else FileItem
self.add_items(root, folder, items, file_model=file_model)
def add_items(self, root, folder, items, file_model=FileItem):
if folder == '.':
for item in items:
self.add_file_row(root, item, file_model)
else:
new_root = self.add_file_row(root, folder, FolderItem)
for _folder, _items in items.items():
self.add_items(new_root, _folder, _items, file_model=file_model)
def add_file_row(self, root, item, file_model):
item_name = str(item.filename.name) if hasattr(item, 'filename') else item
item_model = file_model(item_name)
item_model.setData(item, 1001)
item_model.setData(file_model.file_type, 1002)
print(root)
root.appendRow(item_model)
return item_model
def create_structure(self, prepared, files):
structure = {'.': []}
for *parts, file in zip(*prepared, [f.data() for f in files]):
parts = list(filter(lambda x: x is not None, parts))
parent = structure
for part in parts:
if part not in parent:
parent[part] = {'.': []}
parent = parent[part]
parent['.'].append(file)
return structure
def prepare_files(self, data):
groups = [[], [], []]
for file in data:
if type(file) in [PreviewImage, PreviewVideo]:
group = 1
elif isinstance(file, BaseFileModel):
group = 0
else:
group = 2
item = QStandardItem(str(file.filename.name))
item.setData(file)
groups[group].append(item)
return groups
style = f'''
AssetEditFilesView QTreeView {{
{Fonts.normal}
border: 2px;
border-radius: 8px;
background-color: {Colors.asset_edit_bg};
}}
AssetEditFilesView {{
background-color: {Colors.popup_bg};
}}
AssetEditFilesView {{
background-color: {Colors.popup_bg};
'''
if __name__ == '__main__':
files = [
BaseFileModel(filename=r'c:\temp0036.png'),
BaseFileModel(filename=r'c:\temp0036.png'),
BaseFileModel(filename=r'c:\temp0036.png', asset_path=Path('./huita/huyatina')),
BaseFileModel(filename=r'c:\temp0048.png', asset_path=Path('./huita')),
BaseFileModel(filename=r'c:\temp0127.png', asset_path=Path('./ne_huita')),
BaseFileModel(filename=r'c:\temp0229.png', asset_path=Path('./ne_huita/huiyatina')),
PreviewVideo(filename=r'c:\references.gif', asset_path=Path('./previews')),
PreviewImage(filename=r'c:\temp321.png', asset_path=Path('./previews')),
PreviewImage(filename=r'c:\temp0267.png', asset_path=Path('./previews/generated')),
PreviewImage(filename=r'c:\temp3.png', asset_path=Path('./previews/generated/1/2')),
PreviewImage(filename=r'c:\temp.bin', asset_path=Path('./previews')),
PreviewImage(filename=r'c:\temp.bin.png', asset_path=Path('./previews')),
PreviewImage(filename=r'c:\temp.png', asset_path=Path('./previews/generated/2/1')),
PreviewImage(filename=r'c:\temp.png', asset_path=Path('./previews/generated/1/2')),
PreviewImage(filename=r'c:\temp.png', asset_path=Path('./previews/generated/1')),
]
app = QApplication(sys.argv)
setup_fonts(app)
app.setStyleSheet(style)
view = AssetEditFilesView(files)
view.setGeometry(800, 500, 800, 640)
view.setWindowTitle('QTreeview Example')
view.show()
sys.exit(app.exec())
I have 3 entities:
file (should look like on the 1st screenshot (3 checkboxes and an icon))
folder (must contain a name)
title (in fact the folder only the font should be larger)
how do i implement all entities using 1 QStyledItemDelegate?
Can I use a ready-made custom QWidget as a file entity?
the only thing I managed to do was to implement a more or less working sizeHint, but I have no idea how to add a file widget.
I will be grateful for any help, as now I am close to implementing all this through QScrollArea instead of QTreeView
i need some thing like this:
I also can't figure out how to apply styles to a QStandardItem, QStyledItemDelegate,
The code bellow try to embed visvis figure in a wx application to make a movie with 3D data. The problem is that everytime I rum the code, I get the error
"./src/unix/glx11.cpp(86): assert "xid" failed in SetCurrent(): window
must be shown" right after calling Figure = app.GetFigureClass()
self.fig = Figure(self)
I spent some time researching this error, but none of the answers suited me. Does anyone knows how to fix it?
import wx
import h5py
import numpy as np
import visvis as vv
app = vv.use('wx')
class CPlot3D (wx.Frame) :
"""
Class for plotting 3D Dirac
"""
def data_for_plotting (self, frame_number) :
"""
Load the data to be plotted for the frame with the frame_number
"""
frame = str(self.frame_names[frame_number])
return self.f[frame][...]
def draw_curent_frame (self, event=None) :
"""
Display the current frame
"""
# Load data
data = self.data_for_plotting (self.frame_number.GetValue())
try :
self.volume_plot.SetData (data)
except AttributeError :
vv.clf()
self.volume_plot = vv.volshow (data, clim=(self.global_min, self.global_max), renderStyle='mip', cm=vv.CM_JET)
a = vv.gca()
vv.ColormapEditor(a)
def GetTicks (axis_num, min_val, max_val, label_format="%.2f") :
size = data.shape[axis_num]
# Number of ticks
nticks = int(np.ceil(np.log2(size)))
nticks += 1 - nticks % 2 # Make <nticks> odd
ticks_position = np.linspace(0, size-1, nticks)
ticks_label = map( lambda x : label_format % x, np.linspace(min_val, max_val, nticks) )
return dict( zip(ticks_position, ticks_label) )
a.axis.xTicks = GetTicks(0, self.x_min, self.x_max)
a.axis.xLabel = "x (rel. units)"
a.axis.yTicks = GetTicks(1, self.y_min, self.y_max)
a.axis.yLabel = "y (rel. units)"
a.axis.zTicks = GetTicks(2, self.z_min, self.z_max)
a.axis.zLabel = "z (rel. units)"
self.fig.Draw()
def __init__ (self, parent, file_name, title="Plot Dirac 3D") :
# Open files
self.f = h5py.File (file_name, 'r')
# Extract the dimension
self.x_gridDIM = int(self.f['x_gridDIM'][...])
self.y_gridDIM = int(self.f['y_gridDIM'][...])
self.z_gridDIM = int(self.f['z_gridDIM'][...])
self.dx = self.f['dx'][...]
self.x_min = self.f['x_min'][...]
self.x_max = self.x_min + self.x_gridDIM * self.dx
self.y_min = self.f['y_min'][...]
self.y_max = self.y_min + self.y_gridDIM * self.dx
self.z_min = self.f['z_min'][...]
self.z_max = self.z_min + self.z_gridDIM * self.dx
# Collect the frame names
self.frame_names = []
for key in self.f.keys () :
try : self.frame_names.append (int(key))
except ValueError: pass
self.frame_names.sort ()
print "\nGet global maximum and minimum..."
# Find the min and max values in all the frames
for frame_number in range(len(self.frame_names)) :
data = self.data_for_plotting (frame_number)
try :
self.global_min = min( self.global_min, data.min() )
self.global_max = max( self.global_max, data.max() )
except AttributeError :
self.global_min = data.min()
self.global_max = data.max()
print "\nStart animation..."
# Create GUI
dw, dh = wx.DisplaySize()
wx.Frame.__init__ (self, parent, title=title, size=(0.4*dw, 0.6*dh) )
self.ConstructGUI ()
self.Center()
self.Show ()
wx.EVT_CLOSE(self, self.on_close)
self.On_animation_button ()
def on_close (self, event) :
try : self.animation_timer.Stop()
except AttributeError : pass
self.Destroy()
def ConstructGUI (self) :
"""
Make a GUI
"""
######################### Navigation bar ##############################
panel = wx.Panel(self)
boxsizer = wx.BoxSizer (wx.HORIZONTAL)
# Frame number indicator
boxsizer.Add (wx.StaticText(panel, label="Frame Number:"))
self.frame_number = wx.SpinCtrl (panel, value="0", min=0, max=len(self.frame_names)-1)
self.frame_number.Bind (wx.EVT_SPINCTRL, self.draw_curent_frame )
boxsizer.Add (self.frame_number)
# Go to the beginning button
self.go_beginnign_button = wx.Button (panel, label="<<")
self.Bind (wx.EVT_BUTTON, self.go_to_beginning, self.go_beginnign_button)
boxsizer.Add (self.go_beginnign_button)
# Animation button
self.animation_button_start_label = "Play animation "
self.animation_button_stop_label = "STOP animation"
self.animation_button = wx.Button (panel, label=self.animation_button_start_label)
self.Bind (wx.EVT_BUTTON, self.On_animation_button, self.animation_button)
boxsizer.Add (self.animation_button)
# Go to the end button
self.go_end_button = wx.Button (panel, label=">>")
self.Bind (wx.EVT_BUTTON, self.go_to_end, self.go_end_button)
boxsizer.Add (self.go_end_button)
panel.SetSizer (boxsizer)
############################# Setting up visvis binding #######################################
Figure = app.GetFigureClass()
self.fig = Figure(self)
################################### Layout #####################################################
sizer = wx.BoxSizer (wx.VERTICAL)
sizer.Add (panel, flag=wx.CENTER)
sizer.Add(self.fig._widget, 1, flag=wx.EXPAND)
self.SetSizer(sizer)
self.SetAutoLayout(True)
self.Layout()
def On_animation_button (self, event=None) :
"""
<self.animation_button> was clicked
"""
if self.animation_button.GetLabel() == self.animation_button_start_label :
# Begin playing animation
# Set up timer for animation
timer_id = wx.NewId ()
self.animation_timer = wx.Timer (self, timer_id)
self.animation_timer.Start (200)
def on_animation_timer (event) :
self.draw_curent_frame()
position = self.frame_number.GetValue()
if position > len(self.frame_names)-2 : self.On_animation_button ()
else : self.frame_number.SetValue (position+1)
wx.EVT_TIMER (self, timer_id, on_animation_timer)
# Channing the button's label
self.animation_button.SetLabel(self.animation_button_stop_label)
else : # Stop playing animation
self.animation_timer.Stop ()
del self.animation_timer
# Channing the button's label
self.animation_button.SetLabel(self.animation_button_start_label)
def go_to_beginning (self, event) :
"""
<self.go_beginnign_button> was clicked
"""
self.frame_number.SetValue (0)
self.draw_curent_frame()
def go_to_end (self, event) :
"""
<self.go_end_button> was clicked
"""
self.frame_number.SetValue (len(self.frame_names)-1)
self.draw_curent_frame()
if __name__ == '__main__' :
import sys
app.Create()
# Loading files
if len(sys.argv) <> 2 :
openFileDialog = wx.FileDialog(None, "Open HDF5 file to load 3D Dirac", "", "",
"HDF5 files (*.hdf5)|*.hdf5", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_CHANGE_DIR)
# Check whether user canceled
if openFileDialog.ShowModal() == wx.ID_CANCEL:
print "Error: file name is needed as argument"
exit()
else : filename = openFileDialog.GetPath()
else : filename = sys.argv[1]
CPlot3D (None, filename)
app.Run ()
When you call On_animation_button() manually, your frame is not actually shown yet, even though you had called Show() on it because, at least with X11, showing happens asynchronously. So you need to delay calling it until later. This can be done by e.g. binding a lambda doing this to EVT_SIZE event (because by the time you get it, the window is already initialized) or just by using CallAfter().
I need to highlight a specific word in a text within a tkinter frame. In order to find the word, I put a balise like in html. So in a text like "hello i'm in the |house|" I want to highlight the word "house".
My frame is defined like that:
class FrameCodage(Frame):
self.t2Codage = Text(self, height=20, width=50)
and I insert my text with this code: fenetre.fCodage.t2Codage.insert(END, res) , res being a variable containing my text.
I saw this code on an other post:
class CustomText(tk.Text):
'''A text widget with a new method, highlight_pattern()
example:
text = CustomText()
text.tag_configure("red", foreground="#ff0000")
text.highlight_pattern("this should be red", "red")
The highlight_pattern method is a simplified python
version of the tcl code at http://wiki.tcl.tk/3246
'''
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
def highlight_pattern(self, pattern, tag, start="1.0", end="end",
regexp=False):
'''Apply the given tag to all text that matches the given pattern
If 'regexp' is set to True, pattern will be treated as a regular
expression.
'''
start = self.index(start)
end = self.index(end)
self.mark_set("matchStart", start)
self.mark_set("matchEnd", start)
self.mark_set("searchLimit", end)
count = tk.IntVar()
while True:
index = self.search(pattern, "matchEnd","searchLimit",
count=count, regexp=regexp)
if index == "": break
self.mark_set("matchStart", index)
self.mark_set("matchEnd", "%s+%sc" % (index, count.get()))
self.tag_add(tag, "matchStart", "matchEnd")
But they are few things that I don't understand: how can I apply this function to my case? When did I call this function? What's the pattern and the tag in my case? I'm a beginner in Tkinter so don't hesitate to explain to me this code, or another.
Instead of this:
class FrameCodage(Frame):
self.t2Codage = Text(self, height=20, width=50)
... do this:
class FrameCodage(Frame):
self.t2Codage = CustomText(self, height=20, width=50)
Next, create a "highlight" tag, and configure it however you want:
self.t2Codage.tag_configure("highlight", foreground="red")
Finally, you can call the highlight_pattern method as if it were a standard method:
self.t2Codage.highlight_pattern(r"\|.*?\|", "red", regexp=True)
Below is widget I created to deal with this, hope it helps.
try: import tkinter as tk
except ImportError: import Tkinter as tk
class code_editor(tk.Text):
def __init__(self, parent, case_insensetive = True, current_line_colour = '', word_end_at = r""" .,{}[]()=+-*/\|<>%""", tags = {}, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
self.bind("<KeyRelease>", lambda e: self.highlight())
self.case_insensetive = case_insensetive
self.highlight_current_line = current_line_colour != ''
self.word_end = word_end_at
self.tags = tags
if self.case_insensetive:
for tag in self.tags:
self.tags[tag]['words'] = [word.lower() for word in self.tags[tag]['words']]
#loops through the syntax dictionary to creat tags for each type
for tag in self.tags:
self.tag_config(tag, **self.tags[tag]['style'])
if self.highlight_current_line:
self.tag_configure("current_line", background = current_line_colour)
self.tag_add("current_line", "insert linestart", "insert lineend+1c")
self.tag_raise("sel")
#find what is the last word thats being typed.
def last_word(self):
line, last = self.index(tk.INSERT).split('.')
last=int(last)
#this limit issues when user is a fast typer
last_char = self.get(f'{line}.{int(last)-1}', f'{line}.{last}')
while last_char in self.word_end and last > 0:
last-=1
last_char = self.get(f'{line}.{int(last)-1}', f'{line}.{last}')
first = int(last)
while True:
first-=1
if first<0: break
if self.get(f"{line}.{first}", f"{line}.{first+1}") in self.word_end:
break
return {'word': self.get(f"{line}.{first+1}", f"{line}.{last}"), 'first': f"{line}.{first+1}", 'last': f"{line}.{last}"}
#highlight the last word if its a syntax, See: syntax dictionary on the top.
#this runs on every key release which is why it fails when the user is too fast.
#it also highlights the current line
def highlight(self):
if self.highlight_current_line:
self.tag_remove("current_line", 1.0, "end")
self.tag_add("current_line", "insert linestart", "insert lineend+1c")
lastword = self.last_word()
wrd = lastword['word'].lower() if self.case_insensetive else lastword['word']
for tag in self.tags:
if wrd in self.tags[tag]['words']:
self.tag_add(tag, lastword['first'], lastword['last'])
else:
self.tag_remove(tag, lastword['first'], lastword['last'])
self.tag_raise("sel")
#### example ####
if __name__ == '__main__':
# from pyFilename import code_editor
ms = tk.Tk()
example_text = code_editor(
parent = ms,
case_insensetive = True, #True by default.
current_line_colour = 'grey10', #'' by default which will not highlight the current line.
word_end_at = r""" .,{}[]()=+-*/\|<>%""", #<< by default, this will till the class where is word ending.
tags = {#'SomeTagName': {'style': {'someStyle': 'someValue', ... etc}, 'words': ['word1', 'word2' ... etc]}} this tells it to apply this style to these words
"failSynonyms": {'style': {'foreground': 'red', 'font': 'helvetica 8'}, 'words': ['fail', 'bad']},
"passSynonyms":{'style': {'foreground': 'green', 'font': 'helvetica 12'}, 'words': ['Pass', 'ok']},
"sqlSyntax":{'style': {'foreground': 'blue', 'font': 'italic'}, 'words': ['select', 'from']},
},
font='helvetica 10 bold', #Sandard tkinter text arguments
background = 'black', #Sandard tkinter text arguments
foreground = 'white' #Sandard tkinter text arguments
)
example_text.pack()
ms.mainloop()
I need someone's expertise on this exporting problem of mine.
How it works: Select a camera (animated or not is optional) >> File >> Export Selection >> File Type : .chan (need to load this script as a plugin)
Here's where the problem starts. It is able to create a .text file, however, it is not 'exporting' or writing out the contents into the text file and the file size is of zero bytes.
I am making use of the current API that it has been coded, modifying the code to add in some maya cmds
Can someone kindly help me out?
import math, sys, string, os
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMayaAnim as OpenMayaAnim
import maya.cmds as cmds
import maya.mel as mel
kPluginTranslatorTypeName = "chan Export/Import"
kVersionNumber = "0.5a"
camSel = []
win_name = "chan_window"
class CustomNodeTranslator(OpenMayaMPx.MPxFileTranslator):
def __init__(self):
OpenMayaMPx.MPxFileTranslator.__init__(self)
def haveWriteMethod(self):
return True
def haveReadMethod(self):
return True
def filter(self):
return " .chan"
def defaultExtension(self):
return "chan"
def writer( self, fileObject, optionString, accessMode ):
try:
fullName = fileObject.fullName()
fileHandle = open(fullName,"w")
selectList = OpenMaya.MSelectionList()
OpenMaya.MGlobal.getActiveSelectionList(selectList)
node = OpenMaya.MObject()
depFn = OpenMaya.MFnDependencyNode()
path = OpenMaya.MDagPath()
iterator = OpenMaya.MItSelectionList(selectList)
animationTime = OpenMayaAnim.MAnimControl()
maxTime = int(animationTime.maxTime().value())
minTime = int(animationTime.minTime().value())
while (iterator.isDone() == 0):
iterator.getDependNode(node)
depFn.setObject(node)
iterator.getDagPath(path, node)
cameraObject = OpenMaya.MFnCamera(path)
transform = OpenMaya.MFnTransform(path)
chanMe = fileExporter(transform, minTime, maxTime, cameraObject)
for all in chanMe():
fileHandle.write(all)
iterator.next()
fileHandle.close()
except:
sys.stderr.write( "Failed to write file information\n")
raise
def processLine( self, lineStr ):
self.importTheChan.writeFrameData(lineStr)
class fileExporter():
""" module for exporting chan files from application. arguments: object, startFrame, endFrame """
def __init__(self, transform, startAnimation, endAnimation, cameraObj):
self.fileExport = []
self.transform = transform
self.cameraObj = cameraObj
self.start = startAnimation
self.end = endAnimation
self.exportWin()
def exportWin(self):
self.expWindow = cmds.window(w=150, h=100, title = "Export Selection" )
cmds.columnLayout( adjustableColumn=True )
form = cmds.formLayout(numberOfDivisions=100)
cmds.radioCollection()
self.chk1 = cmds.radioButton( label='option1', onc = self.opt1On, ofc = self.opt1Off )
self.chk2 = cmds.radioButton( label='option2', onc = self.opt2On, ofc = self.opt2Off )
self.okayBtn = cmds.button(label='okay!', command=self.runSel, width=150, height=35)
cmds.formLayout(form, edit=True, attachForm=[\
(self.chk1, 'top', 15),\
(self.chk1, 'left', 15),\
(self.chk2, 'top', 30),\
(self.chk2, 'left', 15),\
(self.okayBtn, 'top', 50),\
(self.okayBtn, 'left', 15)])
cmds.showWindow( self.expWindow )
def opt1On(self, args):
print "User checked option1"
startAnimation = cmds.playbackOptions(query=True, minTime=True)
endAnimation = cmds.playbackOptions(query=True, maxTime=True)
self.start = startAnimation
self.end = endAnimation
def opt1Off(self, args):
print "User un-checked option1"
cmds.radioButton(self.chk2, edit = True, enable = True)
self.start = ""
self.end = ""
def opt2On(self, args):
print "User checked option2"
startAnimation = cmds.findKeyframe(which='first')
endAnimation = cmds.findKeyframe(which='last')
self.start = startAnimation
self.end = endAnimation
#self.start.append(int(startAnimation))
#self.end.append(int(endAnimation))
def opt2Off(self, args):
print "User un-checked option2"
self.start = ""
self.end = ""
def runSel(self, args):
chkVal1 = cmds.radioButton(self.chk1, query=True, sl=1)
chkVal2 = cmds.radioButton(self.chk2, query=True, sl=1)
if chkVal1 == 1:
print "opt1 Pressed!"
print self.start
print self.end
self.test()
self.closeWindow()
elif chkVal2 == 1:
print "opt2 Pressed!"
print self.start
print self.end
self.test()
self.closeWindow()
else:
cmds.warning("Check an option")
def closeWindow(self):
cmds.deleteUI(self.expWindow, window=True)
def test(self):
self.actualExp(self.transform, self.start, self.end, self.cameraObj)
def actualExp(self, transform, startAnimation, endAnimation, cameraObj):
mayaGlobal = OpenMaya.MGlobal()
mayaGlobal.viewFrame(OpenMaya.MTime(1))
# Converts the float arguement into integer
for i in range(int(startAnimation), int(endAnimation + 1)):
focalLength = cameraObj.focalLength()
vFilmApp = cameraObj.verticalFilmAperture()
focalOut = 2 math.degrees(math.atan(vFilmApp 25.4/ (2 focalLength)))
myEuler = OpenMaya.MEulerRotation()
spc = OpenMaya.MSpace.kWorld
trans = transform.getTranslation(spc)
rotation = transform.getRotation(myEuler)
rotVector = OpenMaya.MVector(myEuler.asVector())
self.fileExport.append((str(i) + '\t' + str(trans[0]) + "\t" + str(trans[1]) + "\t" + str(trans[2]) + "\t" + str(math.degrees(rotVector[0])) + "\t" + str(math.degrees(rotVector[1])) + "\t" + str(math.degrees(rotVector[2])) + "\t" + str(focalOut) + "\n"))
mayaGlobal.viewFrame(OpenMaya.MTime(i+1))
def __call__(self, args):
return self.fileExport
def radianToDegree(self, radians):
outDegrees = 0.0
outDegrees = (float(radians) / (math.pi)) 180
return outDegrees
# creator
def translatorCreator():
return OpenMayaMPx.asMPxPtr( CustomNodeTranslator() )
# initialize the script plug-in
def initializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.registerFileTranslator(kPluginTranslatorTypeName, None, translatorCreator)
except:
sys.stderr.write( "Failed to register translator: %s" % kPluginTranslatorTypeName )
raise
# uninitialize the script plug-in
def uninitializePlugin(mobject):
mplugin = OpenMayaMPx.MFnPlugin(mobject)
try:
mplugin.deregisterFileTranslator( kPluginTranslatorTypeName )
except:
sys.stderr.write( "Failed to deregister translator: %s" % kPluginTranslatorTypeName )
raise
the __call__ method is what's supposed to provide the contents of the file. It returns self.fileExport, which is an empty list that is not getting populated.
The problem here is the writer method of the plugin will not wait for your exportWin UI to return the user inputs when you call
chanMe = fileExporter(transform, minTime, maxTime, cameraObject)
By the time the user has entered the inputs, the statements that follow have already been executed:
for all in chanMe():
fileHandle.write(all)
iterator.next()
fileHandle.close()
That is why plugin-based file exporters like these have their options UI tucked away in the option box. These options will be passed prior to call to the plugin's writer().
You will need to export your options UI code (in a certain specific format) using MEL in another script file and specify the name of that file in the optionsScriptName param of the registerFileTranslator call. There is a communication protocol that needs to be followed for communication between this options UI and the writer plugin itself. RobTheBloke's awesome post illustrates this process.
Ideally, the writer() method should have all the details it needs for computing and exporting without having to wait for user input.
Alternatively, if you prefer to have your own UI window and more control over the flow of things, you could write the exporter not as a plugin, but as a simple MEL/Python module. You could still use the power of the API.
Hope this helped!
I am in the process of converting the database from pfaf.org (plant for a future) into a pdf book.
I have hit a bit of a stumbling block regarding pageTemplates.
Each plant may begin on a left or a right aligned page; and may potentially be two or more pages.
I have two templates for the first plant page (left and right) and also two more templates for the potential subsequent pages.
Currently this is handled as follows (for instance):
for i, plant in enumerate(plants):
#the first plant is printed on a right-hand page
if i % 2 == 0:
template = 'left'
second = 'right'
else:
template = 'right'
second = 'left'
Story.append(nextPageTemplate(template,'*', 'secondary_'+template, 'secondary_'+second))
Story.append(PageBreak())
#append various paragraphs, jumping between frames, etc...
The code (as you can probably tell) works fine for single page plants.
It also works (semi) as expected for multi page plants.
However, as you can also probably see, a two (or four,etc) page plant will break the template arrangement, because the above code assumes page position based upon plant number, rather than page number.
I can't see a solution for this in the location of the code above (i.e during the Story.append cycle) - as at that point I can not tell whether the plant has used more than one page, and thus what page I am currently on, as such.
I hoped that I could perhaps tweak the nextPageTemplate structure from my custom docTemplate, but I can't figure out if this is possible.
Is it? Or is there another solution? Would really appreciate any help. Have been reading all over, but the best examples I can find don't quite cover this scenario.
Any questions, please ask.
Thank you
Thank you, Nitzle:
The trouble is that I don't know how many pages each plant will take up.
For instance - a new plant starts on an odd page so I give it a cycle of templates ('right', '*', 'secondaryLeft', 'secondaryRight'). [the secondary pages are just a single frame with appropriate margin.]
If that plant is one page long, no problem, the next plant will have the template cycle opposite to the above.
However, if the plant has, say, two pages, it will cause the following plant to fall again on an odd page again and thus the template cycle should not change... I hope this makes sense.
This is the circumstance I am having trouble solving... If I do as you say, it doesn't allow for multiple page plants. Most of my code is as follows; I have tried to slim it down a little though :) hopefully it still contains all relevant stuff and not too much unnecessary.
import os
import sys
import MySQLdb
from reportlab.platypus import Spacer, Image, Table, TableStyle, PageBreak, FrameBreak, paraparser
from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate, NextPageTemplate, _doNothing
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.platypus.frames import Frame
from reportlab.platypus.flowables import KeepInFrame
from reportlab.platypus.paragraph import Paragraph
from reportlab.lib.units import mm, cm
from reportlab.lib.pagesizes import A4, A5
from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER, TA_RIGHT
from reportlab.lib.styles import StyleSheet1, ParagraphStyle as PS
from reportlab.lib import colors
from reportlab.graphics.shapes import Drawing, Rect, String
from reportlab.pdfbase.pdfmetrics import registerFont, stringWidth
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.rl_config import warnOnMissingFontGlyphs
warnOnMissingFontGlyphs = 0
registerFont(TTFont('Museo_', '/home/wuwei/.fonts/Museo300-Regular.ttf'))
registerFont(TTFont('Museo_M', '/home/wuwei/.fonts/Museo500-Regular.ttf'))
registerFont(TTFont('Trebuchet', '/usr/share/fonts/truetype/msttcorefonts/Trebuchet_MS.ttf'))
registerFont(TTFont('Trebuchet_I', '/usr/share/fonts/truetype/msttcorefonts/Trebuchet_MS_Italic.ttf'))
## SOME VARIABLE DEFINITIONS ##
titleFont = "Museo_M"
subtitleFont = "Museo_"
stdFont = "Trebuchet"
stdItalic = "Trebuchet_I"
#stdSize = 14
"""CREATE GLOBALS"""
_w, _h = A4
_head_w = 17.5*cm
_head_pad = 0.2*cm
_main_w = 17.2*cm
_budge = 0.3*cm
_left_margin = 1.5*cm
_right_margin = 2.0*cm
_top_margin = 1.5*cm
_bottom_margin = 2.0*cm
_latinFontS = 18
#reset superFraction to style 'common name' placement
paraparser.superFraction = 0.15
paraparser.sizeDelta = 0
###########################################################################################################
######################################### ###############################################
######################################## DB FUNCTIONS #############################################
######################################### ###############################################
###########################################################################################################
def connectToDB():
try:
connection = MySQLdb.connect (host = "localhost",
user = "root",
passwd = "****************",
db = "pfaf")
except MySQLdb.Error, e:
print "I guess, either you don't have a local copy of the pfaf db"
print "or something is wrong with your connection details."
print "Error %d: %s" % (e.args[0], e.args[1])
sys.exit (1)
return connection
def close(item, exit=0):
#used to close both database cursors and connections
item.close()
if exit == 1:
sys.exit (0)
def runQuery(q, conn):
results = conn.cursor(MySQLdb.cursors.DictCursor)
results.execute (q)
return results
def Fetch(results, fetchAll=0):
if fetchAll:
print "fetchAll"
# FETCHALL option:
rows = results.fetchall()
#cursor.close()
#conn.close()
'''for row in rows:
print "%s, %s" % (row["Latin Name"], row["Hardyness"])
print "%d rows were returned" % results.rowcount'''
return rows
else:
# FETCHONE option:
##--- Print some debug info to command line ---##
print "Latin Name - Common Name - Hardyness"
while (1):
row = results.fetchone()
if row == None:
break
latin_name = row["Latin Name"]
common_name = row["Common name"]
hardyness = row["Hardyness"]
family = row["Family"]
synonyms = row["Synonyms"]
##--- Print some more useful debug info to command line ---##
print "%s - %s - %s" % (latin_name, common_name, hardyness)
print row
if results.rowcount != 1:
print "%d rows were returned" % results.rowcount
else:
print "%d row was returned" % results.rowcount
return row
###########################################################################################################
######################################### ###############################################
######################################## STORY PROCESSING #############################################
######################################### ###############################################
###########################################################################################################
def drawBorders(canv, side):
canv.saveState()
d = Drawing(0,0)
#header border#
r = Rect( side-_budge, _h-(2.4*cm), _head_w+(_budge*2), 1.2*cm, rx=5, ry=5 )
r.strokeColor = colors.black
r.fillColor = colors.white
r.strokeWidth = 1.5
d.add(r)
#hardyness border#
rad = 5
hWidth = 1.4*cm
if side == _left_margin:
hPos = -rad
else:
hPos = _w - hWidth + rad
r = Rect( hPos, _h-(3.8*cm), hWidth, 1.2*cm, rx=rad, ry=rad )
r.strokeColor = colors.black
r.fillColor = colors.white
r.strokeWidth = 1.5
d.add(r)
d.drawOn(canv, 0, 0)
canv.restoreState()
def drawFooter(canv, doc):
canv.saveState()
canv.setFont(stdFont,10)
canv.drawCentredString((_w/2.0), 1.5*cm, "%d - %s" % (doc.page, doc.latinName))
canv.restoreState()
class LeftPageTemplate(PageTemplate):
def __init__(self):
#allow a bigger margin on the right for binding
latinF = Frame(_left_margin, 27.5*cm, _head_w, 0.8*cm, id='latinL', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
hardyF = Frame(0.1*cm, 26.05*cm, cm, cm, id='hardL', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
synF = Frame(_left_margin, 26.65*cm, _main_w, 0.55*cm, id='synL', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
otherF = Frame(_left_margin, 22.1*cm, 12.4*cm, 4.5*cm, id='otherL', showBoundary=1)
calF = Frame(14.2*cm, 22.1*cm, 4.5*cm, 4.5*cm, id='calL', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
flowF = Frame(_left_margin, 2.0*cm, _main_w, 19.85*cm, id='flowL', showBoundary=1)
PageTemplate.__init__(self,
id='left',
frames=[latinF, hardyF, synF, otherF, calF, flowF],
pagesize=A4)
def beforeDrawPage(self, canv, doc):
drawBorders(canv, _left_margin)
def afterDrawPage(self, canv, doc):
drawFooter(canv, doc)
class RightPageTemplate(PageTemplate):
def __init__(self):
#allow a bigger margin on the left for binding
latinF = Frame(_right_margin, 27.5*cm, _head_w, 0.8*cm, id='latinR', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
hardyF = Frame(_w-1.1*cm, 26.05*cm, cm, cm, id='hardR', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
synF = Frame(_right_margin+_budge, 26.65*cm, _main_w, 0.55*cm, id='synR', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
calF = Frame(_right_margin+_budge, 22.1*cm, 4.5*cm, 4.5*cm, id='calR', showBoundary=0,
rightPadding=0, leftPadding=0, topPadding=0, bottomPadding=0)
otherF = Frame(_right_margin+5.1*cm, 22.1*cm, 12.4*cm, 4.5*cm, id='otherR', showBoundary=1)
flowF = Frame(_right_margin+_budge, 2.0*cm, _main_w, 19.85*cm, id='flowR', showBoundary=1)
PageTemplate.__init__(self,
id='right',
frames=[latinF, hardyF, synF, otherF, calF, flowF],
pagesize=A4)
def beforeDrawPage(self, canv, doc):
drawBorders(canv, _right_margin)
def afterDrawPage(self, canv, doc):
drawFooter(canv, doc)
class MyDocTemplate(BaseDocTemplate):
_invalidInitArgs = ('pageTemplates',)
def __init__(self, filename, **kw):
self.allowSplitting = 0
BaseDocTemplate.__init__(self, filename, **kw)
self.latinName = "(none initially)"
self.latinWidth = 0 #(none initially)
def afterInit(self):
self._calc() #in case we have changed margin sizes etc
self.leftMargin = _left_margin
self.rightMargin = _right_margin
self.topMargin = _top_margin
self.bottomMargin = _bottom_margin
self.width = _w - self.leftMargin - self.rightMargin
self.height = _h - self.topMargin - self.bottomMargin
frameStd = Frame(cm, self.bottomMargin, (_w - 2*cm), (_h - 3*cm), id='cvr', showBoundary=0)
frameToC = Frame(self.rightMargin, self.bottomMargin, self.width, self.height, id='tocFrame', showBoundary=0)
frameL = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='secLeftFrame', showBoundary=1)
frameR = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='secRightFrame', showBoundary=1)
self.addPageTemplates( [PageTemplate(id='Cover', frames=frameStd, onPage=coverPage, pagesize=self.pagesize),
PageTemplate(id='ToC', frames=frameToC, onPage=tocPage, pagesize=self.pagesize),
PageTemplate(id='blank', frames=frameStd, onPage=_doNothing, pagesize=self.pagesize),
LeftPageTemplate(),
RightPageTemplate(),
PageTemplate(id='secondary_left', frames=frameL, onPage=_doNothing, pagesize=self.pagesize),
PageTemplate(id='secondary_right', frames=frameR, onPage=_doNothing, pagesize=self.pagesize)
] )
def afterFlowable(self, flowable):
"""Registers ToC entries - and captures latin name for footer"""
if isinstance(flowable, Paragraph):
style = flowable.style.name
key = None
firstWord = style.split('_',1)[0]
if (style == 'LatinName') or (style == 'LatinNameR') or (firstWord == 'LatinName'):
level = 0
key = 'latin-%s' % self.seq.nextf('LatinName')
self.canv.bookmarkPage(key)
wholeStr = flowable.getPlainText()
if self.page % 2 == 0: #even numbers are on left pages
latinOnly = wholeStr.split('\xc2\xa0\xc2\xa0')[0] #looks for '  ' as divider
else:
latinOnly = wholeStr.split('\xc2\xa0\xc2\xa0')[1]
self.latinName = latinOnly
E = [level, latinOnly, self.page]
if key is not None: E.append(key)
self.notify('TOCEntry', tuple(E))
'''elif (style == 'CommonName'):
self.commonName = flowable.getPlainText()
self.commonWidth = stringWidth(self.commonName, styles['common'].fontName, styles['common'].fontSize)'''
else:
return
""" coverPage and otherPages are intended for non-flowing (i.e standard) parts of the pages """
def coverPage(canvas, doc):
Title = "Plants for a Future"
pageinfo = "The full database collected as a printable book"
canvas.setTitle(Title + " : " + pageinfo)
print "creating cover page..."
canvas.saveState()
d = Drawing(0,0)
r = Rect( 0, 0, 12*cm, 4*cm, rx=5, ry=5 )
r.strokeColor = colors.black
r.fillColor = colors.white
r.strokeWidth = 3
d.add(r)
d.drawOn(canvas, (_w/2.0)-6*cm, _h-(6.2*cm))
canvas.setFont(stdFont, 30)
canvas.drawCentredString(_w/2.0, _h-108, Title)
canvas.setFont(stdItalic, 14)
canvas.drawCentredString(_w/2.0, _h-150, pageinfo)
canvas.restoreState()
def tocPage(canvas, doc):
canvas.saveState()
canvas.setFont(stdFont,10)
canvas.drawCentredString((_w/2.0), 1.5*cm, "Table of Contents")
canvas.restoreState()
def getMedicinal(plant):
p = plant
initial = p["Medicinal"]
return initial
""" Run after 'Story' has been fully populated """
def go():
doc = MyDocTemplate('result01.pdf')
passes = doc.multiBuild(Story)
########################################################################
"""Build StyleSheet"""
styles = buildStyle()
h1 = PS(name = 'HeadingOne',
fontName = stdFont,
fontSize = 14,
leading = 16)
h2 = PS(name = 'HeadingTwo',
fontName = stdFont,
fontSize = 12,
leading = 14,
leftIndent = 1*cm)
Story=[]
a = Story.append
a(NextPageTemplate('blank'))
a(PageBreak())
a(NextPageTemplate('ToC'))
a(PageBreak())
toc = TableOfContents()
toc.levelStyles = [ h1, h2 ]
a(toc)
a(NextPageTemplate('blank'))
a(PageBreak())
"""###LEFT PAGES SHOULD BE STYLED RIGHT-ALIGNED, AND RIGHT PAGES LEFT-ALIGNED###"""
#print type(plants)
for i, plant in enumerate(plants):
### THIS INITIAL CHECK BREAKS AS IT NEEDS TO BE BASED ON PAGE NUMBER, NOT PLANT NUMBER!!! ###
if i %2 == 0: #IF FIRST PLANT APPEARS ON A RIGHTSIDE PAGE, ELSE REVERSE THE R and L
page='R'
template = 'right'
second = 'left'
else:
page='L'
template ='left'
second = 'right'
#FIRST THINGS FIRST:
#Make sure the page templates flow nicely for each plant "chapter"
a(NextPageTemplate([template, '*', ('secondary_'+template), ('secondary_'+second) ]))
a(PageBreak())
'''CAPTURE PLANT INFO IN OBJECTS'''
p = plant
'''for info in plant:
print info, p[info]'''
'''Header'''
latin = p["Latin Name"]
common = p["Common name"]
family = p["Family"]
syn = p["Synonyms"]
"""X. congestum. (Lour.)Merrill. X. racemosum. Miq. Apactis japonica. Croton congestum.
Flacourtia japonica. Walp. Hisingera japonica. H. racemosa."""
hardy = str(p["Hardyness"])
'''Basic Info'''
author = p["Author"]
botanicalrefs = p["Botanical references"]
width = p["Width"]
height = p["Height"]
habit = p["Habit"]
planttype = clean("Deciduous/Evergreen", p)
plantrange = p["Range"]
habitat = p["Habitat"]
soil = clean("Soil", plant)
shade = p["Shade"]
moisture = p["Moisture"]
drained = p["Well-drained"]
nf = p["Nitrogen fixer"]
pH = p["pH"]
acid = p["Acid"]
alkaline = p["Alkaline"]
saline = p["Saline"]
wind = p["Wind"]
rate = clean("Growth rate", plant)
pollution = p["Pollution"]
poorsoil = p["Poor soil"]
drought = p["Drought"]
heavyclay = p["Heavy clay"]
tender = clean("FrostTender", plant)
inleaf = p["In leaf"]
flowering = p["Flowering time"]
seedripens = p["Seed ripens"]
flowertype = p["Flower Type"]
pollinators = p["Pollinators"]
selffertile = clean("Self-fertile", plant)
hazards = p["Known hazards"]
rating_edible = p["Rating"]
rating_med = p["Medicinal Rating"]
edibleuses = p["Edible uses"]
medicinaluses = getMedicinal(plant)
otheruses = p["Uses notes"]
#the following encoding allows for special characters such as degree symbol
cultivation = unicode(p["Cultivation details"], 'latin-1')#'ISO-8859-1')
propagation = p["Propagation 1"]
scented = p["Scented"] #boolean - requires further lookup in `ScentedPlants` table
string = '''%s is %s %s growing to %gm by %gm at a %s rate.<br/>
It's habitats are %s <br/><br/> Range: %s
<br/><br/>
Suitable for %s soils. <br/><br/>
Shade: %s, Moisture: %s <br/>
Well-drained: %d, Nitrogen fixer: %d <br/> ph: %s <br/>
Acid: %d, Alkaline: %d, Saline: %d <br/>
Wind: %s
<br/><br/>
Author: %s <br/> Botanical References: %s''' % (
latin, planttype, habit.lower(), width, height, rate,
habitat[0].lower()+habitat[1:], plantrange,
soil, shade, moisture, drained,
nf, pH, acid, alkaline, saline, wind, author, botanicalrefs )
string = unicode(string, 'latin-1')
latinW = stringWidth(latin, styles['latin'].fontName, styles['latin'].fontSize)
commonW = stringWidth(common, styles['common'].fontName, styles['common'].fontSize)
if (latinW + commonW + (_head_pad*3)) > _head_w:
styleName = "LatinName_" + str(i)
latinStyle = PS( name=styleName,
parent=styles['Normal'],
fontName=titleFont,
fontSize=_latinFontS,
leading=22,
spaceAfter=0)
j = 1
#should the latin string be too long, attempt to shrink until it fits
while (latinW + commonW + (_head_pad*3)) > _head_w:
#change the font size until ok...
latinStyle.fontSize = _latinFontS -j
latinW = stringWidth(latin, latinStyle.fontName, latinStyle.fontSize)
j += 0.2
else:
latinStyle = styles['LatinName']
if page == 'L':
headerText = '''<para align="left">
%s
<font face="%s" size="%d"> <super>%s</super></font>
</para>''' % (latin, subtitleFont, 12, common)
else:
headerText = '''<para align="right">
<font face="%s" size="%d"><super>%s</super> </font>
%s
</para>''' % (subtitleFont, 12, common, latin)
latinPara = Paragraph(headerText, latinStyle)
a(FrameBreak('latin'+page))
a(latinPara)
a(FrameBreak('syn'+page))
a(KeepInFrame(_main_w, 1.5*cm,
[Paragraph(syn, styles['syn'+page])],
mode="shrink")) #can be shrink, truncate or overflow
a(FrameBreak('hard'+page))
a(Paragraph(hardy, styles['hardy']))
a(FrameBreak('cal'+page))
#SHALL BE ULTIMATELY POPULATED VIA DATABASE#
greyOut = [ [0,0,1,1,1,1,1,0,0,0,0,0], [0,0,0,0,0,1,1,1,1,0,0,0], [0,0,0,0,0,0,0,0,1,1,1,0] ]
cal = drawCalendar(greyOut)
a(cal)
a(FrameBreak('flow'+page))
a(Paragraph(string, styles['Normal']))
a(Paragraph("Edible Uses", styles['title']))
a(Paragraph("Medicinal Uses", styles['title']))
a(Paragraph("Other Uses", styles['title']))
a(Paragraph("Cultivation", styles['title']))
a(Paragraph(cultivation, styles['Normal']))
a(Paragraph("Propagation", styles['title']))
a(Paragraph(propagation, styles['Normal']))
##ASSEMBLE PDF###
go()
If you're just switching between a "left" and a "right" template, you could try using the _handle_nextPageTemplate method of the BaseDocTemplate class. One way to keep track of page number would be to use the afterPage hook to increment a page number.
from reportlab.platypus import BaseDocTemplate
class MyDocTemplate(BaseDocTemplate):
def __init__(self, *args, **kwargs):
BaseDocTemplate.__init__(self, *args, **kwargs)
self.__pageNum = 1
def afterPage(self):
"""Called after all flowables have been drawn on a page"""
# Increment pageNum since the page has been completed
self.__pageNum += 1
# If the page number is even, force "left-side" template
if self.__pageNum % 2 == 0:
self._handle_nextPageTemplate('left_template')
else:
self._handle_nextPageTemplate('right_template')
I haven't tested the code above, but you may need to use beforePage instead depending on how it checks page template order.