;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; CLOG - The Common Lisp Omnificent GUI ;;;;
;;;; (c) David Botton ;;;;
;;;; ;;;;
;;;; clog-gui.lisp ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Like clog-web, clog-gui uses w3.css as the underlying framework. w3.css is
;;; a public domain css only framework for layouts, is fast and efficient and
;;; does not require additional components outside of the css file. In addition
;;; clog-gui uses jQueryUI and its default css file to provide client side
;;; movement when needed, if client side movement is not used it is possible
;;; to pass nil to the initilization function for both the jquery-ui-js and
;;; jquery-ui-css options.
(mgl-pax:define-package :clog-gui
(:documentation "CLOG-GUI a desktop GUI abstraction for CLOG")
(:use #:cl #:clog #:mgl-pax))
(cl:in-package :clog-gui)
(defsection @clog-gui (:title "CLOG GUI Objects")
"CLOG-GUI - Desktop GUI abstraction for CLOG"
(clog-gui-initialize function)
"CLOG-GUI - Menus"
(clog-gui-menu-bar class)
(create-gui-menu-bar generic-function)
(clog-gui-menu-drop-down class)
(create-gui-menu-drop-down generic-function)
(clog-gui-menu-item class)
(create-gui-menu-item generic-function)
(create-gui-menu-window-select generic-function)
(create-gui-menu-full-screen generic-function)
(create-gui-menu-icon generic-function)
"CLOG-GUI - Window System"
(current-window generic-function)
(menu-bar generic-function)
(menu-bar-height generic-function)
(window-collection generic-function)
(window-to-top-by-title generic-function)
(window-to-top-by-param generic-function)
(window-by-title generic-function)
(window-by-param generic-function)
(reorient-all-windows generic-function)
(maximize-all-windows generic-function)
(normalize-all-windows generic-function)
(set-on-window-change generic-function)
"CLOG-GUI - Individual Windows"
(clog-gui-window class)
(create-gui-window generic-function)
(window-title generic-function)
(window-icon-area generic-function)
(window-param generic-function)
(window-content generic-function)
(window-focus generic-function)
(window-close generic-function)
(window-valid-p function)
(window-maximized-p generic-function)
(window-maximize generic-function)
(window-normalize generic-function)
(window-toggle-maximize generic-function)
(window-toggle-title-bar generic-function)
(window-toggle-pinned generic-function)
(window-keep-on-top generic-function)
(window-make-modal generic-function)
(window-end-modal generic-function)
(window-center generic-function)
"CLOG-GUI - Individual Window Events"
(set-on-window-focus generic-function)
(set-on-window-blur generic-function)
(set-on-window-can-close generic-function)
(set-on-window-close generic-function)
(set-on-window-can-maximize generic-function)
(set-on-window-can-normalize generic-function)
(set-on-window-can-move generic-function)
(set-on-window-can-size generic-function)
(set-on-window-move generic-function)
(set-on-window-size generic-function)
(set-on-window-move-done generic-function)
(set-on-window-size-done generic-function)
"CLOG-GUI - Dialog Boxes"
(alert-toast function)
(alert-dialog function)
(input-dialog function)
(confirm-dialog function)
(form-dialog function)
(server-file-dialog function)
"CLOG-GUI - Debugger"
(with-clog-debugger macro)
(one-of-dialog function)
(dialog-in-stream class)
(dialog-out-stream class)
(clog-break function)
(clog-probe macro)
(*clog-debug-instance* variable)
"CLOG-GUI - Look and Feel"
(*menu-bar-class* variable)
(*menu-bar-drop-down-class* variable)
(*menu-item-class* variable)
(*menu-window-select-class* variable)
(*menu-full-screen-item* variable)
(*menu-icon-image-class* variable)
(*top-bar-height* variable)
(*default-title-class* variable)
(*default-border-class* variable))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Default Settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLOG GUI based ebugger settings
(defparameter *clog-debug-instance* nil
"Default location to open debugger windows")
;; Menus
(defparameter *menu-bar-class* "w3-bar w3-black w3-card-4")
(defparameter *menu-bar-drop-down-class* "w3-dropdown-content w3-bar-block w3-card-4")
(defparameter *menu-item-class* "w3-bar-item w3-button")
(defparameter *menu-window-select-class* "w3-bar-item w3-button")
(defparameter *menu-full-screen-item* "⤢")
(defparameter *menu-icon-image-class* "w3-button w3-bar-item")
;; New Window placement
(defparameter *top-bar-height* 20
"Overlap on new windows created with top set as nil")
;; Window treatements
(defparameter *default-title-class* "w3-black"
"Window title bar class")
(defparameter *default-border-class* "w3-card-4 w3-white w3-border"
"Window frame border")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - clog-gui - Desktop GUI abstraction
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui ()
((body
:accessor body
:documentation "The body of the main window")
(current-win
:accessor current-win
:initform nil
:documentation "The current window at front")
(windows
:accessor windows
:initform (make-hash-table* :test 'equalp)
:documentation "Window collection indexed by html-id")
(last-z
:accessor last-z
:initform -9999
:documentation "Top z-order for windows")
(last-x
:accessor last-x
:initform 0
:documentation "Last default open x point")
(body-left-offset
:accessor body-left-offset
:initform 0
:documentation "Offset for maximize on left side")
(body-right-offset
:accessor body-right-offset
:initform 0
:documentation "Offset for maximize on right side")
(last-y
:accessor last-y
:initform 0
:documentation "Last default open y point")
(modal-background
:accessor modal-background
:initform nil
:documentation "Modal Background")
(modal-count
:accessor modal-count
:initform 0
:documentation "Count of nested modal windows")
(in-drag
:accessor in-drag
:initform nil
:documentation "Drag window or Size window")
(drag-obj
:accessor drag-obj
:initform nil
:documentation "Drag target object")
(drag-x
:accessor drag-x
:documentation "Location of the left side or width relative to pointer during drag")
(drag-y
:accessor drag-y
:documentation "Location of the top or height relative to pointer during drag")
(menu
:accessor menu
:initform nil
:documentation "Installed menu bar if installed")
(window-select
:accessor window-select
:initform nil
:documentation "If installed a drop down that selects window to maximize")
(on-window-change
:accessor on-window-change
:initform nil
:documentation "Fired when foreground window changed.")))
;;;;;;;;;;;;;;;;;;;;;
;; create-clog-gui ;;
;;;;;;;;;;;;;;;;;;;;;
(defun create-clog-gui (clog-body)
"Create a clog-gui object and places it in CLOG-BODY's connection-data as
\"clog-gui\". (Private)"
(let ((clog-gui (make-instance 'clog-gui)))
(setf (connection-data-item clog-body "clog-gui") clog-gui)
(setf (body clog-gui) (connection-body clog-body))
clog-gui))
;;;;;;;;;;;;;;;;;;;;;;;;
;; with-clog-debugger ;;
;;;;;;;;;;;;;;;;;;;;;;;;
(defmacro with-clog-debugger ((clog-obj &key title
standard-output
standard-input)
&body body)
"body uses a clog-gui based debugger instead of the console"
`(with-open-stream (out-stream (make-instance 'dialog-out-stream))
(with-open-stream (in-stream (make-instance 'dialog-in-stream :clog-obj ,clog-obj :source out-stream))
(labels ((my-debugger (condition encapsulation)
(handler-case
(let ((restart (one-of-dialog ,clog-obj condition (compute-restarts)
:title (format nil "Available Restarts~A"
(if ,title
(format nil " for ~A" ,title)
"")))))
(when restart
(let ((*debugger-hook* encapsulation))
(invoke-restart-interactively restart))))
(end-of-file () ; no reset chosen
nil))))
(let* ((*standard-output* (or ,standard-output
*standard-output*))
(*standard-input* (or ,standard-input
*standard-input*))
(*query-io* (make-two-way-stream in-stream out-stream))
(*debugger-hook* (if clog-connection:*disable-clog-debugging*
*debugger-hook*
#'my-debugger)))
,@body)))))
;;;;;;;;;;;;;;;;
;; clog-break ;;
;;;;;;;;;;;;;;;;
(defun clog-break (&key clog-body run (modal t))
"Stop execution, funcall RUN with CLOG-BODY if set, if run returns :continue,
the execution continues. If CLOG-BODY not set use *clog-debug-instance*. Then
confirm continue execution on current thread or (break)."
(unless clog-body
(setf clog-body *clog-debug-instance*))
(let ((continue (when run
(funcall run clog-body))))
(when (and (validp clog-body)
(not (eq continue :continue)))
(confirm-dialog clog-body
(format nil "Continue thread ~A ?"
(bordeaux-threads:thread-name
(bordeaux-threads:current-thread)))
(lambda (result)
(unless result
(break)))
:width 400
:time-out 600
:modal modal
:title "clog-break in execution"))))
;;;;;;;;;;;;;;;;
;; clog-probe ;;
;;;;;;;;;;;;;;;;
(defmacro clog-probe (symbol &key clog-body
(title "")
(time-out 600)
top left
(width 400) (height 300)
auto-probe
(modal t))
"Pause thread of execution for time-out numnber of seconds or nil to not
block execution, display symbol's value, value is changed if OK pressed at
the moment pressed. When time-out is nil, :q quits the probe and cancel
repeats the probe with out changing value. When time-out is nil modal is
always nil. If auto-probe is set, modal and time-out is set to nil and the
probe is run again in auto-probe seconds. If not tile is set, the symbol is
used for title."
`(let ((body (or ,clog-body
*clog-debug-instance*))
(title (if (equal ,title "")
(format nil "~s" ',symbol)
,title)))
(when (validp body)
(if (and ,time-out (not ,auto-probe))
(let ((value (format nil "~A" ,symbol)))
(setf value (ppcre:regex-replace-all "<" value "<"))
(setf value (ppcre:regex-replace-all ">" value ">"))
(input-dialog body
(format nil "Probe in thread ~A :~A New Value?"
(bordeaux-threads:thread-name
(bordeaux-threads:current-thread))
value)
(lambda (result)
(when (and result
(not (equal result "")))
(setf ,symbol (eval (read-from-string result)))))
:time-out ,time-out
:top ,top :left ,left
:width ,width
:height ,height
:modal ,modal
:title (format nil "clog-probe ~A" title)))
(bordeaux-threads:make-thread
(lambda ()
(loop
(let ((value (format nil "~A" ,symbol)))
(setf value (ppcre:regex-replace-all "<" value "<"))
(setf value (ppcre:regex-replace-all ">" value ">"))
(when (eq (input-dialog body
(format nil "Probe result ~A - New Value or :q to quit?"
value)
(lambda (result)
(when (and result
(not (equalp result "")))
(if (equalp result ":q")
:q
(setf ,symbol (eval (read-from-string result))))))
:time-out (or ,auto-probe 999)
:top ,top :left ,left
:width ,width
:height ,height
:modal nil
:title (format nil "clog-probe ~A" title))
:q)
(return)))))
:name (format nil "clog-probe ~A" title))))))
;;;;;;;;;;;;;;;;;;;;;;;;;
;; clog-gui-initialize ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
(defun clog-gui-initialize (clog-body &key
(body-left-offset 0)
(body-right-offset 0)
(use-clog-debugger nil)
(standard-output nil)
(parent-desktop-obj nil)
(w3-css-url "/css/w3.css")
(jquery-ui-css "/css/jquery-ui.css")
(jquery-ui "/js/jquery-ui.js"))
"Initializes clog-gui and installs a clog-gui object on the connection body.
If W3-CSS-URL has not been loaded before it is installed unless set to nil.
clog-gui uses jQueryUI and its default css file to provide client side
movement when needed, if client side movement is not used it is possible
to pass nil to the initilization function for both the jquery-ui-js and
jquery-ui-css options and there is no need to deliver the jQueryUI it with your
application. BODY-LEFT-OFFSET and BODY-RIGHT-OFFSET limit width on maximize.
parent-desktop-obj is used if this window is a popup or otherwise a
slave of another clog-gui page.
If use-clog-debugger then a graphical debugger is set for all events. If
standard-output is set *standard-output* for every event is redirected
to it.
NOTE: use-clog-debugger should not be set for security issues
on non-secure environments."
(if parent-desktop-obj
(let ((app (connection-data-item parent-desktop-obj "clog-gui")))
(setf (connection-data-item clog-body "clog-gui") app))
(let ((app (create-clog-gui clog-body)))
(setf (body-left-offset app) body-left-offset)
(setf (body-right-offset app) body-right-offset)))
(set-on-full-screen-change (html-document clog-body) 'reorient-all-windows)
(set-on-orientation-change (window clog-body) 'reorient-all-windows)
(set-on-resize (window clog-body) 'reorient-all-windows)
(unless (connection-data-item clog-body "w3-css")
(when w3-css-url
(setf (connection-data-item clog-body "w3-css") t)
(load-css (html-document clog-body) w3-css-url)))
(when jquery-ui-css
(load-css (html-document clog-body) jquery-ui-css))
(when jquery-ui
(load-script (html-document clog-body) jquery-ui))
(when (and use-clog-debugger (not clog-connection:*disable-clog-debugging*))
(unless (or *clog-debug-instance*
(when (and (typep *clog-debug-instance* 'clog-obj)
(validp *clog-debug-instance*))))
(setf *clog-debug-instance* clog-body))
(setf (connection-data-item clog-body "clog-debug")
(lambda (event data)
(with-clog-debugger (clog-body :standard-output standard-output)
(funcall event data))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - Menus
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;
;; menu-bar ;;
;;;;;;;;;;;;;;
(defgeneric menu-bar (clog-obj)
(:documentation "Get/setf window menu-bar. This is set buy
create-gui-menu-bar."))
(defmethod menu-bar ((obj clog-obj))
(let ((app (connection-data-item obj "clog-gui")))
(menu app)))
(defgeneric (setf menu-bar) (value clog-obj)
(:documentation "Set window menu-bar"))
(defmethod (setf menu-bar) (value (obj clog-obj))
(let ((app (connection-data-item obj "clog-gui")))
(setf (menu app) value)))
;;;;;;;;;;;;;;;;;;;;;
;; menu-bar-height ;;
;;;;;;;;;;;;;;;;;;;;;
(defgeneric menu-bar-height (clog-obj)
(:documentation "Get menu-bar height"))
(defmethod menu-bar-height ((obj clog-obj))
(let ((app (connection-data-item obj "clog-gui")))
(if (and app (menu app))
(if (in-clog-popup-p obj)
0
(height (menu app)))
0)))
;;;;;;;;;;;;;;;;;;;;;;;
;; window-collection ;;
;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-collection (clog-obj)
(:documentation "Get hash table of open windows"))
(defmethod window-collection ((obj clog-obj))
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui")))
(windows app)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; window-to-top-by-title ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-to-top-by-title (clog-obj title)
(:documentation "Bring window with TITLE to top and return
window or nil if not found"))
(defmethod window-to-top-by-title ((obj clog-obj) title)
(window-clean-zombies obj)
(when title
(let ((app (connection-data-item obj "clog-gui"))
(r nil))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(when (and (equalp (window-title value) title)
(window-focus value)
(setf r value)))))
(windows app))
r)))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; window-clean-zombies ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-clean-zombies (clog-obj &key use-select)
(:documentation "Clean zombie references to windows that can
occur from browsers being closed or crashing. (private)"))
(defmethod window-clean-zombies ((obj clog-obj) &key use-select)
(let ((app (connection-data-item obj "clog-gui")))
(when use-select
(setf (inner-html use-select) ""))
(maphash (lambda (key value)
(if (window-valid-p value)
(when use-select
(setf (window-select-item value)
(create-option use-select
:content (window-title value)
:selected t
:value key)))
(remhash key (windows app))))
(windows app))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; window-to-top-by-param ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-to-top-by-param (clog-obj param)
(:documentation "Bring window with PARAM to top and return
window or nil if not found"))
(defmethod window-to-top-by-param ((obj clog-obj) param)
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui"))
(r nil))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(when (equalp (win-param value) param)
(window-focus value)
(setf r value))))
(windows app))
r))
;;;;;;;;;;;;;;;;;;;;;
;; window-by-title ;;
;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-by-title (clog-obj title)
(:documentation "Bring window with TITLE to top and return
window or nil if not found"))
(defmethod window-by-title ((obj clog-obj) title)
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui"))
(r nil))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(when (equalp (window-title value) title)
(setf r value))))
(windows app))
r))
;;;;;;;;;;;;;;;;;;;;;;
;; window-icon-area ;;
;;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-icon-area (clog-obj)
(:documentation "Return the clog-obj for the icon-area to allow adding
custom icons on the title bar to the right of the close icon"))
(defmethod window-icon-area ((obj clog-obj))
(icon-area obj))
;;;;;;;;;;;;;;;;;;;;;
;; window-by-param ;;
;;;;;;;;;;;;;;;;;;;;;
(defgeneric window-by-param (clog-obj param)
(:documentation "Bring window with PARAM to top and return
window or nil if not found"))
(defmethod window-by-param ((obj clog-obj) param)
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui"))
(r nil))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(when (equalp (win-param value) param)
(setf r value))))
(windows app))
r))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; maximize-all-windows ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric maximize-all-windows (clog-obj)
(:documentation "Maximize all windows"))
(defmethod maximize-all-windows ((obj clog-obj))
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui")))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(window-maximize value)))
(windows app))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; normalize-all-windows ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric normalize-all-windows (clog-obj)
(:documentation "Normalize all windows"))
(defmethod normalize-all-windows ((obj clog-obj))
(window-clean-zombies obj)
(let ((app (connection-data-item obj "clog-gui")))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(window-normalize value)))
(windows app))))
;;;;;;;;;;;;;;;;;;;;
;; make-in-bounds ;;
;;;;;;;;;;;;;;;;;;;;
(defun make-in-bounds (obj mbh bh bw)
"Insure obj in bounds of gui (private)"
(let* ((top-loc (js-to-integer (top obj)))
(left-loc (js-to-integer (left obj)))
(width-loc (width obj)))
(if (< (+ left-loc width-loc) 25)
(setf (left obj) (unit :px (- 25 width-loc))))
(if (> left-loc bw)
(setf (left obj) (unit :px (- bw 15))))
(if (< top-loc mbh)
(setf (top obj) (unit :px mbh)))
(if (>= top-loc bh)
(setf (top obj) (unit :px (- bh 15))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; reorient-all-windows ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric reorient-all-windows (clog-obj)
(:documentation "Reorient all windows. Remaximized any maximize windows
and leave any normalized windows as normalized. This is called by default
in on-resize, on-full-screen-change and on-orientation-change events."))
(defmethod reorient-all-windows ((obj clog-obj))
(window-clean-zombies obj)
(let* ((app (connection-data-item obj "clog-gui"))
(body (connection-body obj))
(mbh (menu-bar-height obj))
(bh (height (html-document body)))
(bw (width (html-document body)))
(cur (current-window obj)))
(maphash (lambda (key value)
(declare (ignore key))
(when (window-valid-p value)
(cond ((window-maximized-p value)
(window-maximize value :focus nil))
(t
(make-in-bounds value mbh bh bw)))))
(windows app))
(when cur
(window-focus cur))))
;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-bar ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui-menu-bar (clog-div)()
(:documentation "Menu bar"))
(defgeneric create-gui-menu-bar (clog-obj &key class html-id main-menu)
(:documentation "Attached a menu bar to a CLOG-OBJ in general a
clog-body. If main-menu add as main menu bar."))
(defmethod create-gui-menu-bar ((obj clog-obj)
&key (class *menu-bar-class*)
(html-id nil)
(main-menu t))
(let* ((div (create-div obj :class class :html-id html-id))
(blank (create-a div))
(app (connection-data-item obj "clog-gui")))
(declare (ignore blank))
(change-class div 'clog-gui-menu-bar)
(when main-menu
(setf (menu app) div))
div))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-drop-down ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui-menu-drop-down (clog-div)()
(:documentation "Drop down menu"))
(defgeneric create-gui-menu-drop-down (clog-gui-menu-bar
&key content class html-id)
(:documentation "Attached a menu bar drop-down to a CLOG-GUI-MENU-BAR"))
(defmethod create-gui-menu-drop-down ((obj clog-gui-menu-bar)
&key (content "")
(class *menu-bar-drop-down-class*)
(html-id nil))
(let* ((hover (create-div obj :class "w3-dropdown-hover"))
(button (create-button hover :class "w3-button" :content content))
(div (create-div hover :class class :html-id html-id)))
(declare (ignore button))
(change-class div 'clog-gui-menu-drop-down)))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-item ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui-menu-item (clog-span)()
(:documentation "Menu item"))
(defgeneric create-gui-menu-item (clog-gui-menu-drop-down
&key content
on-click
class
html-id)
(:documentation "Attached a menu item to a CLOG-GUI-MENU-DROP-DOWN"))
(defmethod create-gui-menu-item ((obj clog-obj)
&key (content "")
(on-click nil)
(class *menu-item-class*)
(html-id nil))
(let ((span
(create-span obj :content content :class class :html-id html-id)))
(set-on-click span on-click)
(change-class span 'clog-gui-menu-item)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-window-select ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui-menu-window-select (clog-select)()
(:documentation "Drop down containing existing windows."))
(defgeneric create-gui-menu-window-select (clog-obj
&key class
content
html-id)
(:documentation "Attaches a clog-select as a menu item that auto updates
with open windows and focuses them unless is a keep-on-top window. The
first menu-window-select will receive change window notices only."))
(defmethod create-gui-menu-window-select ((obj clog-obj)
&key (class *menu-window-select-class*)
(content "Select Window")
(html-id nil))
(let ((window-select (create-select obj :html-id html-id :class class))
(app (connection-data-item obj "clog-gui")))
(change-class window-select 'clog-gui-menu-window-select)
(unless (window-select app)
(setf (window-select app) window-select))
; on mac on-click after a refill doesn't work
; on pc mouse-enter fires as long as in the control, so..
(flet ((refill (obj)
(set-on-mouse-enter obj nil)
(with-sync-event (obj)
(window-clean-zombies obj :use-select window-select))
(when content
(setf (selectedp (create-option window-select :content content)) t))))
(set-on-mouse-enter window-select (lambda (obj)
(refill obj)))
(set-on-mouse-leave window-select (lambda (obj)
(declare (ignore obj))
(sleep .5)
(set-on-mouse-enter window-select (lambda (obj)
(refill obj))))))
(set-on-change window-select (lambda (obj)
(let ((win (gethash (value obj) (windows app))))
(when win
(unless (keep-on-top win)
(setf (hiddenp win) nil)
(window-focus win))))))
(when content
(create-option window-select :content content))
window-select))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-full-screen ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric create-gui-menu-full-screen (clog-gui-menu-bar &key html-id)
(:documentation "Add as last item in menu bar to allow for a full screen
icon ⤢ (*menu-full-screen-item* default) and full screen mode."))
(defmethod create-gui-menu-full-screen ((obj clog-gui-menu-bar)
&key (html-id nil))
(create-child obj
(format nil
" "
*menu-full-screen-item*)
:html-id html-id
:clog-type 'clog-gui-menu-item))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-menu-icon ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric create-gui-menu-icon (clog-gui-menu-bar &key image-url
on-click
class
html-id)
(:documentation "Add icon as menu bar item."))
(defmethod create-gui-menu-icon ((obj clog-gui-menu-bar)
&key (image-url *default-icon*)
(on-click nil)
(class *menu-icon-image-class*)
(html-id nil))
(set-on-click
(create-child obj
(format nil ""
class
image-url)
:html-id html-id
:clog-type 'clog-gui-menu-item)
on-click))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - Window System
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;
;; current-window ;;
;;;;;;;;;;;;;;;;;;;;
(defgeneric current-window (clog-obj)
(:documentation "Get the current selected clog-gui-window"))
(defmethod current-window ((obj clog-obj))
(let ((app (connection-data-item obj "clog-gui")))
(current-win app)))
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set-on-window-change ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric set-on-window-change (clog-obj handler)
(:documentation "Set the on-window-change HANDLER.
The on-window-change clog-obj received is the new window"))
(defmethod set-on-window-change ((obj clog-obj) handler)
(let ((app (connection-data-item obj "clog-gui")))
(setf (on-window-change app) handler)))
(defmethod fire-on-window-change (obj app)
"Fire handler if set. Change the value of current-win to obj (Private)"
(unless (eq obj (current-win app))
(when (current-win app)
(fire-on-window-blur (current-win app)))
(unless obj
(let (new-order
(order -9999))
(maphash (lambda (key value)
(declare (ignore key))
(setf new-order (z-index value))
(when (window-valid-p value)
(when (and new-order
(>= new-order order))
(setf order new-order)
(setf obj value))))
(windows app))))
(setf (current-win app) obj)
(when (on-window-change app)
(funcall (on-window-change app) obj))
(when obj
(fire-on-window-focus obj))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - Individual Windows
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-gui-window (clog-element)
((win-title
:accessor win-title
:documentation "Window title clog-element")
(win-param
:accessor win-param
:initform nil
:documentation "Window specific parameter")
(title-bar
:accessor title-bar
:documentation "Window title-bar clog-element")
(content
:accessor content
:documentation "Window body clog-element")
(pinner
:accessor pinner
:initform nil
:documentation "Window pinner clog-element if created with has-pinner")
(icon-area
:accessor icon-area
:documentation "Window icon area for adding icons to menu bar")
(closer
:accessor closer
:documentation "Window closer clog-element")
(sizer
:accessor sizer
:initform nil
:documentation "Window sizer clog-element")
(last-width
:accessor last-width
:initform nil
:documentation "Last width before maximize")
(last-height
:accessor last-height
:initform nil
:documentation "Last heigth before maximize")
(last-x
:accessor last-x
:initform nil
:documentation "Last x before maximize")
(last-y
:accessor last-y
:initform nil
:documentation "Last y before maximize")
(pinnedp
:accessor pinnedp
:initform nil
:documentation "True if this window is pinned and nil otherwise")
(keep-on-top
:accessor keep-on-top
:initform nil
:documentation "If t don't change z-order")
(window-size-mutex
:reader window-size-mutex
:initform (bordeaux-threads:make-lock)
:documentation "Sync maximize / normalize events")
(window-select-item
:accessor window-select-item
:initform nil
:documentation "Item in window select")
(on-window-can-close
:accessor on-window-can-close
:initform nil
:documentation "Return t to allow closing of window")
(on-window-can-move
:accessor on-window-can-move
:initform nil
:documentation "Return t to allow moving of window")
(on-window-can-size
:accessor on-window-can-size
:initform nil
:documentation "Return t to allow sizing of window")
(on-window-can-maximize
:accessor on-window-can-maximize
:initform nil
:documentation "Return t to allow maximizing of window")
(on-window-can-normalize
:accessor on-window-can-normalize
:initform nil
:documentation "Return t to allow normalizing of window")
(on-window-focus
:accessor on-window-focus
:initform nil
:documentation "Fired on window focused")
(on-window-blur
:accessor on-window-blur
:initform nil
:documentation "Fired on window blurred")
(on-window-close
:accessor on-window-close
:initform nil
:documentation "Fired on window closed")
(on-window-move
:accessor on-window-move
:initform nil
:documentation "Fired during move of window")
(on-window-size
:accessor on-window-size
:initform nil
:documentation "Fired during size change of window")
(on-window-move-done
:accessor on-window-move-done
:initform nil
:documentation "Fired after move of window")
(on-window-size-done
:accessor on-window-size-done
:initform nil
:documentation "Fired after size change of window")))
;;;;;;;;;;;;;;;;;;;;;;
;; on-gui-drag-down ;;
;;;;;;;;;;;;;;;;;;;;;;
(defun on-gui-drag-down (obj data)
"Handle mouse down on drag items"
(let ((app (connection-data-item obj "clog-gui")))
(unless (in-drag app)
(setf (in-drag app) (attribute obj "data-drag-type"))
(let* ((target (gethash (attribute obj "data-drag-obj") (windows app)))
(pointer-x (getf data ':screen-x))
(pointer-y (getf data ':screen-y))
(obj-top)
(obj-left)
(perform-drag nil))
(when target
(setf (drag-obj app) target)
(cond ((equalp (in-drag app) "m")
(setf obj-top
(js-to-integer (top (drag-obj app))))
(setf obj-left
(js-to-integer (left (drag-obj app))))
(setf perform-drag (fire-on-window-can-move (drag-obj app))))
((equalp (in-drag app) "s")
(setf obj-top (height (drag-obj app)))
(setf obj-left (width (drag-obj app)))
(setf perform-drag (fire-on-window-can-size (drag-obj app))))
(t
(format t "Warning - invalid data-drag-type attribute")))
(unless (keep-on-top (drag-obj app))
(setf (z-index (drag-obj app)) (incf (last-z app))))
(fire-on-window-change (drag-obj app) app)
(setf (drag-y app) (- pointer-y obj-top))
(setf (drag-x app) (- pointer-x obj-left)))
(cond (perform-drag
(set-on-pointer-move obj 'on-gui-drag-move)
(set-on-pointer-cancel obj 'on-gui-drag-stop)
(set-on-pointer-up obj 'on-gui-drag-stop))
(t
(setf (in-drag app) nil)))))))
;;;;;;;;;;;;;;;;;;;;;;
;; on-gui-drag-move ;;
;;;;;;;;;;;;;;;;;;;;;;
(defun on-gui-drag-move (obj data)
"Handle mouse tracking on drag object"
(let* ((app (connection-data-item obj "clog-gui"))
(x (getf data ':screen-x))
(y (getf data ':screen-y))
(adj-y (- y (drag-y app)))
(adj-x (- x (drag-x app))))
(cond ((equalp (in-drag app) "m")
(fire-on-window-move (drag-obj app))
(setf (top (drag-obj app)) (unit :px adj-y))
(setf (left (drag-obj app)) (unit :px adj-x)))
((equalp (in-drag app) "s")
(fire-on-window-size (drag-obj app))
(setf (height (drag-obj app)) (unit :px adj-y))
(setf (width (drag-obj app)) (unit :px adj-x))))))
;;;;;;;;;;;;;;;;;;;;;;
;; on-gui-drag-stop ;;
;;;;;;;;;;;;;;;;;;;;;;
(defun on-gui-drag-stop (obj data)
"Handle end of drag object"
(let ((app (connection-data-item obj "clog-gui")))
(on-gui-drag-move obj data)
(set-on-pointer-move obj nil)
(set-on-pointer-cancel obj nil)
(set-on-pointer-up obj nil)
(cond ((window-maximized-p (drag-obj app))
(window-maximize (drag-obj app) :focus nil))
(t
(let* ((body (connection-body (drag-obj app)))
(mbh (menu-bar-height (drag-obj app)))
(bh (height (html-document body)))
(bw (width (html-document body))))
(make-in-bounds (drag-obj app) mbh bh bw))))
(cond ((equalp (in-drag app) "m")
(fire-on-window-move-done (drag-obj app)))
((equalp (in-drag app) "s")
(fire-on-window-size-done (drag-obj app))))
(setf (in-drag app) nil)
(setf (drag-obj app) nil)))
;;;;;;;;;;;;;;;;;;;;;;;
;; create-gui-window ;;
;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric create-gui-window (clog-obj &key title
content
left top width height
maximize
hide-title-bar
drag-client-area
has-pinner
closer-html
keep-on-top
window-param
hidden
client-movement
no-sizer
border-class
title-class
html-id)
(:documentation "Create a clog-gui-window. If client-movement is t then
use jquery-ui to move/resize and will not work on mobile and touch events
are limitted to clicks. When client-movement is t only on-window-move is fired
once at start of drag and on-window-move-done at end of drag and
on-window-resize at start of resize and on-window-resize-done at end of resize.
If has-pinner a toggle will appear on title bar to allow pinning the window in
place, if keep-on-top t then when pinned also will keep-on-top. If had-pinned
is nil and keep-on-top t then the window will be set to keep-on-top always.
window-param is a general parameter for identifiying the window to use with
window-to-top-by-param or window-by-param."))
(defmethod create-gui-window ((obj clog-obj) &key (title "New Window")
(content "")
(left nil)
(top nil)
(width 300)
(height 200)
(maximize nil)
(hide-title-bar nil)
(drag-client-area nil)
(has-pinner nil)
(closer-html "×")
(keep-on-top nil)
(window-param nil)
(hidden nil)
(client-movement nil)
(no-sizer nil)
(border-class *default-border-class*)
(title-class *default-title-class*)
(html-id nil))
(let ((app (connection-data-item obj "clog-gui"))
(body (connection-body obj)))
(if html-id
(setf html-id (format nil "~A" html-id))
(setf html-id (format nil "~A" (generate-id))))
(when (eql (hash-table-count (windows app)) 0)
;; If previously no open windows reset default position
(setf (last-x app) 0)
(setf (last-y app) 0))
(unless left
;; Generate sensible initial x location
(setf left (last-x app))
(when (> (last-x app) 600)
(setf (last-x app) 0))
(incf (last-x app) 10))
(unless top
;; Generate sensible initial y location
(when (> (last-y app) 400)
(setf (last-y app) 0))
(when (eql (last-y app) 0)
(setf (last-y app) (menu-bar-height obj)))
(setf top (last-y app))
(incf (last-y app) *top-bar-height*)
(when (> top (- (inner-height (window body)) (last-y app)))
(setf (last-y app) (menu-bar-height obj))))
(let ((win (create-child body
(format nil
"
~A
~~A
" (qb intro)))
(n (length choices)) (i))
(do ((c choices (cdr c)) (i 1 (+ i 1)))
((null c))
(setf q (format nil "~A~&[~D] ~A~%
" q i (qb (car c)))))
(do () ((typep i `(integer 1 ,n)))
(let ((trc (make-array '(0) :element-type 'base-char
:fill-pointer 0 :adjustable t)))
(with-output-to-string (s trc)
(uiop:print-condition-backtrace intro :stream s))
(when trc
(format t "~%~A~%" trc)))
(setf q (format nil "~A~&~A:" q prompt))
(setq i (read-from-string (input-dialog obj q (lambda (result)
(cond ((or (eq result nil)
(equal result ""))
(format nil "~A" n))
(t
result)))
:title title
:placeholder-value (format nil "~A" n)
:time-out 999
:modal nil
:width 640
:height 480))))
(nth (- i 1) choices))))
(defparameter *default-icon*
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAcCAYAAAAAwr0iAAAAAXNSR0IArs4c6QAAAKZlWElmTU0A
KgAAAAgABwEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEo
AAMAAAABAAIAAAExAAIAAAAVAAAAcodpAAQAAAABAAAAiAAAAAAAAABIAAAAAQAAAEgAAAABUGl4
ZWxtYXRvciBQcm8gMi4wLjUAAAACoAIABAAAAAEAAAAgoAMABAAAAAEAAAAcAAAAAMSXmL0AAAAJ
cEhZcwAACxMAAAsTAQCanBgAAAQRaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRh
IHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxy
ZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4
LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHht
bG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxu
czp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRp
ZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPGV4aWY6Q29sb3JT
cGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4z
MjwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4y
ODwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxt
YXRvciBQcm8gMi4wLjU8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpNZXRhZGF0YURh
dGU+MjAyMS0wMi0wNFQwMzo0MDoxOVo8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx0aWZm
OlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOlBo
b3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9u
PgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4xPC90aWZmOkNvbXByZXNzaW9uPgogICAgICAg
ICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpY
UmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZm
OllSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgPC9yZGY6
RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CjH2KYwAAAMuSURBVEgN7Za7
a1RBFId3N3GjUQMJNuIDGwVFRW0sFBTRNpWVhYKFWtko2PgH2NgpgtgERK1EUQSxsBIMYpAIgsRX
o4WFYNS8k/X7zc7vOrmbZO+uYOWBb8+ZM3PmzOvObKn0X/7hCtRqtXI+XYMj3+BvyySt0kcFpqEG
y2J5slwuz2G3L3RegW7ozPeCr2shv9tRV4ZK2ytAsGYywyw0qxLlbtQkyD+LXzOWfx1qF2yGLbAc
blP/BB2WRrolodOqEig5sg0q2GOgxBMwjW8TnKTjfuiADzAIK+AudWfQrQuBXY7Cvg6/4BFslx+9
D87CeTgCvW5vje8ODINWq7gQEJKjte+PIZVvFC7DQTgEG9OeKetMhIToAfgEPWmbJW0a6zRrhr3w
HCQzMA5acstFd4SjB1ZHrTOieA1O8gJ0HpoLDZ18DfYrkEyBBiCZg4lg1X9uovryPeM7ACMguQfN
t8CN0H2gfZNMwmyw6smjmQ1I5ddwAnZDP1yDH2C5mh9gQ5mW3vOV2EMxUjN18uiap9LtUMUoaIUs
jj3VkNAOWnbCqqT8NEYredpZdDcoJfH2pJVegS84t7r/TOOsQjgscmLr5D4Aifbcow+OAj8ahFZE
KFYHVnIlS2oDp+7rTCgfgzcgUXCryUNg8vM92h/RG5QIXb9TMMKVjD4KN+AlWLTk7Sb3rL30urT2
x+R/vgCcYfZofRqpqIMie57G2M4fxhEq9sTkHdhh0vlXbFwNkAnQt69G7TxYUzF+Fn0JhmCYN+Id
ifUuzGGHRyw/gPdUSpR03pkI3mI/Tq4Ex0l0y2Fx1lly+3UYvAWHsSW+1Vpd/nTZLygBfek/g57N
QJY0NVTpMrYP4E/sVkSD9cDvJ/2Fa9zlRTXBXoWd2J65Tq1m5TLmgqI2Y7FG+7xWidDFHhuPigAP
Qvf3W7BoALr/jS4lobJnjVkbhPAMo7P/De6/kCbQg9Cnchr0bDaTrzQ4B47NH+5Fc2d7n7ZQR5ze
7B8rZd3be2EHrAe9E/rP9xmewUPaj6LDoUtj5WtLSKpTG2ZUpAO1VUyRtmmbQgGxYw8mXCCxE8c3
fttpliXs3y+7fSKpo8d7AAAAAElFTkSuQmCC")