# MIT License
#
# Copyright (c) 2023 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
__all__ = ['TTkUiLoader','TTkUiSignature']
import json
from TermTk import TTkLog
from TermTk import TTkCfg, TTkK, TTkColor
from TermTk.TTkLayouts import TTkLayout, TTkGridLayout
from TermTk.TTkWidgets import *
from TermTk.TTkTestWidgets import *
from TermTk.TTkUiTools.uiproperties import TTkUiProperties
TTkUiSignature = "TTkUi/Document"
[docs]
class TTkUiLoader():
'''TTkUiLoader
.. _ttkDesigner: https://github.com/ceccopierangiolieugenio/pyTermTk/tree/main/tools/ttkDesigner
`ttkdesigner Tutorial <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/ttkDesigner/textEdit>`_
'''
[docs]
@staticmethod
def loadFile(filePath, baseWidget:TTkWidget=None, kwargs=None) -> TTkWidget:
'''load the file generated by ttkDesigner_
:param filePath: the file path
:type filePath: str
:param baseWidget: the custom widget that will be extended with this ui definition, if not defined a new :py:class:`TTkWidget` will be returned,defaults to **None**
:type baseWidget: :py:class:`TTkWidget`, optional
:param kwargs: the custom initialization args,defaults to **None**
:type kwargs: dictionary, optional
:return: :py:class:`TTkWidget`
'''
with open(filePath) as f:
return TTkUiLoader.loadJson(f.read(), baseWidget, kwargs)
return None
[docs]
@staticmethod
def loadJson(text, baseWidget:TTkWidget=None, kwargs=None) -> TTkWidget:
'''load the json representing the ui definition of the widget
:param text: the representation of the widget in Json format
:type text: json generated by ttkDesigner_
:param baseWidget: the custom widget that will be extended with this ui definition, if not defined a new :py:class:`TTkWidget` will be returned,defaults to **None**
:type baseWidget: :py:class:`TTkWidget`, optional
:param kwargs: the custom initialization args,defaults to **None**
:type kwargs: dictionary, optional
:return: :py:class:`TTkWidget`
'''
return TTkUiLoader.loadDict(json.loads(text), baseWidget, kwargs)
def _convert_2_0_2_to_2_1_0(ui):
return {
"type": TTkUiSignature,
"version": "2.1.0",
"connections" : ui['connections'],
"tui": ui['tui'] }
def _convert_2_0_1_to_2_0_2(ui):
return {
"type": TTkUiSignature,
"version": "2.0.2",
"connections" : ui['connections'],
"tui": ui['tui'] }
def _convert_2_0_0_to_2_0_2(ui):
return {
"type": TTkUiSignature,
"version": "2.0.2",
"connections" : ui['connections'],
"tui": ui['tui'] }
def _convert_1_0_1_to_2_0_0(ui):
def _processWidget(_wid):
if issubclass(globals()[_wid['class']],TTkContainer):
if hasattr(_wid,'layout'):
_wid['layout']['children'] = [_processWidget(_ch) for _ch in _wid['layout']['children']]
elif _wid['layout']['children']:
_wid['class'] = 'TTkContainer'
_wid['layout']['children'] = [_processWidget(_ch) for _ch in _wid['layout']['children']]
else:
_wid.pop('layout',None)
_wid['params'].pop("layout",None)
return _wid
tui = _processWidget(ui['tui'])
return {
"version": "2.0.0",
"connections" : ui['connections'],
"tui": tui }
@staticmethod
def _loadDict_1_0_0(ui, *args, **kwargs) -> None:
ui = TTkUiLoader._convert_1_0_1_to_2_0_0(ui)
return TTkUiLoader._loadDict_2_0_0(ui, *args, **kwargs)
def _loadDict_2_0_0(ui, *args, **kwargs) -> None:
ui = TTkUiLoader._convert_2_0_0_to_2_0_2(ui)
return TTkUiLoader._loadDict_2_0_2(ui, *args, **kwargs)
def _loadDict_2_0_1(ui, *args, **kwargs) -> None:
ui = TTkUiLoader._convert_2_0_1_to_2_0_2(ui)
return TTkUiLoader._loadDict_2_0_2(ui, *args, **kwargs)
def _loadDict_2_0_2(ui, *args, **kwargs) -> None:
ui = TTkUiLoader._convert_2_0_2_to_2_1_0(ui)
return TTkUiLoader._loadDict_2_1_0(ui, *args, **kwargs)
@staticmethod
def _loadDict_2_1_0(ui, baseWidget:TTkWidget=None, args=None):
if (
ui['version'] != '2.1.0' or
'type' not in ui or
ui['type'] != TTkUiSignature):
TTkLog.error("Ui Format not valid")
return None
def _setMenuButton(_menuButtonProp, _menuButton:TTkMenuButton):
if 'submenu' in _menuButtonProp:
for _sm in _menuButtonProp['submenu']:
if _sm == 'spacer':
_menuButton.addSpacer()
continue
_btn = _menuButton.addMenu(text=_sm['params']['Text'], checkable=_sm['params']['Checkable'], checked=_sm['params']['Checked'])
_btn.setName(_sm['params']['Name'])
_btn.setToolTip(_sm['params']['ToolTip'])
_setMenuButton(_sm,_btn)
def _setMenuBar(_menuBarProp, _menuBar:TTkMenuBarLayout):
def __addMenu(__prop, __alignment):
for _bp in __prop:
_btn = _menuBar.addMenu(text=_bp['params']['Text'], checkable=_bp['params']['Checkable'], checked=_bp['params']['Checked'], alignment=__alignment)
_btn.setName(_bp['params']['Name'])
_btn.setToolTip(_bp['params']['ToolTip'])
_setMenuButton(_bp, _btn)
if 'left' in _menuBarProp: __addMenu(_menuBarProp['left'], TTkK.LEFT_ALIGN)
if 'center' in _menuBarProp: __addMenu(_menuBarProp['center'], TTkK.CENTER_ALIGN)
if 'right' in _menuBarProp: __addMenu(_menuBarProp['right'], TTkK.RIGHT_ALIGN)
def _getWidget(_widProp, _baseWidget:TTkWidget=None, _args=None):
properties = {}
ttkClass = globals()[_widProp['class']]
for cc in reversed(ttkClass.__mro__):
if cc.__name__ in TTkUiProperties:
properties |= TTkUiProperties[cc.__name__]['properties']
# Init params used in the constructors
kwargs = {} if _args is None else _args
# Init params to be configured with the setter
setters = []
layout = _getLayout(_widProp['layout']) if 'layout' in _widProp else TTkLayout()
# Process the widget params
for pname in _widProp['params']:
if pname not in properties: continue
if 'init' in properties[pname]:
initp = properties[pname]['init']
name = initp['name']
if initp['type'] is TTkLayout:
value = layout
elif initp['type'] is TTkColor:
value = TTkColor.ansi(_widProp['params'][pname])
else:
value = _widProp['params'][pname]
# TTkLog.debug(f"{name=} {value=}")
if name not in kwargs:
kwargs |= {name: value}
elif 'set' in properties[pname]:
setp = properties[pname]['set']
setcb = setp['cb']
if setp['type'] is TTkLayout:
value = layout
elif setp['type'] is TTkColor:
value = TTkColor.ansi(_widProp['params'][pname])
else:
value = _widProp['params'][pname]
setters.append({
'cb':setcb,
'value': value,
'multi':type(setp['type']) is list})
if _baseWidget is None:
widget = ttkClass(**kwargs)
else:
widget = _baseWidget
if issubclass(type(_baseWidget), ttkClass):
ttkClass.__init__(widget,**kwargs)
else:
error = f"Base Widget '{_baseWidget.__class__.__name__}' is not a subclass of '{ttkClass.__name__}'"
raise TypeError(error)
# Init params that don't have a constrictor
for s in setters:
if s['multi']:
s['cb'](widget, *s['value'])
else:
s['cb'](widget, s['value'])
# Process the optional menuBar params
if 'menuBar' in _widProp:
if 'top' in _widProp['menuBar']:
widget.setMenuBar(mb := TTkMenuBarLayout(), TTkK.TOP)
_setMenuBar(_widProp['menuBar']['top'], mb)
if 'bottom' in _widProp['menuBar']:
widget.setMenuBar(mb := TTkMenuBarLayout(), TTkK.BOTTOM)
_setMenuBar(_widProp['menuBar']['bottom'], mb)
# TTkLog.debug(widget)
return widget
def _getLayout(_layprop, _baseWidget:TTkWidget=None):
properties = {}
ttkClass = globals()[_layprop['class']]
for cc in reversed(ttkClass.__mro__):
if cc.__name__ in TTkUiProperties:
properties |= TTkUiProperties[cc.__name__]['properties']
setters = []
for pname in _layprop['params']:
if 'set' in properties[pname]:
setp = properties[pname]['set']
setcb = setp['cb']
if setp['type'] is TTkLayout:
value = layout
elif setp['type'] is TTkColor:
value = TTkColor.ansi(_layprop['params'][pname])
else:
value = _layprop['params'][pname]
setters.append({
'cb':setcb,
'value': value,
'multi':type(setp['type']) is list})
layout = globals()[_layprop['class']]()
# Init params that don't have a constrictor
for s in setters:
if s['multi']:
s['cb'](layout, *s['value'])
else:
s['cb'](layout, s['value'])
for c in _layprop['children']:
row = c.get('row', 0)
col = c.get('col', 0)
rowspan = c.get('rowspan', 1)
colspan = c.get('colspan', 1)
if issubclass(ttkClass,TTkGridLayout):
if issubclass(globals()[c['class']],TTkLayout):
l = _getLayout(c)
TTkGridLayout.addItem(layout,l,row,col,rowspan,colspan)
else:
w = _getWidget(c)
TTkGridLayout.addWidget(layout,w,row,col,rowspan,colspan)
else:
if issubclass(globals()[c['class']],TTkLayout):
l = _getLayout(c)
l._row, l._col = row, col
l._rowspan, l._colspan = rowspan, colspan
layout.addItem(l)
else:
w = _getWidget(c)
w._row, w._col = row, col
w._rowspan, w._colspan = rowspan, colspan
layout.addWidget(w)
return layout
# TTkLog.debug(ui)
if issubclass(globals()[ui['tui']['class']],TTkLayout):
widget = _getLayout(ui['tui'], baseWidget)
else:
widget = _getWidget(ui['tui'], baseWidget, args)
def _getSignal(sender, name):
for cc in reversed(type(sender).__mro__):
if cc.__name__ in TTkUiProperties:
if name not in TTkUiProperties[cc.__name__]['signals']:
continue
signame = TTkUiProperties[cc.__name__]['signals'][name]['name']
return getattr(sender,signame)
return None
def _getSlot(receiver, name):
for cc in reversed(type(receiver).__mro__):
if cc.__name__ in TTkUiProperties:
if name not in TTkUiProperties[cc.__name__]['slots']:
continue
slotname = TTkUiProperties[cc.__name__]['slots'][name]['name']
return getattr(receiver,slotname)
return None
for conn in ui['connections']:
sender = widget.getWidgetByName(conn['sender'])
receiver = widget.getWidgetByName(conn['receiver'])
signal = conn['signal']
slot = conn['slot']
if None in (sender,receiver): continue
_getSignal(sender,signal).connect(_getSlot(receiver,slot))
return widget
[docs]
@staticmethod
def normalise(ui):
cb = {'1.0.0' : TTkUiLoader._convert_1_0_1_to_2_0_0,
'1.0.1' : TTkUiLoader._convert_1_0_1_to_2_0_0,
'2.0.0' : TTkUiLoader._convert_2_0_0_to_2_0_2,
'2.0.1' : TTkUiLoader._convert_2_0_1_to_2_0_2,
'2.0.2' : TTkUiLoader._convert_2_0_2_to_2_1_0,
'2.1.0' : lambda x: x
}.get(ui['version'], lambda x: x)
return cb(ui)
[docs]
@staticmethod
def loadDict(ui, baseWidget:TTkWidget=None, kwargs=None) -> TTkWidget:
'''load the dictionary representing the ui definition of the widget
:param ui: the representation of the widget
:type ui: dictionary generated by ttkDesigner_
:param baseWidget: the custom widget that will be extended with this ui definition, if not defined a new :py:class:`TTkWidget` will be returned,defaults to **None**
:type baseWidget: :py:class:`TTkWidget`, optional
:param kwargs: the custom initialization args,defaults to **None**
:type kwargs: dictionary, optional
:return: :py:class:`TTkWidget`
'''
cb = {'1.0.0' : TTkUiLoader._loadDict_1_0_0,
'1.0.1' : TTkUiLoader._loadDict_1_0_0,
'2.0.0' : TTkUiLoader._loadDict_2_0_0,
'2.0.1' : TTkUiLoader._loadDict_2_0_1,
'2.0.2' : TTkUiLoader._loadDict_2_0_2,
'2.1.0' : TTkUiLoader._loadDict_2_1_0,
}.get(ui['version'], None)
if cb:
return cb(ui, baseWidget, kwargs)
msg = (f"The used pyTermTk ({TTkCfg.version}) is not able to load this tui version ({ui['version']})")
raise NotImplementedError(msg)