;;; emchat-buddy.el --- "Buddy" code for EMchat ;; Copyright (C) 2007 - 2011 Steve Youngs ;; Author: Steve Youngs ;; Maintainer: Steve Youngs ;; Created: 2002-10-01 ;; Homepage: http://www.emchat.org/ ;; Keywords: comm ICQ ;; This file is part of EMchat. ;; Redistribution and use in source and binary forms, with or without ;; modification, are permitted provided that the following conditions ;; are met: ;; ;; 1. Redistributions of source code must retain the above copyright ;; notice, this list of conditions and the following disclaimer. ;; ;; 2. Redistributions in binary form must reproduce the above copyright ;; notice, this list of conditions and the following disclaimer in the ;; documentation and/or other materials provided with the distribution. ;; ;; 3. Neither the name of the author nor the names of any contributors ;; may be used to endorse or promote products derived from this ;; software without specific prior written permission. ;; ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ;; DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. (eval-and-compile (require 'emchat-menu) (require 'emchat-status) (require 'emchat-world) (require 'emchat-history)) (eval-when-compile (require 'advice) (require 'bbdb) (autoload #'bbdb-search "bbdb-com" nil nil 'macro)) (defgroup emchat-buddy nil "Contact list preferences." :group 'emchat) ;;;###autoload (defcustom emchat-buddy-window-width 20 "*Width of window for `emchat-buddy-buffer'." :group 'emchat-interface) (defcustom emchat-buddy-view 'emchat-connected-aliases "*View of buddy buffer. It determines what aliases to be display in buddy buffer. For example, \(emchat-connected-aliases) means display all connected aliases. See `emchat-buddy-view-all', `emchat-buddy-view-connected', and `emchat-buddy-view-active'." :group 'emchat-buddy :type '(choice (item emchat-all-aliases) (item emchat-connected-aliases) (item emchat-active-aliases)) :initialize 'custom-initialize-default) (defcustom emchat-buddy-show-avatar nil "*When non-nil, display avatar images in the buddy buffer. The images come from BBDB. For an image to display in the buddy buffer there has to be an existing BBDB entry for the contact that has both a `face', or `cface' field, for the image, and a `icqnick' field, to match from the contact name in the buddy buffer." :type 'boolean :group 'emchat-buddy :require 'bbdb) (make-obsolete-variable 'emchat-buddy-show-xface 'emchat-buddy-show-avatar "2015-05-29") (defcustom emchat-buddy-prefer-cface-to-xface (featurep 'png) "*When non-nil, display colour faces instead of X-Face if available." :type 'boolean :group 'emchat-buddy) (defcustom emchat-buddy-xface-foreground "black" "*Foreground colour used for X-Face avatars." :type 'string :group 'emchat-buddy) (defcustom emchat-buddy-xface-background "white" "*Background colour used for X-Face avatars." :type 'string :group 'emchat-buddy) (defface emchat-face-selected '((((background dark)) (:foreground "darkblue" :background "yellow")) (((background light)) (:foreground "darkblue" :background "yellow"))) "Face for OFFLINE status." :group 'emchat-buddy) ;;; Internal variables ;;;###autoload (defvar emchat-buddy-buffer nil "Buffer for contact list.") (defun emchat-buddy-mode () "Major mode for contact list in emchat. Commands: \\{emchat-buddy-mode-map} Turning on `emchat-buddy-mode' runs the hook `emchat-buddy-mode-hook'." (interactive) (kill-all-local-variables) (use-local-map emchat-buddy-mode-map) (setq mode-name "emchat-buddy") (setq major-mode 'emchat-buddy-mode) ;; put easy-menu-add after set mode-name (easy-menu-add emchat-main-easymenu) (easy-menu-add emchat-buddy-menu) (easy-menu-add emchat-log-menu) (set-specifier has-modeline-p nil (cons (current-buffer) nil)) (set-specifier horizontal-scrollbar-visible-p nil (cons (current-buffer) nil)) ;(setq modeline-format "%b") (run-hooks 'emchat-buddy-mode-hook)) (defun emchat-buddy-view-set (&optional symbol value) "Set `emchat-buddy-view'." (set-default symbol value) (emchat-buddy-show-buffer 'new 'no-select)) (defun emchat-face-to-png (face) "Base64 decode a Face header into a PNG. Returns a string." (with-temp-buffer (insert face) (base64-decode-region (point-min) (point-max)) (buffer-string))) (defun emchat-buddy-make-avatar (type extent data) "Returns an avatar glyph. TYPE is a symbol, either `cface', or `xface'. EXTENT is the extent to add the glyph to. DATA is the image data from BBDB." (cond ((eq type 'cface) (let ((glyph (emchat-face-to-png data))) (set-extent-begin-glyph extent (make-glyph `([png :data ,glyph]))))) ((eq type 'xface) (let ((glyph (concat "X-Face: " data))) (set-extent-begin-glyph extent (make-glyph `([xface :data ,glyph :foreground ,emchat-buddy-xface-foreground :background ,emchat-buddy-xface-background]))))) (t nil))) ;;;###autoload (defun emchat-buddy-show-avatar (alias) "Display an avatar image in the buddy buffer." ;; We might already have what we need in emchat-world (let ((cface (emchat-world-getf alias 'cface)) (xface (emchat-world-getf alias 'xface))) (unless (or cface xface) ;; Nope, we don't. Search bbdb. (unless (featurep 'bbdb-autoloads) (error 'unimplemented "BBDB")) (let* ((str (cons 'icqnick (regexp-quote alias))) (records (bbdb-search (bbdb-records) nil nil nil str nil))) (mapcar #'(lambda (record) (when (or (bbdb-record-getprop record 'face) (bbdb-record-getprop record 'cface)) (setq xface (bbdb-record-getprop record 'face)) (setq cface (bbdb-record-getprop record 'cface)) (emchat-world-putf alias 'xface xface) (emchat-world-putf alias 'cface cface))) records))) ;; Now we have what we need. (save-excursion (when (buffer-live-p emchat-buddy-buffer) (set-buffer emchat-buddy-buffer) (goto-char (point-min)) (when (search-forward-regexp (concat "^" (regexp-quote alias) "$") nil t) (let ((ext (extent-at (point)))) ;; put some whitespace between the image and the name (set-extent-begin-glyph (make-extent (point-at-bol) (point-at-eol)) (make-glyph " ")) (cond ((and cface (or emchat-buddy-prefer-cface-to-xface (not xface))) (emchat-buddy-make-avatar 'cface ext cface)) (xface (emchat-buddy-make-avatar 'xface ext xface)) (t nil)))))))) ;;;###autoload (defun emchat-buddy-show-buffer (&optional new no-select) "Switch to `emchat-buddy-buffer'. Create buffer if buffer does not exists already or NEW is non-nil. Don't select buddy window if NO-SELECT is non-nil. See `emchat-buddy-view' and `emchat-buddy-status-color-hint-flag'." (interactive) (when (or (not (buffer-live-p emchat-buddy-buffer)) new) (setq emchat-buddy-buffer (get-buffer-create "*emchat buddy*")) (set-buffer emchat-buddy-buffer) (erase-buffer) (loop for alias in (symbol-value emchat-buddy-view) as status = (emchat-world-getf alias 'status) as face = (emchat-status-face status) do (insert-face (concat alias "\n") face) do (when emchat-buddy-show-avatar (emchat-buddy-show-avatar alias)) do (emchat-buddy-update-face alias)) (emchat-buddy-mode)) (unless no-select (switch-to-buffer emchat-buddy-buffer))) (defun emchat-buddy-view-all () "Display all aliases in `emchat-world'. See `emchat-buddy-view'." (interactive) (emchat-buddy-view-set 'emchat-buddy-view 'emchat-all-aliases)) (defun emchat-buddy-view-connected () "Display all connected aliases. See `emchat-buddy-view' and `emchat-connected-aliases'." (interactive) (emchat-buddy-view-set 'emchat-buddy-view 'emchat-connected-aliases)) (defun emchat-buddy-view-active () "Display all active aliases. See `emchat-buddy-view' and `emchat-active-aliases'." (interactive) (emchat-buddy-view-set 'emchat-buddy-view 'emchat-active-aliases)) (eval-when-compile (defvar emchat-history-directory)) (defun emchat-buddy-show-avatar-in-balloon (alias) "Display an avatar image in the balloon-help buffer." (let ((cface (emchat-world-getf alias 'cface)) (xface (emchat-world-getf alias 'xface)) ext) (when (or cface xface) (setq ext (make-extent (point-min) (point-min)))) (cond ((and cface (or emchat-buddy-prefer-cface-to-xface (not xface))) (emchat-buddy-make-avatar 'cface ext cface)) (xface (emchat-buddy-make-avatar 'xface ext xface)) (t nil)))) (defadvice balloon-help-display-help (after emchat-buddy-show-avatar-in-balloon (&rest args) activate) "Display an X-Face or cface image in the balloon." (when emchat-buddy-show-avatar (let ((alias (progn (set-buffer balloon-help-buffer) (goto-char (point-min)) (when (re-search-forward "\\(^.*\\) (" (point-at-eol) t) (substring (match-string 1) 1))))) (when alias (emchat-buddy-show-avatar-in-balloon alias))))) (defun emchat-buddy-update-face (alias &optional delete) "Update face of ALIAS. Non-nil DELETE means delete alias from buffer." (save-excursion (when (buffer-live-p emchat-buddy-buffer) (set-buffer emchat-buddy-buffer) (goto-char (point-min)) (if (search-forward-regexp ;; use "^" alias "$" so searching "foo" will not get "foobar" (concat "^" ;; to allow funny characters in alias (regexp-quote alias) "$") nil t) ;; old alias (if delete (delete-region (point-at-bol) ;; take care of last line (min (1+ (point-at-eol)) (point-max)))) ;; new alias (unless delete (insert alias "\n") (forward-line -1))) (unless delete (let* ((ext (extent-at (point))) (bhelp (format "%s (%s)\n Status: %s\n Groups: %s\nHistory: %s\n\n\n" alias (emchat-alias-uin alias) (or (emchat-world-getf alias 'status) "offline") (or (emchat-world-getf alias 'group) "none") (or (emchat-world-getf alias 'history) "none"))) (face (emchat-status-face (emchat-world-getf alias 'status)))) (when (extentp ext) (set-extent-property ext 'face face) (set-extent-property ext 'balloon-help bhelp)) (when (emchat-world-getf alias 'selected) ;; highlight first char (put-text-property (+ 0 (point-at-bol)) (+ 1 (point-at-bol)) 'face 'emchat-face-selected))))))) (defun emchat-buddy-select-all-in-view (state &optional predicate) "Select all aliases in current view. See `emchat-group-select-aliases' for STATE. PREDICATE accepts an alias as an argument and limits the application. Current view is `emchat-buddy-view'." (loop for x in (symbol-value emchat-buddy-view) if (or (null predicate) (funcall predicate x)) do (emchat-group-select-aliases state x))) (defun emchat-buddy-select-all-in-view-by-status (status) "Toggle selections of all aliases with STATUS in current view." (interactive (list (emchat-completing-read "status: " emchat-valid-statuses))) (emchat-buddy-select-all-in-view 'toggle (lambda (x) (equal (emchat-world-getf x 'status) status)))) (defun emchat-buddy-select-all-in-view-by-regexp (regexp) "Toggle selections of all aliases matching REGEXP in current view." ;; checked my screenshots? know why i use a symbol prefix now? (interactive "sregexp: ") (emchat-buddy-select-all-in-view 'toggle (lambda (x) (string-match regexp x)))) (defun emchat-buddy-selected-in-view () "Return a list of all selected aliases in current view. Selected means an alias has non-nil 'selected property. Current view is `emchat-buddy-view'." (loop for x in (symbol-value emchat-buddy-view) if (emchat-world-getf x 'selected) collect x)) (provide 'emchat-buddy) ;;; emchat-buddy.el ends here