Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of class 'QUiLoader' in PYA and RBA #730

Closed
Kazzz-S opened this issue Feb 16, 2021 · 8 comments
Closed

Support of class 'QUiLoader' in PYA and RBA #730

Kazzz-S opened this issue Feb 16, 2021 · 8 comments
Assignees
Labels
Milestone

Comments

@Kazzz-S
Copy link
Contributor

Kazzz-S commented Feb 16, 2021

Dear @klayoutmatthias,

This is related to https://www.klayout.de/forum/discussion/comment/3035

In my PYA, I want to use a custom widget (promoted from QTableView) with various features, including keyboard Cut & Paste.
The user interface (*.ui) designed by using Qt Designer is complicated.
To enable these features, I've defined a subclass (promoted in Qt Designer) that is defined as:

class MyTableView( pya.QTableView ):
    def __init_( self, parent=None ):
      super( MyTableView, self ).__init__(parent)
        :

    def keyPressEvent( self, event ):
        :

However, when starting the PYA script that reads the UI file by pya.QFormBuilder().load(), a warning below is delivered.

Warning: "QFormBuilder was unable to create a custom widget of the class 'MyTableView'; defaulting to base class 'QTableView'."

Referring to the idea in:
https://stackoverflow.com/questions/37775472/quiloader-requirements-for-loading-ui-file-with-custom-widgets
Do you think it's possible to newly provide class 'QUiLoader' in PYA and RBA that can be used as follows?

class CustomQUiLoader( pya.QUiLoader ):
    def createWidget( self, className, parent=None, name='' ):
        if className == 'MyTableView':
            ret = MyTableView(parent)
            ret.setObjectName(name)
            return ret
        return super().createWidget(className, parent, name)


dialog = CustomQUiLoader().load( 'myComplicatedTable.ui', parent )
:
:
:

Thanks and best regards,
Kazzz-S

@klayoutmatthias
Copy link
Collaborator

klayoutmatthias commented Feb 23, 2021

I have implemented the new Qt module now. You can use it from Ruby or Python.

Here is my test code which works nicely with this version (Ruby):

EDIT createWidget must not call the super class implementation for custom classes.


# Password dialog
FORM_TEXT = <<"END"
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>PasswordDialog</class>
 <widget class="QDialog" name="PasswordDialog">
 
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>338</width>
    <height>229</height>
   </rect>
  </property>
  
  <property name="windowTitle">
   <string>Authentication Required</string>
  </property>
  
  <layout class="QGridLayout">

   <property name="leftMargin">
    <number>9</number>
   </property>
   <property name="topMargin">
    <number>9</number>
   </property>
   <property name="rightMargin">
    <number>9</number>
   </property>
   <property name="bottomMargin">
    <number>9</number>
   </property>
   <property name="spacing">
    <number>6</number>
   </property>

   <item row="2" column="1" colspan="2">
    <spacer>
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>0</height>
      </size>
     </property>
    </spacer>
   </item>

   <item row="0" column="1">
    <widget class="QLabel" name="label">
     <property name="text">
      <string>User</string>
     </property>
    </widget>
   </item>

   <item row="0" column="2">
    <widget class="QLineEdit" name="user_le">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
    </widget>
   </item>
   
   <item row="1" column="1">
    <widget class="QLabel" name="label_3">
     <property name="text">
      <string>Password  </string>
     </property>
    </widget>
   </item>

   <item row="1" column="2">
    <widget class="PasswordEditBox" name="password_le">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="echoMode">
      <enum>QLineEdit::Password</enum>
     </property>
    </widget>
   </item>

   <item row="0" column="0">
    <spacer>
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="sizeType">
      <enum>QSizePolicy::Expanding</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>10</width>
       <height>20</height>
      </size>
     </property>
    </spacer>
   </item>

   <item row="0" column="3">
    <spacer>
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="sizeType">
      <enum>QSizePolicy::Expanding</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>10</width>
       <height>20</height>
      </size>
     </property>
    </spacer>
   </item>

   <item row="2" column="1" colspan="2">
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeType">
      <enum>QSizePolicy::Fixed</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>10</height>
      </size>
     </property>
    </spacer>
   </item>

   <item row="3" column="0" colspan="4">
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
     </property>
    </widget>
   </item>

  </layout>
  
 </widget>
 
 <tabstops>
  <tabstop>user_le</tabstop>
  <tabstop>password_le</tabstop>
  <tabstop>buttonBox</tabstop>
 </tabstops>
 
 <resources/>
 
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>PasswordDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>PasswordDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
 
</ui>
END


# Test code for QUiLoader customization
# 
# It supplies a custom widget called "PasswordEditBox" which changes
# the background color depending on the password "strength".

class PasswordEditBox < RBA::QLineEdit

  def initialize(parent)
    super(parent)
    self.textEdited += lambda { |s| self.update_background(s) }
    update_background("")
  end
  
  def update_background(s)
    # sets the background depending on the password "strength" 
    # TODO: better assessment of "strength" ...
    if s.size > 12
      color = RBA::QColor::new(192, 255, 192)  # green -> good
    elsif s.size > 7
      color = RBA::QColor::new(255, 255, 192)  # yellow -> medium
    else
      color = RBA::QColor::new(255, 192, 192)  # red -> bad
    end
    p = self.palette
    p.setColor(RBA::QPalette::Base, color)
    self.palette = p
  end
  
end

# Customization of the loader - supports the "PasswordEditBox" custom widget

class MyLoader < RBA::QUiLoader

  def createWidget(className, parent, name)
    if className == "PasswordEditBox"
      w = PasswordEditBox::new(parent)
      w.setObjectName(name)
    else
      w = super
    end
    w
  end

end


# Create the form and show it

file = RBA::QBuffer::new
file.setData(FORM_TEXT)
file.open(RBA::QIODevice::ReadOnly)

loader = MyLoader::new
w = loader.load(file)

file.close

w.exec

@Kazzz-S
Copy link
Contributor Author

Kazzz-S commented Feb 23, 2021

Dear @klayoutmatthias,

Thanks a lot for adding the new feature, which I have confirmed with Linux (Mint 19), as shown below.

RBA-Linux

Mint19-Red Mint19-Yellow
Mint19-Green

However, I got some build errors on Mac even after modifying a source file (a separate PR will be issued).
Please refer to the log file in this directory.

Warm regards,
Kazzz-S

@klayoutmatthias
Copy link
Collaborator

klayoutmatthias commented Feb 23, 2021

I took the liberty of extending the scope of this ticket somewhat: I tried to test the QUiLoader on a standalone Python environment using the Python modules which come with KLayout (not the ones from PyPI). I found some severe issues with QApplication binding and finally had to remove "QApplication#notify" from the list of supported methods in Python (for the reasoning see here: 7352756)

With this, the following code works (provided you have KLayout's Python modules built and in the path):


import klayout.QtCore as qtcore
import klayout.QtGui as qtgui
import klayout.QtWidgets as qtwidgets
import klayout.QtUiTools as qtuitools

# Password dialog
form_text = """<?xml version="1.0" encoding="UTF-8"?>
... (shortened) ...
"""

# Test code for QUiLoader customization
# 
# It supplies a custom widget called "PasswordEditBox" which changes
# the background color depending on the password "strength".

class PasswordEditBox(qtwidgets.QLineEdit):

  def __init__(self, parent):
    super(PasswordEditBox, self).__init__()
    self.textEdited += lambda s: self.update_background(s)
    self.update_background("")
  
  def update_background(self, s):
    # sets the background depending on the password "strength" 
    # TODO: better assessment of "strength" ...
    if len(s) > 12:
      color = qtgui.QColor(192, 255, 192)  # green -> good
    elif len(s) > 7:
      color = qtgui.QColor(255, 255, 192)  # yellow -> medium
    else:
      color = qtgui.QColor(255, 192, 192)  # red -> bad

    p = self.palette
    p.setColor(qtgui.QPalette.Base, color)
    self.palette = p
  
# Customization of the loader - supports the "PasswordEditBox" custom widget

class MyLoader(qtuitools.QUiLoader):

  def __init__(self):
    super(MyLoader, self).__init__()

  def createWidget(self, className, parent, name):
    if className == "PasswordEditBox":
      w = PasswordEditBox(parent)
      w.setObjectName(name)
    else:
      w = super(MyLoader, self).createWidget(className, parent, name)
    return w


# Create the form and show it

app = qtwidgets.QApplication(["test"])

file = qtcore.QBuffer()
file.setData(form_text)
file.open(qtcore.QIODevice.ReadOnly)

loader = MyLoader()
w = loader.load(file)

file.close()

w.exec_()

@Kazzz-S
Copy link
Contributor Author

Kazzz-S commented Feb 24, 2021

Thanks for updating the code with commit 7352756.

I've rebuilt it on Mac with the "-j1" make option to stop at the first occurrence of error.
The last part of the messages is as below.

:
:
Undefined symbols for architecture x86_64:
  "gsi::qtdecl_QObject()", referenced from:
      __GLOBAL__sub_I_gsiDeclQUiLoader.cc in gsiDeclQUiLoader.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[3]: *** [../../../../qt5MP.build.macos-Catalina-release-RsysPsys/libklayout_QtUiTools.0.27.0.dylib] Error 1
make[2]: *** [sub-QtUiTools-make_first] Error 2
make[1]: *** [sub-qt5-make_first] Error 2
make: *** [sub-gsiqt-make_first] Error 2
:
:

Kazzz-S

klayoutmatthias added a commit that referenced this issue Feb 25, 2021
#735)

* #730: providing a new Qt module named QtUiTools for QUiLoader class support.

* Fixed a compile error on Mac

* Added QtUiTools to some more places

* Fixed a linker issue in the QtUiTools Python lib

* On occasion fixed a infinite recursion problem in the debugger

The recursion happened because by mistake I instantiated a
QApplication inside an in-application Python script. This
crashed the debugger due to infinite recursion. This is not
a real use case but to prevent similar issues, a recursion
sentinel was added.

* Removed QCoreApplication#notify from script bindings

Reasoning: "notify" made standalone scripts using QApplication and
QUiLoader virtually impossible.

Problem description:
- When a QApplication object is instantiated, e.g. in Python, the Qt binding
  will install reimplementation hooks as the object may be dynamically
  extended.
- A notify is virtual this means the *every* "notify" call in the application
  is routed through the interpreter.
- For one thing this will slow down the application
- But as "notify" is called a zillion times this has more than this side effect.
- Specifically "notify" is called from within the QWidget constructor to
  indicate a new widget. Then, if a QDialog for example is instatiated, it's
  base class constructor will call "notify" when the object isn't ready yet.
- This has another severe side effect: as the object isn't ready yet, it gets
  registered in the Python space with the wrong class and QDialog is not visible
  as such.

To mitigate these problems, the most efficient solution is to disable "notify"
in general. There is hardly any use case in a script environment (in C++,
apart from hacking the only reasonable use case is exception handling, but
this does not apply to scripts). For providing the call functionality of
"notify" you should better use "postEvent" or "sendEvent" anyway.

So farewell QCoreApplication.notify ...

* Fixed python test for QtUiTools module

* Fixed UiTools test on Qt4 - QUiLoader needs an application object

Co-authored-by: Kazunari Sekigawa <kazunari.sekigawa@gmail.com>
klayoutmatthias added a commit that referenced this issue Feb 25, 2021
… support.

* Fixed a compile error on Mac

* Added QtUiTools to some more places

* Fixed a linker issue in the QtUiTools Python lib

* On occasion fixed a infinite recursion problem in the debugger

The recursion happened because by mistake I instantiated a
QApplication inside an in-application Python script. This
crashed the debugger due to infinite recursion. This is not
a real use case but to prevent similar issues, a recursion
sentinel was added.

* Removed QCoreApplication#notify from script bindings

Reasoning: "notify" made standalone scripts using QApplication and
QUiLoader virtually impossible.

Problem description:
- When a QApplication object is instantiated, e.g. in Python, the Qt binding
  will install reimplementation hooks as the object may be dynamically
  extended.
- A notify is virtual this means the *every* "notify" call in the application
  is routed through the interpreter.
- For one thing this will slow down the application
- But as "notify" is called a zillion times this has more than this side effect.
- Specifically "notify" is called from within the QWidget constructor to
  indicate a new widget. Then, if a QDialog for example is instatiated, it's
  base class constructor will call "notify" when the object isn't ready yet.
- This has another severe side effect: as the object isn't ready yet, it gets
  registered in the Python space with the wrong class and QDialog is not visible
  as such.

To mitigate these problems, the most efficient solution is to disable "notify"
in general. There is hardly any use case in a script environment (in C++,
apart from hacking the only reasonable use case is exception handling, but
this does not apply to scripts). For providing the call functionality of
"notify" you should better use "postEvent" or "sendEvent" anyway.

So farewell QCoreApplication.notify ...

* Fixed python test for QtUiTools module

* Fixed UiTools test on Qt4 - QUiLoader needs an application object

Co-authored-by: Kazunari Sekigawa <kazunari.sekigawa@gmail.com>
@Kazzz-S
Copy link
Contributor Author

Kazzz-S commented Feb 25, 2021

Dear @klayoutmatthias,

I've built the commit f299b3a on Mac, and I still have a linker error.

:
Undefined symbols for architecture x86_64:
  "gsi::qtdecl_QObject()", referenced from:
      __GLOBAL__sub_I_gsiDeclQUiLoader.cc in gsiDeclQUiLoader.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[3]: *** [../../../../qt5MP.build.macos-Catalina-release-RsysPsys/libklayout_QtUiTools.0.26.11.dylib] Error 1
make[2]: *** [sub-QtUiTools-make_first] Error 2
:

I guess this is a similar issue as you explained in the commit log of 4ffcaba.
I have gone through the relevant source files. However, I could not find the cause.
As far as I can see, it looks the care is already taken :-(

Kazzz-S

@Kazzz-S
Copy link
Contributor Author

Kazzz-S commented Feb 26, 2021

@klayoutmatthias
Copy link
Collaborator

Hi @Kazzz,

the branch is already merged to master (otherwise I cannot port it to 0.26).

There is one more commit on master: f993c03

I think this will fix the linker issue.

With this, all Linux and Windows builds are passing. I'll try Catalina too.

Best regards,

Matthias

@Kazzz-S
Copy link
Contributor Author

Kazzz-S commented Feb 27, 2021

Hi @klayoutmatthias,

Thank you for fixing the link error. 💯
I could successfully build branch 889f318 and run the sample scripts (both RBA and PYA) as shown in the images below.

ST-02611

RBA-Mac

RBA-Mac-03RBA-Mac-08
RBA-Mac-13

PYA-Mac

PYA-Mac-03PYA-Mac-08
PYA-Mac-13

Best regards,
Kazzz-S

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants