357a834e4c495329e73641e533a0fa6d158bdad4
[emchat] / emchat-buddy.el
1 ;;; emchat-buddy.el --- "Buddy" code for EMchat
2
3 ;; Copyright (C) 2007 - 2011 Steve Youngs
4
5 ;; Author:        Steve Youngs <steve@emchat.org>
6 ;; Maintainer:    Steve Youngs <steve@emchat.org>
7 ;; Created:       2002-10-01
8 ;; Homepage:      http://www.emchat.org/
9 ;; Keywords:      comm ICQ
10
11 ;; This file is part of EMchat.
12
13 ;; Redistribution and use in source and binary forms, with or without
14 ;; modification, are permitted provided that the following conditions
15 ;; are met:
16 ;;
17 ;; 1. Redistributions of source code must retain the above copyright
18 ;;    notice, this list of conditions and the following disclaimer.
19 ;;
20 ;; 2. Redistributions in binary form must reproduce the above copyright
21 ;;    notice, this list of conditions and the following disclaimer in the
22 ;;    documentation and/or other materials provided with the distribution.
23 ;;
24 ;; 3. Neither the name of the author nor the names of any contributors
25 ;;    may be used to endorse or promote products derived from this
26 ;;    software without specific prior written permission.
27 ;;
28 ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
29 ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
30 ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31 ;; DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
32 ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
33 ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
34 ;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
35 ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
36 ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
37 ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
38 ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
40 (eval-and-compile
41   (require 'emchat-menu)
42   (require 'emchat-status)
43   (require 'emchat-world)
44   (require 'emchat-history))
45
46 (eval-when-compile
47   (require 'advice)
48   (require 'bbdb))
49
50 (defgroup emchat-buddy nil
51   "Contact list preferences."
52   :group 'emchat)
53
54 ;;;###autoload
55 (defcustom emchat-buddy-window-width 20
56   "*Width of window for `emchat-buddy-buffer'."
57   :group 'emchat-interface)
58
59 (defcustom emchat-buddy-view
60   'emchat-connected-aliases
61   "*View of buddy buffer.
62 It determines what aliases to be display in buddy buffer.  For example,
63 \(emchat-connected-aliases) means display all connected aliases.
64
65 See `emchat-buddy-view-all', `emchat-buddy-view-connected', and
66 `emchat-buddy-view-active'."
67   :group 'emchat-buddy
68   :type '(choice (item emchat-all-aliases)
69                  (item emchat-connected-aliases)
70                  (item emchat-active-aliases))
71   :initialize 'custom-initialize-default)
72
73 (defcustom emchat-buddy-show-xface nil
74   "*When non-nil, display XFace images in the buddy buffer.
75
76 The images come from BBDB.  For an image to display in the buddy
77 buffer there has to be an existing BBDB entry for the contact that
78 has both a `face' field, for the image, and a `icqnick' field, to
79 match from the contact name in the buddy buffer."
80   :type 'boolean
81   :group 'emchat-buddy
82   :require 'bbdb)
83
84 (defcustom emchat-buddy-prefer-cface-to-xface (featurep 'png)
85   "*When non-nil, display colour faces instead of X-Face if available."
86   :type 'boolean
87   :group 'emchat-buddy)
88
89 (defface emchat-face-selected
90   '((((background dark))
91      (:foreground "darkblue" :background "yellow"))
92     (((background light))
93      (:foreground "darkblue" :background "yellow")))
94   "Face for OFFLINE status."
95   :group 'emchat-buddy)
96
97 ;;; Internal variables
98
99 ;;;###autoload
100 (defvar emchat-buddy-buffer nil
101   "Buffer for contact list.")
102
103 (defun emchat-buddy-mode ()
104   "Major mode for contact list in emchat.
105 Commands: \\{emchat-buddy-mode-map}
106
107 Turning on `emchat-buddy-mode' runs the hook `emchat-buddy-mode-hook'."
108   (interactive)
109   (kill-all-local-variables)
110   (use-local-map emchat-buddy-mode-map)
111   (setq mode-name "emchat-buddy")
112   (setq major-mode 'emchat-buddy-mode)
113   ;; put easy-menu-add after set mode-name
114   (easy-menu-add emchat-main-easymenu)
115   (easy-menu-add emchat-buddy-menu)
116   (easy-menu-add emchat-log-menu)
117   (set-specifier has-modeline-p nil
118                  (cons (current-buffer) nil))
119   (set-specifier horizontal-scrollbar-visible-p nil
120                  (cons (current-buffer) nil))
121   ;(setq modeline-format "%b")
122
123   (run-hooks 'emchat-buddy-mode-hook))
124
125 (defun emchat-buddy-view-set (&optional symbol value)
126   "Set `emchat-buddy-view'."
127   (set-default symbol value)
128   (emchat-buddy-show-buffer 'new 'no-select))
129
130 (defun emchat-face-to-png (face)
131   "Base64 decode a Face header into a PNG.
132 Returns a string."
133   (with-temp-buffer
134     (insert face)
135     (base64-decode-region (point-min) (point-max))
136     (buffer-string)))
137
138 (defun emchat-buddy-show-xface (alias)
139   "Display an XFace image in the buddy buffer."
140   (unless (featurep '(and xface bbdb-autoloads))
141     (error 'unimplemented "X-Face and/or BBDB"))
142   (save-excursion
143     (when (buffer-live-p emchat-buddy-buffer)
144       (set-buffer emchat-buddy-buffer)
145       (goto-char (point-min))
146       (when (search-forward-regexp (concat "^" (regexp-quote alias) "$") nil t)
147         (let ((ext (extent-at (point)))
148               (all-records (bbdb-records))
149               face cface nick record)
150           (while all-records
151             (setq record (car all-records)
152                   nick (bbdb-record-getprop record 'icqnick)
153                   face (bbdb-record-getprop record 'face)
154                   cface (bbdb-record-getprop record 'cface))
155             (if (stringp nick)
156                 (setq nick (split-string nick ",\\| " 'omitnulls)))
157             (when (and (member alias nick)
158                        (or face cface))
159               ;; put some whitespace between the image and the name
160               (set-extent-begin-glyph
161                (make-extent (point-at-bol) (point-at-eol))
162                (make-glyph " "))
163               ;; Insert the X-Face
164               (when (and face
165                          (or (not emchat-buddy-prefer-cface-to-xface)
166                              (not cface)))
167                 (set-extent-begin-glyph
168                  ext
169                  (make-glyph (list (vector 'xface
170                                            :data (concat "X-Face: " face)
171                                            :foreground "black"
172                                            :background "white")))))
173               ;; Insert the cface
174               (when (and (featurep 'png)
175                          cface
176                          emchat-buddy-prefer-cface-to-xface)
177                 (set-extent-begin-glyph
178                  ext
179                  (make-glyph (list (vector 'png
180                                            :data (emchat-face-to-png cface)))))))
181             (setq all-records (cdr all-records))))))))
182
183 ;;;###autoload
184 (defun emchat-buddy-show-buffer (&optional new no-select)
185   "Switch to `emchat-buddy-buffer'.
186 Create buffer if buffer does not exists already or
187 NEW is non-nil.
188 Don't select buddy window if NO-SELECT is non-nil.
189 See `emchat-buddy-view' and `emchat-buddy-status-color-hint-flag'."
190   (interactive)
191   (when (or (not (buffer-live-p emchat-buddy-buffer))
192             new)
193     (setq emchat-buddy-buffer (get-buffer-create "*emchat buddy*"))
194     (set-buffer emchat-buddy-buffer)
195     (erase-buffer)
196     (loop for alias in (symbol-value emchat-buddy-view)
197       as status = (emchat-world-getf alias 'status)
198       as face = (emchat-status-face status)
199       do (insert-face (concat alias "\n") face)
200       do (when emchat-buddy-show-xface (emchat-buddy-show-xface alias))
201       do (emchat-buddy-update-face alias))
202     (emchat-buddy-mode))
203   (unless no-select
204     (switch-to-buffer emchat-buddy-buffer)))
205
206 (defun emchat-buddy-view-all ()
207   "Display all aliases in `emchat-world'.
208 See `emchat-buddy-view'."
209   (interactive)
210   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-all-aliases))
211
212 (defun emchat-buddy-view-connected ()
213   "Display all connected aliases.
214 See `emchat-buddy-view' and `emchat-connected-aliases'."
215   (interactive)
216   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-connected-aliases))
217
218 (defun emchat-buddy-view-active ()
219   "Display all active aliases.
220 See `emchat-buddy-view' and `emchat-active-aliases'."
221   (interactive)
222   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-active-aliases))
223
224 (eval-when-compile (defvar emchat-history-directory))
225
226 (defun emchat-buddy-show-xface-in-balloon (alias)
227   "Display an XFace image in the balloon-help buffer."
228   (unless (featurep '(and xface bbdb-autoloads))
229     (error 'unimplemented "X-Face and/or BBDB"))
230   (save-excursion
231     (let ((ext (or (extent-at (point))
232                    (make-extent (point-min) (point-min))))
233           (all-records (bbdb-records))
234           face cface nick record)
235       (while all-records
236         (setq record (car all-records)
237               nick (bbdb-record-getprop record 'icqnick)
238               face (bbdb-record-getprop record 'face)
239               cface (bbdb-record-getprop record 'cface))
240         (when (and (equal nick alias)
241                    (or face cface))
242           ;; put some whitespace between the image and the name
243           (set-extent-begin-glyph
244            (make-extent (point-min) (point-min))
245            (make-glyph " "))
246           ;; Insert the X-Face
247           (when (and face
248                      (or (not emchat-buddy-prefer-cface-to-xface)
249                          (not cface)))
250             (set-extent-begin-glyph
251              ext
252              (make-glyph (list (vector 'xface
253                                        :data (concat "X-Face: " face)
254                                        :foreground "black"
255                                        :background "white")))))
256           ;; Insert the cface
257           (when (and (featurep 'png)
258                      cface
259                      emchat-buddy-prefer-cface-to-xface)
260             (set-extent-begin-glyph
261              ext
262              (make-glyph (list (vector 'png
263                                        :data (emchat-face-to-png cface)))))))
264         (setq all-records (cdr all-records))))))
265
266 (defadvice balloon-help-display-help (after emchat-balloon-xface (&rest args) activate)
267   "Display an X-Face or cface image in the balloon."
268   (when emchat-buddy-show-xface
269     (let ((alias (progn
270                    (set-buffer balloon-help-buffer)
271                    (goto-char (point-min))
272                    (when (re-search-forward "\\(^.*\\) (" (point-at-eol) t)
273                      (substring (match-string 1) 1)))))
274       (when alias
275         (emchat-buddy-show-xface-in-balloon alias)))))
276
277 (defun emchat-buddy-update-face (alias &optional delete)
278   "Update face of ALIAS.
279 Non-nil DELETE means delete alias from buffer."
280   (save-excursion
281     (when (buffer-live-p emchat-buddy-buffer)
282       (set-buffer emchat-buddy-buffer)
283       (goto-char (point-min))
284
285       (if (search-forward-regexp
286            ;; use "^" alias "$" so searching "foo" will not get "foobar"
287            (concat "^"
288                    ;; to allow funny characters in alias
289                    (regexp-quote alias)
290                    "$")
291            nil t)
292           ;; old alias
293           (if delete
294               (delete-region
295                (point-at-bol)
296                ;; take care of last line
297                (min (1+ (point-at-eol)) (point-max))))
298         ;; new alias
299         (unless delete
300           (insert alias "\n")
301           (forward-line -1)))
302
303       (unless delete
304         (let* ((ext (extent-at (point)))
305                (bhelp (format
306                        "%s (%s)\n Status: %s\n Groups: %s\nHistory: %s\n\n\n"
307                        alias
308                        (emchat-alias-uin alias)
309                        (or (emchat-world-getf alias 'status)
310                            "offline")
311                        (or (emchat-world-getf alias 'group)
312                            "none")
313                        (or (emchat-world-getf alias 'history)
314                            "none")))
315                (face (emchat-status-face (emchat-world-getf alias 'status))))
316           (when (extentp ext)
317             (set-extent-property ext 'face face)
318             (set-extent-property ext 'balloon-help bhelp))
319
320           (when (emchat-world-getf alias 'selected)
321             ;; highlight first char
322             (put-text-property
323              (+ 0 (point-at-bol)) (+ 1 (point-at-bol))
324              'face 'emchat-face-selected)))))))
325
326 (defun emchat-buddy-select-all-in-view (state &optional predicate)
327   "Select all aliases in current view.
328 See `emchat-group-select-aliases' for STATE.
329 PREDICATE accepts an alias as an argument and limits the application.
330 Current view is `emchat-buddy-view'."
331   (loop for x in (symbol-value emchat-buddy-view)
332     if (or (null predicate)
333            (funcall predicate x))
334     do (emchat-group-select-aliases state x)))
335
336 (defun emchat-buddy-select-all-in-view-by-status (status)
337   "Toggle selections of all aliases with STATUS in current view."
338   (interactive
339    (list (emchat-completing-read "status: " emchat-valid-statuses)))
340   (emchat-buddy-select-all-in-view
341    'toggle
342    (lambda (x)
343      (equal (emchat-world-getf x 'status) status))))
344
345 (defun emchat-buddy-select-all-in-view-by-regexp (regexp)
346   "Toggle selections of all aliases matching REGEXP in current view."
347   ;; checked my screenshots? know why i use a symbol prefix now?
348   (interactive "sregexp: ")
349   (emchat-buddy-select-all-in-view
350    'toggle
351    (lambda (x)
352      (string-match regexp x))))
353
354 (defun emchat-buddy-selected-in-view ()
355   "Return a list of all selected aliases in current view.
356 Selected means an alias has non-nil 'selected property.
357 Current view is `emchat-buddy-view'."
358   (loop for x in (symbol-value emchat-buddy-view)
359     if (emchat-world-getf x 'selected)
360     collect x))
361
362 (provide 'emchat-buddy)
363
364 ;;; emchat-buddy.el ends here