;;; skk-sticky.el --- Improved sticky-shift on SKK -*- coding: iso-2022-jp -*-

;; Copyright (C) 2008  IRIE Tetsuya

;; Author: IRIE Tetsuya <irie@t.email.ne.jp>
;; Version: $Id: skk-sticky.el,v 1.4 2010/08/02 15:21:05 skk-cvs Exp $
;; Keywords: japanese, mule, input method
;; Last Modified: $Date: 2010/08/02 15:21:05 $

;; This file is part of Daredevil SKK.

;; Daredevil SKK 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.

;; Daredevil SKK 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 Daredevil SKK, see the file COPYING.  If not, write to
;; the Free Software Foundation Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; [概要]
;; SKK では通常、変換開始および送り開始の位置を大文字で指定しますが、こ
;; れを任意のキーで指定できるようにし、sticky-shift ライクな操作を可能
;; にします。
;;
;; ~/.skk.el に
;;
;;   (setq skk-sticky-key ";")
;;
;; と書いておくと ";" キーで変換位置が指定できるようになります。例えば
;; 「有る」が
;;
;;   "; a ; r u"
;;
;; でも入力でき、シフトキーを押す必要がなくなります。操作上は
;;
;;   (info "(skk) Q.3-4 左手の小指を SHIFT で酷使したくありません。")
;;
;; などにある通常の sticky-shift と変わりませんが、画面表示は
;;
;;   打鍵  通常のsticky  skk-sticky
;;    ;      変化なし      ▽
;;    a      ▽あ          ▽あ
;;    ;      ▽あ          ▽あ*
;;    r      ▽あ*r        ▽あ*r
;;
;; と遷移します。skk-sticky は ";" を押した時点で表示が変化するので若干
;; 分かり易くなるかと思います。
;;
;; [設定方法]
;; キーの設定方法は割当てるキーの種類によって異なります。
;;
;; 1. 表示可能なキー
;;
;;   ";" などの表示が可能なキーの場合は
;;
;;     (setq skk-sticky-key ";")
;;
;;   のように string を設定して下さい。`skk-sticky-key' に設定した文字
;;   そのものを入力したい場合は2回続けて打つと入力できます。
;;
;; 2. 表示されないキー
;;
;;   "無変換" のような表示を伴わないキーの場合は
;;
;;     (setq skk-sticky-key [muhenkan])	; Windows 環境だと [noconvert]
;;
;;   のようにそのキーを表わす vector を設定して下さい。
;;
;; 3. 同時打鍵
;;
;;   2つのキーを同時に打鍵することでも変換位置を指定できます。例えば
;;   "f" と "j" の同時打鍵で指定する場合は
;;
;;     (setq skk-sticky-key '(?f ?j))
;;
;;   のように character のリストを設定して下さい。
;;   Dvorak 配列のような、押しやすい場所に適当なキーがない環境でもこの
;;   機能を使いたい場合に便利かもしれません。
;;
;; [注意事項]
;; o これはあくまでも「任意のキーで変換位置を指定する」ものであり、
;;   sticky-shift そのものではありません。したがって、latin モードや
;;   abbrev モード、また SKK 以外でも sticky-shift を使いたい場合は前述
;;   の info のような設定を併用する必要があります。
;;
;; o skk-hint.el を併用する場合、`skk-hint-start-char' のデフォルトが
;;   ";" なので設定によっては割当てキーが重複する可能性があります。

;;; TODO

;; * SPC に割当てたいという人も居たりする?

;;; Code:

(eval-when-compile
  (require 'skk-macs)
  (require 'skk-vars))

(defvar skk-sticky-key-orig-output nil)
(skk-deflocalvar skk-sticky-okuri-flag nil)

;;; 初期設定
(cond ((stringp skk-sticky-key)
       (let ((orig-rule (assoc skk-sticky-key
			       (append skk-rom-kana-rule-list
				       skk-rom-kana-base-rule-list))))
	 (setq skk-sticky-key-orig-output (car (cddr orig-rule)))
	 (setq skk-rom-kana-rule-list
	       (cons `(,skk-sticky-key nil skk-sticky-set-henkan-point)
		     (delete orig-rule skk-rom-kana-rule-list)))))
      ((vectorp skk-sticky-key)
       (define-key skk-j-mode-map skk-sticky-key 'skk-sticky-set-henkan-point)))

;;; Functions
(defun skk-sticky-looking-back-okuri-mark ()
  "`point' の直前の文字が `*' ならば t を返す。"
  (and (not (bobp))
       (string= "*" (buffer-substring-no-properties
		     (1- (point)) (point)))))

(defun skk-sticky-set-okuri-mark ()
  "送り開始点を `*' を挿入することで標識し、送りあり変換の待ち状態に入る。"
  (when (eq skk-henkan-mode 'on)
    (unless (and skk-sticky-okuri-flag
		 (skk-sticky-looking-back-okuri-mark))
      (when (and skk-dcomp-activate
		 (skk-dcomp-marked-p))
	(skk-dcomp-before-kakutei))
      (insert-and-inherit "*")
      (setq skk-sticky-okuri-flag t)
      nil)))

(defun skk-sticky-set-henkan-point (&optional arg)
  "変換開始位置もしくは送り開始位置を指定する。"
  (interactive "*P")
  (cond ((not (eq skk-henkan-mode 'on))
	 (skk-set-henkan-point-subr))
	((eq (point) (marker-position skk-henkan-start-point))
	 (let ((data skk-sticky-key-orig-output))
	   (when data
	     (skk-kakutei)
	     (when (functionp data)
	       (setq data (funcall data (skk-make-raw-arg arg))))
	     (when (stringp (if (consp data)
				(car data)
			      data))
	       (if (consp data)
		   (if skk-katakana
		       (car data)
		     (cdr data))
		 data)))))
	(t
	 (skk-sticky-set-okuri-mark))))

(defadvice skk-kakutei (after skk-sticky-ad activate)
  "`skk-sticky-okuri-flag' をクリアする。"
  (setq skk-sticky-okuri-flag nil))

(defadvice keyboard-quit (after skk-sticky-ad activate)
  "`skk-sticky-okuri-flag' をクリアする。"
  (setq skk-sticky-okuri-flag nil))

(defadvice skk-insert (before skk-sticky-ad activate)
  "`*' の直後であれば入力を大文字に変換する。"
  (when (and skk-sticky-okuri-flag
	     (skk-sticky-looking-back-okuri-mark)
	     (string= "" skk-prefix))
    (let ((pair (rassq (skk-last-command-char) skk-downcase-alist)))
      (skk-set-last-command-char (if pair
				     (car pair)
				   (upcase (skk-last-command-char)))))))

(defadvice skk-set-henkan-point (before skk-sticky-ad activate)
  "`point' 直前の `*' を消す。"
  (when (and skk-sticky-okuri-flag
	     (skk-sticky-looking-back-okuri-mark))
    (delete-backward-char 1)))

;; `skk-kana-input' は通常 `cancel-undo-boundary' を呼ぶが、
;; `skk-sticky-key' の2度打ちの際に本来あるべき boundary (C-k の後など)
;; も消してしまうため、advice にて呼ばないように対処する。
;; これにより、`skk-undo-kakutei-word-only' が non-nil でも2度打ちの時
;; に boundary が入ってしまう副作用があるが、先の問題よりはマシだと考え
;; る。
;;; (defadvice skk-kana-input (around skk-sticky-ad activate)
;;;   "▽直後の `skk-sticky-key' の入力の際 `cancel-undo-boundary' を呼ばないように。"
;;;   (if (and (stringp skk-sticky-key)
;;; 	   (eq (skk-last-command-char) (string-to-char skk-sticky-key))
;;; 	   (eq skk-henkan-mode 'on)
;;; 	   (eq (point) (marker-position skk-henkan-start-point)))
;;;       (progn
;;; 	(let ((skk-self-insert-non-undo-count 20))
;;; 	  ad-do-it)
;;; 	(setq skk-self-insert-non-undo-count (1+ skk-self-insert-non-undo-count)))
;;;     ad-do-it))

;;; 同時打鍵関連
(defun skk-sticky-double-p (first next)
  "FIRST と NEXT が同時打鍵であれば non-nil を返す。"
  (let ((char (if (characterp first)
		  first
		(skk-last-command-char))))
    (and (not (eq char next))
	 (memq char skk-sticky-key)
	 (memq next skk-sticky-key))))

(defadvice skk-insert (around skk-sticky-ad-double activate)
  "同時打鍵を検出して処理する。"
  (cond ((not (consp skk-sticky-key))
	 ad-do-it)
	((not (memq (skk-last-command-char) skk-sticky-key))
	 ad-do-it)
	((skk-sit-for skk-sticky-double-interval t)
	 ;; No input in the interval.
	 ad-do-it)
	(t
	 ;; Some key's pressed.
	 (let ((next-event (next-command-event)))
	   (if (skk-sticky-double-p this-command
				    (aref (skk-event-key next-event) 0))
	       (skk-sticky-set-henkan-point)
	     ad-do-it
	     (skk-unread-event next-event))))))

(provide 'skk-sticky)

;;; skk-sticky.el ends here
