;;; copyright (c) Polos Ruetz ;;; ;;; user interface compiler (defpackage :quic (:use :common-lisp :eql) (:export #:run)) (in-package :quic) (defvar *defvars*) (defvar *qlets*) (defvar *lets-ini*) (defvar *lets-tr*) (defvar *main-var*) (defvar *main-class*) (defvar *classes*) (defvar *line-nr*) (defvar *section*) (defmacro with-setq-reset ((&rest vars) &body body) (let ((set-nil (mapcar (lambda (x) (list 'setq x nil)) vars))) `(progn ,@set-nil ,@body ,@set-nil))) (defun load-ui-related-qt-modules () (dolist (module (list :help :multimedia :svg :webengine :webkit)) (eql:qrequire module :quiet))) (defun run (&optional (ui.h "ui.h") (ui.lisp "ui.lisp") (ui-package :ui) properties) (load-ui-related-qt-modules) (with-setq-reset (*defvars* *qlets* *lets-ini* *lets-tr* *main-var* *main-class* *classes* *line-nr* *section*) (setf *defvars* (make-hash-table :test 'equal) *classes* (make-hash-table :test 'equal) *line-nr* 0) (with-open-file (in ui.h :direction :input) (with-open-file (out ui.lisp :direction :output :if-exists :supersede) (format out ";;; THIS FILE IS GENERATED (see 'eql5 -quic')~ ~%~ ~%(defpackage ~(~S~)~% (:use :common-lisp :eql)" ui-package) (let (code tr) (x:while-it (read-line in nil nil) (setf x:it (x:string-substitute "" "QStringLiteral" x:it)) (incf *line-nr*) (x:when-it* (c-to-lisp x:it) (if (eql :tr *section*) (push x:it* tr) (push x:it* code)))) (let ((max-len 0)) (format out "~% (:export~% ;; all DEFVARs except layouts, spacers~{~% #:~A~}~ ~% #:ini~ ~% #:retranslate-ui~ ~% #:show))~ ~%~ ~%(in-package ~(~S~))~ ~%~ ~%(defvar ~A)~A (main widget)~ ~{~%(defvar ~{~A)~A~}~}~ ~%~ ~%(defun ini (&optional show)~ ~% (qlet (~{~A~^ ~})~ ~% (let (~{~A~^ ~})~ ~% (setf ~A (qnew ~S))" (let (export) (maphash (lambda (var x) (setf max-len (max (length var) max-len)) (when (notany (lambda (skip) (search skip (gethash var *classes*))) '("Layout" "Spacer")) (push (var-name var) export))) *defvars*) (sort export 'string<)) ui-package (var-name *main-var*) (format nil "~A ; ~A" (make-string (- max-len (length *main-var*))) *main-class*) (let (defvars) (maphash (lambda (var x) (unless (string= *main-var* var) (push (list (var-name var) (format nil "~A ; ~A" (make-string (- max-len (length var))) (gethash var *classes*))) defvars))) *defvars*) (sort defvars 'string< :key 'first)) (mapcar 'var-name (reverse *qlets*)) (mapcar 'var-name (reverse *lets-ini*)) (var-name *main-var*) *main-class*)) (dolist (line (nreverse code)) (write-string line out)) (format out "~% (retranslate-ui)~ ~% (when show~ ~% (show)))))~ ~%~ ~%(defun show ()~ ~% (qfun ~A ~S))~ ~%~ ~%(defun retranslate-ui ()~ ~% (let (~{~A~^ ~})" (var-name *main-var*) (if (find :maximized properties) "showMaximized" "show") (mapcar 'var-name (reverse *lets-tr*))) (dolist (line (nreverse tr)) (write-string line out)) (format out "))~%")))))) (defun trim (string) (string-trim " .;*" string)) (defun find* (x list) (find x list :test 'string=)) (defun remove-parens (list) (remove ")" list :test 'string=)) (defun from-qt-utf8 (string) (let ((vector (make-array 0 :adjustable t :fill-pointer t))) (flet ((push* (x) (vector-push-extend (if (characterp x) (char-code x) x) vector))) (dotimes (i (length string)) (let ((ch (char string i))) (if (char= #\\ ch) (if (find (char string (1+ i)) '(#\" #\\)) (push* ch) (progn (push* (parse-integer (subseq string (1+ i) (+ 4 i)) :radix 8)) (incf i 3))) (push* ch))))) (qfrom-utf8 vector))) (defun var-name (name) (let ((name* (trim name))) (flet ((find-in (var) (find* name* var))) (let* ((local (or (find-in *qlets*) (find-in *lets-ini*) (find-in *lets-tr*))) (global (and (not local) (gethash name* *defvars*)))) (when global (setf name* (format nil "*~A*" name*))) (if (or global local) (string-downcase (substitute #\- #\_ name*)) name*))))) (defun constructor-arg (class) (dolist (arg '("(QWidget*)" "(QObject*)" "(QListWidget*)" "(QColor)" "(QSizePolicy::Policy,QSizePolicy::Policy)" "(int,int,QSizePolicy::Policy,QSizePolicy::Policy)")) (when (find arg (cadar (qapropos* "constructor" class)) :test 'x:ends-with) (return arg)))) (defun find-qt-method (var name) (let ((class (gethash var *classes*)) (name* (format nil " ~A(" name))) (loop (unless class (return)) (x:when-it (qapropos* name* class) ;; resolve ambiguous (let ((name** (assoc name '(("addAction" . "addAction(QAction*)")) :test 'string=))) (return-from find-qt-method (prin1-to-string (if name** (cdr name**) name))))) (setf class (qsuper-class-name class))))) (defun prepare-args (args) (remove-if (lambda (x) (search "static_cast<" x)) (cond ((and (string= "QColor" (first args)) (find (length args) '(4 5))) (list (format nil "(qfun \"QColor\" \"fromRgb\"~{ ~A~})" (rest args)))) (t (let ((args* (copy-list args)) variant variant-type cursor url) (dotimes (i (length args)) (x:when-it (and (string= "(qfun" (nth i args)) (find-qt-method (nth (1+ i) args) (nth (+ i 2) args))) (setf (nth (+ i 2) args*) x:it))) (mapcar (lambda (arg) (cond (variant (setf variant nil) (if (char= #\Q (char arg 0)) (progn (setf variant-type arg) "") (let ((type (cond ((find* arg '("t" "nil")) "bool") (t (error "QVariant type not implemented for: ~A" arg))))) (format nil "(qnew \"QVariant(~A)\" ~A)" type arg)))) (variant-type (prog1 (format nil "(qnew \"QVariant(~A)\" ~A)" variant-type (cond ((string= "QChar" variant-type) (prin1-to-string (code-char (parse-integer arg)))) ((string= "QCursor" variant-type) (format nil "(qnew \"QCursor(Qt::CursorShape)\" ~A)" arg)) (t (prin1-to-string arg)))) (setf variant-type nil))) (cursor (setf cursor nil) (format nil "(qnew \"QCursor(Qt::CursorShape)\" ~A)" arg)) (url (setf url nil) (format nil "(qnew \"QUrl(QString)\" ~A)" arg)) ((string= "QVariant" arg) (setf variant t) "") ((string= "QCursor" arg) (setf cursor t) "") ((string= "QUrl" arg) (setf url t) "") ((> (count #\| arg) 2) (format nil "(logior |~A|)" (string-trim " |" (string-substitute "| |" "|" arg)))) (t (if (string= "QString" arg) "\"\"" (var-name arg))))) args*)))))) (defun split (line) (let ((ex-char #\?) list in-string arg) (flet ((add-arg () (when arg (push (coerce (nreverse arg) 'string) list) (setf arg nil)))) (x:do-string (char line) (if (and (not in-string) (char= #\Space char)) (add-arg) (progn (push char arg) (when (and (char= #\" char) (char/= #\\ ex-char)) (when in-string (add-arg)) (setf in-string (not in-string))))) (setf ex-char char)) (add-arg)) (nreverse list))) (defun string-substitute (new old str) (let ((l (length old))) (with-output-to-string (s) (do ((e (search old str) (search old str :start2 (+ e l))) (b 0 (+ e l))) ((not e) (write-string (subseq str b) s)) (write-string (subseq str b e) s) (write-string (if (evenp (count #\" (subseq str e))) ; exclude strings new old) s))))) (defun string-substitute* (new-old str) (let ((str* (copy-seq str))) (do ((n-o new-old (cddr n-o))) ((null n-o) str*) (setf str* (string-substitute (first n-o) (second n-o) str*))))) (defun no (no line) (if (stringp no) (string-substitute " " no line) (let (line* in-string) (x:do-string (ch line) (push (if (and (not in-string) (char= no ch)) #\Space ch) line*) (when (char= #\" ch) (setf in-string (not in-string)))) (coerce (nreverse line*) 'string)))) (defun insert-tr (line string-lines-pos) (let* ((pos (position-if (lambda (x) (search ".translate" x)) line)) (tr-string (if pos (nth (+ 2 pos) line) "\"\"")) (shortcut (if pos (string= "setShortcut" (nth (1- pos) line)))) line*) (if pos (let ((start (eql :start string-lines-pos))) (dotimes (n (length line)) (cond ((= n pos) (push (format nil "~A(tr ~A~A" (if shortcut "(qnew \"QKeySequence\" " "") (from-qt-utf8 (if start (string-right-trim "\"\\n" tr-string) tr-string)) (if start "" (if shortcut "))" ")"))) line*)) ((or (< n pos) (> n (+ 4 pos))) (push (nth n line) line*)))) (nreverse line*)) line))) (defun join-args (args) (with-output-to-string (s) (dolist (arg args) (unless (x:empty-string arg) (unless (string= ")" arg) (write-char #\Space s)) (write-string arg s))))) (let (string-lines-pos) (defun qt-to-eql (qt-line &optional tr) (flet ((to-list (line) (remove-if (lambda (x) (find x '("QSize" "QRect" "QString::fromUtf8") :test 'string=)) (split line))) (show-warning (line) (format *debug-io* "~%Not implemented, see line ~D of \"ui.h\":~%~%[Qt] ~A~%[??] ~S~%" *line-nr* qt-line line) (format nil "~%;;[?] ~A" qt-line))) ;; special cases first (cond ((string= ");" qt-line) ;; end string-list (format nil "~%~A ))" (if tr "" " "))) ((x:starts-with "QObject::connect" qt-line) ;; qconnect (let ((line (mapcar (lambda (x) (string-trim " " x)) (x:split qt-line #\,)))) ;; commenting out the connections because of possible custom functions (slots), not defined in Lisp (format nil "~%~A ;; (qconnect ~A \"~A\" ~A \"~A\")" (if tr "" " ") (var-name (subseq (first line) 17)) (subseq (second line) 7 (- (length (second line)) 1)) (var-name (third line)) (subseq (fourth line) 5 (- (length (fourth line)) 3))))) (t ;; multiple line strings (let ((string-start (and tr (x:starts-with "\"" qt-line))) (string-end (and tr (x:ends-with "\"" qt-line)))) (setf string-lines-pos (if tr (cond ((and string-start string-end) :mid) (string-start :end) (string-end :start)) nil))) (case string-lines-pos (:mid (format nil "~A~%" (from-qt-utf8 (string-trim "\"\\n" (subseq qt-line 0 (position #\" qt-line :from-end t)))))) (:end (format nil "~A))" (from-qt-utf8 (subseq qt-line 1 (1+ (position #\" qt-line :from-end t)))))) (t (let* ((line (trim (no #\, (string-substitute* '("()->" "()." "->set" ".set" " " "(" " ) " ")") qt-line)))) (type (cond ((qid (subseq line 0 (position #\Space line))) :new-local) ((search "= new" line) :new) ((x:starts-with "new " line) :new-item) ((search "->" line) :fun)))) (setf line (to-list (string-substitute* '(" (qfun " "->" " (setf " "=") (no "new " line)))) (when (and (string= "const" (first line)) (string= "bool" (second line))) (setf line (nthcdr 1 line) type :new-local)) (when (eql :new type) (let ((name (first line))) (setf (gethash name *defvars*) t (gethash name *classes*) (third line)))) (dotimes (n (length line)) (when (find* (nth n line) '("(qfun" "(setf")) (rotatef (nth (1- n) line) (nth n line)))) (when (eql :new-local type) (if (find* "(setf" line) (let ((name (trim (third line)))) (setf (gethash name *classes*) (first line)) (setf line (rest line)) (if tr (push name *lets-tr*) (push name *lets-ini*))) (let ((name (trim (second line)))) (push name *qlets*) (setf (gethash name *classes*) (first line)) (rotatef (nth 0 line) (nth 1 line)) (setf line (cons "(setf" line))))) (when (search "::" (first line)) (setf type :fun-static)) (dotimes (n (- (length line) 3)) (let ((arg (nth (+ 3 n) line))) (setf (nth (+ 3 n) line) (cond ((search "::" arg) (format nil "|~A|" (string-substitute "." "::" arg))) ((string= "true" arg) "t") ((string= "false" arg) "nil") (t arg))))) (unless (eql :fun type) (setf line (remove-parens line))) (cond ((x:starts-with "<< QApplication::translate" qt-line) ;; single string of string-list (format nil "~%~A (tr ~A)" (if tr "" " ") (fourth line))) ((find type '(:new :new-local)) ;; qnew (if (find* "(qfun" line) (format nil "~%~A ~A ~A~{ ~A~}))" (if tr "" " ") (first line) (var-name (second line)) (prepare-args (nthcdr 2 line))) (format nil "~%~A ~A ~A (qnew \"~A~A\"~{ ~A~}))" (if tr "" " ") (first line) (var-name (second line)) (third line) (if (fourth line) (constructor-arg (third line)) "") (prepare-args (nthcdr 3 line))))) ((eql :new-item type) ;; qnew item (format nil "~%~A (qnew \"~A~A\"~{ ~A~})" (if tr "" " ") (first line) (if (second line) (constructor-arg (first line)) "") (prepare-args (rest line)))) ((eql :fun-static type) ;; static function (let ((p (search "::" (first line)))) (format nil "~%~A (qfun \"~A\" \"~A\"~{ ~A~})" (if tr "" " ") (subseq (first line) 0 p) (subseq (first line) (+ 2 p)) (prepare-args (nthcdr (if (eql :fun type) 2 1) line))))) ((eql :fun type) (unless (find* "(qfun" (rest line)) (setf line (remove-parens line))) (when tr (setf line (insert-tr line string-lines-pos))) (x:when-it (position ")" line :test 'string=) (when (string= "(qfun" (nth (1- x:it) line)) ;; special case: adjust "qfun" position (rotatef (nth (- x:it 1) line) (nth (- x:it 2) line) (nth (- x:it 3) line)) ;; add quotes to method name (setf (nth (1+ x:it) line) (prin1-to-string (nth (1+ x:it) line))))) (let* ((string-list-p (x:ends-with "QStringList()" qt-line)) (fun (format nil "~%~A ~A~A" (if tr "" " ") (join-args (let ((args (prepare-args line))) (if string-list-p (append (nbutlast args 1) (list "(list")) ; start string-list args))) (make-string (if (find "(tr " line :test 'x:starts-with) (if (eql :start string-lines-pos) 0 1) (- (count "(qfun" line :test 'string=) (if string-list-p 1 0))) :initial-element #\) )))) (if (or string-list-p string-lines-pos) fun (multiple-value-bind (x end) ; ensure balanced closing parenthesis (multiple "qfun"s) (read-from-string fun) (subseq fun 0 end))))) (t (show-warning line))))))))))) (defun c-to-lisp (c-line) (let ((line (string-trim '(#\Space #\Tab) c-line))) (when (and (not (x:empty-string line)) (not (find (char line 0) "/*#{")) (not (cond ((x:starts-with "public:" line) (setf *section* :defvar)) ((x:starts-with "void setupUi" line) (let ((main (x:split (subseq line (1+ (position #\( line)) (position #\) line))))) (setf *main-var* (trim (second main)) *main-class* (first main))) (setf (gethash *main-var* *defvars*) t) (setf (gethash *main-var* *classes*) *main-class*) (setf *section* :ini)) ((x:starts-with "void retranslateUi" line) (setf *section* :tr)) ((x:starts-with "}" line) (setf *section* :end)) ((dolist (skip '("QMetaObject::" "retranslateUi")) (when (x:starts-with skip line) (return :skip))))))) (case *section* (:defvar (setf (gethash (trim (subseq line (position #\* line))) *classes*) (subseq line 0 (position #\Space line))) nil) (:ini (unless (x:starts-with "if" line) (qt-to-eql line))) (:tr (qt-to-eql line :tr))))))