bohonghuang / cl-gtk4

GTK4/Libadwaita/WebKit2 bindings for Common Lisp.
GNU Lesser General Public License v3.0
216 stars 9 forks source link

Trouble with GtkListView #19

Closed hamzashahid-blit closed 1 year ago

hamzashahid-blit commented 1 year ago

Hello, love the work you have done! I wanted to create a GtkListView, but didn't know how to. After hours of looking through the GTK docs I came up with this code:

    (let* ((model (gtk:make-list-store :types `(,(gobject:type-from-name "GtkLabel"))))
           (selmodel (gtk:make-single-selection :model model))
           (factory (gtk:make-signal-list-item-factory))
           (view (gtk:make-list-view :model selmodel :factory factory))
           (item1 (gtk:make-label :str "BRUHSEIDh")))
      (gtk::list-box-append model 6)
      (setf (gtk:window-child window) view))

Turns out for a reason I cannot understand, list-box-append gives out an error saying it takes only 1 argument... Please give an example for ListViews or explain this problem to me! I really don't want to drop this application and write it in another language. Thanks a bunch.

Regards, Hamza

bohonghuang commented 1 year ago

The (gobject:type-from-name "GtkLabel") in your code returns 0, which causes (gtk:make-list-store ...) to return a null pointer.

bohonghuang commented 1 year ago

To make (gobject:type-from-name "GtkLabel") return a non-zero value, you need to get this type registered by the GObject system first. For example, you can move the (item1 (make-label :str "BRUHSEIDh")) or add a seemingly meaningless (gtk:make-label :str "") before it.

bohonghuang commented 1 year ago

BTW, Gtk.ListStore has been deprecated in version 4.10, and you should use gio:make-list-store to create it instead.

bohonghuang commented 1 year ago

I just added an example of a ListView for your reference:

https://github.com/bohonghuang/cl-gtk4/blob/ee766c214653745c270c879f96e2be6f0267397d/examples/gtk4.lisp#L187-L230

hamzashahid-blit commented 1 year ago

Thank you so much! I wasn't expecting an answer. That's awesome, I will try it out right now.

hamzashahid-blit commented 1 year ago

In your example, gobj:coerce is not found. I will try and download from latest commit.

bohonghuang commented 1 year ago

In your example, gobj:coerce is not found. I will try and download from latest commit.

It's in cl-glib. You may need to clone it manually or update your Ultralisp distribution.

hamzashahid-blit commented 1 year ago

Yup, I cloned cl-gtk4 and cl-glib and the example works now.

hamzashahid-blit commented 1 year ago

Okay, so ultimately I wanted to create a GtkColumnView. I got to this code:

(gtk:define-application (:name closting
                         :id "com.closting")
  (gtk:define-main-window (window (gtk:make-application-window :application gtk:*application*))
    (setf (gtk:window-title window) "Closting")
    (let* ((main-model (gtk:make-string-list :strings '("HP Zbook 15" "250$" "300$"
                                                        "Infinix S90" "40$" "60$"
                                                        "Casio Calculator" "50$" "80$")))
           (column-view (gtk:make-column-view :model (gtk:make-single-selection :model main-model)))
           (col1-factory (gtk:make-signal-list-item-factory))
           (col1 (gtk:make-column-view-column :title "First Col!" :factory col1-factory))
           (col2-factory (gtk:make-signal-list-item-factory))
           (col2 (gtk:make-column-view-column :title "Second Col!" :factory col2-factory))
           (col3-factory (gtk:make-signal-list-item-factory))
           (col3 (gtk:make-column-view-column :title "Third Col!" :factory col3-factory)))
      (gtk:column-view-append-column column-view col1)
      (gtk:column-view-append-column column-view col2)
      (gtk:column-view-append-column column-view col3)
      (setf (gtk:widget-vexpand-p column-view) t
            (gtk:widget-hexpand-p column-view) t)
      (flet ((setup (factory item)
               (declare (ignore factory))
               (setf (gtk:list-item-child item) (gtk:make-label :str "")))
             (bind (factory item) 
               (declare (ignore factory))
               (format t "POSSSS: ~a~%" (gtk:list-item-position item))
               (when (zerop (mod (1+ (gtk:list-item-position item)) 3))
                 (setf (gtk:label-text (gobj:coerce (gtk:list-item-child item) 'gtk:label))
                       (gobj:value-string (gobj:coerce (gtk:list-item-item item) 'gtk:string-object)))))
             (unbind (factory item) (declare (ignore factory item)))
             (teardown (factory item) (declare (ignore factory item))))
        (gtk:connect col1-factory "setup" #'setup)
        (gtk:connect col1-factory "bind" #'bind)
        (gtk:connect col1-factory "unbind" #'unbind)
        (gtk:connect col1-factory "teardown" #'teardown))
      (setf (gtk:window-child window) column-view))
    (unless (gtk:widget-visible-p window)
      (gtk:window-present window))))

But instead of getting every third value in each cell. I get a lot of blank cells. What I need is to somehow skip over elements in the model. Or how should I go about just creating a simple column view at all? Maybe I am doing it wrong. Also kindly inform me if this comment is out of the scope of this issue. I figured it isn't as ColumnView uses ListView. Thanks

Regards, Hamza

bohonghuang commented 1 year ago

You can use StringList as the elements of your ListModel:

(gtk:define-application (:name closting
                         :id "com.closting")
  (gtk:define-main-window (window (gtk:make-application-window :application gtk:*application*))
    (setf (gtk:window-title window) "Closting")
    (let* ((rows (loop :for row :in '(("HP Zbook 15" "250$" "300$")
                                      ("Infinix S90" "40$" "60$")
                                      ("Casio Calculator" "50$" "80$"))
                       :collect (gtk:make-string-list :strings row)))
           (main-model (gio:make-list-store :item-type (gobj:type-from-name "GtkStringList")))
           (column-view (gtk:make-column-view :model (gtk:make-single-selection :model main-model)))
           (col1-factory (gtk:make-signal-list-item-factory))
           (col1 (gtk:make-column-view-column :title "First Col!" :factory col1-factory))
           (col2-factory (gtk:make-signal-list-item-factory))
           (col2 (gtk:make-column-view-column :title "Second Col!" :factory col2-factory))
           (col3-factory (gtk:make-signal-list-item-factory))
           (col3 (gtk:make-column-view-column :title "Third Col!" :factory col3-factory)))
      (gtk:column-view-append-column column-view col1)
      (gtk:column-view-append-column column-view col2)
      (gtk:column-view-append-column column-view col3)
      (loop :for row :in rows :do (gio:list-store-append main-model row))
      (setf (gtk:widget-vexpand-p column-view) t
            (gtk:widget-hexpand-p column-view) t)
      (flet ((setup (factory item)
               (declare (ignore factory))
               (setf (gtk:list-item-child item) (gtk:make-label :str "")))
             (unbind (factory item) (declare (ignore factory item)))
             (teardown (factory item) (declare (ignore factory item))))
        (loop :for factory :in (list col1-factory col2-factory col3-factory)
              :for index :from 0
              :do (gtk:connect factory "setup" #'setup)
                  (gtk:connect factory "unbind" #'unbind)
                  (gtk:connect factory "teardown" #'teardown)
                  (gtk:connect factory "bind"
                               (let ((i index))
                                 (lambda (factory item) 
                                   (declare (ignore factory))
                                   (setf (gtk:label-text (gobj:coerce (gtk:list-item-child item) 'gtk:label))
                                         (gtk:string-list-get-string (gobj:coerce (gtk:list-item-item item) 'gtk:string-list) i)))))))
      (setf (gtk:window-child window) column-view))
    (unless (gtk:widget-visible-p window)
      (gtk:window-present window))))
hamzashahid-blit commented 1 year ago

Thanks a lot! Just one final question, do you have any idea of how to store custom types in a list store? For example: Instead of every row being a StringList, how can I make it something like:

(defclass product-info (gobject:object)
  ((name :initarg :name
         :accessor name)
   (cost :initarg :cost
         :accessor cost)
   (sale :initarg :sale
         :accessor sale)))

This does not work as it says something about gobject:object being a forward referenced class, whatever that means...

bohonghuang commented 1 year ago

If you don't require eql to guarantee object equality, using print1-to-string and read-from-string seems to be the simplest way. Otherwise, you may need some mechanism to identify objects. For example:

  1. You can put objects in an array and use the array index to retrieve the object.
  2. Use a UUID library or similar to generate a unique integer or string identifier for an object, and use it as a key in a hash table to retrieve the object.
hamzashahid-blit commented 1 year ago

Also, why did we have to put a (let ((i index))) around tthe bind lambda function?

hamzashahid-blit commented 1 year ago

If you don't require eql to guarantee object equality, using print1-to-string and read-from-string seems to be the simplest way. Otherwise, you may need some mechanism to identify objects. For example:

  1. You can put objects in an array and use the array index to retrieve the object.
  2. Use a UUID library or similar to generate a unique integer or string identifier for an object, and use it as a key in a hash table to retrieve the object.

Wait, so what does equality have to do with having custom types?

bohonghuang commented 1 year ago

Also, why did we have to put a (let ((i index))) around tthe bind lambda function?

Since the loop variables in loop are mutable in most implementations, we need to make the closure immutably capture the index so that different columns can access data with different column indexes.

bohonghuang commented 1 year ago

Wait, so what does equality have to do with having custom types?

(eql (read-from-string (prin1-to-string '(1 2 3))) '(1 2 3)) ; => NIL
(equal (read-from-string (prin1-to-string '(1 2 3))) '(1 2 3)) ; => T
hamzashahid-blit commented 1 year ago

Okay, I get that, but I still don't understand how I would go about implementing this to make a new custom type.

bohonghuang commented 1 year ago

This is a simple way you can use when you don't need eql to guarantee object equality:

(defstruct product-info
  name cost sale)

(gtk:define-application (:name closting
                         :id "com.closting")
  (gtk:define-main-window (window (gtk:make-application-window :application gtk:*application*))
    (setf (gtk:window-title window) "Closting")
    (let* ((rows (loop :for (name cost sale) :in '(("HP Zbook 15" "250$" "300$")
                                                   ("Infinix S90" "40$" "60$")
                                                   ("Casio Calculator" "50$" "80$"))
                       :collect (prin1-to-string (make-product-info :name name :cost cost :sale sale))))
           (main-model (gtk:make-string-list :strings rows))
           (column-view (gtk:make-column-view :model (gtk:make-single-selection :model main-model)))
           (col1-factory (gtk:make-signal-list-item-factory))
           (col1 (gtk:make-column-view-column :title "First Col!" :factory col1-factory))
           (col2-factory (gtk:make-signal-list-item-factory))
           (col2 (gtk:make-column-view-column :title "Second Col!" :factory col2-factory))
           (col3-factory (gtk:make-signal-list-item-factory))
           (col3 (gtk:make-column-view-column :title "Third Col!" :factory col3-factory)))
      (gtk:column-view-append-column column-view col1)
      (gtk:column-view-append-column column-view col2)
      (gtk:column-view-append-column column-view col3)
      (setf (gtk:widget-vexpand-p column-view) t
            (gtk:widget-hexpand-p column-view) t)
      (flet ((setup (factory item)
               (declare (ignore factory))
               (setf (gtk:list-item-child item) (gtk:make-label :str "")))
             (unbind (factory item) (declare (ignore factory item)))
             (teardown (factory item) (declare (ignore factory item))))
        (loop :for factory :in (list col1-factory col2-factory col3-factory)
              :for accessor :in (list #'product-info-name #'product-info-cost #'product-info-sale)
              :do (gtk:connect factory "setup" #'setup)
                  (gtk:connect factory "unbind" #'unbind)
                  (gtk:connect factory "teardown" #'teardown)
                  (gtk:connect factory "bind"
                               (let ((accessor accessor))
                                 (lambda (factory item) 
                                   (declare (ignore factory))
                                   (let ((row (read-from-string (gtk:string-object-string (gobj:coerce (gtk:list-item-item item) 'gtk:string-object)))))
                                     (setf (gtk:label-text (gobj:coerce (gtk:list-item-child item) 'gtk:label)) (funcall accessor row))))))))
      (setf (gtk:window-child window) column-view))
    (unless (gtk:widget-visible-p window)
      (gtk:window-present window))))
hamzashahid-blit commented 1 year ago

Ohhhhhhh, Thanks once again! I couldn't have thought of that.

bohonghuang commented 1 year ago

If you want to get the same object under EQL comparison, or other unreadable objects in the bind signal handler, I prefer using UUIDs to identify objects:

(defstruct product-info
  name cost sale)

(gtk:define-application (:name closting
                         :id "com.closting")
  (gtk:define-main-window (window (gtk:make-application-window :application gtk:*application*))
    (setf (gtk:window-title window) "Closting")
    (let ((product-table (make-hash-table :test #'equal)))
      (let* ((rows (loop :for (name cost sale) :in '(("HP Zbook 15" "250$" "300$")
                                                     ("Infinix S90" "40$" "60$")
                                                     ("Casio Calculator" "50$" "80$"))
                         :for product-info := (make-product-info :name name :cost cost :sale sale)
                         :for uuid := (prin1-to-string (uuid:make-v1-uuid))
                         :do (setf (gethash uuid product-table) product-info)
                         :collect uuid))
             (main-model (gtk:make-string-list :strings rows))
             (column-view (gtk:make-column-view :model (gtk:make-single-selection :model main-model)))
             (col1-factory (gtk:make-signal-list-item-factory))
             (col1 (gtk:make-column-view-column :title "First Col!" :factory col1-factory))
             (col2-factory (gtk:make-signal-list-item-factory))
             (col2 (gtk:make-column-view-column :title "Second Col!" :factory col2-factory))
             (col3-factory (gtk:make-signal-list-item-factory))
             (col3 (gtk:make-column-view-column :title "Third Col!" :factory col3-factory)))
        (gtk:column-view-append-column column-view col1)
        (gtk:column-view-append-column column-view col2)
        (gtk:column-view-append-column column-view col3)
        (setf (gtk:widget-vexpand-p column-view) t
              (gtk:widget-hexpand-p column-view) t)
        (flet ((setup (factory item)
                 (declare (ignore factory))
                 (setf (gtk:list-item-child item) (gtk:make-label :str "")))
               (unbind (factory item) (declare (ignore factory item)))
               (teardown (factory item) (declare (ignore factory item))))
          (loop :for factory :in (list col1-factory col2-factory col3-factory)
                :for accessor :in (list #'product-info-name #'product-info-cost #'product-info-sale)
                :do (gtk:connect factory "setup" #'setup)
                    (gtk:connect factory "unbind" #'unbind)
                    (gtk:connect factory "teardown" #'teardown)
                    (gtk:connect factory "bind"
                                 (let ((accessor accessor))
                                   (lambda (factory item) 
                                     (declare (ignore factory))
                                     (let* ((uuid (gtk:string-object-string (gobj:coerce (gtk:list-item-item item) 'gtk:string-object)))
                                            (row (gethash uuid product-table)))
                                       (setf (gtk:label-text (gobj:coerce (gtk:list-item-child item) 'gtk:label)) (funcall accessor row))))))))
        (setf (gtk:window-child window) column-view)))
    (unless (gtk:widget-visible-p window)
      (gtk:window-present window))))
hamzashahid-blit commented 1 year ago

Oh, that's a really cool solution. Will definitely use that!

hamzashahid-blit commented 1 year ago

Okay, even though you closed this, I get unhandled memory errors from this:

(defun create-financial-module-column-view (columns data)
  (let* ((string-list-data (loop :for row :in data
                                 :collect (gtk:make-string-list :strings row)))
         (main-model (gio:make-list-store :item-type (gobj:type-from-name "GtkStringList")))
         (column-view (gtk:make-column-view :model (gtk:make-single-selection :model main-model))))
    (setf (gtk:widget-vexpand-p column-view) t
          (gtk:widget-hexpand-p column-view) t)
    ;; TEMPORARY
    (setf (gtk:column-view-reorderable-p column-view) t)
    (loop :for row :in string-list-data
          :do (gio:list-store-append main-model row))
    (loop :for column-name :in columns
          :for index :from 0
          :do (let* ((col-factory (gtk:make-signal-list-item-factory))
                     (column (gtk:make-column-view-column :title column-name :factory col-factory)))
                (setf (gtk:column-view-column-resizable-p column) t)
                (flet ((setup (factory item)
                         (declare (ignore factory))
                         (setf (gtk:list-item-child item) (gtk:make-label :str "DEFAULT")))
                       (unbind (factory item) (declare (ignore factory item)))
                       (teardown (factory item) (declare (ignore factory item))))
                  (gtk:connect col-factory "setup" #'setup)
                  (gtk:connect col-factory "unbind" #'unbind)
                  (gtk:connect col-factory "teardown" #'teardown)
                  (gtk:connect col-factory "bind" ;(lambda (factory item) (declare (ignore factory item)))
                               (let ((i index))
                                 (lambda (factory item)
                                   (declare (ignore factory))
                                   (format t "PLEAAAAAASEWORKKKK: ~a~%" (gobj:coerce (gtk:list-item-child item) 'gtk:label))
                                   ;; (setf (gtk:label-text (gobj:coerce (gtk:list-item-child item) 'gtk:label))
                                   ;;        (format nil "BRUHRHRUHRUHRUHR, ~a~%" i)
                                   ;;        ;; (gtk:string-list-get-string (gobj:coerce (gtk:list-item-item item)
                                   ;;        ;;                                           'gtk:string-list)
                                   ;;        ;;                              i)
                                   ;;        )
                                   ))))
                (gtk:column-view-append-column column-view column)))
    column-view))

I narrowed the error down to using gobj:coerce to convert from (gtk:list-item-item item) to 'gtk:label. Why does this happen? As a hint, when run another time, it runs normally. Weirdly this causes annoying sbcl errors and I have to restart the REPL.

bohonghuang commented 1 year ago

This may be a memory issue caused by the garbage collector. Please refer to https://github.com/bohonghuang/cl-gtk4/issues/20#issuecomment-1586115138. Could you update cl-gobject-introspection and try again?

hamzashahid-blit commented 1 year ago

Okay.

hamzashahid-blit commented 1 year ago

I wasn't even using that version of cl-gobject-introspection. I spend hours debuggin that, It works now, thanks a bunch!