key press handling in a QTableWidget in Ruby

edited June 2015 in General

Ruby scripts running under Klayout:
I am unable to find a way to respond to key press events in a table. My original goal was to have a Tab character move down the one and only editable column in a table I have created. I have tried all sorts of ways to capture key press events over the table without any luck. I was using 0.23.9 and have moved to 0.24.python eval version and Qt 4.8.5 - all on Linux. I have noticed that many of the documented methods for QTableWidget do not appear to be available e.g. QTableWidget::keyPressEvent. At least the 'methods', 'protected_methods', and 'private_methods' introspection methods do not list many methods.

I have read many suggestions and found none to work. Does anyone have a solution? Any guidance would be appreciated.

Thanks,
Dave

Comments

  • edited July 2015

    RBA::QTableWidget.keyPressEvent is inherited from an ancestor class QWidget. In the official Qt docs, go to QTableWidget, scroll down to "220 public functions inherited from QWidget" and click QWidget. Not all of the inherited methods show up in descendant classes in the RBA documentation, so personally I use the official Qt docs rather than the RBA docs when it comes to Qt.

    QTableWidget::keyPressEvent is indeed there as you can see if you run this:

    q=QTableWidget.new
    q.keyPressEvent # Gives error: protected method `keyPressEvent' called for #<RBA::QTableWidget:0x0000000e6e8b20>
    

    ...but it's a protected method. Exactly what to do with this I don't know sorry.. so I don't know the final solution you are looking for. But maybe there is some small information that helps in future.

    David

  • edited November -1

    Can you post a minimal example? Then maybe I or someone can help get it working.

    David

  • edited July 2015

    David,

    Thanks so much for your help. I have tried so many things and am so confused I'm not sure what makes sense to post. Here's some code that I tried to intercept the keyPressEvent, insert my code, and call the original:

    Given that my variable @table is this: #

    I want to do something like:

    @table.class.instance_exec do
        alias_method :old_kp, :keyPressEvent
        define_method :keyPressEvent do |evt|
            STDERR << "keyPressEvent evt #{evt.inspect}\n"
            # if a tab char was hit, move down one row from the active cell
            old_kp(evt)
        end
    end
    

    This yields:

    ...undefined method 'keyPressEvent' for class 'RBA::QTableWidget_Native'...

    What I want is for the active cell in the table to move down one row upon a user-typed tab character. I don't have to do it using the method above if there's a better way.

    Thanks again,
    Dave

  • edited July 2015

    The following code better explains what I'm attempting. It works but is not using Qt - I've just used the same names for an analogy:

    class QTableWidget
        protected
    
        def keyPressEvent(arg)
            puts "org keyPressEvent(#{arg})"
        end
    end
    
    @table = QTableWidget.new
    
    @table.class.instance_exec do
        alias_method :org_keyPressEvent, :keyPressEvent
        define_method :keyPressEvent do |arg|
            puts "my keyPressEvent(#{arg}) adding custom behavior"
            org_keyPressEvent(arg)
        end
    end
    @table.keyPressEvent("somearg")
    
    # => my keyPressEvent(somearg) adding custom behavior
    # => org keyPressEvent(somearg)
    
  • edited November -1

    Hi Dave,

    I think the QWidget's keyPressEvent method is probably protected because only subclasses of QWidget are meant to override it.

    Here is some code compatible with KLayout 0.24-python-eval which shows a QDockWidget in the main window which contains a MyTableWidget instance which is a subclass of QTableWidget and is able to react to key events.

    include RBA
    
    class MyTableWidget < QTableWidget
      def keyPressEvent(event)
        puts "keyPressEvent(#{event})"
      end
    end
    
    dock = QDockWidget::new("My Dock", Application.instance.main_window)
    dock.setAllowedAreas(Qt_DockWidgetArea::AllDockWidgetAreas)
    
    table = MyTableWidget.new(4, 2, dock)
    dock.setWidget(table)
    
    Application.instance.main_window.addDockWidget(Qt_DockWidgetArea::RightDockWidgetArea, dock)
    

    Hope that helps!

    Regards, Chris

  • edited November -1

    Nice work Chris, your script works well.

    I know you were talking to the other Dave but I'll just mention to him or others: Make sure Tab key is not already in use, otherwise KLayout receives the key event rather than the table widget.

    It may be possible to have your table widget override that, but the easiest short-term thing is to manually disable that shortcut key. So, File > Key Bindings > make sure Tab key isn't used. By default it's used for the Display > Next state ("zoom_menu.next_display_state") action.

    David

  • edited November -1

    David,

    Thanks for the pointer. FYI: I found key bindings under File->Setup->Application->Key Bindings. The key press event was not caught by Chris' code without freeing up the tab key - just as you said. I'm sure you saved me hours digging for that problem.

    Chris,

    Thanks for the working sample code. It convinces me there is a way to do what I want. So, if I understand things, the keyPressEvent method is protected and as such only a subclass of QWidget can override it. In my situation I am given an instance of QTableWidget directly to work with - not sure I can practically change this. Since it looks like I can only override the keyPressEvent from within a subclass' definition, say MyTableWidget, I appear to be stuck. Hmm, not sure what to do. I'll run a few more experiments.

    Well, should you think of anything I'd love to hear it.

    David and Chris,

    I greatly appreciate your help. It's very kind of you both.

    Dave

  • edited November -1

    Not sure what you mean. You can override any method in Ruby.

    Chris overrides it when he does

    class MyTableWidget < QTableWidget
      def keyPressEvent(event)
        puts "keyPressEvent(#{event})"
      end
    end
    

    MyTableWidget is a child of QTableWidget which is ultimately a child of QWidget. He could equally well have done

    class QTableWidget
      def keyPressEvent(event)
        puts "keyPressEvent(#{event})"
        super(event)
      end
    end
    

    which in effect overrides the existing keyPressEvent in QTableWidget. First it does the "puts" and second it calls super and passes the original argument. In other words, 'super' calls what would have happened if you had not changed the above. Ruby's awesome.

    So even though you have an instance of QTableWidget, let's say

    table = MyTableWidget.new(4, 2, dock)
    

    from Chris's code, you can still capture key events.. So I'm not clear what remaining problem there is..?

    David

  • edited November -1

    David,

    I am given an instance of QTableWidget not an instance of a derived class of QTableWidget. My attempts to override the method in the instance failed. My attempts to chain my version of keyPressEvent() with my instance's original version fail when using Qt. In My example above, my code to chain works but not when really using Qt.

    When I put this code into my own it does not catch the event:

    class QTableWidget
        def keyPressEvent(event)
            puts "keyPressEvent(#{event})"
            super(event)
        end
    end
    

    I don't know why, but it just doesn't work. The new keyPressEvent() method is not called. I'm pretty sure that's the first thing I tried some time ago and I tried it again moments ago.

    still puzzled,
    Dave

  • edited July 2015

    David, Chris,

    I suspect I am able to override the keyPressEvent method but the slot/signal routing is still invoking the original keyPressEvent method. If this is the problem, I'll have to learn how to fix it.

    Dave

  • edited July 2015

    Hi Dave,

    I am not sure I understand your last comment - does that mean you did find a way now to catch events with the original QTableWidget instance?

    If you're using the code from your previous comment, i.e.

    class QTableWidget
        def keyPressEvent(event)
            puts "keyPressEvent(#{event})"
            super(event)
        end
    end
    

    the super call would of course cause the event to be passed on to the ancestor's keyPressEvent method. To fix this, you could make this call to super conditional in that you'd only use it if the key being pressed is not handled by your custom code.

    If this is not the code you're using to catch the event, what does your code look like? Ideally a small, self-contained sample like the one I posted earlier?

    Regards, Chris

  • edited July 2015

    Hi again,

    Using the following code I derived from my previous example, I was able to get the key codes for pressed keys using the original QTableWidget object:

    include RBA
    
    dock = QDockWidget::new("My Dock", Application.instance.main_window)
    dock.setAllowedAreas(Qt_DockWidgetArea::AllDockWidgetAreas)
    
    table = QTableWidget.new(4, 2, dock)
    dock.setWidget(table)
    
    def table.keyPressEvent(event)
      puts event.key
    end
    
    Application.instance.main_window.addDockWidget(Qt_DockWidgetArea::RightDockWidgetArea, dock)
    

    This is somewhat different from David's code above which overrides keyPressEvent for every QTableWidget object.

    This code also "consumes" the event, for example the arrow keys don't move the cell selection in the table. This can be solved by calling super(event) inside the keyPressEvent method. I'd probably call super(event) only if I didn't handle the special keys I care about to ensure the behaviour is still the default for all other keys.

    Regards, Chris

  • edited November -1

    Chris, good example, and you are right about my code modifying every QTW object.

    Dave, your original post said "My original goal was to have a Tab character move down the one and only editable column in a table I have created." Actually I just realized it works in this way even without overriding any methods... Unless I am misunderstanding your original post?

    Run the below code, click in the first cell, and press Tab to move down the rows. You can enter a value and then continue to Tab down. When it reaches the bottom, Tab wraps around to the top. It works on Windows, probably also on Linux.

    include RBA
    
    dock = QDockWidget::new("My Dock", Application.instance.main_window)
    dock.setAllowedAreas(Qt_DockWidgetArea::AllDockWidgetAreas)
    
    table = QTableWidget.new(4, 1, dock) # One editable column
    
    dock.setWidget(table)
    
    Application.instance.main_window.addDockWidget(Qt_DockWidgetArea::RightDockWidgetArea, dock)
    

    Anyway maybe I am beating a dead horse. Let us know if your issue is solved now.

    Thanks,
    David

  • edited July 2015

    Hi all,

    I hope I can shed some light on this.

    First of all, the Qt binding classes basically mimic the C++ Qt classes. The C++ way to implement a key event handler is to derive a class from the given one and reimplement the key event handler. It's declared protected by the Qt API so it can only be called from within subclasses.

    The RBA way to doing the same is shown in this example:

    class MyWidget < RBA::QTableWidget
    
      def initialize(parent)
        super(parent)
        self.columnCount = 5
        self.rowCount = 3
      end
    
      def keyPressEvent(kpe)
        puts kpe.key
        super(kpe)  
      end
    
    end  
    
    $w = MyWidget::new(nil)
    $w.show
    

    In that example, MyWidget already handles the Tabs in the desired way, so the Tab issue has a different cause (see below).

    Note that in the sample above you won't get every key event since, when you started editing, the key events will be sent to the edit delegate, not to the table widget.

    I'm not done yet, because in RBA you can't alway subclass. For example, if you take a QTableWidget from an UIC file, you cannot make the UIC form generator instantiate any class for you. Luckily, Ruby is a dynamic language and lets you modify existing objects:

    # same than above:
    
    $w2 = RBA::QTableWidget::new
    $w2.columnCount = 5
    $w2.rowCount = 3
    
    def $w2.keyPressEvent(kpe)
      puts kpe.key
      super(kpe)
    end
    
    $w2.show
    

    This works, because QTableWidget is already a subclass. The basic class is QTableWidget_Native which is a hidden class identical to the Qt class. The subclass is necessary to allow dynamic reimplementation of the virtual methods.

    But I get lost in details ... you wanted to know why Tab is an issue: that's very likely because the dialog parent handles the Tab key for you in order to implement focus navigation. To modify that behaviour you can try:

    • QAbstractItemView#tabKeyNavigation (must be true)
    • QWidget#focusPolicy (set to StrongFocus)

    For details about this and all the other Qt related topics please see the official Qt documentation. The one provided by KLayout is just referring to the formal API syntax, not the functionality. It's generated automatically and I'm not including the Qt documentation. You won't learn a lot about Qt from that documentation.

    Regards,

    Matthias

  • edited July 2015

    Hi David,

    My table has 3 columns and exactly one column is editable in all rows. The tab key moves me from left to right before moving down. Tab key cell changes are not affected whether the items are set to be editable or non-editable.

    I want the tab key to move down without left-to-right motion. Since the tab motion is not what I would like,
    I want to capture the tab key and set the current cell myself.

    Hi Matthias,

    Given:

    • Using KLayout 0.24 python eval (I have also tried 0.23) of KLayout

    • Using Qt 4.8.5

    • Using Ruby 1.9.3 embedded in KLayout.

    The tab key behavior moves the active cell from left to right. Once at the
    right-most column, it moves down a row and to the left-most column. I want
    the tab character to move down in the same column, so I am trying to catch
    the tab key press and manually set the active cell. Nothing I have tried
    allows me to override the keyPressEvent. I believe I am using the method
    you described.

    I am using a .ui file from QtDesigner and QFormBuilder to create a
    QDialog->QFrame->QTableWidget hierarchy. I have a variable, @table, that points
    to the instance of the QTableWidget. I run the following code:

    ...
    puts @table.class # => RBA::QTableWidget_Native
    def @table.keyPressEvent(kpe)
        puts kpe.key # never gets here
        super(kpe)
    end
    @table.insertRow(2) # a test to verify @table points at the right thing
                        # this works as expected. Removing this does not affect
                        # tab behavior or whether @table.keyPressEvent is called.
    ...
    

    The above code shows the @table class as a QTableWidget_Native and seems to run
    the code to override the keyPressEvent method in @table. However, the
    @table.keyPressEvent method never gets called.

    Inside QtDesigner I have set the QTableWidget to have tabKeyNavigation of true,
    and its focusPolicy to StrongFocus.

    Could this be a bug in KLayout / Qt?

    Thanks,
    Dave

  • edited July 2015

    Matthias, David, Chris,

    Here is a simple test that shows how I'm trying to override the keyPressEvent method
    and failing. When I tried this with a table NOT from QtDesigner (.ui file loaded by
    QFormBuilder), the keyPressEvent method does get overridden.

    Test code to be run this way: % klayout -rm blob.rb

    You need to save the following as "blob.rb":

    include RBA
    
    mw   = Application.instance.main_window
    
    dock = QDockWidget::new('My Dock', mw)
    dock.setAllowedAreas(Qt_DockWidgetArea::AllDockWidgetAreas)
    
    ui_file = QFile::new('test_table.ui')
    ui_file.open(QIODevice::ReadOnly)
    dialog  = QFormBuilder::new.load(ui_file, dock)
    ui_file.close
    
    table = dialog.table # has two rows, three columns
    table.insertRow(0) # adds a third row. verifies table points to correct thing.
    
    def table.keyPressEvent(kpe)
        puts "key press event: #{kpe.key}\n" # never gets here
        super(kpe)
    end
    
    dock.setWidget(table)
    
    mw.addDockWidget(Qt_DockWidgetArea::RightDockWidgetArea, dock)
    

    My table comes from QtDesigner as a .ui file. Here it is - you need to save the following as "test_table.ui":

    <?xml version="1.0" encoding="UTF-8"?>
    <ui version="4.0">
     <class>Dialog</class>
     <widget class="QDialog" name="Dialog">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>398</width>
        <height>240</height>
       </rect>
      </property>
      <widget class="QFrame" name="frame">
       <layout class="QHBoxLayout">
        <item>
         <widget class="QTableWidget" name="table">
          <property name="focusPolicy">
           <enum>Qt::StrongFocus</enum>
          </property>
          <property name="rowCount">
           <number>2</number>
          </property>
          <property name="columnCount">
           <number>3</number>
          </property>
          <row/>
          <row/>
          <column/>
          <column/>
          <column/>
         </widget>
        </item>
       </layout>
      </widget>
     </widget>
     <resources/>
     <connections/>
    </ui>
    

    Thanks,
    Dave

  • edited July 2015

    Hi all,

    sorry folks ... a was wrong.

    QFormBuilder will only generate "QTableWidget_Native" objects, never "QTableWidget" objects. In other words: you cannot overwrite the key event handler, since there is not derived class. You can attach signal handlers, but not override protected virtual methods.

    One way is to solve this is to use an event filter to filter the key events, but that beware of a bug that lurks there (see here).

    Matthias

  • edited November -1

    Follow up:

    I am able to build a QTableWidget instance and override its keyPressEvent. Unfortunately, it means
    that I cannot use QtDesigner to setup the table and write a .ui file. Instead have manually instantiated
    a QTableWidget and written a 30 line method to setup the table attributes as needed. I was unable to get an event filter to run under 0.23.9 or 0.24 python eval (I'm using Ruby). Using an event filter might have made it possible to have QtDesigner create the table.

    Thank you all for your help.

    Dave

Sign In or Register to comment.