;;; planner-frame.el --- Devote a frame to the Planner

;; Copyright (C) 2005 Jesse Alama <>

;; This file is not part of GNU Emacs.

;; This 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 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:
;; I find it helpful to look at my planner within one frame, taking up
;; the only window of that frame.  I want to treat the Planner as an
;; application in the sense of giving it its own frame and dealing
;; with that frame intelligently.  I've been thus far dissatisfied
;; with other approaches (e.g., `C-x 5 2 RET M-x plan'), so I decided
;; to try my hand at making an extension of planner that does things
;; the way I want.
;; What is the way I want?  Answer: gnus.  The code that follows is
;; based on the way that gnus regards itself as an application: it
;; provides a function `gnus-other-frame' for reading mail and news in
;; a distinguished frame, `gnus-exit-gnus' to save one's configuration
;; and stop reading mail and news and, and `gnus-suspend-gnus' for
;; making the interface go away temporarily while leaving most of its
;; state intact.  I also find it easy to deal with emacs-w3m, which I
;; regard as another emacs application.
;; Currently planner doesn't seem to fit this way of thinking (or at
;; least: I wasn't smart enough or informed enough to see how Planner
;; could be viewed this way), so I created planner-frame.  The purpose
;; is to enable one to think of the Planner as an emacs application in
;; a way similar to how one can view gnus and emacs-w3m as emacs
;; applications.  (A good deal of the code here is directly adapted,
;; if not outright copied, from gnus.  We have here yet another case
;; where the ability to study and adapt code from other applications
;; was a source of enlightenment, satisfaction, and, I hope, value to
;; others.)
;; The user functions are `plan-other-frame',
;; `planner-suspend-planner', and `planner-exit-planner'.  I've also
;; added a couple new keybindings for the latter two functions.  By
;; analogy with gnus, for these two "quit" functions I've created the
;; hooks `planner-exit-planner-hook' and
;; `planner-suspend-planner-hook'.  Customize
;; `planner-frame-parameters' to modify the frame produced by
;; `plan-other-frame'.
;; To get started, simply add the form
;;   (require 'planner-frame)
;; to your emacs initialization file.  The function `plan-other-frame'
;; will then get your started planning in a distinguished planner
;; frame.
;;; Todo
;;; - Integrate with `planner.el'
;; It could be valuable to somehow integrate the code here with
;; `planner.el', ala gnus, which provides `gnus-other-frame' and its
;; supporting code alongside `gnus'.
;;; - Offer a way to make the planner frame permanently visible.
;;; - Launch applications in other frames
;; It would be nice to make things so that following links to data
;; outside the Planner project (either in the emacs-wiki or muse
;; sense) pops up a new frame.  The idea here is to make the Planner,
;; in its own frame, a kind of repellant force: the only things that
;; really go in that frame are Planner pages; trying to go outside the
;; planner lands one in a different frame.  Enforcing such
;; restrictions could be helpful; thus there would be little chance of
;; getting distracted, with one's planner pages buried deep uneath a
;; pile of erc, bbdb, message, and other buffers.  It would allow one
;; to better focus on planning and what needs to be done.  It would
;; make the Planner a kind of omnipresent application.
;;; - Integration with other applications
;; (Alert: vague rambling follows.)  My idea is to somehow collect PIM
;; applications together.  The Planner isn't the only one: there's the
;; diary and the calendar (already an example of two PIM applications
;; working together), the BBDB, timeclock, and so on.  Somehow it
;; would be nice to gather these together in a single package.  Of
;; course already there is quite a lot of glue between these provided
;; by Planner; that's the purpose of, for example, remember with its
;; annotation functions.  But it seems to me that the connections
;; could be even tighter.  One sense in which the connections could be
;; tighter is the frame and window code; it would be great if,
;; somehow, Planner were the uber-application in its own frame, and it
;; had some "servant" frames.
;; I'd love to hear how you've used planner-frame.el, and how you
;; think it can be improved!  My email address is at the beginning of
;; the file.

(require 'planner)

;;; User Variables

(defcustom planner-frame-parameters nil
  "Frame parameters used by `plan-other-frame' to create a Planner frame.
This should be an alist for Emacs, or a plist for XEmacs."
  :group 'planner
  :type (if (featurep 'xemacs)
	    '(repeat (list :inline t :format "%v"
			   (symbol :tag "Property")
			   (sexp :tag "Value")))
	  '(repeat (cons :format "%v"
			 (symbol :tag "Parameter")
			 (sexp :tag "Value")))))

(defcustom planner-exit-planner-hook nil
  "Hook called when exiting Planner."
  :group 'planner
  :type 'hook)

(defcustom planner-suspend-planner-hook nil
  "Hook called when suspending Planner."
  :group 'planner
  :type 'hook)

;;; Default Keybindings

(define-key planner-mode-map "\C-cz" 'planner-suspend-planner)
(define-key planner-mode-map "\C-cx" 'planner-exit-planner)

;;; Internal Functions

(defun planner-select-frame-set-input-focus (frame)
  "Select `planner-frame', raise it, and set input focus, if possible."
  ;;; taken verbatim from `gnus-select-frame-set-input-focus'
  (cond ((featurep 'xemacs)
	 (raise-frame frame)
	 (select-frame frame)
	 (focus-frame frame))
	;; The function `select-frame-set-input-focus' won't set
	;; the input focus under Emacs 21.2 and X window system.
	;;((fboundp 'select-frame-set-input-focus)
	;; (defalias 'gnus-select-frame-set-input-focus
	;;   'select-frame-set-input-focus)
	;; (select-frame-set-input-focus frame))
	 (raise-frame frame)
	 (select-frame frame)
	 (cond ((and (eq window-system 'x)
		     (fboundp 'x-focus-frame))
		(x-focus-frame frame))
	       ((eq window-system 'w32)
		(w32-focus-frame frame)))
	 (when focus-follows-mouse
	   (set-mouse-position frame
			       (1- (frame-width frame)) 0)))))

(defun planner-frame-or-window-display-name (object)
  "Given a frame or window, return the associated display name.
Return nil otherwise."
  (if (featurep 'xemacs)
      (device-connection (dfw-device object))
    (if (or (framep object)
	    (and (windowp object)
		 (setq object (window-frame object))))
	(let ((display (frame-parameter object 'display)))
	  (if (and (stringp display)
		   ;; Exclude invalid display names.
		   (string-match "\\`[^:]*:[0-9]+\\(\\.[0-9]+\\)?\\'"

(defun planner-buffer-p (buf)
  (with-current-buffer buf
    (string-match mode-name "planner")))

(defun planner-alive-p ()
  "Determine whether Planner is running" 
  ;; planner is running iff there is a buffer anywhere whose mode is
  ;; `planner-mode'.
  (let ((buf-list (buffer-list))
	(found nil))
    (while (and buf-list (not found))
      (let ((buf (car buf-list)))
	(when (planner-buffer-p buf)
	  (setq found t)))
      (setq buf-list (cdr buf-list)))

;;; Internal Variables

(defvar planner-most-recent-buffer nil
  "The most recent planner buffer that has been visited.")

(defvar planner-frame nil
  "A frame object which will be created by `plan-other-frame'.")

;;; Public Functions

(defun plan-other-frame (&optional force-days display)
  "Like `plan', but does so in a new frame.

This will call `plan' and pop up a Planner frame; the value of
FORCE-DAYS is passed to `plan'.  The optional second argument
DISPLAY should be a standard display string such as \"unix:0\" to
specify where to pop up a frame.  If DISPLAY is omitted or the
function `make-frame-on-display' is not available, the current
display is used."
;; Docstring taken from docstring of `gnus-other-frame'.
  (interactive "P")
  (if (fboundp 'make-frame-on-display)
      (unless display
	(setq display (planner-frame-or-window-display-name (selected-frame))))
    (setq display nil))
  (let ((alive (planner-alive-p)))
    (unless (and alive
		 (catch 'found
		    (lambda (window)
			   (or (not display)
			       (equal display
			   (with-current-buffer (window-buffer window)
			     (string-match "\\`planner-"
					   (symbol-name major-mode))))
			 (setq planner-frame (window-frame window)))
			(select-window window)
			(throw 'found t)))
		    'ignore t)))
       (setq planner-frame
	     (if display
		 (make-frame-on-display display planner-frame-parameters)
	       (make-frame planner-frame-parameters))))
      (if alive
	  (switch-to-buffer planner-most-recent-buffer)
	(plan force-days)))))

(defun planner-exit-planner ()
  "Save and then close all Planner buffers, delete the frame that
Planner created, and run the hook `planner-exit-planner-hook'."
  (when (and (frame-live-p planner-frame)
	     (cdr (frame-list)))
    (delete-frame planner-frame))
  (dolist (buf (buffer-list))
    (when (planner-buffer-p buf)
      (kill-buffer buf)))
  (setq planner-frame nil)
  (run-hooks 'planner-exit-planner-hook))

(defun planner-suspend-planner ()
  "Delete the frame that was created for the Planner, then run the
hook `planner-suspend-planner-hook'."
  (when (and (frame-live-p planner-frame)
	     (cdr (frame-list)))
    (delete-frame planner-frame))
  (run-hooks 'planner-suspend-planner-hook))

;;; Export Statements
(provide 'planner-frame)

;;; planner-frame.el ends here