branch: externals/ement
commit 639665fc14970436d756a44527d2b768f23f57ab
Merge: e4512f410d 755020c256
Author: Adam Porter <a...@alphapapa.net>
Commit: Adam Porter <a...@alphapapa.net>

    Merge: Improvements and bug fixes for compose buffer enhancements
---
 README.org    |   1 +
 ement-room.el | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 174 insertions(+), 13 deletions(-)

diff --git a/README.org b/README.org
index f79c497c96..af79b6ef24 100644
--- a/README.org
+++ b/README.org
@@ -301,6 +301,7 @@ Ement.el doesn't support encrypted rooms natively, but it 
can be used transparen
 
 + A variety of enhancements for using compose buffers.  
([[https://github.com/alphapapa/ement.el/issues/140][#140]].  Thanks to 
[[https://github.com/phil-s][Phil Sainty]].)
   - Option ~ement-room-compose-buffer-display-action~ declares how and where a 
new compose buffer window should be displayed.  (By default, in a new window 
below the associated room buffer.)
+  - Option ~ement-room-compose-buffer-window-dedicated~ determines whether 
compose buffers will have dedicated windows.
   - Option ~ement-room-compose-buffer-window-auto-height~ causes dynamic 
scaling of the compose buffer window height so that the full message is visible 
at all times.
   - Option ~ement-room-compose-buffer-window-auto-height-min~ specifies the 
minimum window height when ~ement-room-compose-buffer-window-auto-height~ is 
enabled.
   - Option ~ement-room-compose-buffer-window-auto-height-max~ specifies the 
maximum window height when ~ement-room-compose-buffer-window-auto-height~ is 
enabled.
diff --git a/ement-room.el b/ement-room.el
index d2d742f64f..90ca6ca1a7 100644
--- a/ement-room.el
+++ b/ement-room.el
@@ -186,8 +186,8 @@ keymap directly the issue may be visible.")
 
     ;; Messages
     (define-key map (kbd "RET") #'ement-room-dispatch-new-message)
-    (define-key map (kbd "S-<return>") #'ement-room-dispatch-reply-to-message)
     (define-key map (kbd "M-RET") #'ement-room-dispatch-new-message-alt)
+    (define-key map (kbd "S-<return>") #'ement-room-dispatch-reply-to-message)
     (define-key map (kbd "<insert>") #'ement-room-dispatch-edit-message)
     (define-key map (kbd "C-k") #'ement-room-delete-message)
     (define-key map (kbd "s r") #'ement-room-send-reaction)
@@ -498,19 +498,55 @@ sends the composed message directly."
         '((window-height . 3)
           (inhibit-same-window . t)
           (reusable-frames . nil)))
-  "`display-buffer' action for displaying compose buffers."
+  "`display-buffer' action for displaying compose buffers.
+
+See also `ement-room-compose-buffer-window-auto-height' and
+`ement-room-compose-buffer-window-dedicated'."
   :type display-buffer--action-custom-type
   :risky t)
 
+(defcustom ement-room-compose-buffer-window-dedicated 'created
+  "Whether windows for compose buffers should be dedicated.
+
+A dedicated compose buffer window will not be used to display any
+other buffer, and will be deleted once the message has been sent
+or aborted (see `ement-room-compose-buffer-quit-restore-window').
+
+The values t and nil mean \"always\" and \"never\" respectively.
+
+The value `created' means newly-created windows are dedicated.
+\(The default `ement-room-compose-buffer-display-action' always
+creates a new window.)
+
+The value `auto-height' means that windows will be dedicated if
+the option `ement-room-compose-buffer-window-auto-height' is
+enabled (this option generally keeps the windows too small to
+usefully display other buffers).
+
+See also `set-window-dedicated-p'."
+  :type '(radio (const :tag "Always" t)
+                (const :tag "Never" nil)
+                (const :tag "Newly-created windows" created)
+                (const :tag "When auto-height enabled" auto-height)))
+
 (defcustom ement-room-compose-buffer-window-auto-height t
   "Dynamically match the compose buffer window height to its contents.
 See also `ement-room-compose-buffer-window-auto-height-max' and
 `ement-room-compose-buffer-window-auto-height-min'."
   :type 'boolean)
 
+;; Experimental.  Disabled by default.  Set to 'height to use this.
+(defvar ement-room-compose-buffer-window-auto-height-fixed nil
+  "The buffer-local `window-size-fixed' value in compose buffers.")
+
 (defvar ement-room-compose-buffer-window-auto-height-pixelwise t
   "Whether to adjust the window height for pixel-precise lines.")
 
+;; This is a mutex to ensure that auto-height resizing cannot trigger itself
+;; recursively.  This may prevent desirable resizing in certain cases, but we
+;; get the correct result in the majority of situations, and it is simple.
+(defvar ement-room-compose-buffer-window-auto-height-resizing-p)
+
 (defvar ement-room-compose-buffer-window-auto-height-cache)
 
 (defcustom ement-room-compose-buffer-window-auto-height-min nil
@@ -4224,7 +4260,7 @@ To be called from a minibuffer opened from
         (editing-event ement-room-editing-event)
         (room ement-room)
         (session ement-session))
-    (quit-restore-window nil 'kill)
+    (ement-room-compose-buffer-quit-restore-window)
     (ement-view-room room session)
     (add-to-history 'ement-room-message-history body)
     (list body input-method send-message-filter replying-to-event 
editing-event room session)))
@@ -4285,8 +4321,7 @@ With prefix arg NO-HISTORY, do not add to 
`ement-room-message-history'."
         (room ement-room))
     (unless no-history
       (add-to-history 'ement-room-message-history body))
-    (kill-buffer)
-    (delete-window)
+    (ement-room-compose-buffer-quit-restore-window)
     ;; Make sure we end up with the associated room buffer selected.
     (when-let ((win (catch 'room-win
                       (walk-windows
@@ -4355,15 +4390,46 @@ a copy of the local keymap, and sets 
`header-line-format'."
   ;; Adjust the window height automatically.
   (when ement-room-compose-buffer-window-auto-height
     (add-hook 'post-command-hook
-              #'ement-room-compose-buffer-window-auto-height nil :local)))
+              #'ement-room-compose-buffer-window-auto-height nil :local)
+    ;; Our `window-min-height' comprises header & mode line + body lines.
+    (setq-local window-min-height
+                (+ 2 (if ement-room-compose-buffer-window-auto-height-min
+                         (max 1 
ement-room-compose-buffer-window-auto-height-min)
+                       1)))
+    (when ement-room-compose-buffer-window-auto-height-fixed
+      (setq-local window-size-fixed
+                  ement-room-compose-buffer-window-auto-height-fixed))
+    ;; The following helps when `window--sanitize-window-sizes' adjusts all
+    ;; windows in a frame (e.g. when splitting windows), as otherwise any
+    ;; existing compose buffer windows are liable to be resized line-wise,
+    ;; resulting in excess padding being introduced.
+    (when ement-room-compose-buffer-window-auto-height-pixelwise
+      (setq-local window-resize-pixelwise t)))
+  ;; Other compose buffer window behaviours.
+  (add-hook 'window-state-change-functions
+            #'ement-room-compose-buffer-window-state-change-handler nil :local)
+  (add-hook 'window-buffer-change-functions
+            #'ement-room-compose-buffer-window-buffer-change-handler nil 
:local))
 
 (defun ement-room-compose-buffer-window-auto-height ()
   "Ensure that the compose buffer displays the whole message.
 
 Called via `post-command-hook' if 
`ement-room-compose-buffer-window-auto-height'
 is non-nil."
-  ;; Manipulate the window body height.
-  (unless (window-full-height-p)
+  ;; We use `post-command-hook' (rather than, say, `after-change-functions'),
+  ;; because the required window height might change for reasons other than 
text
+  ;; editing (e.g. changes to the window's width or the font size).
+  ;;
+  ;; Note that changes to the default face size (e.g. via `text-scale-adjust')
+  ;; affect `default-line-height', invalidating the cache even when the text
+  ;; itself didn't change.
+  ;;
+  ;; The following may also clear the cache in order to force a recalculation:
+  ;; - `ement-room-compose-buffer-window-state-change-handler'
+  ;; - `ement-room-compose-buffer-window-buffer-change-handler'
+  (unless (or (bound-and-true-p 
ement-room-compose-buffer-window-auto-height-resizing-p)
+              (window-full-height-p))
+    ;; Manipulate the window body height.
     (let* ((pixelwise (and 
ement-room-compose-buffer-window-auto-height-pixelwise
                            (display-graphic-p)))
            (lineheight (and pixelwise (default-line-height)))
@@ -4376,7 +4442,8 @@ is non-nil."
                    (eql cache 
ement-room-compose-buffer-window-auto-height-cache))
         ;; Otherwise resize the window...
         (setq-local ement-room-compose-buffer-window-auto-height-cache cache)
-        (let* ((minheight (if ement-room-compose-buffer-window-auto-height-min
+        (let* ((ement-room-compose-buffer-window-auto-height-resizing-p t)
+               (minheight (if ement-room-compose-buffer-window-auto-height-min
                               (max 1 
ement-room-compose-buffer-window-auto-height-min)
                             1))
                (maxheight ement-room-compose-buffer-window-auto-height-max)
@@ -4403,6 +4470,13 @@ is non-nil."
             (let ((delta (- reqlines (window-body-height))))
               (when-let ((delta (window-resizable nil delta nil t)))
                 (window-resize nil delta nil t))))
+          ;; Ask Emacs to "preserve" the new height.  So long as the window
+          ;; maintains this height and is displaying this specific buffer, 
Emacs
+          ;; will avoid unnecessary height changes from side-effects of 
commands
+          ;; such as `balance-windows'.  Explicit height changes are allowed.
+          ;; We must update this parameter every time we change the height so
+          ;; that the "preserved" height value is always correct.
+          (window-preserve-size nil nil t)
           ;; In most cases we can fit the whole buffer in the resized window.
           (set-window-start nil (point-min) :noforce)
           ;; The resizing might have obscured the room buffer's window point, 
so
@@ -4411,6 +4485,92 @@ is non-nil."
           (let ((scroll-conservatively 101))
             (redisplay)))))))
 
+(defun ement-room-compose-buffer-window-state-change-handler (win)
+  "Called via buffer-local `window-state-change-functions' in compose buffers.
+
+Called for any window WIN showing a compose buffer if that window
+has been added or assigned another buffer, changed size, or been
+selected or deselected.
+
+This prevents a compose buffer window being stuck at the wrong
+height (until the number of lines changes again) if something
+other than the auto-height feature resizes the window.  We simply
+flush the height cache for these state changes, thus ensuring the
+required height is recalculated on the next cycle).
+
+See also `ement-room-compose-buffer-window-buffer-change-handler'."
+  (when ement-room-compose-buffer-window-auto-height
+    ;; Ignore the window state changes triggered by our auto-height resizing.
+    (unless (bound-and-true-p 
ement-room-compose-buffer-window-auto-height-resizing-p)
+      (let ((buf (current-buffer)))
+        ;; Do nothing if the state change happened while the compose buffer was
+        ;; current, as the buffer-local `post-command-hook' is already dealing
+        ;; with that case.  We only care about window state changes which are
+        ;; triggered from elsewhere.  This means that we skip the case whereby
+        ;; the selected window has just switched to the compose buffer, and so
+        ;; we use `window-buffer-change-functions' as well to capture that 
case.
+        ;; (See `ement-room-compose-buffer-window-buffer-change-handler'.)
+        (with-selected-window win
+          (unless (eq (current-buffer) buf)
+            ;; Clear the height cache for this compose buffer.
+            (when (bound-and-true-p 
ement-room-compose-buffer-window-auto-height-cache)
+              (setq-local ement-room-compose-buffer-window-auto-height-cache 
nil))))))))
+
+(defun ement-room-compose-buffer-window-buffer-change-handler (win)
+  "Called via buffer-local `window-buffer-change-functions' in compose buffers.
+
+Called for any window WIN showing a compose buffer if that window
+has just been created or assigned that buffer.
+
+Like `ement-room-compose-buffer-window-state-change-handler' (see
+which) in purpose, but handling the case where WIN has just
+switched to displaying the compose buffer (which the other
+handler ignores as a side-effect of its conditional logic).
+
+This function also determines whether the composer buffer's
+window has been newly created, which affects the behaviour of
+`ement-room-compose-buffer-quit-restore-window'."
+  (with-selected-window win
+    ;; Clear the height cache for this compose buffer.
+    (when ement-room-compose-buffer-window-auto-height
+      (when (bound-and-true-p 
ement-room-compose-buffer-window-auto-height-cache)
+        (setq-local ement-room-compose-buffer-window-auto-height-cache nil)
+        ;; `window-buffer-change-functions' runs /after/ `post-command-hook',
+        ;; so after flushing the cache we need to invoke the resize handler, as
+        ;; otherwise it wouldn't update until after the /subsequent/ command.
+        ;; We don't want to do this while this hook is still running though
+        ;; (the result is inaccurate), so defer it with a timer.
+        (unless (bound-and-true-p 
ement-room-compose-buffer-window-auto-height-resizing-p)
+          (run-with-timer 0 nil (lambda (win)
+                                  (with-selected-window win
+                                    
(ement-room-compose-buffer-window-auto-height)))
+                          win))))
+    ;; Process `ement-room-compose-buffer-window-dedicated' when the compose
+    ;; buffer is first displayed, to decide whether the window should be
+    ;; dedicated to the buffer.
+    (unless (assq 'ement-room-compose-buffer-window-created-p 
(window-parameters win))
+      (let ((createdp (not (window-prev-buffers))))
+        (set-window-parameter win 'ement-room-compose-buffer-window-created-p
+                              createdp)
+        (when (cl-case ement-room-compose-buffer-window-dedicated
+                (created createdp)
+                (auto-height ement-room-compose-buffer-window-auto-height)
+                (t ement-room-compose-buffer-window-dedicated))
+          (set-window-dedicated-p nil t))))))
+
+(defun ement-room-compose-buffer-quit-restore-window ()
+  "Kill the current compose buffer and deal appropriately with its window.
+
+The default `ement-room-compose-buffer-window-dedicated' value
+ensures that the window is dedicated and therefore that it will
+be deleted.
+
+A non-dedicated window which has displayed another buffer at any
+point will not be deleted."
+  ;; N.b. This function exists primarily for documentation purposes,
+  ;; to clarify the side-effect of using a dedicated window.
+  (quit-restore-window nil 'kill))
+
 (declare-function dabbrev--select-buffers "dabbrev")
 
 (defun ement-compose-dabbrev-select-buffers ()
@@ -5413,10 +5573,10 @@ For use in `completion-at-point-functions'."
                                                                         (cons 
"Org-mode" 'ement-room-send-org-filter))
                                                                   :test 
#'equal))
                                                   'face 'transient-value))))
-              ("RET" "Write message" ement-room-send-message)
-              ("S-RET" "Write reply" ement-room-write-reply)
-              ("M-RET" "Compose message in buffer" ement-room-compose-message)
-              ("<insert>" "Edit message" ement-room-edit-message)
+              ("RET" "Write message" ement-room-dispatch-new-message)
+              ("M-RET" "Write message (alternative)" 
ement-room-dispatch-new-message-alt)
+              ("S-<return>" "Write reply" ement-room-dispatch-reply-to-message)
+              ("<insert>" "Edit message" ement-room-dispatch-edit-message)
               ("C-k" "Delete message" ement-room-delete-message)
               ("s r" "Send reaction" ement-room-send-reaction)
               ("s e" "Send emote" ement-room-send-emote)

Reply via email to