1#!/usr/bin/env python
2# encoding: utf-8
3# Federico Pellegrin, 2016-2019 (fedepell) adapted for Python
4
5"""
6This tool helps with finding Python Qt5 tools and libraries,
7and provides translation from QT5 files to Python code.
8
9The following snippet illustrates the tool usage::
10
11	def options(opt):
12		opt.load('py pyqt5')
13
14	def configure(conf):
15		conf.load('py pyqt5')
16
17	def build(bld):
18		bld(
19			features = 'py pyqt5',
20			source   = 'main.py textures.qrc aboutDialog.ui',
21		)
22
23Here, the UI description and resource files will be processed
24to generate code.
25
26Usage
27=====
28
29Load the "pyqt5" tool.
30
31Add into the sources list also the qrc resources files or ui5
32definition files and they will be translated into python code
33with the system tools (PyQt5, PySide2, PyQt4 are searched in this
34order) and then compiled
35"""
36
37try:
38	from xml.sax import make_parser
39	from xml.sax.handler import ContentHandler
40except ImportError:
41	has_xml = False
42	ContentHandler = object
43else:
44	has_xml = True
45
46import os
47from waflib.Tools import python
48from waflib import Task, Options
49from waflib.TaskGen import feature, extension
50from waflib.Configure import conf
51from waflib import Logs
52
53EXT_RCC = ['.qrc']
54"""
55File extension for the resource (.qrc) files
56"""
57
58EXT_UI  = ['.ui']
59"""
60File extension for the user interface (.ui) files
61"""
62
63
64class XMLHandler(ContentHandler):
65	"""
66	Parses ``.qrc`` files
67	"""
68	def __init__(self):
69		self.buf = []
70		self.files = []
71	def startElement(self, name, attrs):
72		if name == 'file':
73			self.buf = []
74	def endElement(self, name):
75		if name == 'file':
76			self.files.append(str(''.join(self.buf)))
77	def characters(self, cars):
78		self.buf.append(cars)
79
80@extension(*EXT_RCC)
81def create_pyrcc_task(self, node):
82	"Creates rcc and py task for ``.qrc`` files"
83	rcnode = node.change_ext('.py')
84	self.create_task('pyrcc', node, rcnode)
85	if getattr(self, 'install_from', None):
86		self.install_from = self.install_from.get_bld()
87	else:
88		self.install_from = self.path.get_bld()
89	self.install_path = getattr(self, 'install_path', '${PYTHONDIR}')
90	self.process_py(rcnode)
91
92@extension(*EXT_UI)
93def create_pyuic_task(self, node):
94	"Create uic tasks and py for user interface ``.ui`` definition files"
95	uinode = node.change_ext('.py')
96	self.create_task('ui5py', node, uinode)
97	if getattr(self, 'install_from', None):
98		self.install_from = self.install_from.get_bld()
99	else:
100		self.install_from = self.path.get_bld()
101	self.install_path = getattr(self, 'install_path', '${PYTHONDIR}')
102	self.process_py(uinode)
103
104@extension('.ts')
105def add_pylang(self, node):
106	"""Adds all the .ts file into ``self.lang``"""
107	self.lang = self.to_list(getattr(self, 'lang', [])) + [node]
108
109@feature('pyqt5')
110def apply_pyqt5(self):
111	"""
112	The additional parameters are:
113
114	:param lang: list of translation files (\\*.ts) to process
115	:type lang: list of :py:class:`waflib.Node.Node` or string without the .ts extension
116	:param langname: if given, transform the \\*.ts files into a .qrc files to include in the binary file
117	:type langname: :py:class:`waflib.Node.Node` or string without the .qrc extension
118	"""
119	if getattr(self, 'lang', None):
120		qmtasks = []
121		for x in self.to_list(self.lang):
122			if isinstance(x, str):
123				x = self.path.find_resource(x + '.ts')
124			qmtasks.append(self.create_task('ts2qm', x, x.change_ext('.qm')))
125
126
127		if getattr(self, 'langname', None):
128			qmnodes = [k.outputs[0] for k in qmtasks]
129			rcnode = self.langname
130			if isinstance(rcnode, str):
131				rcnode = self.path.find_or_declare(rcnode + '.qrc')
132			t = self.create_task('qm2rcc', qmnodes, rcnode)
133			create_pyrcc_task(self, t.outputs[0])
134
135class pyrcc(Task.Task):
136	"""
137	Processes ``.qrc`` files
138	"""
139	color   = 'BLUE'
140	run_str = '${QT_PYRCC} ${SRC} -o ${TGT}'
141	ext_out = ['.py']
142
143	def rcname(self):
144		return os.path.splitext(self.inputs[0].name)[0]
145
146	def scan(self):
147		"""Parse the *.qrc* files"""
148		if not has_xml:
149			Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
150			return ([], [])
151
152		parser = make_parser()
153		curHandler = XMLHandler()
154		parser.setContentHandler(curHandler)
155		fi = open(self.inputs[0].abspath(), 'r')
156		try:
157			parser.parse(fi)
158		finally:
159			fi.close()
160
161		nodes = []
162		names = []
163		root = self.inputs[0].parent
164		for x in curHandler.files:
165			nd = root.find_resource(x)
166			if nd:
167				nodes.append(nd)
168			else:
169				names.append(x)
170		return (nodes, names)
171
172
173class ui5py(Task.Task):
174	"""
175	Processes ``.ui`` files for python
176	"""
177	color   = 'BLUE'
178	run_str = '${QT_PYUIC} ${SRC} -o ${TGT}'
179	ext_out = ['.py']
180
181class ts2qm(Task.Task):
182	"""
183	Generates ``.qm`` files from ``.ts`` files
184	"""
185	color   = 'BLUE'
186	run_str = '${QT_LRELEASE} ${QT_LRELEASE_FLAGS} ${SRC} -qm ${TGT}'
187
188class qm2rcc(Task.Task):
189	"""
190	Generates ``.qrc`` files from ``.qm`` files
191	"""
192	color = 'BLUE'
193	after = 'ts2qm'
194	def run(self):
195		"""Create a qrc file including the inputs"""
196		txt = '\n'.join(['<file>%s</file>' % k.path_from(self.outputs[0].parent) for k in self.inputs])
197		code = '<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n%s\n</qresource>\n</RCC>' % txt
198		self.outputs[0].write(code)
199
200def configure(self):
201	self.find_pyqt5_binaries()
202
203	# warn about this during the configuration too
204	if not has_xml:
205		Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
206
207@conf
208def find_pyqt5_binaries(self):
209	"""
210	Detects PyQt5 or PySide2 programs such as pyuic5/pyside2-uic, pyrcc5/pyside2-rcc
211	"""
212	env = self.env
213
214	if getattr(Options.options, 'want_pyqt5', True):
215		self.find_program(['pyuic5'], var='QT_PYUIC')
216		self.find_program(['pyrcc5'], var='QT_PYRCC')
217		self.find_program(['pylupdate5'], var='QT_PYLUPDATE')
218	elif getattr(Options.options, 'want_pyside2', True):
219		self.find_program(['pyside2-uic'], var='QT_PYUIC')
220		self.find_program(['pyside2-rcc'], var='QT_PYRCC')
221		self.find_program(['pyside2-lupdate'], var='QT_PYLUPDATE')
222	elif getattr(Options.options, 'want_pyqt4', True):
223		self.find_program(['pyuic4'], var='QT_PYUIC')
224		self.find_program(['pyrcc4'], var='QT_PYRCC')
225		self.find_program(['pylupdate4'], var='QT_PYLUPDATE')
226	else:
227		self.find_program(['pyuic5','pyside2-uic','pyuic4'], var='QT_PYUIC')
228		self.find_program(['pyrcc5','pyside2-rcc','pyrcc4'], var='QT_PYRCC')
229		self.find_program(['pylupdate5', 'pyside2-lupdate','pylupdate4'], var='QT_PYLUPDATE')
230
231	if not env.QT_PYUIC:
232		self.fatal('cannot find the uic compiler for python for qt5')
233
234	if not env.QT_PYRCC:
235		self.fatal('cannot find the rcc compiler for python for qt5')
236
237	self.find_program(['lrelease-qt5', 'lrelease'], var='QT_LRELEASE')
238
239def options(opt):
240	"""
241	Command-line options
242	"""
243	pyqt5opt=opt.add_option_group("Python QT5 Options")
244	pyqt5opt.add_option('--pyqt5-pyqt5', action='store_true', default=False, dest='want_pyqt5', help='use PyQt5 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)')
245	pyqt5opt.add_option('--pyqt5-pyside2', action='store_true', default=False, dest='want_pyside2', help='use PySide2 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)')
246	pyqt5opt.add_option('--pyqt5-pyqt4', action='store_true', default=False, dest='want_pyqt4', help='use PyQt4 bindings as python QT5 bindings (default PyQt5 is searched first, PySide2 after, PyQt4 last)')
247