;;; dired-sort-menu.el --- provide menu/dialogue for dired sort options

;; Copyright (C) 2000, 2001 Francis J. Wright

;; Author: Francis J. Wright <F.J.Wright@maths.qmw.ac.uk>
;; Maintainer: Francis J. Wright <F.J.Wright@maths.qmw.ac.uk>
;; Time-stamp: <26 July 2001>
;; URL: http://centaur.maths.qmw.ac.uk/Emacs/
;; Keywords: dired, sort, menu, dialogue

;; $Id: dired-sort-menu.el,v 1.26 2001-07-26 21:22:48+01 fjw Exp $

;; This file is not part of GNU Emacs.

;; This package 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 2, or (at your option)
;; any later version.

;; This package 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.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; This package, which is intended for use with GNU Emacs 20/21,
;; implements a menu and a dialogue that consist of a set of radio
;; buttons and toggles to control sort order in dired mode.  The menu
;; is added as a submenu to the Immediate menu, and bound to S-mouse-2
;; as a popup menu.  The dialogue can be accessed via the menu or the
;; dired key binding `C-d' or the command `dired-sort-dialogue', which
;; *must* be run in a dired buffer!  The dialogue uses the Emacs
;; widget library, which does not require GUI support but uses it if
;; available.  The dialogue can be used with a character-based
;; display; the main keys to use are <TAB> and <RET> if a mouse is not
;; available or does not work.  With a GUI the dialogue pops up its
;; own frame by default; otherwise it uses part of the dired window.
;; This is controlled by the customizable user option
;; `dired-sort-dialogue-own-frame'.

;; The menu and dialogue support the `ls' sort switches [tSXUucrR].
;; These are all implemented by GNU `ls'; the subset [tSucr] are
;; implemented by the standard Emacs 20 `ls-lisp' emulation and all
;; are implemented by the Emacs 21 `ls-lisp' (see above URL).
;; Changing a main sort option turns off reverse sorting; toggling
;; reverse sorting does not change the main sort option.  Setting any
;; prefix argument before selecting a sort order reverses it
;; (e.g. press `C-u' or `C-number' or `M-number' BEFORE accessing the
;; menu bar).  The menu also supports two Emacs 21 `ls-lisp' switches:
;; `ls-lisp-ignore-case' ignores case in alphanumeric sorts and
;; `ls-lisp-dirs-first' lists all directories first.  (These latter
;; two switches improve compatibility with Microsoft Windows
;; Explorer.)

;; The toggles for reverse sorting, `ls-lisp-ignore-case' and
;; `ls-lisp-dirs-first', are bound respectively to the keys "r", "c"
;; and "b" in the dired map.

;; A `Configuration' submenu allows a dired sort configuration to be
;; saved or restored, the current and saved configurations to be
;; swaped (bound to "T" for "toggle"), or the default to be restored.
;; The saved configuration is saved as a customization, which can also
;; be changed directly.  This submenu also allows
;; `dired-sort-dialogue-own-frame' to be toggled.

;; If a `dired-sort-menu' option causes an error (but not if it is
;; just ignored) then it is automatically disabled via the
;; customization facility.  This is useful because not all `ls' and
;; `ftp' programs support the same sort options.

;; The dialogue allows the setting of arbitrary combinations of dired
;; sort options together, without requiring multiple menu accesses.
;; Each dired buffer has its own dialogue.  This dialogue can be left
;; displayed, in which case it is automatically updated to reflect any
;; changes to its dired buffer that are made by other means.  For
;; tidiness, a dialogue buffer is automatically killed whenever it or
;; its dired buffer becomes invisible.

;; To do:
;;   test `dired-sort-menu-invalid-options' facility;
;;   per-host `dired-sort-menu-invalid-options-remote';
;;   cascade multiple dialogue frames?

;;; Installation:

;; Put this file somewhere where Emacs can find it (i.e. in one of the
;; directories in your `load-path' such as `site-lisp'), optionally
;; byte-compile it (recommended), and put this in your .emacs:
;;
;; (add-hook 'dired-load-hook
;;           (lambda () (require 'dired-sort-menu)))


;;; History:

;; Code for alternative ways to find files split off into
;; `dired-explore'.

;;; Code:

(require 'dired)
(require 'easymenu)
;; Silence compiler:
(eval-when-compile
  (defvar ls-lisp-ignore-case)		; not in current standard version
  (defvar ls-lisp-dirs-first)		; not in current standard version
  (require 'ange-ftp))

(defun dired-sort-menu-remote-p ()
  "Return the host name for a remote ange-ftp directory or nil if local."
  ;; Use the actual host name later!
  ;; The default extended filename syntax is '/user@host:name', where the
  ;; 'user@' part may be omitted.  This syntax can be customised to a certain
  ;; extent by changing ange-ftp-name-format.  There are limitations.
  (and (string-match "\\`/\\([^@:/]+@\\)?\\([^:/]+\\):" default-directory)
       (match-string 2)))

;;;###autoload
(defgroup dired-sort-menu nil
  "Menu and dialogue to control `dired' sort options."
  :group 'dired)

(defcustom dired-sort-menu-saved-config nil
  "*Alist of saved `dired' sort options.  Defaults to None (nil).
Can be customized directly or by saving from Dired sort menu.  If the
AList option is chosen in a customization buffer then it defaults to
the default options, which can be set via the Dired and Ls-Lisp
customization buffers."
  :type `(choice
	  (const :tag "None" nil)
	  (list :tag "AList"
		:value
		((dired-actual-switches
		  . ,dired-listing-switches)
		 (ls-lisp-ignore-case
		  . ,(let ((v (get 'ls-lisp-ignore-case 'standard-value)))
		       (and v (apply 'eval v))))
		 (ls-lisp-dirs-first
		  . ,(let ((v (get 'ls-lisp-dirs-first 'standard-value)))
		       (and v (apply 'eval v)))))
		(cons :tag "Any Ls"
		      (const :tag "Switches" dired-actual-switches)
		      string)
		(cons :tag "Ls-Lisp Only"
		      (const :tag "Ignore Case" ls-lisp-ignore-case)
		      boolean)
		(cons :tag "Ls-Lisp Only"
		      (const :tag "Dirs First" ls-lisp-dirs-first)
		      boolean)))
  :group 'dired-sort-menu)

(defcustom dired-sort-menu-invalid-options nil
  "*List of sort option strings that are invalid for the local ls program.
The corresponding menu/dialogue items are inactive/unavailable.
This variable is automatically updated when a sort option causes an
error.  It can also be set or reset explicitly, and you should reset
it (usually to nil) if you change the ls program used by `dired'.
Ignored when using `ls-lisp'."
  :type '(repeat string)
  :group 'dired-sort-menu)

(defcustom dired-sort-menu-invalid-options-remote nil
  ;; Add option for one such variable per remote host later or some
  ;; kind of alist.
  "*List of sort option strings that are invalid for the remote ls program.
The corresponding menu/dialogue items are inactive/unavailable.
This variable is automatically updated when a sort option causes an
error.  It can also be set or reset explicitly, and you should reset
it if you change the remote host or ls program used by `dired'.
[Note that `ls-lisp' is never used for remote directory listings.]"
  :type '(repeat string)
  :group 'dired-sort-menu)

(defconst dired-sort-display-graphic-p	; window-system
  (if (fboundp 'display-graphic-p)
      (funcall (symbol-function 'display-graphic-p)) ; Emacs 21
    (memq window-system '(x w32)))	; Emacs 20
  "Non-nil if graphic display is possible.")

(defcustom dired-sort-dialogue-own-frame dired-sort-display-graphic-p
  "*If non-nil then dialogues should pop up in their own frames (if possible)."
  :type 'boolean
  :group 'dired-sort-menu)

(defsubst dired-sort-dialogue-own-frame-really ()
  "Return non-nil if dialogues should and can pop up in their own frames."
  (and dired-sort-dialogue-own-frame dired-sort-display-graphic-p))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun ls-lisp-var-p (var)
  "Return non-nil if ls-lisp variable VAR should be used."
  (and (boundp var)
       (boundp 'ls-lisp-use-insert-directory-program)
       (not ls-lisp-use-insert-directory-program)
       (not (dired-sort-menu-remote-p))))

(defsubst dired-sort-menu-help (help-string r)
  "Return HELP-STRING, for reversed sort order if R is non-nil."
  (if r (concat help-string " [reversed]") help-string))

(defun dired-sort-menu-items (&optional r)
  "Return sort-menu item list; reverse sort with optional argument R = \"r\"."
  (list
   (vector
    "Name"
    `(dired-sort-menu-set-switches ,(concat "" r)) :style 'radio
    :selected (if r
		  '(and (not (dired-sort-menu-switch-p "[tSXUuc]"))
			(dired-sort-menu-switch-p "r"))
		'(not (dired-sort-menu-switch-p "[tSXUuc]")))
    :help (dired-sort-menu-help "Sort files by name" r)
    :active t)
   (vector
    "Time Modified"
    `(dired-sort-menu-set-switches ,(concat "t" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "t" ,r)
    :help (dired-sort-menu-help "Sort files by time last modified" r)
    :active t)
   ;; extended sort options ...
   (vector
    "Size"
    `(dired-sort-menu-set-switches ,(concat "S" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "S" ,r)
    :help (dired-sort-menu-help "Sort files by size" r)
    :active '(dired-sort-menu-active-p "S"))
   (vector
    "Extension"
    `(dired-sort-menu-set-switches ,(concat "X" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "X" ,r)
    :help (dired-sort-menu-help "Sort files by extension" r)
    :active '(dired-sort-menu-active-p "X"))
   (vector
    "Unsorted"
    `(dired-sort-menu-set-switches ,(concat "U" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "U" ,r)
    :help (dired-sort-menu-help "Sort files by physical order" r)
    :active '(dired-sort-menu-active-p "U"))
   (vector
    "Time Created"			; Windows
    `(dired-sort-menu-set-switches ,(concat "c" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "c" ,r)
    :help (dired-sort-menu-help "Sort files by time created" r)
    :active '(dired-sort-menu-active-p "c")
    :visible (and (eq system-type 'windows-nt)
		  (not (dired-sort-menu-remote-p))))
   (vector
    "Time Changed"			; not Windows
    `(dired-sort-menu-set-switches ,(concat "c" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "c" ,r)
    :help (dired-sort-menu-help "Sort files by time last changed" r)
    :active '(dired-sort-menu-active-p "c")
    :visible (or (not (eq system-type 'windows-nt))
		 (dired-sort-menu-remote-p)))
   (vector
    "Time Accessed"			; not useful under Windows
    `(dired-sort-menu-set-switches ,(concat "u" r)) :style 'radio
    :selected `(dired-sort-menu-switch-p "u" ,r)
    :help (dired-sort-menu-help "Sort files by time last accessed" r)
    :active '(dired-sort-menu-active-p "u")
    :visible (or (not (eq system-type 'windows-nt))
		 (dired-sort-menu-remote-p)))
   ))

;; Build a menu assigned to `dired-sort-menu' as both a variable and a
;; function, the former to use as a menu-bar sub-menu and the latter
;; as a popup menu:
(easy-menu-define			; (SYMBOL MAPS DOC MENU)
 dired-sort-menu
 nil
 "\"Sort By\" menu for dired mode."
 `("Sort By"
   ,@(dired-sort-menu-items)
   "--"
   ("Reversed Sort By"
    ,@(dired-sort-menu-items "r"))
   "--"
   ["Reverse" dired-sort-menu-toggle-reverse :style toggle
    :selected (dired-sort-menu-switch-p "r")
    :help "Reverse current sort order"
    :active (dired-sort-menu-active-p "r")]
   ["Recursive" dired-sort-menu-toggle-recursive :style toggle
    :selected (dired-sort-menu-switch-p "R")
    :help "Recursively list all subdirectories"
    :active (dired-sort-menu-active-p "R")]
   ["Ignore Case" dired-sort-menu-toggle-ignore-case :style toggle
    :selected ls-lisp-ignore-case :active t
    :help "Ignore case in alphanumeric sorting"
    ;; supported only by (Emacs 21) ls-lisp library and local dired:
    :visible (ls-lisp-var-p 'ls-lisp-ignore-case)]
   ["Dirs First" dired-sort-menu-toggle-dirs-first :style toggle
    :selected ls-lisp-dirs-first :active t
    :help "List subdirectories first [last if reversed]"
    ;; supported only by (Emacs 21) ls-lisp library and local dired:
    :visible (ls-lisp-var-p 'ls-lisp-dirs-first)]
   "--"
   ("Configuration"
    ["Save Current" dired-sort-menu-save-config
     :suffix (dired-sort-menu-current-suffix)
     :help "Save current sort configuration as a customization"
     :active t]
    ["Restore Saved" dired-sort-menu-restore-config
     :suffix (dired-sort-menu-restore-suffix)
     :help "Restore sort configuration saved as a customization"
     :active dired-sort-menu-saved-config]
    ["Swap Current/Saved" dired-sort-menu-swap-config
     :help "Exchange current and saved sort configurations"
     :active dired-sort-menu-saved-config]
    "---"
    ["Restore Default" dired-sort-menu-restore-default
     :suffix (dired-sort-menu-default-suffix)
     :help "Restore default sort configuration from customization"
     :active t]
    ["Customize Saved" (customize-option 'dired-sort-menu-saved-config)
     :help "Customize saved sort configuration"
     :active t]
    "---"
    ["Dialogue Own Frame" (setq dired-sort-dialogue-own-frame
			       (not dired-sort-dialogue-own-frame))
     :style toggle
     :selected dired-sort-dialogue-own-frame
     :help "Sort dialogues use own frame if checked, else dired frame"
     :active t])
   ["Dialogue" dired-sort-dialogue
    :help "Open a dialogue box to control this dired's sort options"
    :active t]
   ))

;; Menu bar "Immediate" menu sub-menu:
(easy-menu-add-item			; (map path item &optional before)
 dired-mode-map '("menu-bar" "immediate")
 dired-sort-menu
 'revert-buffer)

;; Popup menu on "Shift Mouse 2":
(define-key				; (keymap key def)
  dired-mode-map [S-down-mouse-2] 'dired-sort-menu-popup)

;;;###autoload
(defun dired-sort-menu-popup (event)
  "Pop up and run \"Sort By\" menu for dired mode *in EVENT window*."
  (interactive "@e")
  (let ((menu-item (x-popup-menu event dired-sort-menu)))
    ;; menu-item is null if pop-up menu is cancelled
    (and menu-item
	 (command-execute
	  (lookup-key dired-sort-menu (apply 'vector menu-item))))))

;; Add a few key bindings, as suggested by Ed Park <park@sunshine.net>.
;; Mnemonic *r*everse:
(define-key dired-mode-map "r" 'dired-sort-menu-toggle-reverse)
;; Mnemonic *c*asefold:
(define-key dired-mode-map "c" 'dired-sort-menu-toggle-ignore-case)
;; Mnemonic Files at *b*ottom:
(define-key dired-mode-map "b" 'dired-sort-menu-toggle-dirs-first)
;; Mnemonic *T*oggle Current/Saved:
(define-key dired-mode-map "T" 'dired-sort-menu-swap-config)



(defun dired-sort-menu-switch-p (switch &optional r)
  "Return true if regexp SWITCH matches (case sensitive) ls switch string.
With optional regexp R this must also match (case sensitive)."
  (let (case-fold-search)
    (setq switch (string-match switch dired-actual-switches))
    (if r
	(and switch (string-match r dired-actual-switches))
      switch
      )))

(defun dired-sort-menu-active-p (switch)
  "Return true if argument SWITCH not known to be invalid for the current ls."
  (if (dired-sort-menu-remote-p)
      (not (member switch dired-sort-menu-invalid-options-remote))
    (or (and (boundp 'ls-lisp-use-insert-directory-program)
	     (not ls-lisp-use-insert-directory-program)) ; ls-lisp
	(not (member switch dired-sort-menu-invalid-options)))))

;;;###autoload
(defun dired-sort-menu-toggle-ignore-case ()
  "Toggle ls-lisp switch `ls-lisp-ignore-case' and update buffer."
  (interactive)
  (setq ls-lisp-ignore-case (not ls-lisp-ignore-case))
  (revert-buffer))

;;;###autoload
(defun dired-sort-menu-toggle-dirs-first ()
  "Toggle ls-lisp switch `ls-lisp-dirs-first' and update buffer."
  (interactive)
  (setq ls-lisp-dirs-first (not ls-lisp-dirs-first))
  (revert-buffer))

(defun dired-sort-menu-set-switches (switch &optional reverse)
  "Set one ls sort switch SWITCH and update buffer.
Clears *all* other sort switches.
Reverse sort order if optional argument REVERSE is non-nil or if
called with any prefix argument \(e.g. \\[universal-argument] or
C-number or M-number BEFORE accessing menu bar)."
  ;; [FJW: I would like to use modifier keys to reverse the sort, but
  ;; that is not possible!]
  ;; First delete ALL ls sort switches:
  (let (case-fold-search)
    (while (string-match "[rtSXUuc]" dired-actual-switches)
      (setq dired-actual-switches
	    (replace-match "" t t dired-actual-switches))))
  (setq dired-actual-switches
	(concat dired-actual-switches switch))
  (if (or reverse current-prefix-arg)
      (setq dired-actual-switches
	    (concat dired-actual-switches "r")))
  (dired-sort-set-modeline)
  (dired-sort-menu-revert-buffer switch))

(defun dired-sort-menu-revert-buffer (switch)
  "Revert buffer carefully, handling invalid sort options gracefully.
If the current sort option SWITCH causes an error then update
`dired-sort-menu-invalid-options' or
`dired-sort-menu-invalid-options-remote' as appropriate."
  (condition-case nil
      (revert-buffer)
    ('file-error
     (if (dired-sort-menu-remote-p)
	 (customize-save-variable
	  'dired-sort-menu-invalid-options-remote
	  (cons switch dired-sort-menu-invalid-options-remote))
       (customize-save-variable
	'dired-sort-menu-invalid-options
	(cons switch dired-sort-menu-invalid-options)))
     ;; Remove all sort switches:
     (dired-sort-menu-set-switches "")
     (error
      "Invalid sort menu option disabled and directory sorted by name"))))

;;;###autoload
(defun dired-sort-menu-toggle-reverse ()
  "Toggle ls -r switch and update buffer.
Does not affect other sort switches."
  (interactive)
  (let (case-fold-search)
    (setq dired-actual-switches
	  (if (string-match "r" dired-actual-switches)
	      (replace-match "" t t dired-actual-switches)
	    (concat dired-actual-switches "r")))
    (dired-sort-set-modeline)
    (dired-sort-menu-revert-buffer "r")))

(defsubst dired-sort-menu-R-check (switches)
  "Additional processing of -R in ls option string SWITCHES.
Calls `dired-sort-R-check' only if defined (by `dired-fix' or Emacs 21).
Saves `dired-subdir-alist' when R is set and restores saved value
minus any directories explicitly deleted when R is cleared.
To be called first in body of `dired-sort-other', etc."
  (and (fboundp 'dired-sort-R-check)
       (funcall (symbol-function 'dired-sort-R-check) switches)))

;;;###autoload
(defun dired-sort-menu-toggle-recursive ()
  "Toggle ls -R switch and update buffer.
Does not affect other sort switches."
  (interactive)
  (let* (case-fold-search
	 (switches
	  (if (string-match "R" dired-actual-switches)
	      (replace-match "" t t dired-actual-switches)
	    (concat dired-actual-switches "R"))))
    (dired-sort-menu-R-check switches)
    (setq dired-actual-switches switches)
    (dired-sort-set-modeline)
    (dired-sort-menu-revert-buffer "R")))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Code to save and restore dired sort configurations.  This has a lot
;; of possibilities, e.g. saving multiple configurations, which I
;; could implement if there were any demand.

(defsubst dired-sort-menu-current-suffix-flag (flag label)
  "Construct \"Configuration/Save Current\" menu item suffix for FLAG with LABEL."
  (and (boundp flag)
       (concat "; [" (if (eval flag) "X" " ") "] " label)))

(defun dired-sort-menu-current-suffix ()
  "Construct \"Configuration/Save Current\" menu item suffix."
  (concat
   "(ls " dired-actual-switches
   (dired-sort-menu-current-suffix-flag
    'ls-lisp-ignore-case "Ignore Case")
   (dired-sort-menu-current-suffix-flag
    'ls-lisp-dirs-first "Dirs First")
   ")"
   ))

(defsubst dired-sort-menu-restore-suffix-flag (flag label)
  "Construct \"Configuration/Restore Saved\" menu item suffix for FLAG with LABEL."
  (and (setq flag (assq flag dired-sort-menu-saved-config))
       (concat "; [" (if (cdr flag) "X" " ") "] " label)))

(defun dired-sort-menu-restore-suffix ()
  "Construct \"Configuration/Restore Saved\" menu item suffix."
  (and dired-sort-menu-saved-config
       (concat
	"(ls "
	(cdr (assq 'dired-actual-switches
		   dired-sort-menu-saved-config))
	(dired-sort-menu-restore-suffix-flag
	 'ls-lisp-ignore-case "Ignore Case")
	(dired-sort-menu-restore-suffix-flag
	 'ls-lisp-dirs-first "Dirs First")
	")"
	)))

(defsubst dired-sort-menu-default-suffix-flag (flag label)
  "Construct \"Configuration/Restore Default\" menu item suffix for FLAG with LABEL."
  (and (boundp flag)
       (concat "; [" (if (apply 'eval (get flag 'standard-value)) "X" " ") "] " label)))

(defun dired-sort-menu-default-suffix ()
  "Construct \"Configuration/Restore Default\" menu item suffix."
  (concat
   "(ls " dired-listing-switches
   (dired-sort-menu-default-suffix-flag
    'ls-lisp-ignore-case "Ignore Case")
   (dired-sort-menu-default-suffix-flag
    'ls-lisp-dirs-first "Dirs First")
   ")"
   ))

(defun dired-sort-menu-config-alist ()
  "Return `dired' sort configuration as alist."
  (cons
   (cons 'dired-actual-switches dired-actual-switches)
   (and (boundp 'ls-lisp-use-insert-directory-program)
	(not ls-lisp-use-insert-directory-program)
	(append
	 (and (boundp 'ls-lisp-ignore-case)
	      (list (cons 'ls-lisp-ignore-case ls-lisp-ignore-case)))
	 (and (boundp 'ls-lisp-dirs-first)
	      (list (cons 'ls-lisp-dirs-first ls-lisp-dirs-first)))))))

(defun dired-sort-menu-save-config ()
  "Save current `dired' sort configuration."
  (interactive)
  (customize-save-variable 'dired-sort-menu-saved-config
			   (dired-sort-menu-config-alist)))

(defun dired-sort-menu-restore-config (&optional swap)
  "Restore saved `dired' sort configuration.
If optional (interactive prefix) argument SWAP is non-nil then swap
saved and current configurations."
  (interactive "P")
  (when dired-sort-menu-saved-config
    (setq swap (if swap
		   (dired-sort-menu-config-alist)
		 dired-sort-menu-saved-config))
    (while dired-sort-menu-saved-config
      (set (caar dired-sort-menu-saved-config)
	   (cdar dired-sort-menu-saved-config))
      (setq dired-sort-menu-saved-config
	    (cdr dired-sort-menu-saved-config)))
    (dired-sort-set-modeline)
    (revert-buffer)
    (setq dired-sort-menu-saved-config swap)))

;;;###autoload
(defun dired-sort-menu-swap-config ()
  "Swap saved and current `dired' sort configuration."
  (interactive)
  (dired-sort-menu-restore-config t))

(defun dired-sort-menu-restore-default ()
  "Restore default `dired' sort configuration."
  (interactive)
  (setq dired-actual-switches dired-listing-switches)
  (if (boundp 'ls-lisp-ignore-case)
      (setq ls-lisp-ignore-case
	    (apply 'eval (get 'ls-lisp-ignore-case 'standard-value))))
  (if (boundp 'ls-lisp-dirs-first)
      (setq ls-lisp-dirs-first
	    (apply 'eval (get 'ls-lisp-dirs-first 'standard-value))))
  (dired-sort-set-modeline)
  (revert-buffer))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Code to provide a customize-style dialogue-box that allows all sort
;; options to be set at once.  Care must be taken to access
;; `dired-actual-switches' only when the dired buffer is current,
;; because this variable is buffer-local.  There is currently no good
;; way to show that a widget is inactive, so inactive widgets are
;; simply not displayed.

;; Key binding, mnemonic *D*ialogue (d, D and M-d are used):
(define-key dired-mode-map "\C-d" 'dired-sort-dialogue)

(require 'wid-edit)

;; Buffer-local variables set in dired-sort-dialogue:
(make-variable-buffer-local
 (defvar dired-sort-dialogue-dired-buffer))
(make-variable-buffer-local
 (defvar dired-sort-dialogue-radio-widget))
(make-variable-buffer-local
 (defvar dired-sort-dialogue-reverse-widget nil))
(make-variable-buffer-local
 (defvar dired-sort-dialogue-recursive-widget nil))
(make-variable-buffer-local
 (defvar dired-sort-dialogue-ignore-case-widget nil))
(make-variable-buffer-local
 (defvar dired-sort-dialogue-dirs-first-widget nil))

(defun dired-sort-dialogue-choice ()
  "Return string to set Dired sort dialogue radio button choice."
  (let (case-fold-search
	(start (string-match "[tSXUuc]" dired-actual-switches)))
    (if start
	(substring dired-actual-switches start (1+ start))
      "")))

(defsubst dired-sort-dialogue-buffer-p (buffer-name)
  "Return non-nil if BUFFER-NAME corresponds to a dialogue buffer."
  (string-match "\\`\\*<\\(.*\\)> Dired Sort Options\\*\\'" buffer-name))

(defsubst dired-sort-dialogue-buffer-name (&optional buffer)
  "Return name of dialogue buffer corresponding to dired buffer BUFFER.
If BUFFER is nil then current buffer is used."
  (concat "*<" (buffer-name buffer) "> Dired Sort Options*"))

(defconst dired-sort-dialogue-width 23
  "Dired sort dialogue width in characters.")

;;;###autoload
(defun dired-sort-dialogue ()
  "A static dialogue version of the Dired sort menu.
This command *must* be run in the Dired buffer!"
  (interactive)
  (or (eq major-mode 'dired-mode)
      (error "This command may only be run in a Dired buffer"))
  (let
      ;; Must set these variables while still in the dired buffer!
      ((radio (dired-sort-dialogue-choice))
       (reverse (dired-sort-menu-switch-p "r"))
       (recursive (dired-sort-menu-switch-p "R"))
       (dired-buffer (current-buffer))
       ;; Suspend automatic mechanisms:
       window-configuration-change-hook
       kill-buffer-hook)

    ;; Check whether a dialogue buffer for this dired buffer is
    ;; already visible, and if so re-use its window:
    (let ((bufname (dired-sort-dialogue-buffer-name))
	  (bufs (buffer-list)) buf
	  (title (concat "<" (buffer-name dired-buffer) ">")))
      (while (and bufs (not (string= bufname
				     (buffer-name (setq buf (car bufs))))))
	(setq bufs (cdr bufs)))
      (if bufs
	  (progn
	    (if (dired-sort-dialogue-own-frame-really)
		(progn
		  (select-frame (window-frame (get-buffer-window buf t)))
		  (raise-frame))
	      (select-window (get-buffer-window buf t)))
	    (set-window-dedicated-p (selected-window) nil)
	    (kill-buffer buf))
	(if (dired-sort-dialogue-own-frame-really)
	    ;; If room then put dialogue immediately to the right of
	    ;; the dired frame, else at right edge of screen.
	    (let* ((alist (frame-parameters))
		   (top (cdr (assq 'top alist))) ; pixels
		   (left (cdr (assq 'left alist))) ; pixels
		   )
	      ;; Allow form INTEGER or (+ INTEGER):
	      (or (atom left) (setq left (cadr left)))
	      ;; Set left of dialogue frame to avoid falling off right
	      ;; of display:
	      (setq left (+ left (frame-pixel-width)))
	      (setq left (if (> (+ left (* dired-sort-dialogue-width
					   (frame-char-width)))
				(x-display-pixel-width))
			     -10
			   ;; (+ left (* 2 (cdr (assq 'border-width alist))))))
			   (+ left 10)))
	      (select-frame (make-frame
			     `((title . ,title)
			       (top . ,top)
			       (left . ,left)
			       (width . ,dired-sort-dialogue-width)
			       (height . 22)
			       (minibuffer . nil)
			       (vertical-scroll-bars . nil)
			       (horizontal-scroll-bars . nil)
			       (unsplittable . nil)
			       (menu-bar-lines . 0)
			       ))))
	  (split-window			; WINDOW SIZE HORIZONTAL
	   nil (- (window-width) dired-sort-dialogue-width) t)
	  (select-window (next-window))))
      (switch-to-buffer bufname)
      (set-window-dedicated-p (selected-window) t) ; can crash Emacs!
      (kill-all-local-variables)
;       (or buffer-display-table
; 	  (setq buffer-display-table
; 		(or standard-display-table (make-display-table))))
;       (set-display-table-slot buffer-display-table 0 ?_)
      (setq truncate-lines t
	    mode-line-format title))

    (let ((inhibit-read-only t))
      (erase-buffer))
    ;; Must set this only once in the dialogue buffer!
    (setq dired-sort-dialogue-dired-buffer dired-buffer)

    (let ((start (point)))
      (widget-insert "Dired Sort Options")
      (put-text-property start (point) 'face 'bold))
    (widget-insert " for\n<"
		   (buffer-name dired-buffer)
		   ">\n\n(Use any mouse button)\n\n ")
    (setq dired-sort-dialogue-radio-widget
	  (eval `(widget-create
		  'radio-button-choice
		  :indent 1
		  :value radio
		  '(item :tag "Name" "")
		  '(item :tag "Time Modified" "t")
		  ,@(if (dired-sort-menu-active-p "S")
			'('(item :tag "Size" "S")))
		  ,@(if (dired-sort-menu-active-p "X")
			'('(item :tag "Extension" "X")))
		  ,@(if (dired-sort-menu-active-p "U")
			'('(item :tag "Unsorted" "U")))
		  ,@(if (dired-sort-menu-active-p "c")
			`('(item :tag
				 ,(if (or (not (eq system-type 'windows-nt))
					  (dired-sort-menu-remote-p))
				      "Time Changed"
				    "Time Created") "c")))
		  ,@(if (and (dired-sort-menu-active-p "u")
			     (or (not (eq system-type 'windows-nt))
				 (dired-sort-menu-remote-p)))
			'('(item :tag "Time Accessed" "u")))
		  )))
    (widget-insert " _____________________\n\n ")
    (when (dired-sort-menu-active-p "r")
      (setq dired-sort-dialogue-reverse-widget
	    (widget-create 'checkbox
			   :help-echo "Reverse the sort order"
			   reverse))
      (widget-insert " Reverse\n "))
    (when (dired-sort-menu-active-p "R")
      (setq dired-sort-dialogue-recursive-widget
	    (widget-create 'checkbox
			   :help-echo "Recursively list all subdirectories"
			   recursive))
      (widget-insert " Recursive\n "))
    (when (ls-lisp-var-p 'ls-lisp-ignore-case)
      (setq dired-sort-dialogue-ignore-case-widget
	    (widget-create 'checkbox
			   :help-echo "Ignore case when sorting"
			   ls-lisp-ignore-case))
      (widget-insert " Ignore Case\n "))
    (when (ls-lisp-var-p 'ls-lisp-dirs-first)
      (setq dired-sort-dialogue-dirs-first-widget
	    (widget-create 'checkbox
			   :help-echo "Sort directories first"
			   ls-lisp-dirs-first))
      (widget-insert " Dirs First\n "))
    (widget-insert "_____________________\n\n ")
    (widget-create 'push-button
		   :notify 'dired-sort-dialogue-OK
		   :help-echo "Apply the settings and close the window"
		   "OK")
    (widget-insert " ")
    (widget-create 'push-button
		   :notify 'dired-sort-dialogue-close
		   :help-echo "Close the window and ignore the settings"
		   "Cancel")
    (widget-insert " ")
    (widget-create 'push-button
		   :notify 'dired-sort-dialogue-apply
		   :help-echo "Apply the settings without closing the window"
		   "Apply")
    (widget-setup)
    (goto-char (point-min))
;      (use-local-map widget-keymap)
;      (let ((map (make-sparse-keymap)))
;        (suppress-keymap map)
;        (set-keymap-parent map widget-keymap)
;        (define-key map [down-mouse-1] 'widget-button-click)
;        (define-key map [down-mouse-3] 'widget-button-click)
;        (use-local-map map))
    (let ((map widget-keymap))
      ;; (define-key map [t] 'undefined)
      ;; (define-key map [tab] 'widget-forward)
      ;; (define-key map [return] 'widget-button-press)
      (define-key map [down-mouse-1] 'widget-button-click)
      (define-key map [down-mouse-3] 'widget-button-click)
      ;; (define-key map [escape] (lambda () (interactive)
      ;; 			 (dired-sort-dialogue-close)))
      ;; (define-key map "\C-h" 'describe-bindings)
      (use-local-map map)))
  ;; Set up these hooks here to avoid any possibility of causing
  ;; trouble if the dialogue facility is not used:
  (add-hook 'kill-buffer-hook
	    'dired-sort-dialogue-auto-kill-1)
  (add-hook 'window-configuration-change-hook
	    'dired-sort-dialogue-auto-kill-2))

(defun dired-sort-dialogue-apply (&rest ignore)
  "Apply the dired sort dialogue settings (without closing it)."
  (let ((radio-widget dired-sort-dialogue-radio-widget)
	(reverse-widget dired-sort-dialogue-reverse-widget)
	(recursive-widget dired-sort-dialogue-recursive-widget)
	(ignore-case-widget dired-sort-dialogue-ignore-case-widget)
	(dirs-first-widget dired-sort-dialogue-dirs-first-widget)
	window-configuration-change-hook)
    (with-current-buffer dired-sort-dialogue-dired-buffer
      (if dirs-first-widget
	  (setq ls-lisp-dirs-first
		(widget-value dirs-first-widget)))
      (if ignore-case-widget
	  (setq ls-lisp-ignore-case
		(widget-value ignore-case-widget)))
      (if recursive-widget
	  (let* (case-fold-search
		 (switches
		  (if (widget-value recursive-widget)
		      (if (not (string-match "R" dired-actual-switches))
			  (concat dired-actual-switches "R")
			dired-actual-switches)
		    (if (string-match "R" dired-actual-switches)
			(replace-match "" t t dired-actual-switches)
		      dired-actual-switches))))
	    (dired-sort-menu-R-check switches)
	    (setq dired-actual-switches switches)))
      (dired-sort-menu-set-switches
       (widget-value radio-widget)
       (and reverse-widget (widget-value reverse-widget))))))

(defun dired-sort-dialogue-close (&rest ignore)
  "Close the dired sort dialogue (ignoring the settings)."
  (let ((dired-buffer dired-sort-dialogue-dired-buffer)
	window-configuration-change-hook
	kill-buffer-hook)
    (set-window-dedicated-p (selected-window) nil)
    (kill-buffer (current-buffer))
    (if (dired-sort-dialogue-own-frame-really)
	(delete-frame)
      (or (one-window-p t) (delete-window)))
    (select-window (get-buffer-window dired-buffer))))

(defun dired-sort-dialogue-OK (&rest ignore)
  "Apply the dired sort dialogue settings and close it."
  (dired-sort-dialogue-apply)
  (dired-sort-dialogue-close))


;; Code to keep dialogue buffers tidy.

(defadvice show-paren-function (around show-paren-function-advice activate)
  "Do not show matching parens in a dired sort dialogue buffer."
  (or (dired-sort-dialogue-buffer-p (buffer-name)) ad-do-it))

(defadvice dired-revert (after dired-revert-advice activate)
  "Redisplay and update any related dired sort dialogue buffer."
  (if (and (eq major-mode 'dired-mode)
	   (get-buffer (dired-sort-dialogue-buffer-name)))
      (if (dired-sort-dialogue-own-frame-really)
	  (let ((frame (selected-frame)))
	    (dired-sort-dialogue)
	    (raise-frame frame))
	(save-selected-window
	  (dired-sort-dialogue)))))

(defun dired-sort-dialogue-auto-kill-1 ()
  "If dialogue buffer is explicitly killed delete its window/frame.
Also, kill any dialogue buffer related to a killed dired mode buffer.
This function is hung on `kill-buffer-hook'."
  (if (dired-sort-dialogue-buffer-p (buffer-name))
      (if (dired-sort-dialogue-own-frame-really)
	  (delete-frame)
	(delete-window))
    (let (buf kill-buffer-hook window-configuration-change-hook)
      (when (and (eq major-mode 'dired-mode)
		 (setq buf
		       (get-buffer
			(dired-sort-dialogue-buffer-name))))
	(or (one-window-p t) (delete-windows-on buf))
	(kill-buffer buf)))))

(defun dired-sort-dialogue-auto-kill-2 ()
  "Kill any dialogue buffer that has ceased to be visible.
But do nothing if it was explicitly killed.  Also, kill any dialogue
buffer whose dired buffer has ceased to be visible.
This function is hung on `window-configuration-change-hook'."
  (or (eq this-command 'kill-buffer)	; both are needed!
      (eq last-command 'kill-buffer)	; both are needed!
      (let ((bufs (buffer-list)) buf bufname
	    kill-buffer-hook window-configuration-change-hook)
	(while bufs
	  (setq buf (car bufs)
		bufs (cdr bufs)
		bufname (buffer-name buf))
	  (if (dired-sort-dialogue-buffer-p bufname)
	      (cond ((null (get-buffer-window buf 'visible))
		     ;; dialogue hidden...
		     (kill-buffer buf))
		    ((null (get-buffer-window
			    (get-buffer (match-string 1 bufname)) 'visible))
		     ;; dired hidden...
		     (or (one-window-p t)
			 (eq this-command 'mouse-delete-other-windows)
			 (eq this-command 'delete-other-windows)
			 (delete-windows-on buf))
		     (kill-buffer buf))))))))

(defadvice handle-delete-frame
  (before handle-delete-frame-advice activate)
  "Kill dialogue buffer before killing its frame."
  (let* ((frame (posn-window (event-start event)))
	 (buf (car (buffer-list frame))))
    (when (dired-sort-dialogue-buffer-p (buffer-name buf))
      (set-window-dedicated-p (selected-window) nil)
      (kill-buffer buf))))

(provide 'dired-sort-menu)

;;; dired-sort-menu.el ends here
