Fix movement commands in emchat-log-mode.
[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             (when (and (equal nick alias)
156                        (or face cface))
157               ;; put some whitespace between the image and the name
158               (set-extent-begin-glyph
159                (make-extent (point-at-bol) (point-at-eol))
160                (make-glyph " "))
161               ;; Insert the X-Face
162               (when (and face
163                          (or (not emchat-buddy-prefer-cface-to-xface)
164                              (not cface)))
165                 (set-extent-begin-glyph
166                  ext
167                  (make-glyph (list (vector 'xface
168                                            :data (concat "X-Face: " face)
169                                            :foreground "black"
170                                            :background "white")))))
171               ;; Insert the cface
172               (when (and (featurep 'png)
173                          cface
174                          emchat-buddy-prefer-cface-to-xface)
175                 (set-extent-begin-glyph
176                  ext
177                  (make-glyph (list (vector 'png
178                                            :data (emchat-face-to-png cface)))))))
179             (setq all-records (cdr all-records))))))))
180
181 ;;;###autoload
182 (defun emchat-buddy-show-buffer (&optional new no-select)
183   "Switch to `emchat-buddy-buffer'.
184 Create buffer if buffer does not exists already or
185 NEW is non-nil.
186 Don't select buddy window if NO-SELECT is non-nil.
187 See `emchat-buddy-view' and `emchat-buddy-status-color-hint-flag'."
188   (interactive)
189   (when (or (not (buffer-live-p emchat-buddy-buffer))
190             new)
191     (setq emchat-buddy-buffer (get-buffer-create "*emchat buddy*"))
192     (set-buffer emchat-buddy-buffer)
193     (erase-buffer)
194     (loop for alias in (symbol-value emchat-buddy-view)
195       as status = (emchat-world-getf alias 'status)
196       as face = (emchat-status-face status)
197       do (insert-face (concat alias "\n") face)
198       do (when emchat-buddy-show-xface (emchat-buddy-show-xface alias))
199       do (emchat-buddy-update-face alias))
200     (emchat-buddy-mode))
201   (unless no-select
202     (switch-to-buffer emchat-buddy-buffer)))
203
204 (defun emchat-buddy-view-all ()
205   "Display all aliases in `emchat-world'.
206 See `emchat-buddy-view'."
207   (interactive)
208   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-all-aliases))
209
210 (defun emchat-buddy-view-connected ()
211   "Display all connected aliases.
212 See `emchat-buddy-view' and `emchat-connected-aliases'."
213   (interactive)
214   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-connected-aliases))
215
216 (defun emchat-buddy-view-active ()
217   "Display all active aliases.
218 See `emchat-buddy-view' and `emchat-active-aliases'."
219   (interactive)
220   (emchat-buddy-view-set 'emchat-buddy-view 'emchat-active-aliases))
221
222 (eval-when-compile (defvar emchat-history-directory))
223
224 (defun emchat-buddy-show-xface-in-balloon (alias)
225   "Display an XFace image in the balloon-help buffer."
226   (unless (featurep '(and xface bbdb-autoloads))
227     (error 'unimplemented "X-Face and/or BBDB"))
228   (save-excursion
229     (let ((ext (or (extent-at (point))
230                    (make-extent (point-min) (point-min))))
231           (all-records (bbdb-records))
232           face cface nick record)
233       (while all-records
234         (setq record (car all-records)
235               nick (bbdb-record-getprop record 'icqnick)
236               face (bbdb-record-getprop record 'face)
237               cface (bbdb-record-getprop record 'cface))
238         (when (and (equal nick alias)
239                    (or face cface))
240           ;; put some whitespace between the image and the name
241           (set-extent-begin-glyph
242            (make-extent (point-min) (point-min))
243            (make-glyph " "))
244           ;; Insert the X-Face
245           (when (and face
246                      (or (not emchat-buddy-prefer-cface-to-xface)
247                          (not cface)))
248             (set-extent-begin-glyph
249              ext
250              (make-glyph (list (vector 'xface
251                                        :data (concat "X-Face: " face)
252                                        :foreground "black"
253                                        :background "white")))))
254           ;; Insert the cface
255           (when (and (featurep 'png)
256                      cface
257                      emchat-buddy-prefer-cface-to-xface)
258             (set-extent-begin-glyph
259              ext
260              (make-glyph (list (vector 'png
261                                        :data (emchat-face-to-png cface)))))))
262         (setq all-records (cdr all-records))))))
263
264 (defadvice balloon-help-display-help (after emchat-balloon-xface (&rest args) activate)
265   "Display an X-Face or cface image in the balloon."
266   (when emchat-buddy-show-xface
267     (let ((alias (progn
268                    (set-buffer balloon-help-buffer)
269                    (goto-char (point-min))
270                    (when (re-search-forward "\\(^.*\\) (" (eolp) t)
271                      (substring (match-string 1) 1)))))
272       (when alias
273         (emchat-buddy-show-xface-in-balloon alias)))))
274
275 (defun emchat-buddy-update-face (alias &optional delete)
276   "Update face of ALIAS.
277 Non-nil DELETE means delete alias from buffer."
278   (save-excursion
279     (when (buffer-live-p emchat-buddy-buffer)
280       (set-buffer emchat-buddy-buffer)
281       (goto-char (point-min))
282
283       (if (search-forward-regexp
284            ;; use "^" alias "$" so searching "foo" will not get "foobar"
285            (concat "^"
286                    ;; to allow funny characters in alias
287                    (regexp-quote alias)
288                    "$")
289            nil t)
290           ;; old alias
291           (if delete
292               (delete-region
293                (point-at-bol)
294                ;; take care of last line
295                (min (1+ (point-at-eol)) (point-max))))
296         ;; new alias
297         (unless delete
298           (insert alias "\n")
299           (forward-line -1)))
300
301       (unless delete
302         (let* ((ext (extent-at (point)))
303                (bhelp (format
304                        "%s (%s)\n Status: %s\n Groups: %s\nHistory: %s\n\n\n"
305                        alias
306                        (emchat-alias-uin alias)
307                        (or (emchat-world-getf alias 'status)
308                            "offline")
309                        (or (emchat-world-getf alias 'group)
310                            "none")
311                        (or (emchat-world-getf alias 'history)
312                            "none")))
313                (face (emchat-status-face (emchat-world-getf alias 'status))))
314           (when (extentp ext)
315             (set-extent-property ext 'face face)
316             (set-extent-property ext 'balloon-help bhelp))
317
318           (when (emchat-world-getf alias 'selected)
319             ;; highlight first char
320             (put-text-property
321              (+ 0 (point-at-bol)) (+ 1 (point-at-bol))
322              'face 'emchat-face-selected)))))))
323
324 (defun emchat-buddy-select-all-in-view (state &optional predicate)
325   "Select all aliases in current view.
326 See `emchat-group-select-aliases' for STATE.
327 PREDICATE accepts an alias as an argument and limits the application.
328 Current view is `emchat-buddy-view'."
329   (loop for x in (symbol-value emchat-buddy-view)
330     if (or (null predicate)
331            (funcall predicate x))
332     do (emchat-group-select-aliases state x)))
333
334 (defun emchat-buddy-select-all-in-view-by-status (status)
335   "Toggle selections of all aliases with STATUS in current view."
336   (interactive
337    (list (emchat-completing-read "status: " emchat-valid-statuses)))
338   (emchat-buddy-select-all-in-view
339    'toggle
340    (lambda (x)
341      (equal (emchat-world-getf x 'status) status))))
342
343 (defun emchat-buddy-select-all-in-view-by-regexp (regexp)
344   "Toggle selections of all aliases matching REGEXP in current view."
345   ;; checked my screenshots? know why i use a symbol prefix now?
346   (interactive "sregexp: ")
347   (emchat-buddy-select-all-in-view
348    'toggle
349    (lambda (x)
350      (string-match regexp x))))
351
352 (defun emchat-buddy-selected-in-view ()
353   "Return a list of all selected aliases in current view.
354 Selected means an alias has non-nil 'selected property.
355 Current view is `emchat-buddy-view'."
356   (loop for x in (symbol-value emchat-buddy-view)
357     if (emchat-world-getf x 'selected)
358     collect x))
359
360 (provide 'emchat-buddy)
361
362 ;;; emchat-buddy.el ends here