diff --git a/exwm-floating.el b/exwm-floating.el index 88fa207..ff584c0 100644 --- a/exwm-floating.el +++ b/exwm-floating.el @@ -106,8 +106,10 @@ ;; FIXME: check normal hints restrictions (let* ((display-width (frame-pixel-width original-frame)) (display-height (- (frame-pixel-height original-frame) - (window-pixel-height (minibuffer-window - original-frame)) + (if exwm-workspace-minibuffer-position + 0 + (window-pixel-height (minibuffer-window + original-frame))) (* 2 (window-mode-line-height)) (window-header-line-height window) (* 2 exwm-floating-border-width))) @@ -180,12 +182,22 @@ (interactive) (let ((buffer (exwm--id->buffer id))) (with-current-buffer buffer + ;; Reparent the container to the workspace (xcb:+request exwm--connection (make-instance 'xcb:ReparentWindow :window exwm--container :parent (frame-parameter exwm-workspace--current 'exwm-workspace) - :x 0 :y 0))) ;temporary position + :x 0 :y 0)) ;temporary position + ;; Put the container just above the Emacs frame + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window exwm--container + :value-mask (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling (frame-parameter exwm-workspace--current + 'exwm-outer-id) + :stack-mode xcb:StackMode:Above))) (xcb:flush exwm--connection) (with-current-buffer buffer (when exwm--floating-frame ;from floating to non-floating diff --git a/exwm-input.el b/exwm-input.el index a5899d5..757efb7 100644 --- a/exwm-input.el +++ b/exwm-input.el @@ -110,26 +110,25 @@ It's updated in several occasions, and only used by `exwm-input--set-focus'.") (exwm--log "Set focus on #x%x" exwm--id) (exwm-input--set-focus exwm--id) ;; Adjust stacking orders - (if exwm--floating-frame + (when exwm--floating-frame + (if (memq exwm-workspace-minibuffer-position '(top bottom)) + ;; Put this floating X window just below the minibuffer. + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window exwm--container + :value-mask + (logior xcb:ConfigWindow:Sibling + xcb:ConfigWindow:StackMode) + :sibling (frame-parameter + exwm-workspace--minibuffer + 'exwm-container) + :stack-mode xcb:StackMode:Below)) ;; Put this floating X window at top. (xcb:+request exwm--connection (make-instance 'xcb:ConfigureWindow :window exwm--container :value-mask xcb:ConfigWindow:StackMode - :stack-mode xcb:StackMode:TopIf)) - ;; This should be the last X window but one in the siblings. - (with-slots (children) - (xcb:+request-unchecked+reply exwm--connection - (make-instance 'xcb:QueryTree - :window - (frame-parameter exwm--frame - 'exwm-workspace))) - (unless (eq (cadr children) exwm--container) - (xcb:+request exwm--connection - (make-instance 'xcb:ConfigureWindow - :window exwm--container - :value-mask xcb:ConfigWindow:StackMode - :stack-mode xcb:StackMode:Below))))) + :stack-mode xcb:StackMode:Above)))) ;; Make sure Emacs frames are at bottom. (xcb:+request exwm--connection (make-instance 'xcb:ConfigureWindow @@ -142,21 +141,9 @@ It's updated in several occasions, and only used by `exwm-input--set-focus'.") (when (eq (selected-window) exwm-input--focus-window) (exwm--log "Focus on %s" exwm-input--focus-window) (select-frame-set-input-focus (window-frame exwm-input--focus-window) - t) - (xcb:+request exwm--connection - (make-instance 'xcb:ConfigureWindow - :window (frame-parameter - (window-frame exwm-input--focus-window) - 'exwm-outer-id) - :value-mask xcb:ConfigWindow:StackMode - :stack-mode xcb:StackMode:Below)) - (xcb:flush exwm--connection))) + t))) (setq exwm-input--focus-window nil)))) -(defun exwm-input--on-minibuffer-setup () - "Run in minibuffer-setup-hook to set input focus to the frame." - (x-focus-frame (selected-frame))) - (defvar exwm-input--during-key-sequence nil "Non-nil indicates Emacs is waiting for more keys to form a key sequence.") (defvar exwm-input--temp-line-mode nil @@ -278,12 +265,13 @@ It's updated in several occasions, and only used by `exwm-input--set-focus'.") (global-set-key key command) (cl-pushnew key exwm-input--global-keys)) -;; These commands usually call something like `read-char' without using the -;; minibuffer, so they will not get inputs after invoked. It'd be better if we -;; can determine whether there's a command waiting for input so that this -;; variable can be removed. (defvar exwm-input-command-whitelist nil "A list of commands that when active all keys should be forwarded to Emacs.") +(make-obsolete-variable 'exwm-input-command-whitelist + "This variable can be safely removed." "25.1") + +(defvar exwm-input--during-command nil + "Indicate whether between `pre-command-hook' and `post-command-hook'.") ;;;###autoload (defun exwm-input--on-KeyPress-line-mode (key-press) @@ -295,8 +283,8 @@ It's updated in several occasions, and only used by `exwm-input--set-focus'.") (setq event (xcb:keysyms:keysym->event exwm--connection keysym state)) (or exwm-input--during-key-sequence + exwm-input--during-command (setq minibuffer-window (active-minibuffer-window)) - (memq real-this-command exwm-input-command-whitelist) (memq event exwm-input--global-prefix-keys) (memq event exwm-input-prefix-keys) (memq event exwm-input--simulation-prefix-keys))) @@ -501,9 +489,12 @@ SIMULATION-KEYS is a list of alist (key-sequence1 . key-sequence2)." #'exwm-floating--do-moveresize) ;; `pre-command-hook' marks the end of a key sequence (existing or not) (add-hook 'pre-command-hook #'exwm-input--finish-key-sequence) + ;; Control `exwm-input--during-command' + (add-hook 'pre-command-hook (lambda () (setq exwm-input--during-command t))) + (add-hook 'post-command-hook + (lambda () (setq exwm-input--during-command nil))) ;; Update focus when buffer list updates (add-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update) - (add-hook 'minibuffer-setup-hook #'exwm-input--on-minibuffer-setup) ;; Update prefix keys for global keys (exwm-input--update-global-prefix-keys)) diff --git a/exwm-layout.el b/exwm-layout.el index a290876..316bf62 100644 --- a/exwm-layout.el +++ b/exwm-layout.el @@ -279,10 +279,20 @@ "Refresh layout when minibuffer grows." (run-with-idle-timer 0.01 nil ;FIXME (lambda () - (when (and (< 1 (window-height (minibuffer-window))) - (not (and (eq major-mode 'exwm-mode) - exwm--floating-frame))) - (exwm-layout--refresh))))) + (when (< 1 (window-height (minibuffer-window))) + (exwm-layout--refresh)))) + ;; Set input focus on the Emacs frame + (x-focus-frame (window-frame (minibuffer-selected-window)))) + +(defun exwm-layout--on-echo-area-change (&optional dirty) + "Run when message arrives or in `echo-area-clear-hook' to refresh layout." + (when (and (current-message) + (or (cl-position ?\n (current-message)) + (> (length (current-message)) + (frame-width exwm-workspace--current)))) + (if dirty + (exwm-layout--refresh) + (run-with-idle-timer 0.01 nil #'exwm-layout--refresh)))) ;FIXME (defun exwm-layout-enlarge-window (delta &optional horizontal) "Make the selected window DELTA pixels taller. @@ -383,8 +393,11 @@ See also `exwm-layout-enlarge-window'." "Initialize layout module." ;; Auto refresh layout (add-hook 'window-configuration-change-hook #'exwm-layout--refresh) - ;; Refresh when minibuffer grows - (add-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup t)) + (unless (memq exwm-workspace-minibuffer-position '(top bottom)) + ;; Refresh when minibuffer grows + (add-hook 'minibuffer-setup-hook #'exwm-layout--on-minibuffer-setup t) + (run-with-idle-timer 0 t #'exwm-layout--on-echo-area-change t) + (add-hook 'echo-area-clear-hook #'exwm-layout--on-echo-area-change))) diff --git a/exwm-manage.el b/exwm-manage.el index 2f35b14..b32d677 100644 --- a/exwm-manage.el +++ b/exwm-manage.el @@ -360,8 +360,7 @@ Would you like to kill it? " (let ((obj (make-instance 'xcb:ConfigureRequest)) buffer edges) (xcb:unmarshal obj data) - (with-slots (stack-mode window sibling x y width height border-width - value-mask) + (with-slots (window x y width height border-width value-mask) obj (exwm--log "ConfigureRequest from #x%x (#x%x) @%dx%d%+d%+d, border: %d" window value-mask width height x y border-width) @@ -391,14 +390,16 @@ Would you like to kill it? " :border-width 0 :override-redirect 0) exwm--connection)))) (exwm--log "ConfigureWindow (preserve geometry)") - ;; Configure unmanaged windows + ;; Configure the unmanaged window without changing the stacking order. (xcb:+request exwm--connection (make-instance 'xcb:ConfigureWindow :window window - :value-mask value-mask + :value-mask + (logand value-mask + (lognot xcb:ConfigWindow:Sibling) + (lognot xcb:ConfigWindow:StackMode)) :x x :y y :width width :height height - :border-width border-width - :sibling sibling :stack-mode stack-mode))))) + :border-width border-width))))) (xcb:flush exwm--connection)) (defun exwm-manage--on-MapRequest (data _synthetic) diff --git a/exwm-workspace.el b/exwm-workspace.el index 8bdcc63..9669428 100644 --- a/exwm-workspace.el +++ b/exwm-workspace.el @@ -24,7 +24,6 @@ ;; This module adds workspace support for EXWM. ;; Todo: -;; + Auto hide minibuffer, or allow users to place it elsewhere. ;; + Add system tray support. ;;; Code: @@ -100,6 +99,17 @@ (defvar exwm-workspace-current-index 0 "Index of current active workspace.") (defvar exwm-workspace-show-all-buffers nil "Non-nil to show buffers on other workspaces.") +(defvar exwm-workspace--minibuffer nil + "The minibuffer frame shared among all frames.") +(defvar exwm-workspace-minibuffer-position nil + "Position of the minibuffer frame. + +Value nil means to use the default position which is fixed at bottom, while +'top and 'bottom mean to use an auto-hiding minibuffer.") +(defvar exwm-workspace-display-echo-area-timeout 1 + "Timeout for displaying echo area.") +(defvar exwm-workspace--display-echo-area-timer nil + "Timer for auto-hiding echo area.") ;;;###autoload (defun exwm-workspace-switch (index &optional force) @@ -135,7 +145,30 @@ The optional FORCE option is for internal use only." ;; Close the (possible) active minibuffer (when (active-minibuffer-window) (run-with-idle-timer 0 nil (lambda () (abort-recursive-edit)))) - (setq default-minibuffer-frame frame) + (if (not (memq exwm-workspace-minibuffer-position '(top bottom))) + (setq default-minibuffer-frame frame) + ;; Resize/reposition the minibuffer frame + (let ((x 0) + (y (if (eq exwm-workspace-minibuffer-position 'top) + 0 + (- (frame-pixel-height frame) + (frame-pixel-height exwm-workspace--minibuffer)))) + (width (x-display-pixel-width)) + (container (frame-parameter exwm-workspace--minibuffer + 'exwm-container))) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window container + :parent (frame-parameter frame 'exwm-workspace) + :x x :y y)) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window container + :value-mask (logior xcb:ConfigWindow:Width + xcb:ConfigWindow:StackMode) + :width width + :stack-mode xcb:StackMode:Above)) + (set-frame-width exwm-workspace--minibuffer width nil t))) ;; Hide windows in other workspaces by preprending a space (unless exwm-workspace-show-all-buffers (dolist (i exwm--id-buffer-alist) @@ -266,6 +299,146 @@ The optional FORCE option is for internal use only." (xcb:flush exwm--connection) frame)) +(defun exwm-workspace--update-minibuffer (&optional echo-area) + "Update the minibuffer frame." + (let ((height + (with-current-buffer + (window-buffer (minibuffer-window exwm-workspace--minibuffer)) + (max 1 + (if echo-area + (let ((width (frame-width exwm-workspace--minibuffer)) + (result 0)) + (mapc (lambda (i) + (setq result + (+ result + (ceiling (1+ (length i)) width)))) + (split-string (current-message) "\n")) + result) + (count-screen-lines)))))) + (when (and (integerp max-mini-window-height) + (> height max-mini-window-height)) + (setq height max-mini-window-height)) + (set-frame-height exwm-workspace--minibuffer height))) + +(defun exwm-workspace--on-ConfigureNotify (data _synthetic) + "Adjust the container to fit the minibuffer frame." + (let ((obj (make-instance 'xcb:ConfigureNotify)) + value-mask y) + (xcb:unmarshal obj data) + (with-slots (window height) obj + (when (eq (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id) + window) + (when (and (floatp max-mini-window-height) + (> height (* max-mini-window-height + (frame-pixel-height exwm-workspace--current)))) + (setq height (floor + (* max-mini-window-height + (frame-pixel-height exwm-workspace--current)))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window window + :value-mask xcb:ConfigWindow:Height + :height height))) + (if (eq exwm-workspace-minibuffer-position 'top) + (setq value-mask xcb:ConfigWindow:Height + y 0) + (setq value-mask (logior xcb:ConfigWindow:Y xcb:ConfigWindow:Height) + y (- (frame-pixel-height exwm-workspace--current) height))) + (xcb:+request exwm--connection + (make-instance 'xcb:ConfigureWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container) + :value-mask value-mask + :y y + :height height)) + (xcb:flush exwm--connection))))) + +(defun exwm-workspace--display-buffer (buffer alist) + "Display buffer in the current workspace frame. + +This functions is modified from `display-buffer-reuse-window' and +`display-buffer-pop-up-window'." + (when (eq (selected-frame) exwm-workspace--minibuffer) + (setq buffer (get-buffer buffer)) ;convert string to buffer. + (let (window) + (if (setq window (get-buffer-window buffer exwm-workspace--current)) + (window--display-buffer buffer window 'reuse alist) + (when (setq window (or (window--try-to-split-window + (get-largest-window exwm-workspace--current t) + alist) + (window--try-to-split-window + (get-lru-window exwm-workspace--current t) + alist))) + (window--display-buffer + buffer window 'window alist display-buffer-mark-dedicated)))))) + +(defun exwm-workspace--show-minibuffer () + "Show the minibuffer frame." + ;; Cancel pending timer. + (when exwm-workspace--display-echo-area-timer + (cancel-timer exwm-workspace--display-echo-area-timer) + (setq exwm-workspace--display-echo-area-timer nil)) + ;; Show the minibuffer frame. + (xcb:+request exwm--connection + (make-instance 'xcb:MapWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container))) + (xcb:flush exwm--connection) + ;; Unfortunately we need the following lines to workaround a cursor + ;; flickering issue for line-mode floating X windows. They just make the + ;; minibuffer appear to be focused. + (with-current-buffer (window-buffer (minibuffer-window + exwm-workspace--minibuffer)) + (setq cursor-in-non-selected-windows + (frame-parameter exwm-workspace--minibuffer 'cursor-type)))) + +(defun exwm-workspace--hide-minibuffer () + "Hide the minibuffer frame." + ;; Hide the minibuffer frame. + (xcb:+request exwm--connection + (make-instance 'xcb:UnmapWindow + :window (frame-parameter exwm-workspace--minibuffer + 'exwm-container))) + (xcb:flush exwm--connection)) + +(defun exwm-workspace--on-minibuffer-setup () + "Run in minibuffer-setup-hook to show the minibuffer and its container." + (unless (> -1 (minibuffer-depth)) + (add-hook 'post-command-hook #'exwm-workspace--update-minibuffer) + (exwm-workspace--show-minibuffer) + ;; Set input focus on the Emacs frame + (x-focus-frame (window-frame (minibuffer-selected-window))))) + +(defun exwm-workspace--on-minibuffer-exit () + "Run in minibuffer-exit-hook to hide the minibuffer container." + (unless (> -1 (minibuffer-depth)) + (remove-hook 'post-command-hook #'exwm-workspace--update-minibuffer) + (exwm-workspace--hide-minibuffer))) + +(defvar exwm-input--during-command) + +(defun exwm-workspace--on-echo-area-dirty () + "Run when new message arrives to show the echo area and its container." + (when (and (not (active-minibuffer-window)) + (or (current-message) + cursor-in-echo-area)) + (exwm-workspace--update-minibuffer t) + (exwm-workspace--show-minibuffer) + (unless (or (not exwm-workspace-display-echo-area-timeout) + exwm-input--during-command ;e.g. read-event + input-method-use-echo-area) + (setq exwm-workspace--display-echo-area-timer + (run-with-timer exwm-workspace-display-echo-area-timeout nil + #'exwm-workspace--on-echo-area-clear))))) + +(defun exwm-workspace--on-echo-area-clear () + "Run in echo-area-clear-hook to hide echo area container." + (unless (active-minibuffer-window) + (exwm-workspace--hide-minibuffer)) + (when exwm-workspace--display-echo-area-timer + (cancel-timer exwm-workspace--display-echo-area-timer) + (setq exwm-workspace--display-echo-area-timer nil))) + (defun exwm-workspace--init () "Initialize workspace module." (cl-assert (and (< 0 exwm-workspace-number) (>= 10 exwm-workspace-number))) @@ -276,19 +449,79 @@ The optional FORCE option is for internal use only." (0 (y-or-n-p prompt)) (x (yes-or-no-p (format "[EXWM] %d window%s currently alive. %s" x (if (= x 1) "" "s") prompt)))))) - ;; Initialize workspaces - (setq exwm-workspace--list (frame-list)) - (when (< 1 (length exwm-workspace--list)) - ;; Emacs client creates an extra (but unusable) frame - (dolist (i exwm-workspace--list) - (unless (frame-parameter i 'window-id) - (setq exwm-workspace--list (delq i exwm-workspace--list)))) - (cl-assert (= 1 (length exwm-workspace--list))) - ;; Prevent user from deleting this frame by accident - (set-frame-parameter (car exwm-workspace--list) 'client nil)) - ;; Create remaining frames - (dotimes (_ (1- exwm-workspace-number)) - (nconc exwm-workspace--list (list (make-frame '((window-system . x)))))) + (if (not (memq exwm-workspace-minibuffer-position '(top bottom))) + ;; Initialize workspaces with minibuffers. + (progn + (setq exwm-workspace--list (frame-list)) + (when (< 1 (length exwm-workspace--list)) + ;; Emacs client creates an extra (but unusable) frame. + (dolist (i exwm-workspace--list) + (unless (frame-parameter i 'window-id) + (setq exwm-workspace--list (delq i exwm-workspace--list)))) + (cl-assert (= 1 (length exwm-workspace--list))) + ;; Prevent user from deleting this frame by accident. + (set-frame-parameter (car exwm-workspace--list) 'client nil)) + ;; Create remaining frames. + (dotimes (_ (1- exwm-workspace-number)) + (nconc exwm-workspace--list + (list (make-frame '((window-system . x))))))) + ;; Initialize workspaces without minibuffers. + (let ((old-frames (frame-list))) + (setq exwm-workspace--minibuffer + (make-frame '((window-system . x) (minibuffer . only) + (left . 10000) (right . 10000) + (width . 0) (height . 0))) + default-minibuffer-frame exwm-workspace--minibuffer) + ;; Remove/hide existing frames. + (dolist (f old-frames) + (if (frame-parameter f 'client) + (make-frame-invisible f) + (delete-frame f)))) + (let ((outer-id (string-to-number + (frame-parameter exwm-workspace--minibuffer + 'outer-window-id))) + (container (xcb:generate-id exwm--connection))) + (set-frame-parameter exwm-workspace--minibuffer 'exwm-outer-id outer-id) + (set-frame-parameter exwm-workspace--minibuffer 'exwm-container + container) + (xcb:+request exwm--connection + (make-instance 'xcb:CreateWindow + :depth 0 :wid container :parent exwm--root + :x -1 :y -1 :width 1 :height 1 + :border-width 0 :class xcb:WindowClass:CopyFromParent + :visual 0 ;CopyFromParent + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + (exwm--debug + (xcb:+request exwm--connection + (make-instance 'xcb:ewmh:set-_NET_WM_NAME + :window container + :data "Minibuffer container"))) + (xcb:+request exwm--connection + (make-instance 'xcb:ReparentWindow + :window outer-id :parent container :x 0 :y 0)) + ;; Attach event listener for monitoring the frame + (xcb:+request exwm--connection + (make-instance 'xcb:ChangeWindowAttributes + :window outer-id + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:StructureNotify)) + (xcb:+event exwm--connection 'xcb:ConfigureNotify + #'exwm-workspace--on-ConfigureNotify)) + ;; Show/hide minibuffer / echo area when they're active/inactive. + (add-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup) + (add-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit) + (run-with-idle-timer 0 t #'exwm-workspace--on-echo-area-dirty) + (add-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear) + ;; Create workspace frames. + (dotimes (_ exwm-workspace-number) + (push (make-frame `((window-system . x) + (minibuffer . ,(minibuffer-window + exwm-workspace--minibuffer)))) + exwm-workspace--list)) + ;; The default behavior of `display-buffer' (indirectly called by + ;; `minibuffer-completion-help') is not correct here. + (cl-pushnew '(exwm-workspace--display-buffer) display-buffer-alist)) ;; Configure workspaces (dolist (i exwm-workspace--list) (let ((outer-id (string-to-number (frame-parameter i 'outer-window-id))) @@ -296,11 +529,6 @@ The optional FORCE option is for internal use only." ;; Save window IDs (set-frame-parameter i 'exwm-outer-id outer-id) (set-frame-parameter i 'exwm-workspace workspace) - ;; Set OverrideRedirect on all frames - (xcb:+request exwm--connection - (make-instance 'xcb:ChangeWindowAttributes - :window outer-id :value-mask xcb:CW:OverrideRedirect - :override-redirect 1)) (xcb:+request exwm--connection (make-instance 'xcb:CreateWindow :depth 0 :wid workspace :parent exwm--root diff --git a/exwm.el b/exwm.el index 02d4d9c..720274f 100644 --- a/exwm.el +++ b/exwm.el @@ -466,9 +466,13 @@ (make-instance 'xcb:ewmh:set-_NET_DESKTOP_VIEWPORT :window exwm--root :data (make-vector (* 2 exwm-workspace-number) 0))) - ;; Set _NET_WORKAREA (with minibuffer and bottom mode-line excluded) + ;; Set _NET_WORKAREA (with minibuffer excluded) (let* ((workareas - (vector 0 0 (x-display-pixel-width) (x-display-pixel-height))) + (vector 0 0 (x-display-pixel-width) + (- (x-display-pixel-height) + (if exwm-workspace-minibuffer-position + 0 + (window-pixel-height (minibuffer-window)))))) (workareas (mapconcat (lambda (_) workareas) (make-list exwm-workspace-number 0) []))) (xcb:+request exwm--connection