4
votes

Problem

I've run into rather weird problem: QLineEdit objects in the interface cannot be accessed by mouse clicking on them, but using Tab's they receive the focus and respond to keyboard input. But even when they have the focus, they do not respond to clicks. What makes things weirder is that the last added QLineEdit does respond to clicks.

The extra problem: mouse-inaccessible line-edits do not show tool-tips.

I am using Common Lisp's qtools to construct the interface, so I will have to do some explaining how it works. In short, I'm adding line-edits one-by-one in the loop, all of them created in the same manner and added to QGridLayout.

Updated: See below for a possible source of the problem

Parameter widget

Parameter widget is used to represent one parameter input. Each parameter has a name, value and units (of measure). The name and units are shown as labels, the value is editable and is represented by QLineEdit

(define-widget parameter-widget (QWidget)
  ((parameter :initarg :parameter)))

(define-subwidget (parameter-widget label) (q+:make-qlabel parameter-widget)
  (setf (q+:text label) (parameter-base-name parameter)))

(define-subwidget (parameter-widget entry) (q+:make-qlineedit parameter-widget)
  (setf (q+:alignment entry) +align-right+)
  (setf (q+:text entry) (format nil "~A" (parameter-value parameter)))
  (setf (q+:tool-tip entry) (parameter-base-description parameter)))

(define-subwidget (parameter-widget units) (q+:make-qlabel parameter-widget)
  (setf (q+:text units) (parameter-units parameter)))

To indicate that parameter value has been changed, parameter-widget will emmit the new signal parameter-changed (it is translated to a signal "parameterChanged" for Qt):

(define-signal (parameter-widget parameter-changed) ())

The slot for the line edit tries to convert the text to a number, and if successful, updates the underlying parameter and emits parameter-changed:

(define-slot (parameter-widget parameter-changed) ((new-text string))
  (declare (connected entry (text-changed string)))
  (handler-case
      (let ((new-number (parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

This method is just to automatically construct the widget for the object of type parameter (factory method):

(defmethod make-parameter-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

This method is required to align labels for multiple single parameters in parameter-container (see further-below):

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)))

Parameter container

So, if I use parameter-widget on its own - everything is OK. But most of the time I need multiple parameters in the form of parameter-container:

(define-widget parameter-container-widget (QWidget)
  ((parameter-container :initarg :parameter-container)))

This slot captures the signal parameter-changed from all children parameter's and re-emits it for the container

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

layout is QGridLayout to put all individual parameter-widget's for all parameter-children of parameter-container. All it does:

  • it loops over all individual parameters,
  • constructs parameter-widget for each
  • adds to the layout row-by-row
  • and connects the signal parameter-changed to the above-defined slot.

Code:

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (loop for p in (parameter-container-children parameter-container)
     for row from 0
     do (let ((parameter-widget (make-parameter-ui p)))
          (setf (q+:parent parameter-widget) parameter-container-widget)
          (add-to-grid parameter-widget layout row 0)
          (connect! parameter-widget (parameter-changed)
                    parameter-container-widget
                    (parameter-changed)))))

This method is for uniform treatment to parameter-widget and parameter-container:

(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column 1 1))

And generic instantiator (factory method):

(defmethod make-parameter-ui ((object parameter-container))
  (make-instance 'parameter-container-widget :parameter-container object))

System information

Qt4.8, Common Lisp: SBCL 1.3.7, qtools from Quicklisp, OS: Ubuntu 16.04

Update

The same problem on Windows 10

Update 2: Full minimal example

I apologise, the code above uses many definitions from other packages: putting it all would make it really long. The smallest example boils down to this:

(ql:quickload '(:qtools :qtcore :qtgui))

(defpackage :qtexample
  (:use #:cl+qt))

(in-package qtexample)

(in-readtable :qtools)


(defclass parameter ()
  ((name :initarg :name :accessor parameter-name)
   (value :initarg :value :accessor parameter-value)
   (units :initarg :units :accessor parameter-units)))

(defun parameter (name value units)
  (make-instance 'parameter
    :name name
    :value value
    :units units))

(defvar *mass* (parameter "mass" 1d0 "kg"))
(defvar *velocity* (parameter "velocity" 0.5d0 "m/s"))

(defvar *temperature* (parameter "temperature" 300d0 "K"))
(defvar *pressure* (parameter "pressure" 1d5 "Pa"))

(define-widget parameter-widget (QWidget)
  ((parameter
    :initarg :parameter
    :accessor parameter-widget-parameter)))

(define-subwidget (parameter-widget label)
    (q+:make-qlabel parameter-widget)
  (setf (q+:text label) (parameter-name parameter)))

(defmethod make-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

(defconstant +align-right+ 2)

(define-subwidget (parameter-widget entry)
    (q+:make-qlineedit parameter-widget)
  (setf (q+:alignment entry) +align-right+)
  (setf (q+:text entry) (format nil "~A" (parameter-value parameter))))

(define-subwidget (parameter-widget units)
    (q+:make-qlabel parameter-widget)
  (setf (q+:text units) (parameter-units parameter)))

(define-signal (parameter-widget parameter-changed) ())

(define-slot (parameter-widget entry) ((new-text string))
  (declare (connected entry (text-changed string)))
  (format t "~&Parameter has changed~%")
  (handler-case
      (let ((new-number (parse-number:parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)
    (list (1+ row) (+ column 3))))

(define-widget parameter-widget-window (QWidget)
  ((parameter :initarg :parameter)))


(define-subwidget (parameter-widget-window parameter-widget)
    (make-ui parameter))

(define-subwidget (parameter-widget-window grid)
    (q+:make-qgridlayout  parameter-widget-window)
  (add-to-grid parameter-widget grid 0 0))

(defun parameter-example (parameter)
  (with-main-window
      (window (make-instance 'parameter-widget-window
                :parameter parameter))))

(define-widget parameter-container-widget (QWidget)
  ((parameter-container
    :initarg :parameter-container
    :accessor parameter-container-widget-parameter-container)))

(defmethod make-ui ((object list))
  (make-instance 'parameter-container-widget :parameter-container object))

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (let* ((parameter-widgets (loop for p in parameter-container
                               collect (make-ui p))))
    (loop for p in parameter-widgets
       for row from 0
       do (progn
            (setf (q+:parent p) parameter-container-widget)
            (add-to-grid p layout row 0)
            (connect! p
                      (parameter-changed)
                      parameter-container-widget
                      (parameter-changed))))))

(define-widget parameter-container-widget-window (QWidget)
  ((parameter-container :initarg :parameter-container)))

(define-subwidget (parameter-container-widget-window container-widget)
    (make-ui parameter-container)
  (setf (q+:parent container-widget) parameter-container-widget-window))

(define-slot (parameter-container-widget-window parameter-changed) ()
  (declare (connected container-widget (parameter-changed)))
  (format t "~&Got parameter changed~%"))


(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column))

(defun example-parameter-container (parameter-container)
  (with-main-window
      (window (make-instance 'parameter-container-widget-window
                :parameter-container parameter-container))))

;; to run:
(example-parameter-container (list *mass* *velocity*))

While working on it, I stumbled upon a possible solution. This line

(setf (q+:parent p) parameter-container-widget) 

sets the parent for a sub-widget p (in the list of sub-widgets) to be parameter-container-widget. If this line is commented, everything works fine.

p's sub-widgets (which include entry, an instance of QLineEdit) are later added into the grid, but not p itself! In a way, parameter-widget is not a proper widget: it's just a collection of other widgets with the rules of how to add them into the container. But it needs to act as widget in a sense of being able to receive and send signals.

1
Is this complete code? Please make sure that you post everything from a lisp file needed to run it and reproduce the issue.Kuba hasn't forgotten Monica
@KubaOber I've realised too late how many different parts this example used. I've added the full minimal example - I decided not to break it with comments for easy copy-paste.mobiuseng
I'd have had no problem as long as all the code was there. As your update demonstrates, you missed quite a few lines.Kuba hasn't forgotten Monica
Why is your parameter-widget not a parameter-object - a QObject, not QWidget?Kuba hasn't forgotten Monica
In the latest Qtools you can use define-object, which is just a new name for define-widget. The truth is that even if you use define-widget, the automatic widget superclass inherits from QObject, not QWidget. As such, in order to define a QObject subclass you can simply do (define-widget foo (QObject) ()).Shinmera

1 Answers

5
votes

After @KubaOber and @Shinmera suggestions I managed to fix the problem. I'm posting here the fix for the future reference.

Preliminaries didn't change (except, I forgot to add :PARSE-NUMBER system):

(ql:quickload '(:qtools :qtcore :qtgui :parse-number))

(defpackage :qtexample
  (:use #:cl+qt))

(in-package qtexample)

(in-readtable :qtools)


(defclass parameter ()
  ((name :initarg :name :accessor parameter-name)
   (value :initarg :value :accessor parameter-value)
   (units :initarg :units :accessor parameter-units)))

(defun parameter (name value units)
  (make-instance 'parameter
    :name name
    :value value
    :units units))

(defvar *mass* (parameter "mass" 1d0 "kg"))
(defvar *velocity* (parameter "velocity" 0.5d0 "m/s"))

(defvar *temperature* (parameter "temperature" 300d0 "K"))
(defvar *pressure* (parameter "pressure" 1d5 "Pa"))

Since PARAMETER-WIDGET is not really a widget, but just a container for other widgets (and by itself it cannot be added into a proper widget-container - otherwise the alignment of labels will go off - see ADD-TO-GRID method) but it needs to be able to receive and send signals, it is inherited from QObject rather than QWidget. All sub-widgets now are just normal class-slots in it instead of being defined by (DEFINE-SUBWIDGET ...). Notice, no parent was supplied for any sub-widget: parent property will be assigned when these widgets are added to a container-widget.

(define-widget parameter-widget (QObject)
  ((parameter
    :initarg :parameter
    :accessor parameter-widget-parameter)
   (label :initform (q+:make-qlabel))
   (entry :initform (q+:make-qlineedit))
   (units :initform (q+:make-qlabel))))

Initialization goes into INITIALIZE-INSTANCE :AFTER method:

(defconstant +align-right+ 2)

(defmethod initialize-instance :after ((object parameter-widget) &key)
  (with-slots (parameter label entry units) object
    (setf (q+:text label) (parameter-name parameter))
    (setf (q+:text entry) (format nil "~A" (parameter-value parameter)))
    (setf (q+:alignment entry) +align-right+)
    (setf (q+:text units) (parameter-units parameter))))

The rest of definition stays the same (signalling is mostly irrelevant to the question but helps to understand why I am jumping through all these hoops):

(defmethod make-ui ((object parameter))
  (make-instance 'parameter-widget :parameter object))

(define-signal (parameter-widget parameter-changed) ())

(define-slot (parameter-widget entry) ((new-text string))
  (declare (connected entry (text-changed string)))
  (format t "~&Parameter has changed~%")
  (handler-case
      (let ((new-number (parse-number:parse-number new-text)))
        (setf (parameter-value parameter) new-number)
        (signal! parameter-widget (parameter-changed)))
    (error nil)))

(defmethod add-to-grid ((widget parameter-widget) grid row column)
  (with-slots (label entry units) widget
    (q+:add-widget grid label row column 1 1)
    (q+:add-widget grid entry row (1+ column) 1 1)
    (q+:add-widget grid units row (+ column 2) 1 1)
    (list (1+ row) (+ column 3))))

And this is a quick demo that everything works for a single parameter. Now, PARAMETER-WIDGET is no longer a widget, so it is added as a class-slot.

(define-widget parameter-widget-window (QWidget)
  ((parameter :initarg :parameter)
   (parameter-widget)))

The grid of the window:

(define-subwidget (parameter-widget-window grid)
    (q+:make-qgridlayout  parameter-widget-window))

Here the addition of PARAMETER-WIDGET components into the grid was moved out of this definition. The reason: slot PARAMETER-WIDGET at this point is unbound. It does get bound in INITIALIZE-INSTANCE :AFTER method and where all components are added to the grid:

(defmethod initialize-instance :after ((object parameter-widget-window) &key)
  (with-slots (parameter parameter-widget grid) object
    (setf parameter-widget (make-ui parameter))
    (setf (q+:parent parameter-widget) object)
    (add-to-grid parameter-widget grid 0 0)))

And the launcher stays the same:

(defun parameter-example (parameter)
  (with-main-window
      (window (make-instance 'parameter-widget-window
                :parameter parameter))))

To run: (parameter-example *mass*)

PARAMETER-CONTAINER-WIDGET is a proper widget. Its definition didn't change:

(define-widget parameter-container-widget (QWidget)
  ((parameter-container
    :initarg :parameter-container
    :accessor parameter-container-widget-parameter-container)))

(defmethod make-ui ((object list))
  (make-instance 'parameter-container-widget :parameter-container object))

(define-slot (parameter-container-widget parameter-changed) ()
  (format t "~&Re-emitting the signal...~%")
  (signal! parameter-container-widget (parameter-changed)))

And LAYOUT definition doesn't change either. But now it is safe to set the parent property of p (PARAMETER-WIDGET) to PARAMETER-WIDGET-CONTAINER, so it is get destroyed when the container is destroyed.

(define-subwidget (parameter-container-widget layout)
    (q+:make-qgridlayout parameter-container-widget)
  (let* ((parameter-widgets (loop for p in parameter-container
                               collect (make-ui p))))
    (loop for p in parameter-widgets
       for row from 0
       do (progn
            (add-to-grid p layout row 0)
            (let ((pp p))
              (setf (q+:parent pp) parameter-container-widget))
            (connect! p
                      (parameter-changed)
                      parameter-container-widget
                      (parameter-changed))))))

Adding to grid this widget is trivial (yet not completely correct, but fixable, see further-below):

(defmethod add-to-grid ((widget parameter-container-widget) grid row column)
  (q+:add-widget grid widget row column))

The demonstration part didn't change:

(define-widget parameter-container-widget-window (QWidget)
  ((parameter-container :initarg :parameter-container)))

(define-subwidget (parameter-container-widget-window container-widget)
    (make-ui parameter-container)
  (setf (q+:parent container-widget) parameter-container-widget-window))

(define-slot (parameter-container-widget-window parameter-changed) ()
  (declare (connected container-widget (parameter-changed)))
  (format t "~&Got parameter changed~%"))

(defun example-parameter-container (parameter-container)
  (with-main-window
      (window (make-instance 'parameter-container-widget-window
                :parameter-container parameter-container))))

To run: (example-parameter-container (list *mass* *velociy*))

It can also handle hierarchical parameters like so

(example-parameter-container (list *mass* *velocity* (list *temperature* *pressure*))) 

And this run will show why ADD-TO-GRID for PARAMETER-CONTAINER-WIDGET needs to be a bit more complex, but this is a different story.