;;; floating-toolbar.el -- popup toolbar support for XEmacs.
;; Copyright (C) 1997 Kyle E. Jones

;; Author: Kyle Jones <kyle_jones@wonderworks.com>
;; Keywords: lisp

;; This file is part of XEmacs.

;; XEmacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 1, or (at your option)
;; any later version.

;; XEmacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; A copy of the GNU General Public License can be obtained from this
;; program's author (send electronic mail to kyle@uunet.uu.net) or from
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; Popup toolbar for XEmacs (probably require XEmacs 19.14 or later)
;; Send bug reports to kyle_jones@wonderworks.com

;; The command `floating-toolbar' pops up a small frame
;; containing a toolbar.  The command should be bound to a
;; button-press event.  If the mouse press happens over an
;; extent that has a non-nil 'floating-toolbar property, the
;; value of that property is the toolbar instantiator that will
;; be displayed.  Otherwise the toolbar displayed is taken from
;; the variable `floating-toolbar'.  This variable can be made
;; buffer local to produce buffer local floating toolbars.
;;
;; `floating-toolbar-or-popup-mode-menu' works like `floating-toolbar'
;; except that if no toolbar is found, `popup-mode-menu' is called.
;;
;; `floating-toolbar-from-extent-or-popup-mode-menu' works like
;; `floating-toolbar-or-popup-mode-menu' except only extent local
;; toolbars are used; the value of floating-toolbar is not used.
;;
;; Installation:
;;
;; Byte-compile the file floating-toolbar.el (with M-x byte-compile-file)
;; and put the .elc file in a directory in your load-path.  Add the
;; following line to your .emacs:
;;
;; (require 'floating-toolbar)
;;
;; You will also need to bind a mouse click to `floating-toolbar' or to
;; `floating-toolbar-or-popup-mode-menu'.
;; 
;; For 19.12 users:
;;    If you are using fvwm, [tv]twm or ol[v]wm, you can also add
;;    the following lines to various configuration file to use
;;    minimal decorations on the toolbar frame.
;;
;;    In .emacs:
;;       (setq floating-toolbar-frame-name "floating-toolbar")
;;
;;    For ol[v]wm use this in .Xdefaults:
;;       olvwm.NoDecor: floating-toolbar
;;         or
;;       olwm.MinimalDecor: floating-toolbar
;;
;;    For fvvm use this in your .fvwmrc:
;;       NoTitle floating-toolbar
;;    or
;;       Style "floating-toolbar" NoTitle, NoHandles, BorderWidth 0
;;
;;    For twm use this in your .twmrc:
;;       NoTitle { "floating-toolbar" }
;; 
;; Under 19.13 and later versions the floating-toolbar frame uses a
;; transient window that is not normally decorated by window
;; managers.  So the window manager directives should not be
;; needed for XEmacs 19.13 and beyond.

;;; Code:

(provide 'floating-toolbar)

(require 'toolbar)
(require 'x)

(defvar floating-toolbar-version "1.02"
  "Version string for the floating-toolbar package.")

(defvar floating-toolbar-use-sound nil
  "*Non-nil value means play a sound to herald the appearance
and disappearance of the floating toolbar.

`floating-toolbar-appears' will be played when the toolbar appears.
`floating-toolbar-disappears' will be played when the toolbar disappears.

See the documentation for the function `load-sound-file' to see how
define sounds.")

(defvar floating-toolbar nil
  "*Toolbar instantiator used if mouse event is not over an extent
with a non-nil 'floating-toolbar property.  This variable can be
made local to a buffer to have buffer local floating toolbars.")

(defvar floating-toolbar-help-font nil
  "*Non-nil value should be a font to be used to display toolbar help
messages.  The floating toolbar frame will have a minibuffer window
so that it can display any help text that is attached to the toolbar
buttons.")

(defvar floating-toolbar-frame-name nil
  "*The frame name for the frame used to display the floating toolbar.")

;;;
;;; End of user variables.
;;;

(defvar floating-toolbar-frame nil
  "The floating toolbar is displayed in this frame.")

(defvar floating-toolbar-display-pending nil
  "Non-nil value means the toolbar frame will be visible as soon
as the X server gets around to displaying it.  Nil means it
will be invisible as soon as the X server decides to hide it.")

(defun floating-toolbar-displayed ()
  (and (frame-live-p floating-toolbar-frame)
       (frame-visible-p floating-toolbar-frame)))

;;;###autoload
(defun floating-toolbar (event &optional extent-local-only)
  "Popup a toolbar near the current mouse position.
The toolbar instantiator used is taken from the 'floating-toolbar
property of any extent under the mouse.  If no such non-nil
property exists for any extent under the mouse, then the value of the
variable `floating-toolbar' is checked.  If its value si nil, then
no toolbar will be displayed.

This command should be bound to a button press event.

When called from a program, first arg EVENT should be the button
press event.  Optional second arg EXTENT-LOCAL-ONLY specifies
that only extent local toolbars should be used; this means the
`floating-toolbar' variable will not be consulted."
  (interactive "_e")
  (if (not (mouse-event-p event))
      nil
    (let* ((buffer (event-buffer event))
	   (window (event-window event))
	   (frame (event-frame event))
	   (point (and buffer (event-point event)))
	   (glyph-extent (event-glyph-extent event))
	   (glyph-extent (if (and glyph-extent
				  (extent-property glyph-extent
						   'floating-toolbar))
			     glyph-extent))
	   (extent (or glyph-extent
		       (and point
			    (extent-at point buffer 'floating-toolbar))))
	   (toolbar (or (and extent (get extent 'floating-toolbar))
			(and (not extent-local-only)
			     (symbol-value-in-buffer 'floating-toolbar
						     buffer nil))))
	   (x nil)
	   (y nil)
	   (echo-keystrokes 0)
	   (awaiting-release t)
	   (done nil))
      (if (not (consp toolbar))
	  nil
	;; event-[xy]-pixel are relative to the top left corner
	;; of the frame.  The presence of top and left toolbar
	;; and the menubar can move this position down and
	;; leftward, but XEmacs doesn't compensate for this in
	;; the values returned.  So we do it here, as best we
	;; can.
	(let* ((params (frame-parameters frame))
	       (top (cdr (assq 'top params)))
	       (left (cdr (assq 'left params)))
	       (xtop-toolbar-height
		(if (specifier-instance top-toolbar)
		    (specifier-instance top-toolbar-height)
		  0))
	       (xleft-toolbar-width
		(if (specifier-instance left-toolbar)
		    (specifier-instance left-toolbar-width)
		  0))
	       ;; better than nothing
	       (menubar-height (if current-menubar 22 0)))
	  (setq x (+ left xleft-toolbar-width (event-x-pixel event))
		y (+ top xtop-toolbar-height menubar-height
		     (event-y-pixel event))))
	;; for toolbar spec buffer local variable values
	(and buffer (set-buffer buffer))
	(floating-toolbar-display-toolbar toolbar x y)
	(while (not done)
	  (setq event (next-command-event))
	  (cond ((and awaiting-release (button-release-event-p event))
		 (setq awaiting-release nil))
		((and (button-release-event-p event)
		      (event-over-toolbar-p event)
		      (eq floating-toolbar-frame (event-frame event)))
		 (floating-toolbar-undisplay-toolbar)
		 (and window (select-frame (window-frame window)))
		 (and window (select-window window))
		 (dispatch-event event)
		 (setq done t))
		((and (button-press-event-p event)
		      (event-over-toolbar-p event)
		      (eq floating-toolbar-frame (event-frame event)))
		 (setq awaiting-release nil)
		 (dispatch-event event))
		(t
		 ;; push back the event if it was in another frame.
		 ;; eat it if it was in the toolbar frame.
		 (if (and (event-frame event)
			  (not (eq floating-toolbar-frame
				   (event-frame event))))
		     (setq unread-command-events
			   (cons event unread-command-events)))
		 (floating-toolbar-undisplay-toolbar)
		 (setq done t))))
	t ))))

;;;###autoload
(defun floating-toolbar-or-popup-mode-menu (event)
  "Like floating-toolbar, but if no toolbar is displayed
run popup-mode-menu."
  (interactive "_e")
  (or (floating-toolbar event) (popup-mode-menu)))

;;;###autoload
(defun floating-toolbar-from-extent-or-popup-mode-menu (event)
  "Like floating-toolbar-or-popup-mode-menu, but search only for an
extent local toolbar."
  (interactive "_e")
  (or (floating-toolbar event t) (popup-mode-menu)))

(defun floating-toolbar-display-toolbar (toolbar x y)
  (if (not (frame-live-p floating-toolbar-frame))
      (setq floating-toolbar-frame (floating-toolbar-make-toolbar-frame x y)))
  (set-specifier top-toolbar
		 (cons (window-buffer
			(frame-selected-window floating-toolbar-frame))
			toolbar))
  (floating-toolbar-resize-toolbar-frame toolbar)
  ;; fiddle with the x value to try to center the toolbar relative to
  ;; the mouse position.
  (setq x (max 0 (- x (/ (frame-pixel-width floating-toolbar-frame) 2))))
  (floating-toolbar-set-toolbar-frame-position x y)
  (floating-toolbar-expose-toolbar-frame))

(defun floating-toolbar-undisplay-toolbar ()
  (floating-toolbar-hide-toolbar-frame))

(defun floating-toolbar-hide-toolbar-frame ()
  (if (floating-toolbar-displayed)
      (progn
	(make-frame-invisible floating-toolbar-frame)
	(if (and floating-toolbar-use-sound floating-toolbar-display-pending)
	    (play-sound 'floating-toolbar-disappears))
	(setq floating-toolbar-display-pending nil))))

(defun floating-toolbar-expose-toolbar-frame ()
  (if (not (floating-toolbar-displayed))
      (progn
	(make-frame-visible floating-toolbar-frame)
	(if (and floating-toolbar-use-sound
		 (null floating-toolbar-display-pending))
	    (play-sound 'floating-toolbar-appears))
	(setq floating-toolbar-display-pending t))))

(defun floating-toolbar-resize-toolbar-frame (toolbar)
  (let ((width 0)
	(height nil)
	(bevel (* 2 (or (cdr (assq 'toolbar-shadow-thickness (frame-parameters)))
			0)))
	(captioned (specifier-instance toolbar-buttons-captioned-p))
	button glyph glyph-list)
    (while toolbar
      (setq button (car toolbar))
      (cond ((null button)
	     (setq width (+ width 8)))
	    ((eq (elt button 0) ':size)
	     (setq width (+ width (elt button 1))))
	    ((and (eq (elt button 0) ':style)
		  (= (length button) 4)
		  (eq (elt button 2) ':size))
	     (setq width (+ width bevel (elt button 3))))
	    (t
	      (setq glyph-list (elt button 0))
	      (if (symbolp glyph-list)
		  (setq glyph-list (symbol-value glyph-list)))
	      (if (and captioned (> (length glyph-list) 3))
		  (setq glyph (or (nth 3 glyph-list)
				  (nth 4 glyph-list)
				  (nth 5 glyph-list)))
		(setq glyph (car glyph-list)))
	      (setq width (+ width bevel (glyph-width glyph)))
	      (or height (setq height (+ bevel (glyph-height glyph))))))
      (setq toolbar (cdr toolbar)))
    (set-specifier top-toolbar-height height floating-toolbar-frame)
    (set-frame-width floating-toolbar-frame
		     (1+ (/ width (font-width (face-font 'default)
					  floating-toolbar-frame))))))

(defun floating-toolbar-set-toolbar-frame-position (x y)
  (set-frame-position floating-toolbar-frame x y))

(defun floating-toolbar-make-junk-frame ()
  (let ((window-min-height 1)
	(window-min-width 1))
    (save-excursion
      (set-buffer (generate-new-buffer "*junk-frame-buffer*"))
      (prog1
	  (make-frame '(minibuffer t initially-unmapped t width 1 height 1))
	(rename-buffer " *junk-frame-buffer*" t)))))

(defun floating-toolbar-make-toolbar-frame (x y)
  (save-excursion
    (let ((window-min-height 1)
	  (window-min-width 1)
	  (bg-color (or (x-get-resource "backgroundToolBarColor"
					"BackgroundToolBarColor"
					'string
					'global
					(selected-device)
					t)
			"grey75"))
	  (buffer (get-buffer-create " *floating-toolbar-buffer*"))
	  (frame nil))
      (set-buffer buffer)
      (set-buffer-menubar nil)
      (if floating-toolbar-help-font
	  (progn (set-buffer (window-buffer (minibuffer-window)))
		 (set-buffer-menubar nil)))
      (setq frame (make-frame (list
			       '(initially-unmapped . t)
			       ;; try to evade frame decorations
			       (cons 'name (or floating-toolbar-frame-name
					       "xclock"))
			       '(border-width . 2)
			       (cons 'border-color bg-color)
			       (cons 'top y)
			       (cons 'left x)
			       (cons 'popup
				     (floating-toolbar-make-junk-frame))
			       (if floating-toolbar-help-font
				   '(minibuffer . only)
				 '(minibuffer . nil))
			       '(width . 3)
			       '(height . 1))))
      (set-specifier text-cursor-visible-p (cons frame nil))
      (if floating-toolbar-help-font
	  (set-face-font 'default floating-toolbar-help-font frame)
	(set-face-font 'default "nil2" frame))
      (set-face-background 'default bg-color frame)
      (set-face-background 'modeline bg-color frame)
      (set-specifier modeline-shadow-thickness (cons frame 1))
      (set-specifier has-modeline-p (cons frame nil))
      (set-face-background-pixmap 'default "" frame)
      (set-window-buffer (frame-selected-window frame) buffer)
      (set-specifier top-toolbar-height (cons frame 0))
      (set-specifier left-toolbar-width (cons frame 0))
      (set-specifier right-toolbar-width (cons frame 0))
      (set-specifier bottom-toolbar-height (cons frame 0))
      (set-specifier top-toolbar (cons frame nil))
      (set-specifier left-toolbar (cons frame nil))
      (set-specifier right-toolbar (cons frame nil))
      (set-specifier bottom-toolbar (cons frame nil))
      (set-specifier scrollbar-width (cons frame 0))
      (set-specifier scrollbar-height (cons frame 0))
      frame )))

;; first popup should be faster if we go ahead and make the frame now.
(or floating-toolbar-frame
    (not (eq (device-type) 'x))
    (setq floating-toolbar-frame (floating-toolbar-make-toolbar-frame 0 0)))

;;; floating-toolbar.el ends here
