devwiki:python_qt

Differences

This shows you the differences between two versions of the page.


Previous revision
devwiki:python_qt [2024/01/10 03:08] (current) – [Layout and Container] ying
Line 1: Line 1:
 +====== Python and Qt - The Story That Gets Complex ======
 +
 +  * **If you don't want read or already read all below, The Story all can be simplified into my Python Qt universal_tool_template.py** since 2016.07
 +    * my internal wiki link (slight slow to update): [[devwiki:template:universal_tool_template|universal_tool_template]]
 +    * my external github link (more updated): https://github.com/shiningdesign/universal_tool_template.py
 +
 +
 +  * Qt is a GUI library, which almost any programming language can use, default designed for C++
 +    * Qt has currently 2 active version: 
 +      - Qt4: old and stable
 +      - Qt5: new and code structure changes
 +
 +  * Python is programming language, default can also run in console command mode, bundle with tk GUI that no one use
 +    * Python has current 2 major active version: Python 2.x and Python 3.x; which popular version are
 +      - Python 2.7 32bit: (version I use for my own apps)
 +      - Python 2.7 64bit: (version used by Maya2014 64bit, Maya2017 64bit, Houdini v15 64bit, Nuke 10 64bit)
 +      - Python 3.4 32bit
 +      - Python 3.4 64bit
 +      - Python 3.5.1 32bit
 +      - Python 3.5.1 64bit: (version used by Blender 2.78)
 +
 +  * Python to Qt need a binding library, which let python to call those Qt library to create GUI
 +    * Python has currently 2 major active binding library: PySide, PyQt, which popular version are
 +      - PySide: python + Qt4
 +      - PySide2: Python + Qt5
 +      - PyQt4: python + Qt4
 +      - PyQt5: python + Qt5
 +
 +  * **The Selection Table by use case**: 
 +    * However, you can manually setup the library and use in any combination with match pyVersion+osVersion+qtVersion.
 +    * You don't need get Qt4 or Qt5 package seperately, as PySide,PySide2,PyQt4,PyQt5 all have Qt library included
 +    * **IMPORTANT NOTE: if you manually install PySide, PyQt library into Python, make sure the package version matching your Python Major+Sub version + Bit choice, like package for extactly py35x64 ** 
 +
 +^ Python version and bit ^^ Qt binding ^^ Default Used by ^ Install Options |
 +^ Python 2.7 | 32bit ^ PyQt4 | Qt4 | my personal tools | https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/ |
 +^ Python 2.7 | 64bit ^ PySide | Qt4 | Maya2014 64bit, Houdini v15 64bit, Nuke 10 64bit |
 +^ Python 2.7 | 64bit ^ PySide2 | Qt5 | Maya2017 64bit |
 +^ Python 3.4 | 64bit ^ PyQt4 | Qt4 | | https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/ |
 +^ Python 3.4 | 64bit ^ PyQt5 | Qt5 |  | pip3 install PyQt5 |
 +^ Python 3.5 | 64bit ^ PyQt4  | Qt4 | | http://www.lfd.uci.edu/%7Egohlke/pythonlibs/#pyqt4 |
 +^ Python 3.5 | 64bit ^ PyQt5  | Qt5 | | pip3 install PyQt5 \\ whl https://pypi.python.org/pypi/PyQt5 |
 +
 +  * note: pip.exe, pip3.exe are in python's script folder, run OS command console
 +    * install from a whl using pip: pip install some-package.whl
 +  * pyside download by pip: https://pypi.python.org/pypi/PySide
 +  * pyside2 download: https://wiki.qt.io/PySide2
 +
 +  * **Quick Conclusion from observe**
 +    * Python 2.7 is most popular choice for software script integration, for its stable and no change in long time
 +    * Python 64bit is best choice for modern computer, since all PC got more than 16GB ram nowadays
 +    * PySide (Qt4 inside) is most popular choice for Commercial software qt integration, for its easy-to-use LGPL license, and also for its stable Qt4 library
 +    * However, 
 +      * Python 3.5 64bit is for open mind modern Development, since it is newest
 +      * PyQt4 and PyQt5 license more friendly to open source and internal projects
 +      * PySide and PySide2 license are friendly to both commercial and open source use, but documentation may not be on PyQt4,PyQt5 level
 +      * Qt4 library is old and stable like Python 2.7
 +      * Qt5 library is new and more for people willing to adapt
 +
 +===== Install a PythonQt binding into existing software is may be straight forward =====
 +
 +  * those PySide, PyQt binding are built with a target, if they are built for the operation system and python version
 +
 +
 +==== Qt for Blender ====
 +  * Same case for Blender 2.7.8, since it comes no pyside or pyqt, you have build/find a version for targeted Blender, and you **May** just grab standard python 3.5.1's PyQt library and throw the path inside.
 +
 +  * Blender target version of PyQt4 download: http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyqt4
 +    * install PyQt4 for normal Python 3.5 64bit, copy the PyQt4 folder and SIP.pyd to blenderApp\2.78\python\lib\site-packages
 +    * run python console in Blender, it works.
 +
 +==== Qt for Maya ====
 +
 +  * PySide is with Maya by defaut, you also can get PyQt4 for Maya from here:
 +    * Maya target version of PyQt4 download:
 +      * PyQt4 for Maya 2012 - 2016 (win,mac): https://github.com/zclongpop123/PyQt4
 +      * PyQt5 for maya 2017 (win): https://github.com/zclongpop123/PyQt5-for-Maya2017
 +
 +  * Qt for Maya tutorial: http://animateshmanimate.com/2010/07/19/maya-and-qt-tutorial/
 +  * Quick Qt button commander to Maya: http://www.janpijpers.com/qt-and-maya-how-to-use-a-qt-designer-ui-with-maya-no-pyqt/
 +    * similar guide: http://www.creativecrash.com/tutorials/using-qt-designer-for-mel-interfaces#tabs
 +
 +Quick notes:
 +  * loadUI mel command is for loading Qt UI file, and if detect maya compatible UI element, it auto generate maya UI element instead, like button with -c flag.
 +  * pyQt method is directly generating Qt element, but mixing Qt and Maya element is possible through hard way: example: http://www.justinfx.com/2011/11/20/mixing-pyqt4-widgets-and-maya-ui-objects/
 +
 +^ Qt UI ^ Maya UI ^
 +| push button (Buttons group) | button |
 +| radio button (Buttons group) | radioButton |
 +| check box(Buttons group) | checkBox |
 +| combo box (containers group) | optionMenu |
 +| line edit (input widgets group) | textField |
 +| spin box (input widgets group) | NONE |
 +| double spine box (input widgets group)| NONE |
 +| dial (input widgets group) | NONE |
 +| list widget (item widget item based) | textScrollList |
 +| horizontal slider (input widgets) | intSlider |
 +| label (display widgets group) | NONE |
 +| progress bar (display widgets group) | progressBar |
 +| vertical slider (input widgets) | intSlider |
 +| horizontal line (input widgets) | NONE |
 +| vertical line (input widgets) | NONE |
 +| group box (containers group) | NONE |
 +| tab widget (container group) | tabLayout |
 +| main window/QWidget | window |
 +===== Install PythonQt binding into Operating System =====
 +
 +==== install on windows =====
 +
 +  * For windows, it is available as ready-to-use package install
 +    * make sure pip is up to date <code dos>python -m pip install --upgrade pip</code>
 +    * install pyside, which is same and easy <code dos>python -m pip install pyside</code>
 +
 +==== install on Mac ====
 +
 +  * For Mac, you may find you may do quite a bit work to get it done, 
 +    * you need compile ..... build... download... ok, enough
 +      * **anywhere**, you will always need to get Xcode and Command line developer tools installed
 +        * __**Xcode**__: https://developer.apple.com/xcode/download/
 +        * __**Command Line Developer Tools**__: shell cmd: //xcode-select --install//
 +    * luckily, someone pack a similar package installer like the windows one, called PyQtX (**But unfortunately it is out of date**)
 +      * **anywhere**, you always need to download the official python (not mac built-in one) here: 
 +        * __**Python Mac OS X 64-bit/32-bit installer**__ from https://www.python.org/downloads/release/python-2711/
 +    * ok, then I tried MacPorts, it is like pip for python, it loads and installs packages for mac,
 +      - __**install macport**__: https://www.macports.org/install.php
 +      - test in command: //port//
 +      - __**install pyqt4**__: //sudo port install py27-pyqt4//
 +        - if ask for cmd line dev tools to install (actually you should have done this earlier), do this in another shell: //xcode-select --install//
 +      - after wait it run through, wow, it works, //python// then //import PyQt4//
 +    * ref: https://bitbucket.org/tortoisehg/thg/wiki/developers/MacOSX#!install-from-source-for-os-x-1011-el-capitan
 +    * Error and Fix for PyQt4 on Mac:
 +      * libqtclucene.4.dylib error, if you use everything with macports for install, then everything should work fine.
 +      * Follow exact these steps:
 +        - Install __**Xcode**__: https://developer.apple.com/xcode/download/
 +        - Install __**Command Line Developer Tools**__: shell cmd <code bash>xcode-select --install</code>
 +        - Install __**MacPorts**__: https://www.macports.org/install.php
 +        - use MacPorts to install Python27 and py27-pyqt4 <code bash>
 +sudo port install python27 python_select
 +sudo port select python python27
 +sudo port install py27-pyqt4 py27-qscintilla
 +vi ~/.bash_profile
 +</code>
 +        - check following in ~/.bash_profile, then Esc > : > q<code bash>
 +PATH="/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}"
 +export PATH
 +</code>
 +    * Error: QCocoaView handleTabletEvent warnings
 +
 +
 +
 +====== Complex Combo to Single Code ======
 +
 +**Write Python Code the way that work in any code**
 +  * the Code can break in **"Python 2 vs Python 3"**, **"PyQt* vs PySide*"**, **"Qt4 vs Qt5"**
 +
 +===== Break 1: Python 2 vs Python 3 =====
 +
 +  * detail ref: http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html
 +
 +**Print Function**
 +
 +  * python 2 <code python>print "Good Job" # works
 +print("Good Job") # works</code>
 +  * python 3 <code python>print("Good Job") # only</code>
 +  * **universal code choice** <code python>print("Good Job")
 +
 +# upgrade old code by convert line
 +import re
 +old_line = 'print "Good job"'
 +new_line = re.sub(r"(print)\s(.+)",r"\1(\2)", line) # print("Good Job")
 +
 +# upgrade old code by notepad++ regular replace
 +# find : (print)\s(.+)
 +# replace: $1\($2\), in new npp, \1(\2)
 +</code>
 +
 +**Run Python File**
 +  * python 2 <code python>execfile("testScript.py")</code>
 +  * python 3 <code python>exec(open("testScript.py").read(), globals())</code>
 +
 +**Reload module**
 +  * python 2 <code python>reload(myModule)</code>
 +  * python 3 <code python>import imp;imp.reload(myModule);</code>
 +
 +**user input module**
 +  * python 2 <code python>a = int(raw_input())</code>
 +  * python 3 <code python>a = int(input())</code>
 +===== Break 2: PySide vs PyQt =====
 +
 +  * ref: 
 +    * https://wiki.qt.io/Differences_Between_PySide_and_PyQt
 +    * http://askubuntu.com/questions/140740/should-i-use-pyqt-or-pyside-for-a-new-qt-project
 +
 +  * **the C++ pointer to Python reference convertor library**
 +    * in PySide,  is: **shiboken**
 +    * in PySide2, is: **shiboken2**
 +    * in PyQt4,PyQt5, is: **sip**
 +
 +  * **signal connection synatx**
 +    * code in PySide, PySide2, PyQt4, BUT not in PyQt5 <code python>
 +QtCore.QObject.connect(my_QButton, QtCore.SIGNAL("clicked()"), self.default_action)
 +QtCore.QObject.connect(my_QAction, QtCore.SIGNAL("triggered()"), self.default_action)
 +QtCore.QObject.connect(my_QCheckBox, QtCore.SIGNAL("toggled(bool)"), self.check_toggle_action)
 +</code>
 +    * code in PyQt5, **universal code for all**<code python>
 +my_QButton.clicked.connect(self.default_action)
 +my_QAction.triggered.connect(self.default_action)
 +my_QCheckBox.toggled[bool].connect(self.check_toggle_action)
 +</code>
 +
 +  * **File Dialog**
 +    * QtGui.QFileDialog.getOpenFileName() return value difference, note QtWidgets for Qt5
 +      * PyQt: return filename
 +      * PySide: return (filename, extensionFilter)
 +
 +  * old and outdate QtMiddleMan.py (just as it is for reference) will handle the proper auto detection for PyQt4 and PySide
 +    * code <code python QtMiddleMan.py>
 +import sys
 +import os
 +
 +default_variant = 'PySide'
 +
 +env_api = os.environ.get('QT_API', 'pyqt')
 +if '--pyside' in sys.argv:
 +    variant = 'PySide'
 +elif '--pyqt4' in sys.argv:
 +    variant = 'PyQt4'
 +elif env_api == 'pyside':
 +    variant = 'PySide'
 +elif env_api == 'pyqt':
 +    variant = 'PyQt4'
 +else:
 +    variant = default_variant
 +
 +if variant == 'PySide':
 +    from PySide import QtGui, QtCore
 +    # This will be passed on to new versions of matplotlib
 +    os.environ['QT_API'] = 'pyside'
 +    def QtLoadUI(uifile):
 +        from PySide import QtUiTools
 +        return QtUiTools.QUiLoader().load(uifile)
 +elif variant == 'PyQt4':
 +    import sip
 +    api2_classes = [
 +            'QData', 'QDateTime', 'QString', 'QTextStream',
 +            'QTime', 'QUrl', 'QVariant',
 +            ]
 +    for cl in api2_classes:
 +        sip.setapi(cl, 2)
 +    from PyQt4 import QtGui, QtCore
 +    QtCore.Signal = QtCore.pyqtSignal
 +    QtCore.QString = str
 +    os.environ['QT_API'] = 'pyqt'
 +    def QtLoadUI(uifile):
 +        from PyQt4 import uic
 +        return uic.loadUi(uifile)
 +else:
 +    raise ImportError("Python Variant not specified")
 +
 +__all__ = [QtGui, QtCore, QtLoadUI, variant]
 +</code>
 +  * My code on qtMode detection <code python>
 +# ---- qtMode ----
 +qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5
 +qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5')
 +try:
 +    from PySide import QtGui, QtCore
 +    import PySide.QtGui as QtWidgets
 +    qtMode = 0
 +    if hostMode == "maya":
 +        import shiboken
 +except ImportError:
 +    try:
 +        from PySide2 import QtCore, QtGui, QtWidgets
 +        qtMode = 2
 +        if hostMode == "maya":
 +            import shiboken2 as shiboken
 +    except ImportError:
 +        try:
 +            from PyQt4 import QtGui,QtCore
 +            import PyQt4.QtGui as QtWidgets
 +            import sip
 +            qtMode = 1
 +        except ImportError:
 +            from PyQt5 import QtGui,QtCore,QtWidgets
 +            import sip
 +            qtMode = 3
 +print('Qt: {0}'.format(qtModeList[qtMode]))
 +</code>
 +    * condition import other module based on qtMode <code python>
 +QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork')
 +</code>
 +===== Break 3: Qt4 vs Qt5 =====
 +
 +  * Qt5:
 +    * all widgets moved to "QtWidgets" class
 +    * items that are still in QtGui <code>
 +QIcon
 +QKeySequence
 +QCursor
 +QTextFormat
 +QPainter
 +QPixmap
 +QPalette
 +</code>
 +
 +
 +
 +====== Qt - Common Widget Code ======
 +
 +===== Keystroke Detection =====
 +
 +  * modifier key detection <code python>
 +modifiers = QtWidgets.QApplication.queryKeyboardModifiers()
 +clickMode = 0 # basic mode
 +if modifiers == QtCore.Qt.ControlModifier:
 +    clickMode = 1 # ctrl
 +elif modifiers == QtCore.Qt.ShiftModifier:
 +    clickMode = 2 # shift
 +elif modifiers == QtCore.Qt.AltModifier:
 +    clickMode = 3 # alt
 +elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier | QtCore.Qt.AltModifier:
 +    clickMode = 4 # ctrl+shift+alt
 +elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
 +    clickMode = 5 # ctrl+alt
 +elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier:
 +    clickMode = 6 # ctrl+shift
 +elif modifiers == QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier:
 +    clickMode = 7 # alt+shift
 +</code>
 +
 +
 +===== Visibility and Style =====
 +
 +**Visibility control**
 +  * visibility and disable widget<code python>
 +# disable
 +self.uiList['namePublish_input'].setDisabled(1)
 +self.my_pushButton.setEnabled(False)
 +# hide
 +self.uiList[form_ui_name].hide()
 +
 +# toggle hidden
 +self.uiList['server_grp'].setVisible( self.uiList['server_grp'].isHidden() )
 +</code>
 +
 +**Style Sheet code**
 +
 +  * note: setSyleSheet will affect its child style as well
 +
 +  * pushButton <code python>my_pushButton.setStyleSheet("text-align: left; color: rgb(255, 128, 128)")
 +# set button text alignment, and text color pink</code>
 +  * more example: http://qt-project.org/doc/qt-4.8/stylesheet-examples.html
 +
 +  * global style set <code python>
 +QtWidgets.QApplication.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks'))
 +QtWidgets.QStyleFactory.keys()
 +# Mac Only: QMacStyle as Mac
 +# QWindowsXPStyle as WindowsXP
 +</code>
 +
 +  * global style for disabled ui element or readonly element <code python>
 +self.setStyleSheet("QLineEdit:disabled{background-color: gray;}")
 +main_color = self.palette().color(QtWidgets.QPalette.Window).name()
 +self.setStyleSheet('QTextEdit[readOnly="true"] { background-color: '+main_color+'; border: 0px }')
 +</code>
 +  * get global text color <code python>
 +text_color = self.palette().color(QtGui.QPalette.Text).name()
 +</code>
 +  * list all current color palette of default in app <code python>
 +from PySide2 import QtCore, QtGui, QtWidgets
 +import MyToolName
 +ui = MyToolName.main()
 +color_info = ui.palette().color(QtGui.QPalette.Dark).name()
 +print('color info {}'.format(color_info))
 +
 +color_roles = [
 +    QtGui.QPalette.WindowText, QtGui.QPalette.Button, QtGui.QPalette.Light, QtGui.QPalette.Midlight,
 +    QtGui.QPalette.Dark, QtGui.QPalette.Mid, QtGui.QPalette.Text, QtGui.QPalette.BrightText,
 +    QtGui.QPalette.ButtonText, QtGui.QPalette.Base, QtGui.QPalette.Window, QtGui.QPalette.Shadow,
 +    QtGui.QPalette.Highlight, QtGui.QPalette.HighlightedText, QtGui.QPalette.Link, QtGui.QPalette.LinkVisited,
 +    QtGui.QPalette.AlternateBase, QtGui.QPalette.NoRole
 +]
 +
 +print("Color roles and their corresponding colors:")
 +for role in color_roles:
 +    color = ui.palette().color(role)
 +    print(f"{role}: {color.name()}")
 +</code>
 +  * apply style sheet file <code python>
 +sshFile="darkorange.stylesheet"
 +with open(sshFile,"r") as fh:
 +    self.setStyleSheet(fh.read())
 +</code>
 +
 +  * QListWidget style with icon-like widget elements<code python>
 +self.uiList['proc_list'].setStyleSheet("QListWidget::item {background-color: #656565; padding: 2px;border-style: solid;border: 1px solid black;border-radius:8px; }")
 +</code>
 +  * app font size control <code python>
 +self.memoData['font_size_default'] = QtGui.QFont().pointSize()
 +self.memoData['font_size'] = self.memoData['font_size_default']
 +
 +def fontNormal_action(self):
 +    self.memoData['font_size'] = self.memoData['font_size_default']
 +    self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size'])
 +def fontUp_action(self):
 +    self.memoData['font_size'] += 2
 +    self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size'])
 +def fontDown_action(self):
 +    if self.memoData['font_size'] >= self.memoData['font_size_default']:
 +        self.memoData['font_size'] -= 2
 +        self.setStyleSheet("QLabel,QPushButton { font-size: %dpt;}" % self.memoData['font_size'])
 +</code>
 +  * get app or widget palette style sample code <code python>
 +from PySide import QtGui
 +
 +groups = ['Disabled', 'Active', 'Inactive', 'Normal']
 +roles = ['Window',
 +        'Background',
 +        'WindowText',
 +        'Foreground',
 +        'Base',
 +        'AlternateBase',
 +        'ToolTipBase',
 +        'ToolTipText',
 +        'Text',
 +        'Button',
 +        'ButtonText',
 +        'BrightText']
 +
 +# ref: http://forums.cgsociety.org/showthread.php?t=1289854
 +def getPaletteInfo(palette = None):
 +    #build a dict with all the colors
 +    if palette == None: 
 +        palette = QtGui.QApplication.palette()
 +    result = {}   
 +    for role in roles:
 +        for group in groups:
 +            qGrp = getattr(QtGui.QPalette, group)
 +            qRl = getattr(QtGui.QPalette, role)
 +            result['%s:%s' % (role, group)] =  palette.color(qGrp, qRl).rgba()
 +    return result
 +
 +def setPaletteFromDct(styleDict):
 +    palette = QtGui.QPalette()
 +    for role in roles:
 +        for group in groups:
 +            color = QtGui.QColor(styleDict['%s:%s' % (role, group)])
 +            qGrp = getattr(QtGui.QPalette, group)
 +            qRl = getattr(QtGui.QPalette, role)
 +            palette.setColor(qGrp, qRl, color)
 +    QtGui.QApplication.setPalette(palette)
 +# -- example 
 +# get maya app style
 +maya_style = getPaletteInfo() 
 +# -- get a QMainWindow style
 +piper = Piper.main()
 +p_style = getPaletteInfo(piper.palette())
 +</code>
 +===== Variable Access =====
 +
 +  * access by parent.child <code python>
 +self.my_pushButton=my_pushButton # self is the parent, put the child as parent's property
 +</code>
 +  * access by Object Name <code python>
 +root_pushButton.setObjectName("root_pushButton")
 +
 +my_app_instance.findChild(QtGui.QPushButton, QtCore.QString("root_pushButton")) # PyQt4 format
 +</code>
 +
 +===== Connection =====
 +
 +**common connection**
 +  * old style <code python>
 +QtCore.QObject.connect(self.uiList['proj_choice'], QtCore.SIGNAL("activated(QString)"), self.proj_choice_action)
 +QtCore.QObject.connect(self.uiList['item_tree'], QtCore.SIGNAL("itemExpanded(QTreeWidgetItem*)"), self.itemTreeExpand_action)
 +QtCore.QObject.connect(self.uiList['item_tree'], QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), self.itemTreeSelect_action)
 +QtCore.QObject.connect(self.uiList['autoPublish_check'], QtCore.SIGNAL("toggled(bool)"), self.uiList['namePublish_input'].setDisabled)
 +</code>
 +  * new style <code python>
 +self.uiList['proj_choice'].activated[str].connect(self.proj_choice_action)
 +self.uiList['item_tree'].itemExpanded[QtWidgets.QTreeWidgetItem].connect(self.itemTreeExpand_action)
 +self.uiList['item_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.itemTreeSelect_action)
 +self.uiList['autoPublish_check'].toggled[bool].connect(self.uiList['namePublish_input'].setDisabled)
 +self.uiList['search_input'].textChanged.connect(self.tree_search_action)
 +self.uiList['client_input'].returnPressed.connect(self.client_sendMsg)
 +
 +tree_name = 'vtx_bone_tree'
 +self.uiList[tree_name'_search_input'].textChanged.connect( getattr(self, tree_name+"_search_action", partial(self.filter_tree,tree_name)) )
 +</code>
 +
 +**Python Qt multiple signal to single slot connection**
 +  * method 1: partial <code python>
 +from functools import partial
 +
 +self.my_tableWidget.setRowCount(len(my_List));
 +self.my_tableWidget.setColumnCount(2);
 +self.my_tableWidget.setHorizontalHeaderLabels(['Current Name', 'Previous Name']);
 +
 +for i in range(0, len(my_List)):
 +    new_item_btn= QPushButton(self.my_tableWidget)
 +    new_item_btn.setText(my_List[i][0])
 +    #QtCore.QObject.connect(new_item_btn, QtCore.SIGNAL("clicked()"),partial(self.buttonSelect, my_List[i][0])) # old way of connect
 +    new_item_btn.clicked.connect( partial(self.buttonSelect, my_List[i][0]) ) # new universal way of connect
 +    self.my_tableWidget.setCellWidget(i, 0, new_item_btn)
 +
 +def buttonSelect(self, btn_name):
 +    print btn_name
 +</code>
 +  * method 2: get sender()<code python>
 +#QtCore.QObject.connect(new_item_btn, QtCore.SIGNAL("clicked()"),self.buttonSelect) # old way of connect
 +new_item_btn.clicked.connect(self.buttonSelect) # new universal way of connect
 +
 +def buttonSelect(self):
 +    sender=self.sender()
 +    print str(sender.text())
 +</code>
 +
 +**Pass User Defined Function to Class Function variable holder**
 +
 +  * code <python>
 +wip
 +</code>
 +===== Hotkey and Shortcut =====
 +
 +  * QShortcut needs a parent widget to be able to catch user input
 +
 +  * ref: 
 +    * enum Qt::Key: http://doc.qt.io/qt-4.8/qt.html
 +    * QtGui.QKeySequence: http://doc.qt.io/qt-4.8/qkeysequence.html
 +    * wheel and modify key: http://programtalk.com/python-examples/PyQt4.QtGui.QApplication.queryKeyboardModifiers/
 +
 +  * QAction based shortcut example <code python>
 +self.actionZoomIn = QtGui.QAction('Zoom In', self)
 +self.actionZoomOut = QtGui.QAction('Zoom Out', self)
 +key = QtCore.Qt.CTRL | QtCore.Qt.Key_Equal
 +self.actionZoomIn.setShortcut(QtGui.QKeySequence(key))
 +
 +self.actionZoomOut.setShortcut(QtGui.QKeySequence('Ctrl+-'))
 +</code>
 +  * Main window standalone hotkey example <code python>
 +self.hotkey['tab_1'] = QtWidgets.QShortcut(QtGui.QKeySequence( "Ctrl+1"), self)
 +self.hotkey['tab_1'].activated.connect( partial(self.showTab, 0) )
 +
 +def showTab(self, index):
 +    self.uiList['main_tab'].setCurrentIndex(index)
 +</code>
 +  * per widget level hotkey example (note: defaultly focused widget shortcuts override window shortcuts ) \\ ref: https://stackoverflow.com/questions/44914888/qt-override-widget-shortcut-window-shortcut \\ <code python>
 +#self.hotkey['node_tree_search_clear'] = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self.uiList['node_tree'], None, None, QtCore.Qt.WidgetShortcut)
 +self.hotkey['node_tree_search_clear'] = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+D"), self.uiList['main_tab'].widget(1), None, None, QtCore.Qt.WidgetWithChildrenShortcut)
 +self.hotkey['node_tree_search_clear'].activated.connect(self.node_tree_search_clear_action)
 +</code>
 +  * **Special Note**: Ctrl and Enter key is actually refer as "Ctrl+Return"
 +==== system wide hotkey ====
 +
 +  * **PyGlobalShortcut**: a simple easy module to achieve cross-platform for PyQt4, PyQt5 situation (no PySide yet)
 +    - install from source require compile, while installing from wheel can work out of box, download
 +    - src download: https://pypi.python.org/pypi/PyGlobalShortcut/
 +    - wheel binary download: https://github.com/Asvel/pygs/releases
 +    - example <code python test_global_hotkey.py>
 +import os, sys
 +
 +from PyQt4.QtGui import QApplication, QKeySequence
 +from pygs import QxtGlobalShortcut
 +
 +SHORTCUT_SHOW = "Ctrl+Alt+S"  # Ctrl maps to Command on Mac OS X
 +SHORTCUT_EXIT = "Ctrl+Alt+F"  # again, Ctrl maps to Command on Mac OS X
 +def show_activated():
 +    print("Shortcut Activated!")
 +
 +app = QApplication([])
 +
 +shortcut_show = QxtGlobalShortcut()
 +shortcut_show.setShortcut(QKeySequence(SHORTCUT_SHOW))
 +shortcut_show.activated.connect(show_activated)
 +
 +shortcut_exit = QxtGlobalShortcut()
 +shortcut_exit.setShortcut(QKeySequence(SHORTCUT_EXIT))
 +shortcut_exit.activated.connect(app.exit)
 +
 +return_code = app.exec_()
 +
 +del shortcut_show
 +del shortcut_exit
 +sys.exit(return_code)
 +</code>
 +
 +  * other windows OS specific method without extra 3rd module is using win32 API's RegisterHotKey, GetMessageA 
 +    * ref: 
 +      * http://timgolden.me.uk/python/win32_how_do_i/catch_system_wide_hotkeys.html
 +===== Event =====
 +
 +  * Event can be handled by 
 +    - re-creation the implementation of the Class's event function
 +    - install a global event filter to handle all its childs' event
 +  * all Event types: http://doc.qt.io/qt-5/qevent.html
 +==== Everything about Drag and Drop in Qt ====
 +
 +  * The Drag to Drop event process
 +    - you start drag  something (a text, a file or something)
 +    - you mouse move to a widget with "setAcceptDrops(1)"
 +    - the widget has user dragEnterEvent() and dropEvent() implemented or has "installEventFilter(handlerObj)" with a handler object. a event and object will be passed to the handing function
 +    - the "dragEnterEvent" is activated, and its handler will catch the event
 +      - event comes with "mimeData()", which can have "urls()" with "path()" for files and "text()" for texts
 +      - of course, better check "mimeData()" for "hasText()" and "hasUrls()" and "hasFormat()"
 +    - if handler determine it is ok to process, it will "accept()" for next drop event and ask handler function to return true
 +    - after "event.accept()", then "dropEvent()" will be going through drop related process with given event and object
 +    - additionally, event can also 
 +
 +^ Event functions ^^
 +| accept() | set accepted flag to indicate the receiver want the event |
 +| ignore() | set accepted flag off |
 +^ QDropEvent functions (ref: http://doc.qt.io/qt-5/qdropevent.html) ^^
 +| acceptProposedAction() | Sets the drop action to be the proposed action |
 +| setDropAction() | set what action the receive to do with data |
 +| ... | QtCore.Qt.CopyAction | Copy the data to the target |
 +| ... | QtCore.Qt.MoveAction | Move the data to the target |
 +| ... | QtCore.Qt.LinkAction | Link the source to the target |
 +| ... | QtCore.Qt.IgnoreAction | do nothing |
 +| source() | return where the drag starts, can be a widget or zero for outside | Note, not all event type has source, so check the target object first |
 +| mimeData() | 
 +| pos() | drop position |
 +^ QDragEnterEvent functions (ref:http://doc.qt.io/qt-5/qdragenterevent.html) ^^
 +| same as above ||
 +| setDropAction() | above Action option |
 +| accept() | accept and allow drop event |
 +
 +**QMimeData**
 +  * full mime list: http://www.iana.org/assignments/media-types/media-types.xhtml
 +
 +^ Tester ^ Getter ^ Setter ^ MIME Types ^
 +| hasText() | text() | setText() | text/plain |
 +| hasHtml() | html() | setHtml() | text/html |
 +| hasUrls() | urls() | setUrls() | text/uri-list |
 +| hasImage() | imageData() | setImageData() | image/* |
 +| hasColor() | colorData() | setColorData() | application/x-color |
 +
 +  * in structure widget, like tree widget and list widget, it has its internal drag and drop and external drag and drop.
 +
 +^ for external drag and drop (outside widget, can be from other App or other widget ^^
 +^ my_widget.setAcceptDrops(1) | will accept drop from others |
 +^ my_widget.setDragEnabled(1) | will allow drag starts from itself to others |
 +^ for internal drag and drop ^^
 +| setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) | internally move around |
 +| setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) | individual toggle select status |
 +| setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) | standard explorer type selection handling |
 +| setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection) | forced continuous selection |
 +| setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) | single select |
 +| setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) | no select allowed |
 +
 +  * add event listener to UI element without re-implement the class by using eventFilter <code python>
 +# add drag and drop event for a QLineEdit, so it accept file path to set its text
 +self.uiList['proj_filePath_input'].installEventFilter(self) # let main windows handle its event
 +
 +# the main window event filter function
 +def eventFilter(self, object, event):
 +    if event.type() == QtCore.QEvent.DragEnter:
 +        data = event.mimeData()
 +        urls = data.urls()
 +        if object is self.uiList['proj_filePath_input'] and (urls and urls[0].scheme() == 'file'):
 +            event.acceptProposedAction()
 +        return 1
 +    elif event.type() == QtCore.QEvent.Drop:
 +        data = event.mimeData()
 +        urls = data.urls()
 +        if object is self.uiList['proj_filePath_input'] and (urls and urls[0].scheme() == 'file'):
 +            filePath = unicode(urls[0].path())[1:]
 +            print(filePath)
 +            self.uiList['proj_filePath_input'].setText(filePath)
 +        return 1
 +    return 0
 +
 +</code>
 +
 +  * For QTreeWidget, you can use 1) re-implement method (the startDrag function), 2) listener event method
 +    - the startDrag re-implement method <code python>
 +class MyTree(QtWidgets.QTreeWidget):
 +    def __init__(self):
 +        QtWidgets.QTreeWidget.__init__(self)
 +        self.setDragEnabled(1)
 +        # setupUI
 +        node_list = [ 'file', 'place2dTexture', 'reverse']
 +        [self.invisibleRootItem().addChild( QtWidgets.QTreeWidgetItem([x]) ) for x in node_list]
 +        
 +    def startDrag(self, dropActions ):
 +        # item drag activate this
 +        print('start drag')
 +        item = self.currentItem()
 +        mimeData = QtCore.QMimeData()
 +        mimeData.setText(item.text(0)) # work with Qt object, itself or other application
 +        #mimeData.setData('text/plain', unicode(item.text(0))) # not work as text like above
 +        print(item.text(0))
 +        
 +        drag = QtGui.QDrag(self)
 +        drag.setMimeData(mimeData)
 +        drag.setHotSpot(QtCore.QPoint(12,12))
 +        
 +        # this make it work, return the result action
 +        # if drag.start is old method, exec conflict in old code, use exec_
 +        if drag.exec_(QtCore.Qt.CopyAction) == QtCore.Qt.CopyAction: 
 +            print('Drag dropped ok.')
 +    # for demo purpose as below
 +    def dragMoveEvent(self):
 +        print('Move')
 +        currentNode = self.currentItem()
 +        if currentNode:
 +            print(currentNode.text(0))
 +    def event(self, event):
 +        if event.type() == QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Tab:
 +            print('Tab key now')
 +            return 1
 +        return 0
 +</code>
 +    - the event listener method for tree <code python>
 +    cur_tree = self.uiList['main_browse'].uiList['file_tree']
 +    cur_tree.setDragEnabled(1)
 +    cur_tree.installEventFilter(self)
 +
 +    def eventFilter(self, object, event):
 +        cur_tree = self.uiList['main_browse'].uiList['file_tree']
 +        if object is cur_tree and event.type()== QtCore.QEvent.ChildRemoved: # the tree drag event name
 +            #print(event.type())
 +            currentNode = cur_tree.currentItem()
 +            if currentNode:
 +                # for multi selection case
 +                path_text = '\n'.join([unicode(node.text(0)) for node in cur_tree.selectedItems()])
 +                # for single selection case
 +                #cur_path = unicode(currentNode.text(0))
 +                #print(cur_path)
 +                mimeData = QtCore.QMimeData()
 +                mimeData.setText(path_text)
 +                drag = QtGui.QDrag(self)
 +                drag.setMimeData(mimeData)
 +                drag.setHotSpot(QtCore.QPoint(12,12))
 +                if drag.exec_(QtCore.Qt.CopyAction) == QtCore.Qt.CopyAction: 
 +                    print('Drag dropped ok.')
 +                return 1
 +        return 0
 +        
 +</code>
 +
 +==== QDrag and QMimeData ====
 +
 +  * mimedata is a object that contains the dragging information, all applications support it and pass information in that object format. like Explorer, Nuke, etc.
 +  * Drag is the object handle the drag Action (the whole process of dragging), it contains information about dragging and that mimedata.
 +
 +^ QDrag's method ^
 +| setMimeData() | store the mimedata |
 +| setPixmap() |
 +| setHotSpot() |
 +| exec_() | start/init/exec the drag action |
 +===== QThread and Threading =====
 +
 +  * for now, most simple and straight forward process function is written in the UI class, which means when running the process function, the UI is not responding.
 +  * so common solution is using QThread object to wrap the process function, and create the QThread from main UI code, and use signal from QThread and slot on main UI to pass information around
 +    * ref: https://www.daniweb.com/programming/software-development/threads/423037/qt-signal-to-change-the-gui-out-side-the-main-thread
 +
 +===== QNetwork =====
 +
 +  * import QtNetwork <code python>qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5
 +qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5')
 +QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork')
 +</code>
 +  * Server side <code python>
 +self.server = QtNetwork.QTcpServer()
 +self.server_connection_list = []
 +
 +# responding callback function
 +self.server.newConnection.connect(self.server_newConnection)
 +
 +# format prepare
 +self.server_SIZEOF_UINT32 = 4 # sizeof_uint16 = 2
 +
 +# try start server
 +if not self.server.listen(QtNetwork.QHostAdress('0.0.0.0'), 20180):
 +    print('Server is unable to start: %s.' % self.server.errorString())
 +    self.server.close()
 +    
 +# get server port if it you not use a port number
 +# self.server.serverPort()
 +
 +def server_newConnection(self):
 +    # get incoming client
 +    new_client = self.server.nextPendingConnection()
 +    
 +    self.server_connection_list.append(new_client) # for msg them later
 +    
 +    new_client.disconnected.connect(new_client.deleteLater)
 +    new_client.readyRead.connect(self.server_receiveMsg) # for optional 2 way msg
 +    new_client.error.connect(self.server_error) # for optional error feedback
 +    
 +    # prepare msg
 +    msg = 'Welcome from Server'
 +    reply = QtCore.QByteArray()
 +    stream = QtCore.QDataStream(reply, QtCore.QIODevice.WriteOnly)
 +    stream.setVersion(QtCore.QDataStream.Qt_4_2)
 +    stream.writeUInt32(0) # not sure why, some writes writeUInt16()
 +    stream.writeQString(msg) # some writes writeString()
 +    stream.device().seek(0)
 +    stream.writeUInt32(reply.size() - self.server_SIZEOF_UINT32)
 +    
 +    # msg client
 +    new_client.write(reply)
 +    # optional one time client msg
 +    # new_client.disconnectFromHost()
 +# close server by
 +self.server.close()
 +</code>
 +  * client side <code python>
 +# python 2,3 support unicode function
 +try:
 +    UNICODE_EXISTS = bool(type(unicode))
 +except NameError:
 +    # lambda s: str(s) # this works for function but not for class check
 +    unicode = str
 +
 +self.socket = QtNetwork.QTcpSocket()
 +
 +# read note
 +self.nextBlockSize = 0
 +
 +# responding callback function
 +self.socket.readyRead.connect(self.client_readMsg)
 +self.socket.disconnected.connect(self.client_serverOff) # optional server off respond
 +self.socket.error[QtNetwork.QAbstractSocket.SocketError].connect(self.client_serverError) 
 +# error[x] to client_serverError()
 +# or error to  client_serverError(e)
 +
 +# socket stop
 +self.socket.abort()
 +# sock connect
 +self.socket.connectToHost('127.0.0.1', 20180)
 +
 +def client_sendMsg(self, text):
 +    request = QtCore.QByteArray()
 +    stream = QtCore.QDataStream(request, QtCore.QIODevice.WriteOnly)
 +    stream.setVersion(QtCore.QDataStream.Qt_4_2)
 +    stream.writeUInt32(0)
 +    stream.writeQString(text) # or writeString()
 +    stream.device().seek(0)
 +    stream.writeUInt32(request.size() - self.server_SIZEOF_UINT32)
 +    self.socket.write(request)
 +    self.nextBlockSize = 0 # reset ???
 +    
 +# responding functions
 +def client_readMsg(self):
 +    stream = QtCore.QDataStream(self.socket)
 +    stream.setVersion(QtCore.QDataStream.Qt_4_2)
 +    if self.nextBlockSize == 0:
 +        if self.socket.bytesAvailable() < self.server_SIZEOF_UINT32:
 +            return
 +        self.nextBlockSize = stream.readUInt32()
 +    if self.socket.bytesAvailable() < self.nextBlockSize:
 +        return
 +    textFromServer = unicode(stream.readQString()) # or readString()
 +    # process textFromServer as you like
 +def client_serverError(self):
 +    print("Error: {}".format(self.socket.errorString()))
 +</code>
 +===== Main UI response =====
 +
 +
 +===== menu item and action =====
 +
 +  * QAction can be a menu item or a tool icon button, when as menu item, it can have a checkbox instead of icon beside it. <code python>
 +def quickMenuAction(self, objName, title, tip, icon, menuObj):
 +    self.uiList[objName] = QtWidgets.QAction(QtGui.QIcon(icon), title, self)        
 +    self.uiList[objName].setStatusTip(tip)
 +    menuObj.addAction(self.uiList[objName])
 +self.quickMenuAction('toggleDragMode_atn','&Enable Drag Mode','Enable Drag Mode.','', cur_menu)
 +self.uiList['toggleDragMode_atn'].setShortcut(QtGui.QKeySequence("Ctrl+E"))
 +self.uiList['toggleDragMode_atn'].setCheckable(1)
 +self.uiList['toggleDragMode_atn'].setChecked(0)
 +</code>
 +
 +  * widget level right click menu assign process <code python>
 +self.uiList['my_btn'].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
 +self.uiList['my_menu'] = QtWidgets.QMenu()
 +self.uiList['select_load_atn'] = QtWidgets.QAction('Load selection', self)
 +self.uiList['select_add_atn'] = QtWidgets.QAction('Add to selection', self)
 +self.uiList['my_menu'].addAction(self.uiList['select_load_atn'])
 +self.uiList['my_menu'].addAction(self.uiList['select_add_atn'])
 +
 +for ui_name in self.uiList.keys():
 +    prefix = ui_name.rsplit('_', 1)[0]
 +    if ui_name.endswith('_btn'):
 +        self.uiList[ui_name].clicked.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name)))
 +    elif ui_name.endswith('_atn'):
 +        self.uiList[ui_name].triggered.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name)))
 +self.uiList['my_btn'].customContextMenuRequested.connect(self.my_menu_call)
 +
 +def my_menu_call(self, point):
 +   self.uiList['vtx_assist_menu'].exec_(self.uiList['my_btn'].mapToGlobal(point))
 +def select_load_action(self):
 +   print('process load')
 +def select_add_action(self):
 +   print('process add')
 +def my_action(self):
 +   print('btn clicked')
 +</code>
 +  * menu call custom and default fallback function <code python>
 +for tree_name in ['vtx_bone_tree','vtx_vmp_tree','vtx_smp_tree', 'vtx_bsmp_tree']:
 +    self.uiList[tree_name].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
 +    self.uiList[tree_name].customContextMenuRequested.connect( getattr(self, tree_name+"_menu_call", partial(self.default_menu_call, tree_name)) )
 +    
 +def default_menu_call(self, ui_name, point):
 +    if ui_name in self.uiList.keys() and ui_name+'_menu' in self.uiList.keys():
 +        self.uiList[ui_name+'_menu'].exec_(self.uiList[ui_name].mapToGlobal(point))
 +def vtx_bsmp_tree_menu_call(self, point):
 +    tree_name = 'vtx_bsmp_tree'
 +    cur_tree = self.uiList[tree_name]
 +    cur_node = cur_tree.currentItem()
 +    pattern = 'vtx_bsmp_tree_{0}_atn'
 +    for x in ['rename','delete','update','setBoneAll','applyAll','remove','setBone','apply','import','export']:
 +        self.uiList[pattern.format(x)].setEnabled(0)
 +    if cur_node:
 +        cur_type = str(cur_node.text(3))
 +        if cur_type == 'bs':
 +            for x in ['rename','delete','update','setBoneAll','applyAll','remove']:
 +                self.uiList[pattern.format(x)].setEnabled(1)
 +        elif cur_type == 'attr':
 +            for x in ['rename','delete','setBone','apply','import','export']:
 +                self.uiList[pattern.format(x)].setEnabled(1)
 +    self.uiList[tree_name+'_menu'].exec_(self.uiList[tree_name].mapToGlobal(point))
 +</code>
 +===== Simple Window =====
 +
 +  * toggle always on top window flag <code python>
 +def toggleTopFlag_action(self):
 +    if self.uiList['toggleTopFlag_atn'].isChecked():
 +        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
 +        self.show()
 +    else:
 +        self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowStaysOnTopHint)
 +        self.show()
 +        
 +def toggleTop_action(self):
 +    self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint)
 +    self.show()
 +</code>
 +  * in mac, there is like <code python>
 +self.show()
 +self.raise_()
 +</code>
 +  * convert QMainWindow instance into QWidget to be able into embed into other widget <code python>
 +new_CustomMainWindowClass.setWindowFlags(QtCore.Qt.Widget)
 +
 +def setWidgetMode(self):
 +    self.setWindowFlags(QtCore.Qt.Widget)
 +    self.menuBar().hide()
 +    self.statusBar().hide()
 +    self.uiList['main_layout'].setContentsMargins(0, 0, 0, 0)
 +def setWidgetMode(self, win_ui):
 +    win_ui.setWindowFlags(QtCore.Qt.Widget)
 +    win_ui.menuBar().hide()
 +    win_ui.statusBar().hide()
 +    win_ui.uiList['main_layout'].setContentsMargins(0, 0, 0, 0)
 +</code>
 +
 +  * close event <code python>
 +def closeEvent(self, event):
 +    print('Closing now')
 +</code>
 +
 +  * window right click menu <code python>
 +def contextMenuEvent(self, event):
 +    menu = QtWidgets.QMenu(self)
 +    quitAction = menu.addAction("Quit")
 +    action = menu.exec_(self.mapToGlobal(event.pos()))
 +    if action == quitAction:
 +        self.close()
 +</code>
 +  * window drag and move <code python>
 +self.drag_position=QtGui.QCursor.pos() # need init it first after creation
 +
 +def mouseMoveEvent(self, event):
 +    if (event.buttons() == QtCore.Qt.LeftButton):
 +        self.move(event.globalPos().x() - self.drag_position.x(),
 +            event.globalPos().y() - self.drag_position.y())
 +    event.accept()
 +def mousePressEvent(self, event):
 +    if (event.button() == QtCore.Qt.LeftButton):
 +        self.drag_position = event.globalPos() - self.pos()
 +    event.accept()
 +</code>
 +
 +  * QDialog and QMainWindow are specifically designed to be their own floating window UIs
 +
 +**using Qt Designer ui binding for Python**
 +  * ref: (ref: http://zetcode.com/tutorials/pyqt4/
 +  * code <code python PyQtDemo.py>
 +import PyQt4
 +from PyQt4 import QtGui,QtCore, uic
 +
 +ui_file = 'my_qt_design_ui_file.ui' # in Qt Designer, the pushButton object name is set as "my_pushButton"
 +form, base = uic.loadUiType(ui_file)
 +
 +class TestWinUI(base, form):
 +    def __init__(self):
 +        super(base,self).__init__()
 +        self.setupUi(self)
 +        self.Establish_Connections()
 +    
 +    # handler functions
 +    def my_btn_fn(self):  
 +        print('do something'   
 +
 +    # action listener binding
 +    def Establish_Connections(self):
 +        # old connection
 +        QtCore.QObject.connect(self.my_pushButton, QtCore.SIGNAL("clicked()"),self.my_btn_fn)
 +        # new connection format
 +        #self.my_pushButton.clicked.connect(self.my_btn_fn)
 +
 +def main():
 +    global myWin
 +    myWin=TestWinUI()
 +    myWin.show()
 +    
 +if __name__=="__main__":
 +    main()
 +</code>
 +
 +**Manually Code GUI**
 +
 +  * window in PyQt4 format <code python CodeWinUI.py>
 +import PyQt4
 +from PyQt4 import QtGui,QtCore
 +
 +class TestWinUI(QtGui.QMainWindow):
 +    def __init__(self):
 +        QtGui.QMainWindow.__init__(self)
 + 
 +        # always on top
 +        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
 +        #location and size
 +        self.setGeometry(300, 300, 300, 300)
 +        #title
 +        self.setWindowTitle("My Tool - v1.0")
 +        #resize
 +        self.resize(250,250)
 +        #icon
 +        self.setWindowIcon(QtGui.QIcon('icon.png'))
 +        #center
 +        self.center()
 +        #fixed size
 +        self.setFixedHeight(500)
 +        self.setMinimumHeight( 0 )
 +        self.setMaximumHeight( 2048 )
 +        self.setMinimumSize(128, 128)
 +        self.setMaximumSize(128, 128)
 +        # maximized
 +        self.showMaximized()
 +        # show and hide
 +        self.setVisible(True)
 + 
 +    def center(self):
 +        qr = self.frameGeometry()
 +        cp = QtGui.QDesktopWidget().availableGeometry().center()
 +        qr.moveCenter(cp)
 +        self.move(qr.topLeft())  
 + 
 +def main():
 +    global myWin
 +    myWin=TestWinUI()
 +    myWin.show()
 +</code>
 +
 +**Convert ui file to Source file**
 +
 +  * PyQt4 case example:
 +    * for c++ code conversion from ui <code bash>uic-qt4 my.ui -o myUI.cpp</code>
 +    * for python code conversion from ui file <code bash>pyuic4 my.ui -o myUI.py</code>
 +      * note: you can find pyuic4 in the Qt package folder, same with QtDeisgner \Python27\Lib\site-packages\PyQt4
 +      * note: for windows, it is pyuic4.bat file
 +
 +**compile resource like images into a py file**
 +
 +  * if you have .qrc file, it is the link to the local resource files, you can use utility pyrccc4 (pyqt4 case) to convert into binary data contained py file, example <code python>pyrcc4 -py3 resource.qrc -o resource_rc.py</code>
 +  * inside qrc <code>
 +<!DOCTYPE RCC><RCC version="1.0">
 +<qresource>
 +    <file>images/icon.png</file>
 +</qresource>
 +</RCC>
 +</code>
 +  * refer to resource_rc.py by <code python>
 +import resource_rc
 +QtGui.QPixmap(':/images/icon.png')
 +</code>
 +===== Dialog =====
 +
 +  * message dialog <code python>
 +try:
 +    from PySide import QtGui, QtCore
 +    import PySide.QtGui as QtWidgets
 +    print("PySide Try")
 +    qtMode = 0
 +except ImportError:
 +    try:
 +        from PySide2 import QtCore, QtGui, QtWidgets
 +        print("PySide2 Try")
 +        qtMode = 2
 +    except ImportError:
 +        try:
 +            from PyQt4 import QtGui,QtCore
 +            import PyQt4.QtGui as QtWidgets
 +            import sip
 +            qtMode = 1
 +            print("PyQt4 Try")
 +        except ImportError:
 +            from PyQt5 import QtGui,QtCore,QtWidgets
 +            import sip
 +            qtMode = 3
 +            print("PyQt5 Try")
 +            
 +def quickMsg(msg):
 +    tmpMsg = QtWidgets.QMessageBox() # for simple msg that no need for translation
 +    tmpMsg.setWindowTitle("Info")
 +    tmpMsg.setText(msg)
 +    tmpMsg.addButton("OK",QtWidgets.QMessageBox.YesRole)
 +    tmpMsg.exec_()
 +</code>
 +  * prompt dialog <code python>
 +def quickMsgAsk(msg, mode=0, choice=[]):
 +    # getItem, getInteger, getDouble, getText
 +    modeOpt = (QtGui.QLineEdit.Normal, QtGui.QLineEdit.NoEcho, QtGui.QLineEdit.Password, QtGui.QLineEdit.PasswordEchoOnEdit)
 +    # option: QtWidgets.QInputDialog.UseListViewForComboBoxItems
 +    if len(choice)==0:
 +        txt, ok = QtGui.QInputDialog.getText(None, "Input", msg, modeOpt[mode])
 +        return (unicode(txt), ok)
 +    else:
 +        txt, ok = QtGui.QInputDialog.getItem(None, "Input", msg, choice, 0, 0) # current, editable
 +        return (unicode(txt), ok)
 +</code>
 +  * prompt dialog in list style instead of combo box <code python>
 +def input_dialog(title, items):
 +    input_dialog = QtGui.QInputDialog()
 +    input_dialog.setComboBoxItems(items)
 +    input_dialog.setLabelText(title)
 +    #input_dialog.setComboBoxEditable(False)
 +    input_dialog.setOption(QtGui.QInputDialog.UseListViewForComboBoxItems)
 +    done = input_dialog.exec_()
 +    user_choice = input_dialog.textValue()
 +    return user_choice,done
 +</code>
 +  * font dialog <code python>
 +def font_action(self):
 +    font, ok = QtWidgets.QFontDialog.getFont()
 +    if ok:
 +        self.uiList['font_label'].setFont(font)
 +</code>
 +
 +===== Layout and Container =====
 +
 +  * Great ref on layout: http://doc.qt.io/qt-5/qtwidgets-layouts-basiclayouts-example.html
 +  
 +  * QVBoxLayout and QHBoxLayout <code python>
 +my_verticalLayout.setContentsMargins(0,0,0,0) # set margin of the boundingbox of layout
 +
 +# UI alignment
 +my_layout.setAlignment(QtCore.Qt.AlignTop)
 +</code>
 +  * QGridLayout <code python>my_grid_layout.setSpacing(0) # 0 tight grid like maya grid 
 +
 +# -- make grid equal size for all cells
 +cur_layout = ui['region_grid']
 +# for i in range( cur_layout.rowCount() ):
 +#    cur_layout.setRowStretch( i, 1)
 +for j in range( cur_layout.columnCount() ):
 +    cur_layout.setColumnStretch( j, 1 )
 +
 +</code>
 +  * QFormLayout
 +  * QSplitter <code python>
 +my_QSplitter = QtGui.QSplitter()
 +my_QSplitter.setObjectName("my_QSplitter")
 +my_QSplitter.setOrientation(QtCore.Qt.Vertical) # so the split into top to bottom panels
 +my_QSplitter.addWidget(each_widget) # add to each split panel
 +my_QSplitter.setSizes([200,500])
 +</code>
 +  * QGroup
 +  * QTabWidget
 +    * customize look <code python>
 +my_tab.setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }")
 +</code>
 +  * QScrollArea
 +    * put widget into a scroll area <code python>
 +# main_layout is top most layout
 +main_scroll = QtWidgets.QScrollArea()
 +main_scroll.setWidgetResizable(1)
 +
 +content_widget = QtWidgets.QWidget()
 +content_layout = QtWidgets.QVBoxLayout(content_widget)
 +
 +main_scroll.setWidget(content_widget)
 +
 +test_label = QtWidgets.QLabel('Example Text')
 +test_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
 +content_layout.addWidget(tmpLabel)
 +
 +main_layout.addWidget(main_scroll)
 +# optional
 +main_layout.setStyleSheet("QScrollArea{min-width:500 px; min-height: 400px}")
 +</code>
 +===== QWidget Property =====
 +
 +  * set width <code python>self.uiList['filter_label'].setFixedWidth(80)</code>
 +  * get current widget in focus and get its class name <code python>QtWidgets.QApplication.focusWidget().metaObject().className()</code>
 +  * get parent widget <code python>my_widget.parentWidget()</code>
 +  * get most top widget, mostly the main window object <code python>my_widget.window()</code>
 +  * get base class <code python>for base in self.__class__.__bases__:
 +    print(base.__name__)
 +</code>
 +
 +===== Text Widget =====
 +
 +  * note: in PyQt4, PyQt5, the text returned is QString instead of python str object, so you need put str(result) to get python string. For PySide, PySide2, it automatically return python str object
 +
 +  * get label or content of widget<code python>test.root_lineEdit.text()</code>
 +
 +  * python str to qt string <code python>qt_txt = QtCore.QString(u'normal text')
 +normal_txt=str(qt_txt)
 +</code>
 +
 +  * QLineEdit emit typing signal <code python>
 +self.uiList['search_input'].textChanged.connect(self.dir_tree_search_action)
 +</code>
 +  * QLineEdit readOnly <code python>
 +self.uiList['user_input'].setDisabled(1) # grey out look, can use style to tune better
 +self.uiList['user_input'].setReadOnly(1) # normal look, but not editable
 +</code>
 +  * QLineEdit password hide <code python>
 +self.uiList['user_input'].setEchoMode(QtWidgets.QLineEdit.Password)
 +</code>
 +  * QTextEdit make it accept plain text only <code python>
 +# make it plain text editor as pasted
 +self.uiList['env_txt'].setAcceptRichText(0)
 +# get back text
 +env_text = unicode(self.uiList['env_txt'].toPlainText())
 +</code>
 +  * QTextEdit enable drag and drop files into area, (this code also works for other widget like window) <code python>
 +def dragEnterEvent( self, event ):
 +    data = event.mimeData()
 +    urls = data.urls()
 +    if ( urls and urls[0].scheme() == 'file' ):
 +        event.acceptProposedAction()
 +def dragMoveEvent( self, event ):
 +    data = event.mimeData()
 +    urls = data.urls()
 +    if ( urls and urls[0].scheme() == 'file' ):
 +        event.acceptProposedAction()
 +def dropEvent( self, event ):
 +    data = event.mimeData()
 +    urls = data.urls()
 +    if ( urls and urls[0].scheme() == 'file' ):
 +        txt = "\n".join( [unicode(url.path())[1:] for url in urls] ) # remove 1st / char
 +        self.insertPlainText( txt ) 
 +</code>
 +
 +  * QComboBox (dropdown list, user choice) <code python>
 +# current selected item and index
 +self.uiList['template_choice'].currentText()
 +self.uiList['template_choice'].currentIndex()
 +# item count
 +self.uiList['template_choice'].count()
 +self.uiList['template_choice'].itemText(0)
 +
 +# ui response
 +self.uiList['project_choice'].activated[str].connect(self.daily_path_update_process)
 +self.uiList['project_choice'].activated[int].connect(self.daily_path_update_process)
 +
 +# make the combobox ui size auto grow to content size if possible
 +cur_choice.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
 +# make the combobox dropdown size auto grow to content size, not affect combobox ui size
 +cur_choice.view().setMinimumWidth(cur_choice.view().sizeHintForColumn(0))
 +# make the combobox dropdown size to a manual fix witdh
 +cur_choice.view().setMinimumWidth(500)
 +# make the combobox dropdown size to auto grow, but not always show to fit max content width
 +cur_choice.view().setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
 +
 +</code>
 +===== Table widget =====
 +
 +  * **qtableview vs qtablewidget**
 +    * QTableWidget inherits QTableView
 +    * The QTableWidget class provides an item-based table view with a default model.
 +    * If you want a table that uses your own data model you should use QTableView rather than this class.
 +    * use an QTableWidget, you can insert anything into any cell with it's setCellWidget() function
 +    * QTableView is not designed to have editable headers.
 +      * A solution to achieve this can be done with a QLineEdit [doc.qt.nokia.com]. The idea is to create an edit box over the expected header section and make it behave “like” an usual cell when edited.
 +    * PyQt widgets, including QListWidget, QTableWidget, and QTreeWidget, are views with models and delegates aggregated inside them.
 +    * The widgets set on the cells have nothing to do with the contents of the table, so you won’t get signals from the table in such a case. If you have a row or column of widgets potentially emitting signals, and you want one slot to be notified of the row/column index of the widget that was triggered, then QSignalMapper
 +
 +  * QTableWidget cell width and height <code python>
 +cur_table.verticalHeader().setDefaultSectionSize(20) # set default row height
 +cur_table.horizontalHeader().setDefaultSectionSize(80) # set default column width
 +cur_table.setColumnWidth(0,200) # set colume 0 width
 +cur_table.setRowHeight(0,20) # set row 0 height
 +</code>
 +  * QTable content <code python>
 +# get table content like (cellWidget, text)
 +for i in range(my_tableWidget.rowCount()):
 +    print( str(my_tableWidget.item(i,1).text() ) # for cell with item
 +    print( str(my_tableWidget.cellWidget(i,0).text() ) # for cell with widget
 +</code>
 +  * QTable add row or columne at end<code python>
 +cur_table.insertColumn(cur_table.columnCount())
 +cur_table.insertRow(cur_table.rowCount())
 +</code>
 +  * QTable header <code python>cur_table.setHorizontalHeaderLabels( ['name','address'] )
 +cur_table.setVerticalHeaderLabels( ['Admin','User'] )
 +</code>
 +  * QTable add cell content <code python>cur_item = QtGui.QTableWidgetItem("Good One")
 +cur_table.setItem(3, 0, cur_item)
 +</code>
 +  * QTable clear <code python>
 +# remove everything and clear cell size as well
 +cur_table.clear()
 +cur_table.setRowCount(0)
 +cur_table.setColumnCount(0)
 +# remove content only
 +cur_table.clearContents()
 +</code>
 +===== List Widget =====
 +
 +  * List widget is like Tree widget, but with only single column, so if you need anything more than 1 column, tree is the way to go
 +
 +**Customize look**
 +  * set alternative row color <code python>my_list.setAlternatingRowColors(1)</code>
 +  * round corner list item <code python>my_list.setStyleSheet("QListWidget::item {background-color: #656565; padding: 2px;border-style: solid;border: 1px solid black;border-radius:8px; }")</code>
 +
 +===== Tree Widget =====
 +
 +  * **QTreeView vs QTreeWidget**
 +    * QTreeView is a view, just like view in database, it is interface to the database data model, and you can multiple type and design of view with same common shared data model, and you only need to update the model, and all views linking to it auto updates.
 +    * QTreeWidget is more like a widget with its own content, and you need to update the widget contently manually if you want to update its content, and its content is not shared with others, and you have to update contents for each widget even their content is the same.
 +    * Note: if you select a text element in tree widget, Ctrl+C will copy the text
 +
 +  * QTreeView <code python>
 +model = QtGui.QDirModel() # directory tree
 +tree = QtGui.QTreeView(self)
 +tree.setModel(model)
 +</code>
 +  * QTreeWidget <code python>
 +# clear content
 +self.uiList['my_tree'].clear()
 +
 +# header related
 +self.uiList['my_tree'].setHeaderLabels(["Name", "URL"])
 +self.uiList['item_tree'].setHeaderHidden(1)
 +# set on one column for its header label
 +cur_tree.headerItem().setText(0,cur_version)
 +
 +# hide column 1
 +self.uiList['item_tree'].setColumnHidden(1,1)
 +
 +# toggle column hidden
 +def toggle_path_visible_action(self):
 +    cur_tree = self.uiList['result_tree']
 +    for col in [1,2]:
 +        cur_tree.setColumnHidden(col,1-cur_tree.isColumnHidden(col))
 +
 +# column width (not working)
 +self.uiList['file_tree'].setColumnWidth(0,200)
 +self.uiList['file_tree'].setColumnWidth(1,50)
 +
 +# auto fix width to content for request column index
 +self.uiList['element_tree'].resizeColumnToContents(0)
 +# prevent above auto fit cause too small column look for empty column
 +self.uiList['element_tree'].header().setMinimumSectionSize(100) 
 +
 +# auto column width, and no super long last column, 2 col in this example
 +cur_tree = self.uiList['node_tree']
 +cur_tree.header().setStretchLastSection(0)
 +#if qtMode < 2:
 +if your_qt == 'qt4':
 +    cur_tree.header().setResizeMode(0, QtWidgets.QHeaderView.Stretch) # important column stretch as much as it like
 +    cur_tree.header().setResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # not important column take what it need
 +else:
 +    cur_tree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) # qt5
 +    cur_tree.header().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) # qt5
 +
 +# sort
 +self.uiList['file_tree'].setSortingEnabled(1)
 +# force a sort
 +cur_tree.sortByColumn(0, QtCore.Qt.AscendingOrder)
 +
 +# drag (ref: http://doc.qt.io/qt-4.8/qabstractitemview.html#DragDropMode-enum)
 +self.uiList['config_tree'].setDragEnabled(1)
 +self.uiList['config_tree'].setDragDropMode(QtGui.QAbstractItemView.InternalMove)
 +# drag disable
 +self.uiList['config_tree'].setDragEnabled(1)
 +self.uiList['config_tree'].setDragDropMode(QtGui.QAbstractItemView.NoDragDrop)
 +
 +
 +# connection and response
 +self.uiList['item_tree'].itemExpanded[QtWidgets.QTreeWidgetItem].connect(self.itemTreeExpand_action)
 +self.uiList['item_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.contentUpdate_action)
 +self.uiList['dir_tree'].itemDoubleClicked.connect( partial(self.tree_open_action,'dir_tree') )
 +self.uiList['dev_file_tree'].header().sectionDoubleClicked[int].connect(self.dev_file_tree_update_process)
 +
 +cur_tree.addTopLevelItem(new_node)
 +cnt = cur_tree.topLevelItemCount()
 +
 +# QTreeWidgetItem
 +# root
 +root = cur_tree.invisibleRootItem()
 +# node
 +cur_node.addChild(item)
 +selected_node.setExpanded(1)
 +
 +# add
 +node_list = [ 'file', 'place2dTexture', 'reverse']
 +cur_tree.clear()
 +[cur_tree.invisibleRootItem().addChild( QtWidgets.QTreeWidgetItem([x]) ) for x in node_list]
 +
 +# add : method 2
 +self.uiList['app_tree'].addTopLevelItems([ QtWidgets.QTreeWidgetItem([os.path.basename(txt),txt,'file']) for txt in txt_list])
 +
 +# remove
 +for item in cur_tree.selectedItems():
 +    (item.parent() or root).removeChild(item)
 +    
 +# make tree item editable for all columes
 +new_node.setFlags(new_node.flags()|QtCore.Qt.ItemIsEditable)
 +
 +# change align
 +new_node.setTextAlignment(i, QtCore.Qt.AlignRight )
 +
 +# change bg to pink
 +new_node.setBackgroundColor(0, QtGui.QColor(255,192,203, 100))
 +
 +# set to brush and reset to default
 +self.brush['fav'] = QtGui.QBrush(QtGui.QColor("#F280BF"))
 +currentNode.setBackground(0, self.brush['fav'])
 +currentNode.setData(0, QtCore.Qt.BackgroundRole, None)
 +</code>
 +  * make tree item editable only for some columes <code python>
 +# ref: https://stackoverflow.com/questions/2801959/making-only-one-column-of-a-qtreewidgetitem-editable
 +# 1. disable edit on tree level
 +self.uiList['vtx_smp_tree'].setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
 +self.uiList['vtx_smp_tree'].itemDoubleClicked.connect( partial(self.tree_edit_action,'vtx_smp_tree') )
 +# 2. enable edit on tree item level
 +new_item.setFlags(new_item.flags()|QtCore.Qt.ItemIsEditable)
 +# 3. catch double click action on tree to enable editing box on that item in that column after checking column id
 +def tree_edit_action(self, tree_name, item, col):
 +    cur_tree = self.uiList[tree_name]
 +    if tree_name == 'vtx_smp_tree':
 +        if col == 0:
 +            cur_tree.editItem(item, col)
 +</code>
 +  * expand tree <code python>
 +# expand top node
 +for i in range(cur_tree.topLevelItemCount()):
 +    cur_tree.topLevelItem(i).setExpanded(1)
 +    
 +# expand all
 +cur_tree.expandAll()
 +
 +# expand to depth for tree view only
 +cur_tree.expandToDepth(1)
 +
 +# use this code to expand to level
 +    def tree_expand(self, node, level=0):
 +        if isinstance(node, (str, unicode)):
 +            node = self.uiList[node].invisibleRootItem()
 +        # expand top node
 +        for i in range(node.childCount()):
 +            node.child(i).setExpanded(1)
 +            # sub node
 +            if level == -1:
 +                self.tree_expand(node.child(i), level)
 +            elif level > 0:
 +                level -=1
 +                self.tree_expand(node.child(i), level)
 +</code>
 +  * common tree quick creation function<code python>
 +def tree_newNode_action(self):
 +    cur_tree = self.uiList['config_tree']
 +    node_name,ok = self.quickMsgAsk('New Setting Name')
 +    if ok and node_name != '':
 +        self.quickTree(cur_tree, [node_name, ''], 1) # all editable
 +def tree_removeNode_action(self):
 +    self.quickTreeRemove(self.uiList['config_tree'])
 +    
 +def quickTree(self, cur_tree, node_data, editable=0):
 +    # not per-column control on editable
 +    if not isinstance(node_data, (list, tuple)):
 +        node_data = [node_data]
 +    # 1. get current selection
 +    selected_node = cur_tree.selectedItems()
 +    if len(selected_node) > 0:
 +        selected_node = selected_node[0]
 +    else:
 +        selected_node = cur_tree.invisibleRootItem()
 +    # 2. create a new node
 +    new_node = QtWidgets.QTreeWidgetItem()
 +    for i,name in enumerate(node_data):
 +        new_node.setText(i, name)
 +    if editable == 1:
 +        new_node.setFlags(new_node.flags()|QtCore.Qt.ItemIsEditable)
 +    # 3. add it
 +    selected_node.addChild(new_node)
 +    # 4. expand it
 +    selected_node.setExpanded(1)
 +def quickTreeRemove(self, cur_tree):
 +    root = cur_tree.invisibleRootItem()
 +    for item in cur_tree.selectedItems():
 +        (item.parent() or root).removeChild(item)
 +def quickTreeInfo(self, cur_tree):
 +    data = []
 +    root = cur_tree.invisibleRootItem()
 +    child_count = root.childCount()
 +    for i in range(child_count):
 +        cur_node = root.child(i)
 +        cur_info = []
 +        for j in range( cur_node.columnCount() ):
 +            cur_info.append( unicode(cur_node.text(j)) )
 +        data.append(cur_info)
 +    return data
 +def quickTreeUpdate(self, cur_tree, data):
 +    root = cur_tree.invisibleRootItem()
 +    data_len = len(data)
 +    for i in range(data_len):
 +        self.quickTree(cur_tree, data[i], 1)
 +
 +</code>
 +
 +  * tree item selection operation<code python>
 +self.uiList['config_tree'].itemClicked[QtWidgets.QTreeWidgetItem,int].connect(self.config_tree_select_action)
 +def config_tree_select_action(self):
 +    currentNode = self.uiList['config_tree'].currentItem()
 +    if currentNode:
 +        self.uiList['source_txt'].setText(currentNode.text(1))
 +</code>
 +
 +  * tree data export import as list type data <code python>
 +def TreeToData(self, tree, cur_node):
 +    col_count = tree.columnCount()
 +    child_count = cur_node.childCount()
 +    node_info = [ unicode( cur_node.text(i) ) for i in range(col_count) ]
 +    node_info_child = []
 +    for i in range(child_count):
 +        node_info_child.append( self.TreeToData(tree, cur_node.child(i) ) )
 +    return (node_info, node_info_child)
 +def DataToTree(self, tree, cur_node, data):
 +    node_info = data[0]
 +    node_info_child = data[1]
 +    [cur_node.setText(i, node_info[i]) for i in range(len(node_info))]
 +    for sub_data in node_info_child:
 +        new_node = QtWidgets.QTreeWidgetItem()
 +        cur_node.addChild(new_node)
 +        self.DataToTree(tree, new_node, sub_data)
 +</code>
 +  * tree to node list (flat hierarchy order) <code python>
 +def tree_to_node_list(self, cur_node):
 +    node_list = []
 +    node_list.append(cur_node)
 +    for i in range(cur_node.childCount()):
 +        node_list.extend(self.tree_to_node_list(cur_node.child(i)))
 +    return node_list
 +</code>
 +
 +  * TreeWidgetItem selectable, editable <code python>
 +item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable)
 +</code>
 +  * not selectable <code python>
 +item.setFlags(QtCore.Qt.ItemIsEnabled)
 +</code>
 +  * checkable with a checkbox<code python>
 +new_item.setFlags(new_item.flags() or QtCore.Qt.ItemIsUserCheckable)
 +new_item.setCheckState(0, QtCore.Qt.Checked)
 +</code>
 +  * conversion between text and checkable for plain tree data export <code python>
 +def node_check_to_text(self,cur_node, col_list):
 +    for i in col_list:
 +        if cur_node.checkState(i):
 +            cur_node.setText(i, '1')
 +        else:
 +            cur_node.setText(i, '0')
 +    for i in range(cur_node.childCount()):
 +        self.node_check_to_text(cur_node.child(i), col_list)
 +def node_text_to_check(self, cur_node, col_list):
 +    for i in col_list:
 +        if cur_node.text(i) == '1':
 +            cur_node.setCheckState(i, QtCore.Qt.Checked)
 +        else:
 +            cur_node.setCheckState(i, QtCore.Qt.Unchecked)
 +        cur_node.setText(i,'')
 +    for i in range(cur_node.childCount()):
 +        self.node_text_to_check(cur_node.child(i), col_list)
 +</code>
 +  * set active item in tree <code python>
 +cur_tree.setCurrentItem(cur_node) # QTreewidgetItem
 +</code>
 +  * hide header <code python>cur_tree.header().hide()</code>
 +  * QTreeWidgetItem set foreground(text) or background color and reset to default <code python>
 +self.uiList['gold_brush'] = QtGui.QBrush(QtGui.QColor(255,165,0)) # gold color
 +self.uiList['normal_brush'] = self.uiList['vtx_bone_tree'].palette().text() # get default tree foreground color
 +# current tree widget item
 +col=2
 +cur_item.setForeground(col,self.uiList['gold_brush']) # set text to gold color
 +cur_item.setForeground(col,self.uiList['normal_brush']) # reset to normal
 +# note: style not working for tree widget item
 +# cur_item.setStyleSheet("color: rgb(255, 165, 0)") # not working
 +</code>
 +
 +| QtCore.Qt.NoItemFlags | 0 | It does not have any properties set |
 +| QtCore.Qt.ItemIsSelectable | 1 | It can be selected |
 +| QtCore.Qt.ItemIsEditable | 2 | It can be edited |
 +| QtCore.Qt.ItemIsDragEnabled | 4 | It can be dragged |
 +| QtCore.Qt.ItemIsDropEnabled | 8 | It can be used as a drop target |
 +| QtCore.Qt.ItemIsUserCheckable | 16 | It can be checked or unchecked by the user |
 +| QtCore.Qt.ItemIsEnabled | 32 | The user can interact with the item |
 +| QtCore.Qt.ItemIsTristate | 64 | The item is checkable with three separate states |
 +===== Tab Widget =====
 +
 +  * set tab widget tab width <code python>self.uiList['main_tab'].setStyleSheet("QTabBar::tab { height: 30px; width: 60px; }")</code>
 +  * set tab widget tab alignment <code python>self.uiList['main_tab'].setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }")</code>
 +  * get and set active tab index <code python>self.uiList['main_tab'].currentIndex()
 +self.uiList['main_tab'].setCurrentIndex(1)
 +self.uiList['main_tab'].count() # return total tabs
 +</code>
 +  * put tab on bottom <code python>self.uiList['main_tab'].setTabPosition(QtWidgets.QTabWidget.South)</code>
 +  * link ctrl+1,2,3,4 to the tab switch <code python>
 +cur_tab = self.uiList['main_tab']
 +self.hotkey = {}
 +for i in range( cur_tab.count() ):
 +    self.hotkey['tab_{}'.format(i)] = QtWidgets.QShortcut(QtGui.QKeySequence( "Ctrl+{}".format(i+1) ), self)
 +    self.hotkey['tab_{}'.format(i)].activated.connect(partial(cur_tab.setCurrentIndex, i))
 +</code>
 +===== QTimer Object =====
 +
 +  * a continuously trigger clock setup <code python>
 +# first, create a timer object
 +my_timer = QtCore.QTimer()
 +# 2nd, link alarm to a function
 +my_timer.timeout.connect(self.time_show)
 +
 +# start the clock / stopwatch with a interval
 +my_timer.start(1000)
 +
 +# end the clock
 +my_timer.stop()
 +</code>
 +
 +  * timer example, a proper alternative to Python sleep <code python>
 +hostMode=''
 +# ---- QtMode detection ----
 +qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5
 +qtModeList = ("PySide", "PyQt4", "PySide2", "PyQt5")
 +try:
 +    from PySide import QtGui, QtCore
 +    import PySide.QtGui as QtWidgets
 +    print("PySide Try")
 +    qtMode = 0
 +    if hostMode == "maya":
 +        import shiboken
 +except ImportError:
 +    try:
 +        from PySide2 import QtCore, QtGui, QtWidgets
 +        print("PySide2 Try")
 +        qtMode = 2
 +        if hostMode == "maya":
 +            import shiboken2 as shiboken
 +    except ImportError:
 +        try:
 +            from PyQt4 import QtGui,QtCore
 +            import PyQt4.QtGui as QtWidgets
 +            import sip
 +            qtMode = 1
 +            print("PyQt4 Try")
 +        except ImportError:
 +            from PyQt5 import QtGui,QtCore,QtWidgets
 +            import sip
 +            qtMode = 3
 +            print("PyQt5 Try")
 +
 +import nuke 
 +
 +def frameHold_update(node_name, start_frame=1, end_frame=10, step_sec=3):
 +    node=nuke.toNode(node_name)
 +    node['first_frame'].setValue(start_frame)
 +    my_timer = QtCore.QTimer()
 +    # inner function for timer
 +    def update_process():
 +        cur_val = node['first_frame'].value()
 +        # create a node called 'stop' to if you want to force it stop
 +        if cur_val < end_frame and not nuke.exists('stop'):
 +            node['first_frame'].setValue(cur_val+1)
 +            print('{0}/{1}'.format(cur_val+1, end_frame) )
 +        else:
 +            my_timer.stop()
 +            print('finish')
 +    my_timer.timeout.connect(update_process)
 +    # start
 +    my_timer.start(1000*step_sec)
 +
 +# make your settting change below
 +frameHold_update('FrameHold1', start_frame=2, end_frame=5, step_sec=2)
 +</code>
 +
 +===== QtWebKit and QtNetwork =====
 +
 +  * Web and Browser related Qt coding.
 +  * universally import QtNetwork <code python>
 +# ---- qtMode ----
 +qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5
 +qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5')
 +try:
 +    from PySide import QtGui, QtCore
 +    import PySide.QtGui as QtWidgets
 +    qtMode = 0
 +    if hostMode == "maya":
 +        import shiboken
 +except ImportError:
 +    try:
 +        from PySide2 import QtCore, QtGui, QtWidgets
 +        qtMode = 2
 +        if hostMode == "maya":
 +            import shiboken2 as shiboken
 +    except ImportError:
 +        try:
 +            from PyQt4 import QtGui,QtCore
 +            import PyQt4.QtGui as QtWidgets
 +            import sip
 +            qtMode = 1
 +        except ImportError:
 +            from PyQt5 import QtGui,QtCore,QtWidgets
 +            import sip
 +            qtMode = 3
 +print('Qt: {0}'.format(qtModeList[qtMode]))
 +
 +QtNetwork = getattr(__import__(qtModeList[qtMode], fromlist=['QtNetwork']), 'QtNetwork')
 +</code>
 +
 +  * Build a advanced browser with Qt
 +    * https://pawelmhm.github.io/python/pyqt/qt/webkit/2015/09/08/browser.html
 +===== Custom Widget - Screen Grab with QLabel QRubberBand =====
 +
 +  * a customized QLabel with QRubberBand built-in, code for Py2.7 and Py3.5 and PyQt4,5, PySide1,2
 +  * how to use in your window class <code python>
 +self.my_preview_screen = PreviewScreen()
 +self.my_preview_screen.setMinimumSize(240, 160)
 +
 +def preview_grab_action(self):
 +    self.hide()
 +    QtCore.QTimer.singleShot(1 * 1000, self.preview_update_action)
 +def preview_update_action(self):
 +    self.my_preview_screen.update()
 +    self.show()
 +def preview_crop_action():
 +    self.my_preview_screen.crop()
 +</code>
 +  * source code <code python>
 +class PreviewScreen(QtWidgets.QLabel):
 +    def __init__(self):
 +        QtWidgets.QLabel.__init__(self)
 +        self.rubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self)
 +        self.screen = None
 +        self.preview = None
 +        self.origin = None
 +    def mousePressEvent(self,e):
 +        self.origin = e.pos()
 +        self.rubberBand.setGeometry(QtCore.QRect(self.origin , QtCore.QSize()));
 +        self.rubberBand.show()
 +    def mouseMoveEvent(self,e):
 +        self.rubberBand.setGeometry(QtCore.QRect(self.origin , e.pos()).normalized());
 +    def mouseReleaseEvent(self,e):
 +        pass
 +    def update(self):
 +        self.screen = QtGui.QPixmap.grabWindow(QtWidgets.QApplication.desktop().winId()) # Qt5 grab()
 +        self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
 +        self.setPixmap(self.preview)
 +    def crop(self):
 +        if self.screen == None:
 +            return
 +        select_rect = self.rubberBand.geometry()
 +        preview_rect = self.preview.rect()
 +        cross_rect = select_rect.intersect(preview_rect) # now the widget can't be center as pixmap seems on left
 +        print(cross_rect.x(), cross_rect.y(), cross_rect.width(), cross_rect.height())
 +        ratio = (self.screen.height()*1.0) / self.preview.height()
 +        para = [cross_rect.x(), cross_rect.y(), cross_rect.width(), cross_rect.height() ]
 +        self.screen = self.screen.copy(QtCore.QRect(*[x*ratio for x in para]))
 +        self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
 +        self.setPixmap(self.preview)
 +        self.rubberBand.hide()
 +    def save(self, filepath, filetype):
 +        if self.screen != None:
 +            self.screen.save(filepath, filetype)
 +    def load(self, filepath):
 +        self.screen = QtGui.QPixmap(filepath)
 +        self.preview = self.screen.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
 +        self.setPixmap(self.preview)
 +    def clear(self):
 +        self.screen = None
 +        self.preview = None
 +</code>
 +
 +===== mix Win32 and QWidget in each other =====
 +
 +  * possible solution
 +    * win32gui or ctypes and SetParent method to force hwnd based window to be in each other <code python>
 +qwidget_hwnd = qwidget_var.winId()
 +
 +win32gui.SetParent(child_hwnd, parent_hwnd)
 +ctypes.windll.user32.SetParent(child_hwnd, parent_hwnd)
 +</code>
 +      * ref:
 +        * https://github.com/shotgunsoftware/tk-photoshop/blob/master/python/tk_photoshop/win_32_api.py
 +        * http://code.activestate.com/lists/python-win32/11295/
 +        * http://pyqt.riverbankcomputing.narkive.com/RNoK1vJw/convert-windows-app-to-pyqt-widget
 +        * https://stackoverflow.com/questions/31098786/opening-other-application-in-single-window-using-python
 +        * https://forums.adobe.com/thread/1343097
 +    * Qt4 QtWinMigrate and QWinWidget: (only in Qt4)
 +      * https://area.autodesk.com/blogs/the-3ds-max-blog/pyqt-ui-in-3ds-max-2014-extension/
 +      * https://sourceforge.net/projects/blur-dev/
 +      * http://www.losart3d.com/?p=890
 +      * https://stackoverflow.com/questions/293774/how-to-create-a-qwidget-with-a-hwnd-as-parent
 +      * http://www.qtcentre.org/threads/61738-Qt5-QWidget-create()-with-Win32-HWND-embedding-not-working-after-port-from-Qt4
 +      * http://www.qtforum.org/article/18242/convert-windows-hwnd-to-qwidget-pointer.html
 +    * QWinHost
 +      * http://docs.huihoo.com/qt/solutions/4/qtwinmigrate/qwinhost.html
 +    * QThread
 +      * https://nikolak.com/pyqt-threading-tutorial/
 +===== Problem and Solution =====
 +
 +  * Problem: **libpng warning: iCCP: known incorrect sRGB profile**
 +    * Solution: delete color profile inside the PNG file, for example, Photoshop > assign profile > don't use color management and save
 +
 +
 +
 +====== Qt - System level code ======
 +
 +===== System Info =====
 +
 +**QDesktopServices**
 +  * access desktop common location and openURL
 +    * ref: http://doc.qt.io/qt-4.8/qdesktopservices.html
 +
 +** QLocale **
 +  * get system default language <code python>
 +local = str(QtCore.QLocale.system().name())
 +print(local)
 +if local.startswith('zh') and 'CN' in self.memoData['lang']:
 +    self.setLang('CN')
 +</code>
 +
 +** QClipboard **
 +
 +  * send to clipboard <code python>
 +clipboard = QtWidgets.QApplication.clipboard()
 +clipboard.setText('Information !!')
 +</code>
 +
 +** QTime **
 +
 +  * get current time <code python>
 +cur_time = QtCore.QTime.currentTime()
 +cur_date = str(QtCore.QDateTime.currentDateTime().toString('d'))
 +cur_date_format_text = str( QtCore.QDateTime.currentDateTime().toString('yyyy.MM.dd') )
 +text = str(cur_time.toString("hh:mm"))
 +</code>
 +
 +===== File operation =====
 +
 +  * copy file and rename file <code python>
 +QtCore.QFile.copy(src_path, new_path)
 +QtCore.QFile.rename(src_path, new_path)
 +</code>
 +
 +===== QtNetwork =====
 +
 +  * chat server example:
 +    * http://stackoverflow.com/questions/9355511/pyqt-qtcpserver-how-to-return-data-to-multiple-clients
 +    * http://stackoverflow.com/questions/8863502/chat-programming-with-pyqt-and-socket-standard-library
 +    * https://www.youtube.com/watch?v=crcPNFNQKDU
 +    * https://gist.github.com/1995eaton/97db2d6e62bb58205778
 +    * https://www.pythonstudio.us/pyqt-programming/networking.html
 +    * https://www.reddit.com/r/Python/comments/y9up0/pyqt_qtcpsocket_problem/
 +
 +  * QtTCPSocket, python socket, python twisted protocol communication
 +      * QIODevice write <code python>
 +write(const char * data, qint64 maxSize)
 +write(const char * data)
 +write(const QByteArray & byteArray)
 +</code>
 +    * http://www.chongchonggou.com/g_425289891.html
 +    * https://www.reddit.com/r/Python/comments/y9up0/pyqt_qtcpsocket_problem/
 +    * https://github.com/pyqt/examples/blob/master/network/loopback.py
 +    * http://www.bogotobogo.com/Qt/Qt5_QTcpSocket.php
 +    * https://www.youtube.com/watch?v=1Tw15kIcQ14
 +    * https://stackoverflow.com/questions/21233340/sending-string-via-socket-python
 +    * http://twistedmatrix.com/documents/15.0.0/core/howto/servers.html
 +    * https://pymotw.com/2/socket/binary.html
 +    * https://www.pythonsheets.com/notes/python-socket.html
 +    * https://stackoverflow.com/questions/18930835/how-can-twisted-call-qt-back
 +    * http://twistedmatrix.com/documents/15.0.0/core/howto/servers.html
 +    * http://stupidpythonideas.blogspot.com/2013/05/sockets-are-byte-streams-not-message.html
 +    * telnet, putty raw connection to the socket
 +      * https://stackoverflow.com/questions/12730293/how-does-telnet-differ-from-a-raw-tcp-connection
 +
 +
 +====== Reference and Tutorials ======
 +
 +  * Python 2,3 universal coding way: http://python-future.org/compatible_idioms.html#unicode-text-string-literals
 +  * PyQt4 vs PyQt5 difference list: http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html
 +  * tray icon demo: http://cognifty.com/main.page/php-qt_system_tray_icon_demo.html
 +    * **by Cognifty blog: a maker of Cognify PHP framework (Open Source) for php web app development**
 +
 +  * Qt tutorial
 +    * https://www.youtube.com/user/VoidRealms/videos
 +  * PyQt tutorial
 +    * GUI Programming with Python: QT Edition
 +      * http://www.commandprompt.com/community/pyqt/
 +      * http://zetcode.com/tutorials/pyqt4/
 +  * PySide tutorial
 +    * http://qt-project.org/wiki/PySide_Video_Tutorials
 +
 +**Learn from others' tool**
 +
 +  * Python, 3D, Pipe: https://fredrikaverpil.github.io
 +
 +====== My Words about Qt ======
 +
 +  * I tried out wxWidget a little bit with Python, together called wxPython;
 +    * wxPython is quite good, as everything is defined and coded in Python, with a plain text editor, it does what you need
 +
 +  * As I work with Maya more often, and Maya now since 2011, use QT framework as the interface, so build Maya tool in the same fashion, make me, more likely use QT in the workflow.
 +
 +  * **old write date: 2014 01,** 
 +    * after making a Python-Qt tool for Maya, I found Qt is so good for making tools, 
 +    * you can make Qt interface with only code, like those mel UI commands
 +    * layout and UI control can all be done in code as well, like HTML, you can use either Dreamweaver(Qt Designer) or Notepad, the stylesheet are just like CSS code; the whole process is like HTML+Javascript+CSS
 +    * method and function call are easy in one sentence, reference of object is dot.property like.
 +    * advance widgets like table and splitter also available and easy to acccess
 +
 +  * Qt GUI are just codes for GUI, it is convert-able from ui file to python file or c++ file
 +    * the language does not matter, the interaction are all the same
 +    * so the PyQt code are similar to C++ Qt code, easier to translate, learning either is the same
 +
 +  * **old write date: 2016 06,**
 +    * more words for Qt, OK, so Qt is the GUI library, many language can use this library, the default combination is C++ and Qt, and another popular combination is Python and Qt.
 +    * The existing Python and Qt combination package (so called module or library) are PySide and PyQt (PyQt5, PyQt4 and older)
 +      * PySide and PyQt are quite similar, most time just change the first line "import PyQt4" into "import PySide" will get both to work; and they do have detail difference.
 +      * One tricky part is, PyQt need a commercial license to use for commercial, PySide are free to use for commercial. detail license difference here: http://askubuntu.com/questions/339723/can-i-sell-my-pyqt4-app-without-having-a-pyqt-license
 +      * that may be why, Maya comes with PySide by default, and you need compile PyQt4 yourself for Maya to use it.
 +
 +  * **old write date: 2017 01,**
 +    * after so many years of using Python Qt, it is now, I think, the perfect solution to 3D graphic software tool solution, with PySide or PyQt, it is super fast and convinient to get the tool out in all platforms and integrate to all software support python or support python through a 3rd scripting language
 +    * so now I am moving to Qt + C++ solutions to explore more binary tools development using the same knowledge from python QT side.
 +
 +
 +====== Problem and Solution ======
 +
 +  * Problem: qt.qpa.fonts: Unable to open default EUDC font: "C:\\WINDOWS\\FONTS\\EUDC.TTE"
 +    * end-user-defined characters (EUDCs), just delete related regedit entry \HKEY_CURRENT_USER\EUDC\1252
 +    * ref: https://bugreports.qt.io/browse/PYSIDE-1620