lisp/dired.el (dired-insert-directory): Revert change in 2013-06-21T12:24:37Z!lekktu...
[bpt/emacs.git] / lisp / frameset.el
CommitLineData
9421876d
JB
1;;; frameset.el --- save and restore frame and window setup -*- lexical-binding: t -*-
2
3;; Copyright (C) 2013 Free Software Foundation, Inc.
4
5;; Author: Juanma Barranquero <lekktu@gmail.com>
6;; Keywords: convenience
7
8;; This file is part of GNU Emacs.
9
10;; GNU Emacs is free software: you can redistribute it and/or modify
11;; it under the terms of the GNU General Public License as published by
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22
23;;; Commentary:
24
25;; This file provides a set of operations to save a frameset (the state
26;; of all or a subset of the existing frames and windows), both
27;; in-session and persistently, and restore it at some point in the
28;; future.
29;;
30;; It should be noted that restoring the frames' windows depends on
31;; the buffers they are displaying, but this package does not provide
32;; any way to save and restore sets of buffers (see desktop.el for
33;; that). So, it's up to the user of frameset.el to make sure that
34;; any relevant buffer is loaded before trying to restore a frameset.
35;; When a window is restored and a buffer is missing, the window will
36;; be deleted unless it is the last one in the frame, in which case
37;; some previous buffer will be shown instead.
38
39;;; Code:
40
41(require 'cl-lib)
42
43\f
9421876d
JB
44(cl-defstruct (frameset (:type list) :named
45 (:copier nil)
063233c3
JB
46 (:predicate nil)
47 ;; A BOA constructor, not the default "keywordy" one.
48 (:constructor make-frameset (properties states)))
49
50 "A frameset encapsulates a serializable view of a set of frames and windows.
51
52It contains the following slots, which can be accessed with
53\(frameset-SLOT fs) and set with (setf (frameset-SLOT fs) VALUE):
54
55 version A non-modifiable version number, identifying the format
56 of the frameset struct. Currently its value is 1.
57 properties A property list, to store both frameset-specific and
58 user-defined serializable data (some suggested properties
59 are described below).
60 states An alist of items (FRAME-PARAMETERS . WINDOW-STATE), in no
61 particular order. Each item represents a frame to be
62 restored. FRAME-PARAMETERS is a frame's parameter list,
63 extracted with (frame-parameters FRAME) and filtered through
64 `frame-parameters-alist' or a similar filter alist.
65 WINDOW-STATE is the output of `window-state-get', when
66 applied to the root window of the frame.
67
68Some suggested properties:
69
70 :app APPINFO Can be used by applications and packages to indicate the
71 intended (but by no means exclusive) use of the frameset.
72 Freeform. For example, currently desktop.el framesets set
73 :app to `(desktop . ,desktop-file-version).
74 :name NAME The name of the frameset instance; a string.
75 :desc TEXT A description for user consumption (to show in a menu to
76 choose among framesets, etc.); a string.
77
78A frameset is intended to be used through the following simple API:
79
80 - `frameset-save' captures all or a subset of the live frames, and returns
81 a serializable snapshot of them (a frameset).
82 - `frameset-restore' takes a frameset, and restores the frames and windows
83 it describes, as faithfully as possible.
84 - `frameset-p' is the predicate for the frameset type. It returns nil
85 for non-frameset objects, and the frameset version number (see below)
86 for frameset objects.
87 - `frameset-copy' returns a deep copy of a frameset.
88 - `frameset-prop' is a `setf'able accessor for the contents of the
89 `properties' slot.
90 - The `frameset-SLOT' accessors described above."
91
92 (version 1 :read-only t)
93 properties states)
94
95(defun frameset-copy (frameset)
9421876d
JB
96 "Return a copy of FRAMESET.
97This is a deep copy done with `copy-tree'."
98 (copy-tree frameset t))
99
51d30f2c 100;;;###autoload
9421876d 101(defun frameset-p (frameset)
063233c3 102 "If FRAMESET is a frameset, return its version number.
9421876d 103Else return nil."
063233c3
JB
104 (and (eq (car-safe frameset) 'frameset) ; is a list
105 (integerp (nth 1 frameset)) ; version is an int
106 (nth 2 frameset) ; properties is non-null
107 (nth 3 frameset) ; states is non-null
108 (nth 1 frameset))) ; return version
9421876d 109
2613dea2
JB
110;; A setf'able accessor to the frameset's properties
111(defun frameset-prop (frameset prop)
112 "Return the value of the PROP property of FRAMESET.
113
063233c3 114Properties can be set with
2613dea2
JB
115
116 (setf (frameset-prop FRAMESET PROP) NEW-VALUE)"
117 (plist-get (frameset-properties frameset) prop))
118
6475c94b
JB
119(gv-define-setter frameset-prop (val fs prop)
120 (macroexp-let2 nil v val
121 `(progn
6475c94b
JB
122 (setf (frameset-properties ,fs)
123 (plist-put (frameset-properties ,fs) ,prop ,v))
124 ,v)))
2613dea2 125
9421876d
JB
126\f
127;; Filtering
128
d5671a82
JB
129;;;###autoload
130(defvar frameset-live-filter-alist
063233c3 131 '((name . :never)
d5671a82
JB
132 (minibuffer . frameset-filter-minibuffer)
133 (top . frameset-filter-iconified))
134 "Minimum set of parameters to filter for live (on-session) framesets.
135See `frameset-filter-alist' for a full description.")
136
137;;;###autoload
138(defvar frameset-persistent-filter-alist
139 (nconc
140 '((background-color . frameset-filter-sanitize-color)
063233c3
JB
141 (buffer-list . :never)
142 (buffer-predicate . :never)
143 (buried-buffer-list . :never)
144 (font . frameset-filter-save-param)
d5671a82 145 (foreground-color . frameset-filter-sanitize-color)
063233c3
JB
146 (fullscreen . frameset-filter-save-param)
147 (GUI:font . frameset-filter-restore-param)
148 (GUI:fullscreen . frameset-filter-restore-param)
149 (GUI:height . frameset-filter-restore-param)
150 (GUI:width . frameset-filter-restore-param)
151 (height . frameset-filter-save-param)
d5671a82 152 (left . frameset-filter-iconified)
063233c3
JB
153 (outer-window-id . :never)
154 (parent-id . :never)
d5671a82
JB
155 (tty . frameset-filter-tty-to-GUI)
156 (tty-type . frameset-filter-tty-to-GUI)
063233c3
JB
157 (width . frameset-filter-save-param)
158 (window-id . :never)
159 (window-system . :never))
d5671a82
JB
160 frameset-live-filter-alist)
161 "Recommended set of parameters to filter for persistent framesets.
162See `frameset-filter-alist' for a full description.")
163
164;;;###autoload
165(defvar frameset-filter-alist frameset-persistent-filter-alist
9421876d
JB
166 "Alist of frame parameters and filtering functions.
167
063233c3
JB
168This alist is the default value of the :filters arguments of
169`frameset-save' and `frameset-restore' (which see). On saving,
170PARAMETERS is the parameter list of each frame processed, and
171FILTERED is the parameter list that gets saved to the frameset.
172On restoring, PARAMETERS is the parameter list extracted from the
173frameset, and FILTERED is the resulting frame parameter list used
174to restore the frame.
175
176Elements of this alist are conses (PARAM . ACTION), where PARAM
177is a parameter name (a symbol identifying a frame parameter), and
178ACTION can be:
179
180 nil The parameter is copied to FILTERED.
181 :never The parameter is never copied to FILTERED.
182 :save The parameter is copied only when saving the frame.
183 :restore The parameter is copied only when restoring the frame.
d5671a82 184 FILTER A filter function.
9421876d
JB
185
186FILTER can be a symbol FILTER-FUN, or a list (FILTER-FUN ARGS...).
063233c3
JB
187FILTER-FUN is called with four arguments CURRENT, FILTERED, PARAMETERS and
188SAVING, plus any additional ARGS:
9421876d
JB
189
190 CURRENT A cons (PARAM . VALUE), where PARAM is the one being
d5671a82 191 filtered and VALUE is its current value.
063233c3 192 FILTERED The resulting alist (so far).
9421876d 193 PARAMETERS The complete alist of parameters being filtered,
063233c3
JB
194 SAVING Non-nil if filtering before saving state, nil if filtering
195 before restoring it.
9421876d 196
063233c3
JB
197FILTER-FUN must return:
198 nil Skip CURRENT (do not add it to FILTERED).
199 t Add CURRENT to FILTERED as is.
200 (NEW-PARAM . NEW-VALUE) Add this to FILTERED instead of CURRENT.
201
202Frame parameters not on this alist are passed intact, as if they were
203defined with ACTION = nil.")
9421876d 204
9421876d
JB
205
206(defvar frameset--target-display nil
207 ;; Either (minibuffer . VALUE) or nil.
208 ;; This refers to the current frame config being processed inside
063233c3 209 ;; `frameset-restore' and its auxiliary functions (like filtering).
9421876d
JB
210 ;; If nil, there is no need to change the display.
211 ;; If non-nil, display parameter to use when creating the frame.
212 "Internal use only.")
213
214(defun frameset-switch-to-gui-p (parameters)
215 "True when switching to a graphic display.
216Return t if PARAMETERS describes a text-only terminal and
217the target is a graphic display; otherwise return nil.
218Only meaningful when called from a filtering function in
219`frameset-filter-alist'."
d5671a82
JB
220 (and frameset--target-display ; we're switching
221 (null (cdr (assq 'display parameters))) ; from a tty
222 (cdr frameset--target-display))) ; to a GUI display
9421876d
JB
223
224(defun frameset-switch-to-tty-p (parameters)
225 "True when switching to a text-only terminal.
226Return t if PARAMETERS describes a graphic display and
227the target is a text-only terminal; otherwise return nil.
228Only meaningful when called from a filtering function in
229`frameset-filter-alist'."
230 (and frameset--target-display ; we're switching
d5671a82 231 (cdr (assq 'display parameters)) ; from a GUI display
9421876d
JB
232 (null (cdr frameset--target-display)))) ; to a tty
233
d5671a82 234(defun frameset-filter-tty-to-GUI (_current _filtered parameters saving)
063233c3
JB
235 "Remove CURRENT when switching from tty to a graphic display.
236
237For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
238see the docstring of `frameset-filter-alist'."
d5671a82
JB
239 (or saving
240 (not (frameset-switch-to-gui-p parameters))))
241
9421876d
JB
242(defun frameset-filter-sanitize-color (current _filtered parameters saving)
243 "When switching to a GUI frame, remove \"unspecified\" colors.
063233c3
JB
244Useful as a filter function for tty-specific parameters.
245
246For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
247see the docstring of `frameset-filter-alist'."
9421876d
JB
248 (or saving
249 (not (frameset-switch-to-gui-p parameters))
250 (not (stringp (cdr current)))
251 (not (string-match-p "^unspecified-[fb]g$" (cdr current)))))
252
253(defun frameset-filter-minibuffer (current _filtered _parameters saving)
063233c3
JB
254 "When saving, convert (minibuffer . #<window>) parameter to (minibuffer . t).
255
256For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
257see the docstring of `frameset-filter-alist'."
9421876d
JB
258 (or (not saving)
259 (if (windowp (cdr current))
260 '(minibuffer . t)
261 t)))
262
063233c3
JB
263(defun frameset-filter-save-param (current _filtered parameters saving
264 &optional prefix)
9421876d 265 "When switching to a tty frame, save parameter P as PREFIX:P.
063233c3
JB
266The parameter can be later restored with `frameset-filter-restore-param'.
267PREFIX defaults to `GUI'.
268
269For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
270see the docstring of `frameset-filter-alist'."
9421876d
JB
271 (unless prefix (setq prefix 'GUI))
272 (cond (saving t)
273 ((frameset-switch-to-tty-p parameters)
274 (let ((prefix:p (intern (format "%s:%s" prefix (car current)))))
275 (if (assq prefix:p parameters)
276 nil
277 (cons prefix:p (cdr current)))))
278 ((frameset-switch-to-gui-p parameters)
279 (not (assq (intern (format "%s:%s" prefix (car current))) parameters)))
280 (t t)))
281
063233c3 282(defun frameset-filter-restore-param (current filtered parameters saving)
9421876d 283 "When switching to a GUI frame, restore PREFIX:P parameter as P.
063233c3
JB
284CURRENT must be of the form (PREFIX:P . value).
285
286For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
287see the docstring of `frameset-filter-alist'."
9421876d
JB
288 (or saving
289 (not (frameset-switch-to-gui-p parameters))
290 (let* ((prefix:p (symbol-name (car current)))
291 (p (intern (substring prefix:p
292 (1+ (string-match-p ":" prefix:p)))))
293 (val (cdr current))
294 (found (assq p filtered)))
295 (if (not found)
296 (cons p val)
297 (setcdr found val)
298 nil))))
299
300(defun frameset-filter-iconified (_current _filtered parameters saving)
301 "Remove CURRENT when saving an iconified frame.
063233c3 302This is used for positional parameters `left' and `top', which are
9421876d 303meaningless in an iconified frame, so the frame is restored in a
063233c3
JB
304default position.
305
306For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
307see the docstring of `frameset-filter-alist'."
9421876d
JB
308 (not (and saving (eq (cdr (assq 'visibility parameters)) 'icon))))
309
9421876d
JB
310(defun frameset-filter-params (parameters filter-alist saving)
311 "Filter parameter list PARAMETERS and return a filtered list.
312FILTER-ALIST is an alist of parameter filters, in the format of
313`frameset-filter-alist' (which see).
314SAVING is non-nil while filtering parameters to save a frameset,
315nil while the filtering is done to restore it."
316 (let ((filtered nil))
317 (dolist (current parameters)
318 (pcase (cdr (assq (car current) filter-alist))
319 (`nil
320 (push current filtered))
063233c3 321 (:never
9421876d 322 nil)
9421876d 323 (:restore
063233c3
JB
324 (unless saving (push current filtered)))
325 (:save
9421876d
JB
326 (when saving (push current filtered)))
327 ((or `(,fun . ,args) (and fun (pred fboundp)))
f078d570 328 (let ((this (apply fun current filtered parameters saving args)))
9421876d
JB
329 (when this
330 (push (if (eq this t) current this) filtered))))
331 (other
332 (delay-warning 'frameset (format "Unknown filter %S" other) :error))))
333 ;; Set the display parameter after filtering, so that filter functions
334 ;; have access to its original value.
335 (when frameset--target-display
336 (let ((display (assq 'display filtered)))
337 (if display
338 (setcdr display (cdr frameset--target-display))
339 (push frameset--target-display filtered))))
340 filtered))
341
342\f
38276e01 343;; Frame ids
9421876d
JB
344
345(defun frameset--set-id (frame)
38276e01 346 "Set FRAME's id if not yet set.
9421876d 347Internal use only."
d5671a82 348 (unless (frame-parameter frame 'frameset--id)
9421876d 349 (set-frame-parameter frame
d5671a82 350 'frameset--id
9421876d
JB
351 (mapconcat (lambda (n) (format "%04X" n))
352 (cl-loop repeat 4 collect (random 65536))
353 "-"))))
38276e01
JB
354;;;###autoload
355(defun frameset-frame-id (frame)
356 "Return the frame id of FRAME, if it has one; else, return nil.
357A frame id is a string that uniquely identifies a frame.
358It is persistent across `frameset-save' / `frameset-restore'
359invocations, and once assigned is never changed unless the same
360frame is duplicated (via `frameset-restore'), in which case the
361newest frame keeps the id and the old frame's is set to nil."
362 (frame-parameter frame 'frameset--id))
363
364;;;###autoload
365(defun frameset-frame-id-equal-p (frame id)
366 "Return non-nil if FRAME's id matches ID."
367 (string= (frameset-frame-id frame) id))
368
369;;;###autoload
370(defun frameset-locate-frame-id (id &optional frame-list)
371 "Return the live frame with id ID, if exists; else nil.
372If FRAME-LIST is a list of frames, check these frames only.
373If nil, check all live frames."
374 (cl-find-if (lambda (f)
375 (and (frame-live-p f)
376 (frameset-frame-id-equal-p f id)))
377 (or frame-list (frame-list))))
378
379\f
380;; Saving framesets
9421876d
JB
381
382(defun frameset--process-minibuffer-frames (frame-list)
383 "Process FRAME-LIST and record minibuffer relationships.
d5671a82 384FRAME-LIST is a list of frames. Internal use only."
9421876d
JB
385 ;; Record frames with their own minibuffer
386 (dolist (frame (minibuffer-frame-list))
387 (when (memq frame frame-list)
388 (frameset--set-id frame)
389 ;; For minibuffer-owning frames, frameset--mini is a cons
390 ;; (t . DEFAULT?), where DEFAULT? is a boolean indicating whether
391 ;; the frame is the one pointed out by `default-minibuffer-frame'.
392 (set-frame-parameter frame
393 'frameset--mini
394 (cons t (eq frame default-minibuffer-frame)))))
395 ;; Now link minibufferless frames with their minibuffer frames
396 (dolist (frame frame-list)
397 (unless (frame-parameter frame 'frameset--mini)
398 (frameset--set-id frame)
399 (let* ((mb-frame (window-frame (minibuffer-window frame)))
38276e01 400 (id (and mb-frame (frameset-frame-id mb-frame))))
9421876d 401 (if (null id)
063233c3 402 (error "Minibuffer frame %S for %S is not being saved" mb-frame frame)
9421876d 403 ;; For minibufferless frames, frameset--mini is a cons
d5671a82
JB
404 ;; (nil . FRAME-ID), where FRAME-ID is the frameset--id
405 ;; of the frame containing its minibuffer window.
9421876d
JB
406 (set-frame-parameter frame
407 'frameset--mini
408 (cons nil id)))))))
409
51d30f2c 410;;;###autoload
9421876d
JB
411(cl-defun frameset-save (frame-list &key filters predicate properties)
412 "Return the frameset of FRAME-LIST, a list of frames.
413If nil, FRAME-LIST defaults to all live frames.
414FILTERS is an alist of parameter filters; defaults to `frameset-filter-alist'.
415PREDICATE is a predicate function, which must return non-nil for frames that
416should be saved; it defaults to saving all frames from FRAME-LIST.
417PROPERTIES is a user-defined property list to add to the frameset."
063233c3
JB
418 (let* ((list (or (copy-sequence frame-list) (frame-list)))
419 (frames (cl-delete-if-not #'frame-live-p
420 (if predicate
421 (cl-delete-if-not predicate list)
422 list))))
9421876d 423 (frameset--process-minibuffer-frames frames)
063233c3
JB
424 (make-frameset properties
425 (mapcar
426 (lambda (frame)
427 (cons
428 (frameset-filter-params (frame-parameters frame)
429 (or filters frameset-filter-alist)
430 t)
431 (window-state-get (frame-root-window frame) t)))
432 frames))))
9421876d
JB
433
434\f
435;; Restoring framesets
436
437(defvar frameset--reuse-list nil
063233c3
JB
438 "The list of frames potentially reusable.
439Its value is only meaningful during execution of `frameset-restore'.
440Internal use only.")
9421876d
JB
441
442(defun frameset--compute-pos (value left/top right/bottom)
443 (pcase value
444 (`(+ ,val) (+ left/top val))
445 (`(- ,val) (+ right/bottom val))
446 (val val)))
447
448(defun frameset--move-onscreen (frame force-onscreen)
449 "If FRAME is offscreen, move it back onscreen and, if necessary, resize it.
450For the description of FORCE-ONSCREEN, see `frameset-restore'.
451When forced onscreen, frames wider than the monitor's workarea are converted
452to fullwidth, and frames taller than the workarea are converted to fullheight.
453NOTE: This only works for non-iconified frames. Internal use only."
454 (pcase-let* ((`(,left ,top ,width ,height) (cl-cdadr (frame-monitor-attributes frame)))
d5671a82
JB
455 (right (+ left width -1))
456 (bottom (+ top height -1))
457 (fr-left (frameset--compute-pos (frame-parameter frame 'left) left right))
458 (fr-top (frameset--compute-pos (frame-parameter frame 'top) top bottom))
9421876d
JB
459 (ch-width (frame-char-width frame))
460 (ch-height (frame-char-height frame))
d5671a82
JB
461 (fr-width (max (frame-pixel-width frame) (* ch-width (frame-width frame))))
462 (fr-height (max (frame-pixel-height frame) (* ch-height (frame-height frame))))
463 (fr-right (+ fr-left fr-width -1))
464 (fr-bottom (+ fr-top fr-height -1)))
9421876d 465 (when (pcase force-onscreen
d5671a82
JB
466 ;; A predicate.
467 ((pred functionp)
468 (funcall force-onscreen
469 frame
470 (list fr-left fr-top fr-width fr-height)
471 (list left top width height)))
9421876d 472 ;; Any corner is outside the screen.
d5671a82 473 (:all (or (< fr-bottom top) (> fr-bottom bottom)
9421876d
JB
474 (< fr-left left) (> fr-left right)
475 (< fr-right left) (> fr-right right)
d5671a82 476 (< fr-top top) (> fr-top bottom)))
9421876d 477 ;; Displaced to the left, right, above or below the screen.
d5671a82 478 (`t (or (> fr-left right)
9421876d 479 (< fr-right left)
d5671a82 480 (> fr-top bottom)
9421876d
JB
481 (< fr-bottom top)))
482 ;; Fully inside, no need to do anything.
483 (_ nil))
484 (let ((fullwidth (> fr-width width))
485 (fullheight (> fr-height height))
486 (params nil))
487 ;; Position frame horizontally.
488 (cond (fullwidth
489 (push `(left . ,left) params))
490 ((> fr-right right)
491 (push `(left . ,(+ left (- width fr-width))) params))
492 ((< fr-left left)
493 (push `(left . ,left) params)))
494 ;; Position frame vertically.
495 (cond (fullheight
496 (push `(top . ,top) params))
497 ((> fr-bottom bottom)
498 (push `(top . ,(+ top (- height fr-height))) params))
499 ((< fr-top top)
500 (push `(top . ,top) params)))
501 ;; Compute fullscreen state, if required.
502 (when (or fullwidth fullheight)
503 (push (cons 'fullscreen
504 (cond ((not fullwidth) 'fullheight)
505 ((not fullheight) 'fullwidth)
506 (t 'maximized)))
507 params))
508 ;; Finally, move the frame back onscreen.
509 (when params
510 (modify-frame-parameters frame params))))))
511
512(defun frameset--find-frame (predicate display &rest args)
513 "Find a frame in `frameset--reuse-list' satisfying PREDICATE.
514Look through available frames whose display property matches DISPLAY
515and return the first one for which (PREDICATE frame ARGS) returns t.
516If PREDICATE is nil, it is always satisfied. Internal use only."
517 (cl-find-if (lambda (frame)
518 (and (equal (frame-parameter frame 'display) display)
519 (or (null predicate)
520 (apply predicate frame args))))
521 frameset--reuse-list))
522
523(defun frameset--reuse-frame (display frame-cfg)
524 "Look for an existing frame to reuse.
525DISPLAY is the display where the frame will be shown, and FRAME-CFG
526is the parameter list of the frame being restored. Internal use only."
527 (let ((frame nil)
528 mini)
529 ;; There are no fancy heuristics there. We could implement some
530 ;; based on frame size and/or position, etc., but it is not clear
531 ;; that any "gain" (in the sense of reduced flickering, etc.) is
532 ;; worth the added complexity. In fact, the code below mainly
533 ;; tries to work nicely when M-x desktop-read is used after a
534 ;; desktop session has already been loaded. The other main use
535 ;; case, which is the initial desktop-read upon starting Emacs,
536 ;; will usually have only one frame, and should already work.
537 (cond ((null display)
538 ;; When the target is tty, every existing frame is reusable.
539 (setq frame (frameset--find-frame nil display)))
540 ((car (setq mini (cdr (assq 'frameset--mini frame-cfg))))
541 ;; If the frame has its own minibuffer, let's see whether
542 ;; that frame has already been loaded (which can happen after
543 ;; M-x desktop-read).
544 (setq frame (frameset--find-frame
545 (lambda (f id)
38276e01 546 (frameset-frame-id-equal-p f id))
d5671a82 547 display (cdr (assq 'frameset--id frame-cfg))))
9421876d
JB
548 ;; If it has not been loaded, and it is not a minibuffer-only frame,
549 ;; let's look for an existing non-minibuffer-only frame to reuse.
550 (unless (or frame (eq (cdr (assq 'minibuffer frame-cfg)) 'only))
551 (setq frame (frameset--find-frame
552 (lambda (f)
553 (let ((w (frame-parameter f 'minibuffer)))
554 (and (window-live-p w)
555 (window-minibuffer-p w)
556 (eq (window-frame w) f))))
557 display))))
558 (mini
559 ;; For minibufferless frames, check whether they already exist,
560 ;; and that they are linked to the right minibuffer frame.
561 (setq frame (frameset--find-frame
a04d36a0 562 (lambda (f id mini-id)
38276e01
JB
563 (and (frameset-frame-id-equal-p f id)
564 (frameset-frame-id-equal-p (window-frame
565 (minibuffer-window f))
566 mini-id)))
d5671a82 567 display (cdr (assq 'frameset--id frame-cfg)) (cdr mini))))
9421876d
JB
568 (t
569 ;; Default to just finding a frame in the same display.
570 (setq frame (frameset--find-frame nil display))))
571 ;; If found, remove from the list.
572 (when frame
573 (setq frameset--reuse-list (delq frame frameset--reuse-list)))
574 frame))
575
d5671a82
JB
576(defun frameset--initial-params (frame-cfg)
577 "Return parameters from FRAME-CFG that should not be changed later.
578Setting position and size parameters as soon as possible helps reducing
579flickering; other parameters, like `minibuffer' and `border-width', must
580be set when creating the frame because they can not be changed later.
581Internal use only."
582 (cl-loop for param in '(left top with height border-width minibuffer)
583 collect (assq param frame-cfg)))
584
063233c3 585(defun frameset--restore-frame (frame-cfg window-cfg filters force-onscreen)
9421876d
JB
586 "Set up and return a frame according to its saved state.
587That means either reusing an existing frame or creating one anew.
588FRAME-CFG is the frame's parameter list; WINDOW-CFG is its window state.
063233c3 589For the meaning of FILTERS and FORCE-ONSCREEN, see `frameset-restore'.
d5671a82 590Internal use only."
9421876d
JB
591 (let* ((fullscreen (cdr (assq 'fullscreen frame-cfg)))
592 (lines (assq 'tool-bar-lines frame-cfg))
593 (filtered-cfg (frameset-filter-params frame-cfg filters nil))
594 (display (cdr (assq 'display filtered-cfg))) ;; post-filtering
595 alt-cfg frame)
596
597 ;; This works around bug#14795 (or feature#14795, if not a bug :-)
598 (setq filtered-cfg (assq-delete-all 'tool-bar-lines filtered-cfg))
599 (push '(tool-bar-lines . 0) filtered-cfg)
600
601 (when fullscreen
602 ;; Currently Emacs has the limitation that it does not record the size
603 ;; and position of a frame before maximizing it, so we cannot save &
604 ;; restore that info. Instead, when restoring, we resort to creating
605 ;; invisible "fullscreen" frames of default size and then maximizing them
606 ;; (and making them visible) which at least is somewhat user-friendly
607 ;; when these frames are later de-maximized.
608 (let ((width (and (eq fullscreen 'fullheight) (cdr (assq 'width filtered-cfg))))
609 (height (and (eq fullscreen 'fullwidth) (cdr (assq 'height filtered-cfg))))
610 (visible (assq 'visibility filtered-cfg)))
611 (setq filtered-cfg (cl-delete-if (lambda (p)
612 (memq p '(visibility fullscreen width height)))
613 filtered-cfg :key #'car))
614 (when width
615 (setq filtered-cfg (append `((user-size . t) (width . ,width))
616 filtered-cfg)))
617 (when height
618 (setq filtered-cfg (append `((user-size . t) (height . ,height))
619 filtered-cfg)))
620 ;; These are parameters to apply after creating/setting the frame.
621 (push visible alt-cfg)
622 (push (cons 'fullscreen fullscreen) alt-cfg)))
623
624 ;; Time to find or create a frame an apply the big bunch of parameters.
625 ;; If a frame needs to be created and it falls partially or fully offscreen,
626 ;; sometimes it gets "pushed back" onscreen; however, moving it afterwards is
627 ;; allowed. So we create the frame as invisible and then reapply the full
628 ;; parameter list (including position and size parameters).
629 (setq frame (or (and frameset--reuse-list
630 (frameset--reuse-frame display filtered-cfg))
631 (make-frame-on-display display
632 (cons '(visibility)
d5671a82 633 (frameset--initial-params filtered-cfg)))))
9421876d
JB
634 (modify-frame-parameters frame
635 (if (eq (frame-parameter frame 'fullscreen) fullscreen)
636 ;; Workaround for bug#14949
637 (assq-delete-all 'fullscreen filtered-cfg)
638 filtered-cfg))
639
640 ;; If requested, force frames to be onscreen.
641 (when (and force-onscreen
642 ;; FIXME: iconified frames should be checked too,
643 ;; but it is impossible without deiconifying them.
644 (not (eq (frame-parameter frame 'visibility) 'icon)))
645 (frameset--move-onscreen frame force-onscreen))
646
647 ;; Let's give the finishing touches (visibility, tool-bar, maximization).
648 (when lines (push lines alt-cfg))
649 (when alt-cfg (modify-frame-parameters frame alt-cfg))
650 ;; Now restore window state.
651 (window-state-put window-cfg (frame-root-window frame) 'safe)
652 frame))
653
063233c3 654(defun frameset--minibufferless-last-p (state1 state2)
9421876d
JB
655 "Predicate to sort frame states in a suitable order to be created.
656It sorts minibuffer-owning frames before minibufferless ones."
657 (pcase-let ((`(,hasmini1 ,id-def1) (assq 'frameset--mini (car state1)))
658 (`(,hasmini2 ,id-def2) (assq 'frameset--mini (car state2))))
659 (cond ((eq id-def1 t) t)
660 ((eq id-def2 t) nil)
661 ((not (eq hasmini1 hasmini2)) (eq hasmini1 t))
662 ((eq hasmini1 nil) (string< id-def1 id-def2))
663 (t t))))
664
d5671a82
JB
665(defun frameset-keep-original-display-p (force-display)
666 "True if saved frames' displays should be honored."
667 (cond ((daemonp) t)
668 ((eq system-type 'windows-nt) nil)
669 (t (not force-display))))
670
063233c3
JB
671(defun frameset-minibufferless-first-p (frame1 _frame2)
672 "Predicate to sort minibufferless frames before other frames."
9421876d
JB
673 (not (frame-parameter frame1 'minibuffer)))
674
51d30f2c 675;;;###autoload
d5671a82
JB
676(cl-defun frameset-restore (frameset
677 &key filters reuse-frames force-display force-onscreen)
9421876d
JB
678 "Restore a FRAMESET into the current display(s).
679
51d30f2c 680FILTERS is an alist of parameter filters; defaults to `frameset-filter-alist'.
9421876d 681
063233c3 682REUSE-FRAMES selects the policy to use to reuse frames when restoring:
d5671a82
JB
683 t Reuse any existing frame if possible; delete leftover frames.
684 nil Restore frameset in new frames and delete existing frames.
685 :keep Restore frameset in new frames and keep the existing ones.
063233c3
JB
686 LIST A list of frames to reuse; only these are reused (if possible),
687 and any leftover ones are deleted; other frames not on this
688 list are left untouched.
9421876d
JB
689
690FORCE-DISPLAY can be:
063233c3
JB
691 t Frames are restored in the current display.
692 nil Frames are restored, if possible, in their original displays.
693 :delete Frames in other displays are deleted instead of restored.
694 PRED A function called with one argument, the parameter list;
695 it must return t, nil or `:delete', as above but affecting
d5671a82 696 only the frame that will be created from that parameter list.
9421876d
JB
697
698FORCE-ONSCREEN can be:
d5671a82
JB
699 t Force onscreen only those frames that are fully offscreen.
700 nil Do not force any frame back onscreen.
063233c3
JB
701 :all Force onscreen any frame fully or partially offscreen.
702 PRED A function called with three arguments,
d5671a82
JB
703 - the live frame just restored,
704 - a list (LEFT TOP WIDTH HEIGHT), describing the frame,
063233c3
JB
705 - a list (LEFT TOP WIDTH HEIGHT), describing the workarea.
706 It must return non-nil to force the frame onscreen, nil otherwise.
d5671a82
JB
707
708Note the timing and scope of the operations described above: REUSE-FRAMES
709affects existing frames, FILTERS and FORCE-DISPLAY affect the frame being
710restored before that happens, and FORCE-ONSCREEN affects the frame once
711it has been restored.
9421876d
JB
712
713All keywords default to nil."
714
715 (cl-assert (frameset-p frameset))
716
d5671a82 717 (let (other-frames)
9421876d
JB
718
719 ;; frameset--reuse-list is a list of frames potentially reusable. Later we
720 ;; will decide which ones can be reused, and how to deal with any leftover.
721 (pcase reuse-frames
d5671a82 722 ((or `nil `:keep)
9421876d
JB
723 (setq frameset--reuse-list nil
724 other-frames (frame-list)))
725 ((pred consp)
726 (setq frameset--reuse-list (copy-sequence reuse-frames)
727 other-frames (cl-delete-if (lambda (frame)
728 (memq frame frameset--reuse-list))
729 (frame-list))))
730 (_
731 (setq frameset--reuse-list (frame-list)
732 other-frames nil)))
733
734 ;; Sort saved states to guarantee that minibufferless frames will be created
735 ;; after the frames that contain their minibuffer windows.
736 (dolist (state (sort (copy-sequence (frameset-states frameset))
063233c3 737 #'frameset--minibufferless-last-p))
9421876d
JB
738 (condition-case-unless-debug err
739 (pcase-let* ((`(,frame-cfg . ,window-cfg) state)
740 ((and d-mini `(,hasmini . ,mb-id))
741 (cdr (assq 'frameset--mini frame-cfg)))
742 (default (and (booleanp mb-id) mb-id))
d5671a82
JB
743 (force-display (if (functionp force-display)
744 (funcall force-display frame-cfg)
745 force-display))
9421876d
JB
746 (frame nil) (to-tty nil))
747 ;; Only set target if forcing displays and the target display is different.
d5671a82
JB
748 (cond ((frameset-keep-original-display-p force-display)
749 (setq frameset--target-display nil))
750 ((eq (frame-parameter nil 'display) (cdr (assq 'display frame-cfg)))
751 (setq frameset--target-display nil))
752 (t
753 (setq frameset--target-display (cons 'display
754 (frame-parameter nil 'display))
755 to-tty (null (cdr frameset--target-display)))))
9421876d
JB
756 ;; Time to restore frames and set up their minibuffers as they were.
757 ;; We only skip a frame (thus deleting it) if either:
758 ;; - we're switching displays, and the user chose the option to delete, or
759 ;; - we're switching to tty, and the frame to restore is minibuffer-only.
760 (unless (and frameset--target-display
d5671a82 761 (or (eq force-display :delete)
9421876d
JB
762 (and to-tty
763 (eq (cdr (assq 'minibuffer frame-cfg)) 'only))))
d5671a82
JB
764 ;; If keeping non-reusable frames, and the frameset--id of one of them
765 ;; matches the id of a frame being restored (because, for example, the
766 ;; frameset has already been read in the same session), remove the
767 ;; frameset--id from the non-reusable frame, which is not useful anymore.
768 (when (and other-frames
769 (or (eq reuse-frames :keep) (consp reuse-frames)))
38276e01
JB
770 (let ((dup (frameset-locate-frame-id (cdr (assq 'frameset--id frame-cfg))
771 other-frames)))
d5671a82
JB
772 (when dup
773 (set-frame-parameter dup 'frameset--id nil))))
9421876d
JB
774 ;; Restore minibuffers. Some of this stuff could be done in a filter
775 ;; function, but it would be messy because restoring minibuffers affects
776 ;; global state; it's best to do it here than add a bunch of global
777 ;; variables to pass info back-and-forth to/from the filter function.
778 (cond
779 ((null d-mini)) ;; No frameset--mini. Process as normal frame.
780 (to-tty) ;; Ignore minibuffer stuff and process as normal frame.
781 (hasmini ;; Frame has minibuffer (or it is minibuffer-only).
782 (when (eq (cdr (assq 'minibuffer frame-cfg)) 'only)
783 (setq frame-cfg (append '((tool-bar-lines . 0) (menu-bar-lines . 0))
784 frame-cfg))))
785 (t ;; Frame depends on other frame's minibuffer window.
38276e01 786 (let* ((mb-frame (or (frameset-locate-frame-id mb-id)
9421876d
JB
787 (error "Minibuffer frame %S not found" mb-id)))
788 (mb-param (assq 'minibuffer frame-cfg))
789 (mb-window (minibuffer-window mb-frame)))
790 (unless (and (window-live-p mb-window)
791 (window-minibuffer-p mb-window))
792 (error "Not a minibuffer window %s" mb-window))
793 (if mb-param
794 (setcdr mb-param mb-window)
d5671a82
JB
795 (push (cons 'minibuffer mb-window) frame-cfg)))))
796 ;; OK, we're ready at last to create (or reuse) a frame and
797 ;; restore the window config.
063233c3
JB
798 (setq frame (frameset--restore-frame frame-cfg window-cfg
799 (or filters frameset-filter-alist)
800 force-onscreen))
d5671a82
JB
801 ;; Set default-minibuffer if required.
802 (when default (setq default-minibuffer-frame frame))))
9421876d
JB
803 (error
804 (delay-warning 'frameset (error-message-string err) :error))))
805
806 ;; In case we try to delete the initial frame, we want to make sure that
807 ;; other frames are already visible (discussed in thread for bug#14841).
808 (sit-for 0 t)
809
810 ;; Delete remaining frames, but do not fail if some resist being deleted.
d5671a82 811 (unless (eq reuse-frames :keep)
9421876d
JB
812 (dolist (frame (sort (nconc (if (listp reuse-frames) nil other-frames)
813 frameset--reuse-list)
063233c3
JB
814 ;; Minibufferless frames must go first to avoid
815 ;; errors when attempting to delete a frame whose
816 ;; minibuffer window is used by another frame.
817 #'frameset-minibufferless-first-p))
9421876d
JB
818 (condition-case err
819 (delete-frame frame)
820 (error
821 (delay-warning 'frameset (error-message-string err))))))
822 (setq frameset--reuse-list nil)
823
824 ;; Make sure there's at least one visible frame.
825 (unless (or (daemonp) (visible-frame-list))
826 (make-frame-visible (car (frame-list))))))
827
828(provide 'frameset)
829
830;;; frameset.el ends here