* lisp/frameset.el: Miscellaneous cleanups.
[bpt/emacs.git] / lisp / frameset.el
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
44 (cl-defstruct (frameset (:type list) :named
45 (:copier nil)
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
52 It 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
68 Some 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
78 A 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)
96 "Return a copy of FRAMESET.
97 This is a deep copy done with `copy-tree'."
98 (copy-tree frameset t))
99
100 ;;;###autoload
101 (defun frameset-p (frameset)
102 "If FRAMESET is a frameset, return its version number.
103 Else return nil."
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
109
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
114 Properties can be set with
115
116 (setf (frameset-prop FRAMESET PROP) NEW-VALUE)"
117 (plist-get (frameset-properties frameset) prop))
118
119 (gv-define-setter frameset-prop (val fs prop)
120 (macroexp-let2 nil v val
121 `(progn
122 (setf (frameset-properties ,fs)
123 (plist-put (frameset-properties ,fs) ,prop ,v))
124 ,v)))
125
126 \f
127 ;; Filtering
128
129 ;;;###autoload
130 (defvar frameset-live-filter-alist
131 '((name . :never)
132 (minibuffer . frameset-filter-minibuffer)
133 (top . frameset-filter-iconified))
134 "Minimum set of parameters to filter for live (on-session) framesets.
135 See `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)
141 (buffer-list . :never)
142 (buffer-predicate . :never)
143 (buried-buffer-list . :never)
144 (font . frameset-filter-save-param)
145 (foreground-color . frameset-filter-sanitize-color)
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)
152 (left . frameset-filter-iconified)
153 (outer-window-id . :never)
154 (parent-id . :never)
155 (tty . frameset-filter-tty-to-GUI)
156 (tty-type . frameset-filter-tty-to-GUI)
157 (width . frameset-filter-save-param)
158 (window-id . :never)
159 (window-system . :never))
160 frameset-live-filter-alist)
161 "Recommended set of parameters to filter for persistent framesets.
162 See `frameset-filter-alist' for a full description.")
163
164 ;;;###autoload
165 (defvar frameset-filter-alist frameset-persistent-filter-alist
166 "Alist of frame parameters and filtering functions.
167
168 This alist is the default value of the :filters arguments of
169 `frameset-save' and `frameset-restore' (which see). On saving,
170 PARAMETERS is the parameter list of each frame processed, and
171 FILTERED is the parameter list that gets saved to the frameset.
172 On restoring, PARAMETERS is the parameter list extracted from the
173 frameset, and FILTERED is the resulting frame parameter list used
174 to restore the frame.
175
176 Elements of this alist are conses (PARAM . ACTION), where PARAM
177 is a parameter name (a symbol identifying a frame parameter), and
178 ACTION 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.
184 FILTER A filter function.
185
186 FILTER can be a symbol FILTER-FUN, or a list (FILTER-FUN ARGS...).
187 FILTER-FUN is called with four arguments CURRENT, FILTERED, PARAMETERS and
188 SAVING, plus any additional ARGS:
189
190 CURRENT A cons (PARAM . VALUE), where PARAM is the one being
191 filtered and VALUE is its current value.
192 FILTERED The resulting alist (so far).
193 PARAMETERS The complete alist of parameters being filtered,
194 SAVING Non-nil if filtering before saving state, nil if filtering
195 before restoring it.
196
197 FILTER-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
202 Frame parameters not on this alist are passed intact, as if they were
203 defined with ACTION = nil.")
204
205
206 (defvar frameset--target-display nil
207 ;; Either (minibuffer . VALUE) or nil.
208 ;; This refers to the current frame config being processed inside
209 ;; `frameset-restore' and its auxiliary functions (like filtering).
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.
216 Return t if PARAMETERS describes a text-only terminal and
217 the target is a graphic display; otherwise return nil.
218 Only meaningful when called from a filtering function in
219 `frameset-filter-alist'."
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
223
224 (defun frameset-switch-to-tty-p (parameters)
225 "True when switching to a text-only terminal.
226 Return t if PARAMETERS describes a graphic display and
227 the target is a text-only terminal; otherwise return nil.
228 Only meaningful when called from a filtering function in
229 `frameset-filter-alist'."
230 (and frameset--target-display ; we're switching
231 (cdr (assq 'display parameters)) ; from a GUI display
232 (null (cdr frameset--target-display)))) ; to a tty
233
234 (defun frameset-filter-tty-to-GUI (_current _filtered parameters saving)
235 "Remove CURRENT when switching from tty to a graphic display.
236
237 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
238 see the docstring of `frameset-filter-alist'."
239 (or saving
240 (not (frameset-switch-to-gui-p parameters))))
241
242 (defun frameset-filter-sanitize-color (current _filtered parameters saving)
243 "When switching to a GUI frame, remove \"unspecified\" colors.
244 Useful as a filter function for tty-specific parameters.
245
246 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
247 see the docstring of `frameset-filter-alist'."
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)
254 "When saving, convert (minibuffer . #<window>) parameter to (minibuffer . t).
255
256 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
257 see the docstring of `frameset-filter-alist'."
258 (or (not saving)
259 (if (windowp (cdr current))
260 '(minibuffer . t)
261 t)))
262
263 (defun frameset-filter-save-param (current _filtered parameters saving
264 &optional prefix)
265 "When switching to a tty frame, save parameter P as PREFIX:P.
266 The parameter can be later restored with `frameset-filter-restore-param'.
267 PREFIX defaults to `GUI'.
268
269 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
270 see the docstring of `frameset-filter-alist'."
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
282 (defun frameset-filter-restore-param (current filtered parameters saving)
283 "When switching to a GUI frame, restore PREFIX:P parameter as P.
284 CURRENT must be of the form (PREFIX:P . value).
285
286 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
287 see the docstring of `frameset-filter-alist'."
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.
302 This is used for positional parameters `left' and `top', which are
303 meaningless in an iconified frame, so the frame is restored in a
304 default position.
305
306 For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING,
307 see the docstring of `frameset-filter-alist'."
308 (not (and saving (eq (cdr (assq 'visibility parameters)) 'icon))))
309
310 (defun frameset-filter-params (parameters filter-alist saving)
311 "Filter parameter list PARAMETERS and return a filtered list.
312 FILTER-ALIST is an alist of parameter filters, in the format of
313 `frameset-filter-alist' (which see).
314 SAVING is non-nil while filtering parameters to save a frameset,
315 nil 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))
321 (:never
322 nil)
323 (:restore
324 (unless saving (push current filtered)))
325 (:save
326 (when saving (push current filtered)))
327 ((or `(,fun . ,args) (and fun (pred fboundp)))
328 (let ((this (apply fun current filtered parameters saving args)))
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
343 ;; Saving framesets
344
345 (defun frameset--set-id (frame)
346 "Set FRAME's `frameset--id' if not yet set.
347 Internal use only."
348 (unless (frame-parameter frame 'frameset--id)
349 (set-frame-parameter frame
350 'frameset--id
351 (mapconcat (lambda (n) (format "%04X" n))
352 (cl-loop repeat 4 collect (random 65536))
353 "-"))))
354
355 (defun frameset--process-minibuffer-frames (frame-list)
356 "Process FRAME-LIST and record minibuffer relationships.
357 FRAME-LIST is a list of frames. Internal use only."
358 ;; Record frames with their own minibuffer
359 (dolist (frame (minibuffer-frame-list))
360 (when (memq frame frame-list)
361 (frameset--set-id frame)
362 ;; For minibuffer-owning frames, frameset--mini is a cons
363 ;; (t . DEFAULT?), where DEFAULT? is a boolean indicating whether
364 ;; the frame is the one pointed out by `default-minibuffer-frame'.
365 (set-frame-parameter frame
366 'frameset--mini
367 (cons t (eq frame default-minibuffer-frame)))))
368 ;; Now link minibufferless frames with their minibuffer frames
369 (dolist (frame frame-list)
370 (unless (frame-parameter frame 'frameset--mini)
371 (frameset--set-id frame)
372 (let* ((mb-frame (window-frame (minibuffer-window frame)))
373 (id (and mb-frame (frame-parameter mb-frame 'frameset--id))))
374 (if (null id)
375 (error "Minibuffer frame %S for %S is not being saved" mb-frame frame)
376 ;; For minibufferless frames, frameset--mini is a cons
377 ;; (nil . FRAME-ID), where FRAME-ID is the frameset--id
378 ;; of the frame containing its minibuffer window.
379 (set-frame-parameter frame
380 'frameset--mini
381 (cons nil id)))))))
382
383 ;;;###autoload
384 (cl-defun frameset-save (frame-list &key filters predicate properties)
385 "Return the frameset of FRAME-LIST, a list of frames.
386 If nil, FRAME-LIST defaults to all live frames.
387 FILTERS is an alist of parameter filters; defaults to `frameset-filter-alist'.
388 PREDICATE is a predicate function, which must return non-nil for frames that
389 should be saved; it defaults to saving all frames from FRAME-LIST.
390 PROPERTIES is a user-defined property list to add to the frameset."
391 (let* ((list (or (copy-sequence frame-list) (frame-list)))
392 (frames (cl-delete-if-not #'frame-live-p
393 (if predicate
394 (cl-delete-if-not predicate list)
395 list))))
396 (frameset--process-minibuffer-frames frames)
397 (make-frameset properties
398 (mapcar
399 (lambda (frame)
400 (cons
401 (frameset-filter-params (frame-parameters frame)
402 (or filters frameset-filter-alist)
403 t)
404 (window-state-get (frame-root-window frame) t)))
405 frames))))
406
407 \f
408 ;; Restoring framesets
409
410 (defvar frameset--reuse-list nil
411 "The list of frames potentially reusable.
412 Its value is only meaningful during execution of `frameset-restore'.
413 Internal use only.")
414
415 (defun frameset--compute-pos (value left/top right/bottom)
416 (pcase value
417 (`(+ ,val) (+ left/top val))
418 (`(- ,val) (+ right/bottom val))
419 (val val)))
420
421 (defun frameset--move-onscreen (frame force-onscreen)
422 "If FRAME is offscreen, move it back onscreen and, if necessary, resize it.
423 For the description of FORCE-ONSCREEN, see `frameset-restore'.
424 When forced onscreen, frames wider than the monitor's workarea are converted
425 to fullwidth, and frames taller than the workarea are converted to fullheight.
426 NOTE: This only works for non-iconified frames. Internal use only."
427 (pcase-let* ((`(,left ,top ,width ,height) (cl-cdadr (frame-monitor-attributes frame)))
428 (right (+ left width -1))
429 (bottom (+ top height -1))
430 (fr-left (frameset--compute-pos (frame-parameter frame 'left) left right))
431 (fr-top (frameset--compute-pos (frame-parameter frame 'top) top bottom))
432 (ch-width (frame-char-width frame))
433 (ch-height (frame-char-height frame))
434 (fr-width (max (frame-pixel-width frame) (* ch-width (frame-width frame))))
435 (fr-height (max (frame-pixel-height frame) (* ch-height (frame-height frame))))
436 (fr-right (+ fr-left fr-width -1))
437 (fr-bottom (+ fr-top fr-height -1)))
438 (when (pcase force-onscreen
439 ;; A predicate.
440 ((pred functionp)
441 (funcall force-onscreen
442 frame
443 (list fr-left fr-top fr-width fr-height)
444 (list left top width height)))
445 ;; Any corner is outside the screen.
446 (:all (or (< fr-bottom top) (> fr-bottom bottom)
447 (< fr-left left) (> fr-left right)
448 (< fr-right left) (> fr-right right)
449 (< fr-top top) (> fr-top bottom)))
450 ;; Displaced to the left, right, above or below the screen.
451 (`t (or (> fr-left right)
452 (< fr-right left)
453 (> fr-top bottom)
454 (< fr-bottom top)))
455 ;; Fully inside, no need to do anything.
456 (_ nil))
457 (let ((fullwidth (> fr-width width))
458 (fullheight (> fr-height height))
459 (params nil))
460 ;; Position frame horizontally.
461 (cond (fullwidth
462 (push `(left . ,left) params))
463 ((> fr-right right)
464 (push `(left . ,(+ left (- width fr-width))) params))
465 ((< fr-left left)
466 (push `(left . ,left) params)))
467 ;; Position frame vertically.
468 (cond (fullheight
469 (push `(top . ,top) params))
470 ((> fr-bottom bottom)
471 (push `(top . ,(+ top (- height fr-height))) params))
472 ((< fr-top top)
473 (push `(top . ,top) params)))
474 ;; Compute fullscreen state, if required.
475 (when (or fullwidth fullheight)
476 (push (cons 'fullscreen
477 (cond ((not fullwidth) 'fullheight)
478 ((not fullheight) 'fullwidth)
479 (t 'maximized)))
480 params))
481 ;; Finally, move the frame back onscreen.
482 (when params
483 (modify-frame-parameters frame params))))))
484
485 (defun frameset--find-frame (predicate display &rest args)
486 "Find a frame in `frameset--reuse-list' satisfying PREDICATE.
487 Look through available frames whose display property matches DISPLAY
488 and return the first one for which (PREDICATE frame ARGS) returns t.
489 If PREDICATE is nil, it is always satisfied. Internal use only."
490 (cl-find-if (lambda (frame)
491 (and (equal (frame-parameter frame 'display) display)
492 (or (null predicate)
493 (apply predicate frame args))))
494 frameset--reuse-list))
495
496 (defun frameset--reuse-frame (display frame-cfg)
497 "Look for an existing frame to reuse.
498 DISPLAY is the display where the frame will be shown, and FRAME-CFG
499 is the parameter list of the frame being restored. Internal use only."
500 (let ((frame nil)
501 mini)
502 ;; There are no fancy heuristics there. We could implement some
503 ;; based on frame size and/or position, etc., but it is not clear
504 ;; that any "gain" (in the sense of reduced flickering, etc.) is
505 ;; worth the added complexity. In fact, the code below mainly
506 ;; tries to work nicely when M-x desktop-read is used after a
507 ;; desktop session has already been loaded. The other main use
508 ;; case, which is the initial desktop-read upon starting Emacs,
509 ;; will usually have only one frame, and should already work.
510 (cond ((null display)
511 ;; When the target is tty, every existing frame is reusable.
512 (setq frame (frameset--find-frame nil display)))
513 ((car (setq mini (cdr (assq 'frameset--mini frame-cfg))))
514 ;; If the frame has its own minibuffer, let's see whether
515 ;; that frame has already been loaded (which can happen after
516 ;; M-x desktop-read).
517 (setq frame (frameset--find-frame
518 (lambda (f id)
519 (string= (frame-parameter f 'frameset--id) id))
520 display (cdr (assq 'frameset--id frame-cfg))))
521 ;; If it has not been loaded, and it is not a minibuffer-only frame,
522 ;; let's look for an existing non-minibuffer-only frame to reuse.
523 (unless (or frame (eq (cdr (assq 'minibuffer frame-cfg)) 'only))
524 (setq frame (frameset--find-frame
525 (lambda (f)
526 (let ((w (frame-parameter f 'minibuffer)))
527 (and (window-live-p w)
528 (window-minibuffer-p w)
529 (eq (window-frame w) f))))
530 display))))
531 (mini
532 ;; For minibufferless frames, check whether they already exist,
533 ;; and that they are linked to the right minibuffer frame.
534 (setq frame (frameset--find-frame
535 (lambda (f id mini-id)
536 (and (string= (frame-parameter f 'frameset--id) id)
537 (string= (frame-parameter (window-frame (minibuffer-window f))
538 'frameset--id)
539 mini-id)))
540 display (cdr (assq 'frameset--id frame-cfg)) (cdr mini))))
541 (t
542 ;; Default to just finding a frame in the same display.
543 (setq frame (frameset--find-frame nil display))))
544 ;; If found, remove from the list.
545 (when frame
546 (setq frameset--reuse-list (delq frame frameset--reuse-list)))
547 frame))
548
549 (defun frameset--initial-params (frame-cfg)
550 "Return parameters from FRAME-CFG that should not be changed later.
551 Setting position and size parameters as soon as possible helps reducing
552 flickering; other parameters, like `minibuffer' and `border-width', must
553 be set when creating the frame because they can not be changed later.
554 Internal use only."
555 (cl-loop for param in '(left top with height border-width minibuffer)
556 collect (assq param frame-cfg)))
557
558 (defun frameset--restore-frame (frame-cfg window-cfg filters force-onscreen)
559 "Set up and return a frame according to its saved state.
560 That means either reusing an existing frame or creating one anew.
561 FRAME-CFG is the frame's parameter list; WINDOW-CFG is its window state.
562 For the meaning of FILTERS and FORCE-ONSCREEN, see `frameset-restore'.
563 Internal use only."
564 (let* ((fullscreen (cdr (assq 'fullscreen frame-cfg)))
565 (lines (assq 'tool-bar-lines frame-cfg))
566 (filtered-cfg (frameset-filter-params frame-cfg filters nil))
567 (display (cdr (assq 'display filtered-cfg))) ;; post-filtering
568 alt-cfg frame)
569
570 ;; This works around bug#14795 (or feature#14795, if not a bug :-)
571 (setq filtered-cfg (assq-delete-all 'tool-bar-lines filtered-cfg))
572 (push '(tool-bar-lines . 0) filtered-cfg)
573
574 (when fullscreen
575 ;; Currently Emacs has the limitation that it does not record the size
576 ;; and position of a frame before maximizing it, so we cannot save &
577 ;; restore that info. Instead, when restoring, we resort to creating
578 ;; invisible "fullscreen" frames of default size and then maximizing them
579 ;; (and making them visible) which at least is somewhat user-friendly
580 ;; when these frames are later de-maximized.
581 (let ((width (and (eq fullscreen 'fullheight) (cdr (assq 'width filtered-cfg))))
582 (height (and (eq fullscreen 'fullwidth) (cdr (assq 'height filtered-cfg))))
583 (visible (assq 'visibility filtered-cfg)))
584 (setq filtered-cfg (cl-delete-if (lambda (p)
585 (memq p '(visibility fullscreen width height)))
586 filtered-cfg :key #'car))
587 (when width
588 (setq filtered-cfg (append `((user-size . t) (width . ,width))
589 filtered-cfg)))
590 (when height
591 (setq filtered-cfg (append `((user-size . t) (height . ,height))
592 filtered-cfg)))
593 ;; These are parameters to apply after creating/setting the frame.
594 (push visible alt-cfg)
595 (push (cons 'fullscreen fullscreen) alt-cfg)))
596
597 ;; Time to find or create a frame an apply the big bunch of parameters.
598 ;; If a frame needs to be created and it falls partially or fully offscreen,
599 ;; sometimes it gets "pushed back" onscreen; however, moving it afterwards is
600 ;; allowed. So we create the frame as invisible and then reapply the full
601 ;; parameter list (including position and size parameters).
602 (setq frame (or (and frameset--reuse-list
603 (frameset--reuse-frame display filtered-cfg))
604 (make-frame-on-display display
605 (cons '(visibility)
606 (frameset--initial-params filtered-cfg)))))
607 (modify-frame-parameters frame
608 (if (eq (frame-parameter frame 'fullscreen) fullscreen)
609 ;; Workaround for bug#14949
610 (assq-delete-all 'fullscreen filtered-cfg)
611 filtered-cfg))
612
613 ;; If requested, force frames to be onscreen.
614 (when (and force-onscreen
615 ;; FIXME: iconified frames should be checked too,
616 ;; but it is impossible without deiconifying them.
617 (not (eq (frame-parameter frame 'visibility) 'icon)))
618 (frameset--move-onscreen frame force-onscreen))
619
620 ;; Let's give the finishing touches (visibility, tool-bar, maximization).
621 (when lines (push lines alt-cfg))
622 (when alt-cfg (modify-frame-parameters frame alt-cfg))
623 ;; Now restore window state.
624 (window-state-put window-cfg (frame-root-window frame) 'safe)
625 frame))
626
627 (defun frameset--minibufferless-last-p (state1 state2)
628 "Predicate to sort frame states in a suitable order to be created.
629 It sorts minibuffer-owning frames before minibufferless ones."
630 (pcase-let ((`(,hasmini1 ,id-def1) (assq 'frameset--mini (car state1)))
631 (`(,hasmini2 ,id-def2) (assq 'frameset--mini (car state2))))
632 (cond ((eq id-def1 t) t)
633 ((eq id-def2 t) nil)
634 ((not (eq hasmini1 hasmini2)) (eq hasmini1 t))
635 ((eq hasmini1 nil) (string< id-def1 id-def2))
636 (t t))))
637
638 (defun frameset-keep-original-display-p (force-display)
639 "True if saved frames' displays should be honored."
640 (cond ((daemonp) t)
641 ((eq system-type 'windows-nt) nil)
642 (t (not force-display))))
643
644 (defun frameset-minibufferless-first-p (frame1 _frame2)
645 "Predicate to sort minibufferless frames before other frames."
646 (not (frame-parameter frame1 'minibuffer)))
647
648 ;;;###autoload
649 (cl-defun frameset-restore (frameset
650 &key filters reuse-frames force-display force-onscreen)
651 "Restore a FRAMESET into the current display(s).
652
653 FILTERS is an alist of parameter filters; defaults to `frameset-filter-alist'.
654
655 REUSE-FRAMES selects the policy to use to reuse frames when restoring:
656 t Reuse any existing frame if possible; delete leftover frames.
657 nil Restore frameset in new frames and delete existing frames.
658 :keep Restore frameset in new frames and keep the existing ones.
659 LIST A list of frames to reuse; only these are reused (if possible),
660 and any leftover ones are deleted; other frames not on this
661 list are left untouched.
662
663 FORCE-DISPLAY can be:
664 t Frames are restored in the current display.
665 nil Frames are restored, if possible, in their original displays.
666 :delete Frames in other displays are deleted instead of restored.
667 PRED A function called with one argument, the parameter list;
668 it must return t, nil or `:delete', as above but affecting
669 only the frame that will be created from that parameter list.
670
671 FORCE-ONSCREEN can be:
672 t Force onscreen only those frames that are fully offscreen.
673 nil Do not force any frame back onscreen.
674 :all Force onscreen any frame fully or partially offscreen.
675 PRED A function called with three arguments,
676 - the live frame just restored,
677 - a list (LEFT TOP WIDTH HEIGHT), describing the frame,
678 - a list (LEFT TOP WIDTH HEIGHT), describing the workarea.
679 It must return non-nil to force the frame onscreen, nil otherwise.
680
681 Note the timing and scope of the operations described above: REUSE-FRAMES
682 affects existing frames, FILTERS and FORCE-DISPLAY affect the frame being
683 restored before that happens, and FORCE-ONSCREEN affects the frame once
684 it has been restored.
685
686 All keywords default to nil."
687
688 (cl-assert (frameset-p frameset))
689
690 (let (other-frames)
691
692 ;; frameset--reuse-list is a list of frames potentially reusable. Later we
693 ;; will decide which ones can be reused, and how to deal with any leftover.
694 (pcase reuse-frames
695 ((or `nil `:keep)
696 (setq frameset--reuse-list nil
697 other-frames (frame-list)))
698 ((pred consp)
699 (setq frameset--reuse-list (copy-sequence reuse-frames)
700 other-frames (cl-delete-if (lambda (frame)
701 (memq frame frameset--reuse-list))
702 (frame-list))))
703 (_
704 (setq frameset--reuse-list (frame-list)
705 other-frames nil)))
706
707 ;; Sort saved states to guarantee that minibufferless frames will be created
708 ;; after the frames that contain their minibuffer windows.
709 (dolist (state (sort (copy-sequence (frameset-states frameset))
710 #'frameset--minibufferless-last-p))
711 (condition-case-unless-debug err
712 (pcase-let* ((`(,frame-cfg . ,window-cfg) state)
713 ((and d-mini `(,hasmini . ,mb-id))
714 (cdr (assq 'frameset--mini frame-cfg)))
715 (default (and (booleanp mb-id) mb-id))
716 (force-display (if (functionp force-display)
717 (funcall force-display frame-cfg)
718 force-display))
719 (frame nil) (to-tty nil))
720 ;; Only set target if forcing displays and the target display is different.
721 (cond ((frameset-keep-original-display-p force-display)
722 (setq frameset--target-display nil))
723 ((eq (frame-parameter nil 'display) (cdr (assq 'display frame-cfg)))
724 (setq frameset--target-display nil))
725 (t
726 (setq frameset--target-display (cons 'display
727 (frame-parameter nil 'display))
728 to-tty (null (cdr frameset--target-display)))))
729 ;; Time to restore frames and set up their minibuffers as they were.
730 ;; We only skip a frame (thus deleting it) if either:
731 ;; - we're switching displays, and the user chose the option to delete, or
732 ;; - we're switching to tty, and the frame to restore is minibuffer-only.
733 (unless (and frameset--target-display
734 (or (eq force-display :delete)
735 (and to-tty
736 (eq (cdr (assq 'minibuffer frame-cfg)) 'only))))
737 ;; If keeping non-reusable frames, and the frameset--id of one of them
738 ;; matches the id of a frame being restored (because, for example, the
739 ;; frameset has already been read in the same session), remove the
740 ;; frameset--id from the non-reusable frame, which is not useful anymore.
741 (when (and other-frames
742 (or (eq reuse-frames :keep) (consp reuse-frames)))
743 (let ((dup (cl-find (cdr (assq 'frameset--id frame-cfg))
744 other-frames
745 :key (lambda (frame)
746 (frame-parameter frame 'frameset--id))
747 :test #'string=)))
748 (when dup
749 (set-frame-parameter dup 'frameset--id nil))))
750 ;; Restore minibuffers. Some of this stuff could be done in a filter
751 ;; function, but it would be messy because restoring minibuffers affects
752 ;; global state; it's best to do it here than add a bunch of global
753 ;; variables to pass info back-and-forth to/from the filter function.
754 (cond
755 ((null d-mini)) ;; No frameset--mini. Process as normal frame.
756 (to-tty) ;; Ignore minibuffer stuff and process as normal frame.
757 (hasmini ;; Frame has minibuffer (or it is minibuffer-only).
758 (when (eq (cdr (assq 'minibuffer frame-cfg)) 'only)
759 (setq frame-cfg (append '((tool-bar-lines . 0) (menu-bar-lines . 0))
760 frame-cfg))))
761 (t ;; Frame depends on other frame's minibuffer window.
762 (let* ((mb-frame (or (cl-find-if
763 (lambda (f)
764 (string= (frame-parameter f 'frameset--id)
765 mb-id))
766 (frame-list))
767 (error "Minibuffer frame %S not found" mb-id)))
768 (mb-param (assq 'minibuffer frame-cfg))
769 (mb-window (minibuffer-window mb-frame)))
770 (unless (and (window-live-p mb-window)
771 (window-minibuffer-p mb-window))
772 (error "Not a minibuffer window %s" mb-window))
773 (if mb-param
774 (setcdr mb-param mb-window)
775 (push (cons 'minibuffer mb-window) frame-cfg)))))
776 ;; OK, we're ready at last to create (or reuse) a frame and
777 ;; restore the window config.
778 (setq frame (frameset--restore-frame frame-cfg window-cfg
779 (or filters frameset-filter-alist)
780 force-onscreen))
781 ;; Set default-minibuffer if required.
782 (when default (setq default-minibuffer-frame frame))))
783 (error
784 (delay-warning 'frameset (error-message-string err) :error))))
785
786 ;; In case we try to delete the initial frame, we want to make sure that
787 ;; other frames are already visible (discussed in thread for bug#14841).
788 (sit-for 0 t)
789
790 ;; Delete remaining frames, but do not fail if some resist being deleted.
791 (unless (eq reuse-frames :keep)
792 (dolist (frame (sort (nconc (if (listp reuse-frames) nil other-frames)
793 frameset--reuse-list)
794 ;; Minibufferless frames must go first to avoid
795 ;; errors when attempting to delete a frame whose
796 ;; minibuffer window is used by another frame.
797 #'frameset-minibufferless-first-p))
798 (condition-case err
799 (delete-frame frame)
800 (error
801 (delay-warning 'frameset (error-message-string err))))))
802 (setq frameset--reuse-list nil)
803
804 ;; Make sure there's at least one visible frame.
805 (unless (or (daemonp) (visible-frame-list))
806 (make-frame-visible (car (frame-list))))))
807
808 (provide 'frameset)
809
810 ;;; frameset.el ends here