Commit | Line | Data |
---|---|---|
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 | |
a912c016 | 44 | (cl-defstruct (frameset (:type vector) :named |
76c5e5ab JB |
45 | ;; Copier is defined below. |
46 | (:copier nil)) | |
063233c3 JB |
47 | |
48 | "A frameset encapsulates a serializable view of a set of frames and windows. | |
49 | ||
50 | It contains the following slots, which can be accessed with | |
51 | \(frameset-SLOT fs) and set with (setf (frameset-SLOT fs) VALUE): | |
52 | ||
a912c016 JB |
53 | version A read-only version number, identifying the format |
54 | of the frameset struct. Currently its value is 1. | |
55 | timestamp A read-only timestamp, the output of `current-time'. | |
56 | app A symbol, or a list whose first element is a symbol, which | |
3677ffeb JB |
57 | identifies the creator of the frameset and related info; |
58 | for example, desktop.el sets this slot to a list | |
59 | `(desktop . ,desktop-file-version). | |
a912c016 JB |
60 | name A string, the name of the frameset instance. |
61 | description A string, a description for user consumption (to show in | |
3677ffeb | 62 | menus, messages, etc). |
063233c3 | 63 | properties A property list, to store both frameset-specific and |
a912c016 JB |
64 | user-defined serializable data. |
65 | states A list of items (FRAME-PARAMETERS . WINDOW-STATE), in no | |
66 | particular order. Each item represents a frame to be | |
024b38fc | 67 | restored. FRAME-PARAMETERS is a frame's parameter alist, |
a912c016 | 68 | extracted with (frame-parameters FRAME) and filtered |
3677ffeb | 69 | through `frameset-filter-params'. |
a912c016 | 70 | WINDOW-STATE is the output of `window-state-get' applied |
3677ffeb | 71 | to the root window of the frame. |
063233c3 | 72 | |
024b38fc JB |
73 | To avoid collisions, it is recommended that applications wanting to add |
74 | private serializable data to `properties' either store all info under a | |
75 | single, distinctive name, or use property names with a well-chosen prefix. | |
76 | ||
063233c3 JB |
77 | A frameset is intended to be used through the following simple API: |
78 | ||
024b38fc JB |
79 | - `frameset-save', the type's constructor, captures all or a subset of the |
80 | live frames, and returns a serializable snapshot of them (a frameset). | |
063233c3 JB |
81 | - `frameset-restore' takes a frameset, and restores the frames and windows |
82 | it describes, as faithfully as possible. | |
76c5e5ab JB |
83 | - `frameset-p' is the predicate for the frameset type. |
84 | - `frameset-valid-p' checks a frameset's validity. | |
063233c3 JB |
85 | - `frameset-copy' returns a deep copy of a frameset. |
86 | - `frameset-prop' is a `setf'able accessor for the contents of the | |
87 | `properties' slot. | |
88 | - The `frameset-SLOT' accessors described above." | |
89 | ||
a912c016 JB |
90 | (version 1 :read-only t) |
91 | (timestamp (current-time) :read-only t) | |
92 | (app nil) | |
93 | (name nil) | |
94 | (description nil) | |
95 | (properties nil) | |
96 | (states nil)) | |
063233c3 | 97 | |
76c5e5ab JB |
98 | ;; Add nicer docstrings for built-in predicate and accessors. |
99 | (put 'frameset-p 'function-documentation | |
100 | "Return non-nil if OBJECT is a frameset, nil otherwise.\n\n(fn OBJECT)") | |
101 | (put 'frameset-version 'function-documentation | |
102 | "Return the version number of FRAMESET.\n | |
103 | It is an integer that identifies the format of the frameset struct. | |
104 | This slot cannot be modified.\n\n(fn FRAMESET)") | |
105 | (put 'frameset-timestamp 'function-documentation | |
106 | "Return the creation timestamp of FRAMESET.\n | |
107 | The value is in the format returned by `current-time'. | |
108 | This slot cannot be modified.\n\n(fn FRAMESET)") | |
109 | (put 'frameset-app 'function-documentation | |
110 | "Return the application identifier for FRAMESET.\n | |
111 | The value is either a symbol, like `my-app', or a list | |
112 | \(my-app ADDITIONAL-DATA...).\n\n(fn FRAMESET)") | |
113 | (put 'frameset-name 'function-documentation | |
114 | "Return the name of FRAMESET (a string).\n\n(fn FRAMESET)") | |
115 | (put 'frameset-description 'function-documentation | |
116 | "Return the description of FRAMESET (a string).\n\n(fn FRAMESET)") | |
117 | (put 'frameset-properties 'function-documentation | |
118 | "Return the property list of FRAMESET.\n | |
119 | This list is useful to store both frameset-specific and user-defined | |
120 | serializable data. The simplest way to access and modify it is | |
121 | through `frameset-prop' (which see).\n\n(fn FRAMESET)") | |
122 | (put 'frameset-states 'function-documentation | |
123 | "Return the list of frame states of FRAMESET.\n | |
124 | A frame state is a pair (FRAME-PARAMETERS . WINDOW-STATE), where | |
125 | FRAME-PARAMETERS is a frame's parameter alist, extracted with | |
126 | \(frame-parameters FRAME) and filtered through `frameset-filter-params', | |
127 | and WINDOW-STATE is the output of `window-state-get' applied to the | |
128 | root window of the frame.\n | |
129 | IMPORTANT: Modifying this slot may cause frameset functions to fail, | |
130 | unless the type constraints defined above are respected.\n\n(fn FRAMESET)") | |
131 | ||
77187e6f JB |
132 | ;;;###autoload (autoload 'frameset-p "frameset" |
133 | ;;;###autoload "Return non-nil if OBJECT is a frameset, nil otherwise." nil) | |
134 | ||
063233c3 | 135 | (defun frameset-copy (frameset) |
a912c016 JB |
136 | "Return a deep copy of FRAMESET. |
137 | FRAMESET is copied with `copy-tree'." | |
9421876d JB |
138 | (copy-tree frameset t)) |
139 | ||
51d30f2c | 140 | ;;;###autoload |
76c5e5ab | 141 | (defun frameset-valid-p (object) |
c03c02ee JB |
142 | "Return non-nil if OBJECT is a valid frameset, nil otherwise. |
143 | ||
144 | The return value is nil if OBJECT is not a frameset, or not | |
145 | a valid one, and the frameset version if it is valid." | |
a912c016 | 146 | (and (vectorp object) ; a vector |
c03c02ee | 147 | (>= (length object) 8) ; of the right length (future-proof) |
a912c016 | 148 | (eq (aref object 0) 'frameset) ; tagged as `frameset' |
76c5e5ab JB |
149 | (integerp (aref object 1)) ; VERSION is an int |
150 | (consp (aref object 2)) ; TIMESTAMP is a non-null list | |
151 | (let ((app (aref object 3))) | |
152 | (or (null app) ; APP is nil | |
153 | (symbolp app) ; or a symbol | |
154 | (and (consp app) ; or a list | |
155 | (symbolp (car app))))) ; starting with a symbol | |
156 | (stringp (or (aref object 4) "")) ; NAME is a string or nil | |
157 | (stringp (or (aref object 5) "")) ; DESCRIPTION is a string or nil | |
158 | (listp (aref object 6)) ; PROPERTIES is a list | |
7ec326db | 159 | (listp (aref object 7)) ; and STATES is, too |
76c5e5ab JB |
160 | (cl-every #'consp (aref object 7)) ; and an alist |
161 | (aref object 1))) ; return VERSION | |
9421876d | 162 | |
2613dea2 | 163 | ;; A setf'able accessor to the frameset's properties |
a912c016 JB |
164 | (defun frameset-prop (frameset property) |
165 | "Return the value for FRAMESET of PROPERTY. | |
2613dea2 | 166 | |
063233c3 | 167 | Properties can be set with |
2613dea2 | 168 | |
bd0c3c0b | 169 | (setf (frameset-prop FRAMESET PROPERTY) NEW-VALUE)" |
a912c016 | 170 | (plist-get (frameset-properties frameset) property)) |
2613dea2 | 171 | |
6475c94b JB |
172 | (gv-define-setter frameset-prop (val fs prop) |
173 | (macroexp-let2 nil v val | |
174 | `(progn | |
6475c94b JB |
175 | (setf (frameset-properties ,fs) |
176 | (plist-put (frameset-properties ,fs) ,prop ,v)) | |
177 | ,v))) | |
2613dea2 | 178 | |
9421876d JB |
179 | \f |
180 | ;; Filtering | |
181 | ||
a912c016 JB |
182 | ;; What's the deal with these "filter alists"? |
183 | ;; | |
184 | ;; Let's say that Emacs' frame parameters were never designed as a tool to | |
185 | ;; precisely record (or restore) a frame's state. They grew organically, | |
186 | ;; and their uses and behaviors reflect their history. In using them to | |
187 | ;; implement framesets, the unwary implementor, or the prospective package | |
188 | ;; writer willing to use framesets in their code, might fall victim of some | |
189 | ;; unexpected... oddities. | |
190 | ;; | |
191 | ;; You can find frame parameters that: | |
192 | ;; | |
193 | ;; - can be used to get and set some data from the frame's current state | |
194 | ;; (`height', `width') | |
195 | ;; - can be set at creation time, and setting them afterwards has no effect | |
196 | ;; (`window-state', `minibuffer') | |
197 | ;; - can be set at creation time, and setting them afterwards will fail with | |
198 | ;; an error, *unless* you set it to the same value, a noop (`border-width') | |
199 | ;; - act differently when passed at frame creation time, and when set | |
200 | ;; afterwards (`height') | |
201 | ;; - affect the value of other parameters (`name', `visibility') | |
202 | ;; - can be ignored by window managers (most positional args, like `height', | |
203 | ;; `width', `left' and `top', and others, like `auto-raise', `auto-lower') | |
204 | ;; - can be set externally in X resources or Window registry (again, most | |
205 | ;; positional parameters, and also `toolbar-lines', `menu-bar-lines' etc.) | |
206 | ;, - can contain references to live objects (`buffer-list', `minibuffer') or | |
207 | ;; code (`buffer-predicate') | |
208 | ;; - are set automatically, and cannot be changed (`window-id', `parent-id'), | |
209 | ;; but setting them produces no error | |
210 | ;; - have a noticeable effect in some window managers, and are ignored in | |
211 | ;; others (`menu-bar-lines') | |
212 | ;; - can not be safely set in a tty session and then copied back to a GUI | |
213 | ;; session (`font', `background-color', `foreground-color') | |
214 | ;; | |
215 | ;; etc etc. | |
216 | ;; | |
217 | ;; Which means that, in order to save a parameter alist to disk and read it | |
218 | ;; back later to reconstruct a frame, some processing must be done. That's | |
219 | ;; what `frameset-filter-params' and the `frameset-*-filter-alist' variables | |
220 | ;; are for. | |
221 | ;; | |
76c5e5ab | 222 | ;; First, a clarification. The word "filter" in these names refers to both |
a912c016 JB |
223 | ;; common meanings of filter: to filter out (i.e., to remove), and to pass |
224 | ;; through a transformation function (think `filter-buffer-substring'). | |
225 | ;; | |
226 | ;; `frameset-filter-params' takes a parameter alist PARAMETERS, a filtering | |
227 | ;; alist FILTER-ALIST, and a flag SAVING to indicate whether we are filtering | |
228 | ;; parameters with the intent of saving a frame or restoring it. It then | |
76c5e5ab | 229 | ;; accumulates an output alist, FILTERED, by checking each parameter in |
a912c016 JB |
230 | ;; PARAMETERS against FILTER-ALIST and obeying any rule found there. The |
231 | ;; absence of a rule just means the parameter/value pair (called CURRENT in | |
232 | ;; filtering functions) is copied to FILTERED as is. Keyword values :save, | |
233 | ;; :restore and :never tell the function to copy CURRENT to FILTERED in the | |
234 | ;; respective situations, that is, when saving, restoring, or never at all. | |
235 | ;; Values :save and :restore are not used in this package, because usually if | |
236 | ;; you don't want to save a parameter, you don't want to restore it either. | |
237 | ;; But they can be useful, for example, if you already have a saved frameset | |
238 | ;; created with some intent, and want to reuse it for a different objective | |
239 | ;; where the expected parameter list has different requirements. | |
240 | ;; | |
241 | ;; Finally, the value can also be a filtering function, or a filtering | |
242 | ;; function plus some arguments. The function is called for each matching | |
243 | ;; parameter, and receives CURRENT (the parameter/value pair being processed), | |
244 | ;; FILTERED (the output alist so far), PARAMETERS (the full parameter alist), | |
245 | ;; SAVING (the save/restore flag), plus any additional ARGS set along the | |
246 | ;; function in the `frameset-*-filter-alist' entry. The filtering function | |
247 | ;; then has the possibility to pass along CURRENT, or reject it altogether, | |
248 | ;; or pass back a (NEW-PARAM . NEW-VALUE) pair, which does not even need to | |
249 | ;; refer to the same parameter (so you can filter `width' and return `height' | |
250 | ;; and vice versa, if you're feeling silly and want to mess with the user's | |
251 | ;; mind). As a help in deciding what to do, the filtering function has | |
252 | ;; access to PARAMETERS, but must not change it in any way. It also has | |
253 | ;; access to FILTERED, which can be modified at will. This allows two or | |
254 | ;; more filters to coordinate themselves, because in general there's no way | |
255 | ;; to predict the order in which they will be run. | |
256 | ;; | |
257 | ;; So, which parameters are filtered by default, and why? Let's see. | |
258 | ;; | |
259 | ;; - `buffer-list', `buried-buffer-list', `buffer-predicate': They contain | |
260 | ;; references to live objects, or in the case of `buffer-predicate', it | |
261 | ;; could also contain an fbound symbol (a predicate function) that could | |
262 | ;; not be defined in a later session. | |
263 | ;; | |
264 | ;; - `window-id', `outer-window-id', `parent-id': They are assigned | |
265 | ;; automatically and cannot be set, so keeping them is harmless, but they | |
266 | ;; add clutter. `window-system' is similar: it's assigned at frame | |
267 | ;; creation, and does not serve any useful purpose later. | |
268 | ;; | |
269 | ;; - `left', `top': Only problematic when saving an iconified frame, because | |
270 | ;; when the frame is iconified they are set to (- 32000), which doesn't | |
271 | ;; really help in restoring the frame. Better to remove them and let the | |
272 | ;; window manager choose a default position for the frame. | |
273 | ;; | |
274 | ;; - `background-color', `foreground-color': In tty frames they can be set | |
275 | ;; to "unspecified-bg" and "unspecified-fg", which aren't understood on | |
276 | ;; GUI sessions. They have to be filtered out when switching from tty to | |
277 | ;; a graphical display. | |
278 | ;; | |
279 | ;; - `tty', `tty-type': These are tty-specific. When switching to a GUI | |
76c5e5ab | 280 | ;; display they do no harm, but they clutter the parameter alist. |
a912c016 JB |
281 | ;; |
282 | ;; - `minibuffer': It can contain a reference to a live window, which cannot | |
283 | ;; be serialized. Because of Emacs' idiosyncratic treatment of this | |
284 | ;; parameter, frames created with (minibuffer . t) have a parameter | |
285 | ;; (minibuffer . #<window...>), while frames created with | |
286 | ;; (minibuffer . #<window...>) have (minibuffer . nil), which is madness | |
287 | ;; but helps to differentiate between minibufferless and "normal" frames. | |
288 | ;; So, changing (minibuffer . #<window...>) to (minibuffer . t) allows | |
289 | ;; Emacs to set up the new frame correctly. Nice, uh? | |
290 | ;; | |
291 | ;; - `name': If this parameter is directly set, `explicit-name' is | |
292 | ;; automatically set to t, and then `name' no longer changes dynamically. | |
293 | ;; So, in general, not saving `name' is the right thing to do, though | |
294 | ;; surely there are applications that will want to override this filter. | |
295 | ;; | |
296 | ;; - `font', `fullscreen', `height' and `width': These parameters suffer | |
76c5e5ab | 297 | ;; from the fact that they are badly mangled when going through a |
a912c016 JB |
298 | ;; tty session, though not all in the same way. When saving a GUI frame |
299 | ;; and restoring it in a tty, the height and width of the new frame are | |
300 | ;; those of the tty screen (let's say 80x25, for example); going back | |
301 | ;; to a GUI session means getting frames of the tty screen size (so all | |
302 | ;; your frames are 80 cols x 25 rows). For `fullscreen' there's a | |
303 | ;; similar problem, because a tty frame cannot really be fullscreen or | |
304 | ;; maximized, so the state is lost. The problem with `font' is a bit | |
305 | ;; different, because a valid GUI font spec in `font' turns into | |
306 | ;; (font . "tty") in a tty frame, and when read back into a GUI session | |
307 | ;; it fails because `font's value is no longer a valid font spec. | |
308 | ;; | |
309 | ;; In most cases, the filtering functions just do the obvious thing: remove | |
310 | ;; CURRENT when it is meaningless to keep it, or pass a modified copy if | |
311 | ;; that helps (as in the case of `minibuffer'). | |
312 | ;; | |
313 | ;; The exception are the parameters in the last set, which should survive | |
314 | ;; the roundtrip though tty-land. The answer is to add "stashing | |
315 | ;; parameters", working in pairs, to shelve the GUI-specific contents and | |
316 | ;; restore it once we're back in pixel country. That's what functions | |
c03c02ee | 317 | ;; `frameset-filter-shelve-param' and `frameset-filter-unshelve-param' do. |
a912c016 JB |
318 | ;; |
319 | ;; Basically, if you set `frameset-filter-shelve-param' as the filter for | |
320 | ;; a parameter P, it will detect when it is restoring a GUI frame into a | |
321 | ;; tty session, and save P's value in the custom parameter X:P, but only | |
322 | ;; if X:P does not exist already (so it is not overwritten if you enter | |
323 | ;; the tty session more than once). If you're not switching to a tty | |
324 | ;; frame, the filter just passes CURRENT along. | |
325 | ;; | |
326 | ;; The parameter X:P, on the other hand, must have been setup to be | |
327 | ;; filtered by `frameset-filter-unshelve-param', which unshelves the | |
328 | ;; value: if we're entering a GUI session, returns P instead of CURRENT, | |
329 | ;; while in other cases it just passes it along. | |
330 | ;; | |
331 | ;; The only additional trick is that `frameset-filter-shelve-param' does | |
332 | ;; not set P if switching back to GUI and P already has a value, because | |
333 | ;; it assumes that `frameset-filter-unshelve-param' did set it up. And | |
334 | ;; `frameset-filter-unshelve-param', when unshelving P, must look into | |
335 | ;; FILTERED to determine if P has already been set and if so, modify it; | |
336 | ;; else just returns P. | |
337 | ;; | |
338 | ;; Currently, the value of X in X:P is `GUI', but you can use any prefix, | |
339 | ;; by passing its symbol as argument in the filter: | |
340 | ;; | |
341 | ;; (my-parameter frameset-filter-shelve-param MYPREFIX) | |
342 | ;; | |
343 | ;; instead of | |
344 | ;; | |
345 | ;; (my-parameter . frameset-filter-shelve-param) | |
346 | ;; | |
347 | ;; Note that `frameset-filter-unshelve-param' does not need MYPREFIX | |
348 | ;; because it is available from the parameter name in CURRENT. Also note | |
349 | ;; that the colon between the prefix and the parameter name is hardcoded. | |
350 | ;; The reason is that X:P is quite readable, and that the colon is a | |
351 | ;; very unusual character in symbol names, other than in initial position | |
352 | ;; in keywords (emacs -Q has only two such symbols, and one of them is a | |
353 | ;; URL). So the probability of a collision with existing or future | |
354 | ;; symbols is quite insignificant. | |
355 | ;; | |
c03c02ee JB |
356 | ;; Now, what about the filter alist variables? There are three of them, |
357 | ;; though only two sets of parameters: | |
a912c016 JB |
358 | ;; |
359 | ;; - `frameset-session-filter-alist' contains these filters that allow to | |
360 | ;; save and restore framesets in-session, without the need to serialize | |
361 | ;; the frameset or save it to disk (for example, to save a frameset in a | |
362 | ;; register and restore it later). Filters in this list do not remove | |
363 | ;; live objects, except in `minibuffer', which is dealt especially by | |
364 | ;; `frameset-save' / `frameset-restore'. | |
365 | ;; | |
366 | ;; - `frameset-persistent-filter-alist' is the whole deal. It does all | |
367 | ;; the filtering described above, and the result is ready to be saved on | |
368 | ;; disk without loss of information. That's the format used by the | |
369 | ;; desktop.el package, for example. | |
370 | ;; | |
c03c02ee | 371 | ;; IMPORTANT: These variables share structure and should NEVER be modified. |
a912c016 JB |
372 | ;; |
373 | ;; - `frameset-filter-alist': The value of this variable is the default | |
374 | ;; value for the FILTERS arguments of `frameset-save' and | |
375 | ;; `frameset-restore'. It is set to `frameset-persistent-filter-alist', | |
376 | ;; though it can be changed by specific applications. | |
377 | ;; | |
378 | ;; How to use them? | |
379 | ;; | |
380 | ;; The simplest way is just do nothing. The default should work | |
381 | ;; reasonably and sensibly enough. But, what if you really need a | |
382 | ;; customized filter alist? Then you can create your own variable | |
383 | ;; | |
384 | ;; (defvar my-filter-alist | |
385 | ;; '((my-param1 . :never) | |
386 | ;; (my-param2 . :save) | |
387 | ;; (my-param3 . :restore) | |
388 | ;; (my-param4 . my-filtering-function-without-args) | |
389 | ;; (my-param5 my-filtering-function-with arg1 arg2) | |
390 | ;; ;;; many other parameters | |
391 | ;; ) | |
392 | ;; "My customized parameter filter alist.") | |
393 | ;; | |
394 | ;; or, if you're only changing a few items, | |
395 | ;; | |
396 | ;; (defvar my-filter-alist | |
397 | ;; (nconc '((my-param1 . :never) | |
398 | ;; (my-param2 . my-filtering-function)) | |
399 | ;; frameset-filter-alist) | |
400 | ;; "My brief customized parameter filter alist.") | |
401 | ;; | |
402 | ;; and pass it to the FILTER arg of the save/restore functions, | |
403 | ;; ALWAYS taking care of not modifying the original lists; if you're | |
404 | ;; going to do any modifying of my-filter-alist, please use | |
405 | ;; | |
406 | ;; (nconc '((my-param1 . :never) ...) | |
407 | ;; (copy-sequence frameset-filter-alist)) | |
408 | ;; | |
409 | ;; One thing you shouldn't forget is that they are alists, so searching | |
410 | ;; in them is sequential. If you just want to change the default of | |
411 | ;; `name' to allow it to be saved, you can set (name . nil) in your | |
412 | ;; customized filter alist; it will take precedence over the latter | |
413 | ;; setting. In case you decide that you *always* want to save `name', | |
414 | ;; you can add it to `frameset-filter-alist': | |
415 | ;; | |
416 | ;; (push '(name . nil) frameset-filter-alist) | |
417 | ;; | |
418 | ;; In certain applications, having a parameter filtering function like | |
419 | ;; `frameset-filter-params' can be useful, even if you're not using | |
420 | ;; framesets. The interface of `frameset-filter-params' is generic | |
421 | ;; and does not depend of global state, with one exception: it uses | |
422 | ;; the internal variable `frameset--target-display' to decide if, and | |
423 | ;; how, to modify the `display' parameter of FILTERED. But that | |
424 | ;; should not represent any problem, because it's only meaningful | |
425 | ;; when restoring, and customized uses of `frameset-filter-params' | |
426 | ;; are likely to use their own filter alist and just call | |
427 | ;; | |
428 | ;; (setq my-filtered (frameset-filter-params my-params my-filters t)) | |
429 | ;; | |
430 | ;; In case you want to use it with the standard filters, you can | |
431 | ;; wrap the call to `frameset-filter-params' in a let form to bind | |
432 | ;; `frameset--target-display' to nil or the desired value. | |
433 | ;; | |
434 | ||
d5671a82 | 435 | ;;;###autoload |
a912c016 | 436 | (defvar frameset-session-filter-alist |
76c5e5ab | 437 | '((name . :never) |
307764cc | 438 | (left . frameset-filter-iconified) |
76c5e5ab JB |
439 | (minibuffer . frameset-filter-minibuffer) |
440 | (top . frameset-filter-iconified)) | |
d5671a82 | 441 | "Minimum set of parameters to filter for live (on-session) framesets. |
f9dbf1cb | 442 | DO NOT MODIFY. See `frameset-filter-alist' for a full description.") |
d5671a82 JB |
443 | |
444 | ;;;###autoload | |
445 | (defvar frameset-persistent-filter-alist | |
446 | (nconc | |
76c5e5ab JB |
447 | '((background-color . frameset-filter-sanitize-color) |
448 | (buffer-list . :never) | |
449 | (buffer-predicate . :never) | |
063233c3 | 450 | (buried-buffer-list . :never) |
76c5e5ab JB |
451 | (font . frameset-filter-shelve-param) |
452 | (foreground-color . frameset-filter-sanitize-color) | |
453 | (fullscreen . frameset-filter-shelve-param) | |
454 | (GUI:font . frameset-filter-unshelve-param) | |
455 | (GUI:fullscreen . frameset-filter-unshelve-param) | |
456 | (GUI:height . frameset-filter-unshelve-param) | |
457 | (GUI:width . frameset-filter-unshelve-param) | |
458 | (height . frameset-filter-shelve-param) | |
459 | (outer-window-id . :never) | |
460 | (parent-id . :never) | |
461 | (tty . frameset-filter-tty-to-GUI) | |
462 | (tty-type . frameset-filter-tty-to-GUI) | |
463 | (width . frameset-filter-shelve-param) | |
464 | (window-id . :never) | |
465 | (window-system . :never)) | |
a912c016 JB |
466 | frameset-session-filter-alist) |
467 | "Parameters to filter for persistent framesets. | |
f9dbf1cb | 468 | DO NOT MODIFY. See `frameset-filter-alist' for a full description.") |
d5671a82 JB |
469 | |
470 | ;;;###autoload | |
471 | (defvar frameset-filter-alist frameset-persistent-filter-alist | |
9421876d JB |
472 | "Alist of frame parameters and filtering functions. |
473 | ||
a912c016 JB |
474 | This alist is the default value of the FILTERS argument of |
475 | `frameset-save' and `frameset-restore' (which see). | |
476 | ||
f9dbf1cb JB |
477 | Initially, `frameset-filter-alist' is set to, and shares the value of, |
478 | `frameset-persistent-filter-alist'. You can override any item in | |
479 | this alist by `push'ing a new item onto it. If, for some reason, you | |
480 | intend to modify existing values, do | |
481 | ||
482 | (setq frameset-filter-alist (copy-tree frameset-filter-alist)) | |
483 | ||
484 | before changing anything. | |
485 | ||
a912c016 JB |
486 | On saving, PARAMETERS is the parameter alist of each frame processed, |
487 | and FILTERED is the parameter alist that gets saved to the frameset. | |
488 | ||
024b38fc JB |
489 | On restoring, PARAMETERS is the parameter alist extracted from the |
490 | frameset, and FILTERED is the resulting frame parameter alist used | |
063233c3 JB |
491 | to restore the frame. |
492 | ||
024b38fc JB |
493 | Elements of `frameset-filter-alist' are conses (PARAM . ACTION), |
494 | where PARAM is a parameter name (a symbol identifying a frame | |
495 | parameter), and ACTION can be: | |
063233c3 JB |
496 | |
497 | nil The parameter is copied to FILTERED. | |
498 | :never The parameter is never copied to FILTERED. | |
76c5e5ab | 499 | :save The parameter is copied only when saving the frame. |
063233c3 | 500 | :restore The parameter is copied only when restoring the frame. |
76c5e5ab | 501 | FILTER A filter function. |
9421876d JB |
502 | |
503 | FILTER can be a symbol FILTER-FUN, or a list (FILTER-FUN ARGS...). | |
024b38fc JB |
504 | FILTER-FUN is invoked with |
505 | ||
506 | (apply FILTER-FUN CURRENT FILTERED PARAMETERS SAVING ARGS) | |
507 | ||
508 | where | |
9421876d JB |
509 | |
510 | CURRENT A cons (PARAM . VALUE), where PARAM is the one being | |
d5671a82 | 511 | filtered and VALUE is its current value. |
063233c3 | 512 | FILTERED The resulting alist (so far). |
9421876d | 513 | PARAMETERS The complete alist of parameters being filtered, |
76c5e5ab | 514 | SAVING Non-nil if filtering before saving state, nil if filtering |
a912c016 | 515 | before restoring it. |
024b38fc | 516 | ARGS Any additional arguments specified in the ACTION. |
9421876d | 517 | |
307764cc JB |
518 | FILTER-FUN is allowed to modify items in FILTERED, but no other arguments. |
519 | It must return: | |
76c5e5ab JB |
520 | nil Skip CURRENT (do not add it to FILTERED). |
521 | t Add CURRENT to FILTERED as is. | |
063233c3 JB |
522 | (NEW-PARAM . NEW-VALUE) Add this to FILTERED instead of CURRENT. |
523 | ||
524 | Frame parameters not on this alist are passed intact, as if they were | |
525 | defined with ACTION = nil.") | |
9421876d | 526 | |
9421876d JB |
527 | |
528 | (defvar frameset--target-display nil | |
529 | ;; Either (minibuffer . VALUE) or nil. | |
530 | ;; This refers to the current frame config being processed inside | |
063233c3 | 531 | ;; `frameset-restore' and its auxiliary functions (like filtering). |
9421876d JB |
532 | ;; If nil, there is no need to change the display. |
533 | ;; If non-nil, display parameter to use when creating the frame. | |
534 | "Internal use only.") | |
535 | ||
536 | (defun frameset-switch-to-gui-p (parameters) | |
537 | "True when switching to a graphic display. | |
a912c016 JB |
538 | Return non-nil if the parameter alist PARAMETERS describes a frame on a |
539 | text-only terminal, and the frame is being restored on a graphic display; | |
540 | otherwise return nil. Only meaningful when called from a filtering | |
541 | function in `frameset-filter-alist'." | |
76c5e5ab JB |
542 | (and frameset--target-display ; we're switching |
543 | (null (cdr (assq 'display parameters))) ; from a tty | |
544 | (cdr frameset--target-display))) ; to a GUI display | |
9421876d JB |
545 | |
546 | (defun frameset-switch-to-tty-p (parameters) | |
547 | "True when switching to a text-only terminal. | |
a912c016 JB |
548 | Return non-nil if the parameter alist PARAMETERS describes a frame on a |
549 | graphic display, and the frame is being restored on a text-only terminal; | |
550 | otherwise return nil. Only meaningful when called from a filtering | |
551 | function in `frameset-filter-alist'." | |
76c5e5ab JB |
552 | (and frameset--target-display ; we're switching |
553 | (cdr (assq 'display parameters)) ; from a GUI display | |
554 | (null (cdr frameset--target-display)))) ; to a tty | |
9421876d | 555 | |
d5671a82 | 556 | (defun frameset-filter-tty-to-GUI (_current _filtered parameters saving) |
063233c3 JB |
557 | "Remove CURRENT when switching from tty to a graphic display. |
558 | ||
559 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 560 | see `frameset-filter-alist'." |
d5671a82 JB |
561 | (or saving |
562 | (not (frameset-switch-to-gui-p parameters)))) | |
563 | ||
9421876d JB |
564 | (defun frameset-filter-sanitize-color (current _filtered parameters saving) |
565 | "When switching to a GUI frame, remove \"unspecified\" colors. | |
063233c3 JB |
566 | Useful as a filter function for tty-specific parameters. |
567 | ||
568 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 569 | see `frameset-filter-alist'." |
9421876d JB |
570 | (or saving |
571 | (not (frameset-switch-to-gui-p parameters)) | |
572 | (not (stringp (cdr current))) | |
573 | (not (string-match-p "^unspecified-[fb]g$" (cdr current))))) | |
574 | ||
575 | (defun frameset-filter-minibuffer (current _filtered _parameters saving) | |
a912c016 | 576 | "When saving, convert (minibuffer . #<window>) to (minibuffer . t). |
063233c3 JB |
577 | |
578 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 579 | see `frameset-filter-alist'." |
9421876d JB |
580 | (or (not saving) |
581 | (if (windowp (cdr current)) | |
582 | '(minibuffer . t) | |
583 | t))) | |
584 | ||
a912c016 JB |
585 | (defun frameset-filter-shelve-param (current _filtered parameters saving |
586 | &optional prefix) | |
9421876d | 587 | "When switching to a tty frame, save parameter P as PREFIX:P. |
a912c016 | 588 | The parameter can be later restored with `frameset-filter-unshelve-param'. |
063233c3 JB |
589 | PREFIX defaults to `GUI'. |
590 | ||
591 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 592 | see `frameset-filter-alist'." |
9421876d JB |
593 | (unless prefix (setq prefix 'GUI)) |
594 | (cond (saving t) | |
595 | ((frameset-switch-to-tty-p parameters) | |
596 | (let ((prefix:p (intern (format "%s:%s" prefix (car current))))) | |
597 | (if (assq prefix:p parameters) | |
598 | nil | |
599 | (cons prefix:p (cdr current))))) | |
600 | ((frameset-switch-to-gui-p parameters) | |
601 | (not (assq (intern (format "%s:%s" prefix (car current))) parameters))) | |
602 | (t t))) | |
603 | ||
a912c016 | 604 | (defun frameset-filter-unshelve-param (current filtered parameters saving) |
9421876d | 605 | "When switching to a GUI frame, restore PREFIX:P parameter as P. |
063233c3 JB |
606 | CURRENT must be of the form (PREFIX:P . value). |
607 | ||
608 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 609 | see `frameset-filter-alist'." |
9421876d JB |
610 | (or saving |
611 | (not (frameset-switch-to-gui-p parameters)) | |
612 | (let* ((prefix:p (symbol-name (car current))) | |
613 | (p (intern (substring prefix:p | |
614 | (1+ (string-match-p ":" prefix:p))))) | |
615 | (val (cdr current)) | |
616 | (found (assq p filtered))) | |
617 | (if (not found) | |
618 | (cons p val) | |
619 | (setcdr found val) | |
620 | nil)))) | |
621 | ||
622 | (defun frameset-filter-iconified (_current _filtered parameters saving) | |
623 | "Remove CURRENT when saving an iconified frame. | |
063233c3 | 624 | This is used for positional parameters `left' and `top', which are |
9421876d | 625 | meaningless in an iconified frame, so the frame is restored in a |
063233c3 JB |
626 | default position. |
627 | ||
628 | For the meaning of CURRENT, FILTERED, PARAMETERS and SAVING, | |
a912c016 | 629 | see `frameset-filter-alist'." |
9421876d JB |
630 | (not (and saving (eq (cdr (assq 'visibility parameters)) 'icon)))) |
631 | ||
9421876d | 632 | (defun frameset-filter-params (parameters filter-alist saving) |
024b38fc | 633 | "Filter parameter alist PARAMETERS and return a filtered alist. |
9421876d JB |
634 | FILTER-ALIST is an alist of parameter filters, in the format of |
635 | `frameset-filter-alist' (which see). | |
636 | SAVING is non-nil while filtering parameters to save a frameset, | |
637 | nil while the filtering is done to restore it." | |
638 | (let ((filtered nil)) | |
639 | (dolist (current parameters) | |
024b38fc JB |
640 | ;; When saving, the parameter alist is temporary, so modifying it |
641 | ;; is not a problem. When restoring, the parameter alist is part | |
307764cc JB |
642 | ;; of a frameset, so we must copy parameters to avoid inadvertent |
643 | ;; modifications. | |
9421876d JB |
644 | (pcase (cdr (assq (car current) filter-alist)) |
645 | (`nil | |
307764cc | 646 | (push (if saving current (copy-tree current)) filtered)) |
063233c3 | 647 | (:never |
9421876d | 648 | nil) |
9421876d | 649 | (:restore |
307764cc | 650 | (unless saving (push (copy-tree current) filtered))) |
063233c3 | 651 | (:save |
9421876d JB |
652 | (when saving (push current filtered))) |
653 | ((or `(,fun . ,args) (and fun (pred fboundp))) | |
307764cc JB |
654 | (let* ((this (apply fun current filtered parameters saving args)) |
655 | (val (if (eq this t) current this))) | |
656 | (when val | |
657 | (push (if saving val (copy-tree val)) filtered)))) | |
9421876d JB |
658 | (other |
659 | (delay-warning 'frameset (format "Unknown filter %S" other) :error)))) | |
660 | ;; Set the display parameter after filtering, so that filter functions | |
661 | ;; have access to its original value. | |
662 | (when frameset--target-display | |
663 | (let ((display (assq 'display filtered))) | |
664 | (if display | |
665 | (setcdr display (cdr frameset--target-display)) | |
666 | (push frameset--target-display filtered)))) | |
667 | filtered)) | |
668 | ||
669 | \f | |
38276e01 | 670 | ;; Frame ids |
9421876d JB |
671 | |
672 | (defun frameset--set-id (frame) | |
38276e01 | 673 | "Set FRAME's id if not yet set. |
9421876d | 674 | Internal use only." |
d5671a82 | 675 | (unless (frame-parameter frame 'frameset--id) |
9421876d | 676 | (set-frame-parameter frame |
d5671a82 | 677 | 'frameset--id |
9421876d JB |
678 | (mapconcat (lambda (n) (format "%04X" n)) |
679 | (cl-loop repeat 4 collect (random 65536)) | |
680 | "-")))) | |
38276e01 JB |
681 | ;;;###autoload |
682 | (defun frameset-frame-id (frame) | |
683 | "Return the frame id of FRAME, if it has one; else, return nil. | |
684 | A frame id is a string that uniquely identifies a frame. | |
685 | It is persistent across `frameset-save' / `frameset-restore' | |
686 | invocations, and once assigned is never changed unless the same | |
687 | frame is duplicated (via `frameset-restore'), in which case the | |
688 | newest frame keeps the id and the old frame's is set to nil." | |
689 | (frame-parameter frame 'frameset--id)) | |
690 | ||
691 | ;;;###autoload | |
692 | (defun frameset-frame-id-equal-p (frame id) | |
693 | "Return non-nil if FRAME's id matches ID." | |
694 | (string= (frameset-frame-id frame) id)) | |
695 | ||
696 | ;;;###autoload | |
a912c016 | 697 | (defun frameset-frame-with-id (id &optional frame-list) |
38276e01 JB |
698 | "Return the live frame with id ID, if exists; else nil. |
699 | If FRAME-LIST is a list of frames, check these frames only. | |
700 | If nil, check all live frames." | |
701 | (cl-find-if (lambda (f) | |
702 | (and (frame-live-p f) | |
703 | (frameset-frame-id-equal-p f id))) | |
704 | (or frame-list (frame-list)))) | |
705 | ||
706 | \f | |
707 | ;; Saving framesets | |
9421876d | 708 | |
a912c016 | 709 | (defun frameset--record-minibuffer-relationships (frame-list) |
9421876d | 710 | "Process FRAME-LIST and record minibuffer relationships. |
d5671a82 | 711 | FRAME-LIST is a list of frames. Internal use only." |
9421876d JB |
712 | ;; Record frames with their own minibuffer |
713 | (dolist (frame (minibuffer-frame-list)) | |
714 | (when (memq frame frame-list) | |
715 | (frameset--set-id frame) | |
716 | ;; For minibuffer-owning frames, frameset--mini is a cons | |
717 | ;; (t . DEFAULT?), where DEFAULT? is a boolean indicating whether | |
718 | ;; the frame is the one pointed out by `default-minibuffer-frame'. | |
719 | (set-frame-parameter frame | |
720 | 'frameset--mini | |
721 | (cons t (eq frame default-minibuffer-frame))))) | |
722 | ;; Now link minibufferless frames with their minibuffer frames | |
723 | (dolist (frame frame-list) | |
724 | (unless (frame-parameter frame 'frameset--mini) | |
725 | (frameset--set-id frame) | |
726 | (let* ((mb-frame (window-frame (minibuffer-window frame))) | |
38276e01 | 727 | (id (and mb-frame (frameset-frame-id mb-frame)))) |
9421876d | 728 | (if (null id) |
063233c3 | 729 | (error "Minibuffer frame %S for %S is not being saved" mb-frame frame) |
9421876d | 730 | ;; For minibufferless frames, frameset--mini is a cons |
d5671a82 JB |
731 | ;; (nil . FRAME-ID), where FRAME-ID is the frameset--id |
732 | ;; of the frame containing its minibuffer window. | |
9421876d JB |
733 | (set-frame-parameter frame |
734 | 'frameset--mini | |
735 | (cons nil id))))))) | |
736 | ||
51d30f2c | 737 | ;;;###autoload |
a912c016 JB |
738 | (cl-defun frameset-save (frame-list |
739 | &key app name description | |
740 | filters predicate properties) | |
741 | "Return a frameset for FRAME-LIST, a list of frames. | |
307764cc JB |
742 | Dead frames and non-frame objects are silently removed from the list. |
743 | If nil, FRAME-LIST defaults to the output of `frame-list' (all live frames). | |
a912c016 JB |
744 | APP, NAME and DESCRIPTION are optional data; see the docstring of the |
745 | `frameset' defstruct for details. | |
746 | FILTERS is an alist of parameter filters; if nil, the value of the variable | |
747 | `frameset-filter-alist' is used instead. | |
9421876d | 748 | PREDICATE is a predicate function, which must return non-nil for frames that |
024b38fc | 749 | should be saved; if PREDICATE is nil, all frames from FRAME-LIST are saved. |
9421876d | 750 | PROPERTIES is a user-defined property list to add to the frameset." |
063233c3 JB |
751 | (let* ((list (or (copy-sequence frame-list) (frame-list))) |
752 | (frames (cl-delete-if-not #'frame-live-p | |
753 | (if predicate | |
754 | (cl-delete-if-not predicate list) | |
c03c02ee JB |
755 | list))) |
756 | fs) | |
a912c016 | 757 | (frameset--record-minibuffer-relationships frames) |
c03c02ee JB |
758 | (setq fs (make-frameset |
759 | :app app | |
760 | :name name | |
761 | :description description | |
762 | :properties properties | |
763 | :states (mapcar | |
764 | (lambda (frame) | |
765 | (cons | |
766 | (frameset-filter-params (frame-parameters frame) | |
767 | (or filters | |
768 | frameset-filter-alist) | |
769 | t) | |
770 | (window-state-get (frame-root-window frame) t))) | |
771 | frames))) | |
772 | (cl-assert (frameset-valid-p fs)) | |
773 | fs)) | |
9421876d JB |
774 | |
775 | \f | |
776 | ;; Restoring framesets | |
777 | ||
778 | (defvar frameset--reuse-list nil | |
063233c3 JB |
779 | "The list of frames potentially reusable. |
780 | Its value is only meaningful during execution of `frameset-restore'. | |
781 | Internal use only.") | |
9421876d | 782 | |
024b38fc JB |
783 | (defun frameset-compute-pos (value left/top right/bottom) |
784 | "Return an absolute positioning value for a frame. | |
785 | VALUE is the value of a positional frame parameter (`left' or `top'). | |
786 | If VALUE is relative to the screen edges (like (+ -35) or (-200), it is | |
787 | converted to absolute by adding it to the corresponding edge; if it is | |
788 | an absolute position, it is returned unmodified. | |
789 | LEFT/TOP and RIGHT/BOTTOM indicate the dimensions of the screen in | |
790 | pixels along the relevant direction: either the position of the left | |
791 | and right edges for a `left' positional parameter, or the position of | |
792 | the top and bottom edges for a `top' parameter." | |
9421876d JB |
793 | (pcase value |
794 | (`(+ ,val) (+ left/top val)) | |
795 | (`(- ,val) (+ right/bottom val)) | |
796 | (val val))) | |
797 | ||
307764cc | 798 | (defun frameset-move-onscreen (frame force-onscreen) |
9421876d JB |
799 | "If FRAME is offscreen, move it back onscreen and, if necessary, resize it. |
800 | For the description of FORCE-ONSCREEN, see `frameset-restore'. | |
801 | When forced onscreen, frames wider than the monitor's workarea are converted | |
802 | to fullwidth, and frames taller than the workarea are converted to fullheight. | |
307764cc | 803 | NOTE: This only works for non-iconified frames." |
9421876d | 804 | (pcase-let* ((`(,left ,top ,width ,height) (cl-cdadr (frame-monitor-attributes frame))) |
d5671a82 JB |
805 | (right (+ left width -1)) |
806 | (bottom (+ top height -1)) | |
024b38fc JB |
807 | (fr-left (frameset-compute-pos (frame-parameter frame 'left) left right)) |
808 | (fr-top (frameset-compute-pos (frame-parameter frame 'top) top bottom)) | |
9421876d JB |
809 | (ch-width (frame-char-width frame)) |
810 | (ch-height (frame-char-height frame)) | |
d5671a82 JB |
811 | (fr-width (max (frame-pixel-width frame) (* ch-width (frame-width frame)))) |
812 | (fr-height (max (frame-pixel-height frame) (* ch-height (frame-height frame)))) | |
813 | (fr-right (+ fr-left fr-width -1)) | |
814 | (fr-bottom (+ fr-top fr-height -1))) | |
9421876d | 815 | (when (pcase force-onscreen |
d5671a82 JB |
816 | ;; A predicate. |
817 | ((pred functionp) | |
818 | (funcall force-onscreen | |
819 | frame | |
820 | (list fr-left fr-top fr-width fr-height) | |
821 | (list left top width height))) | |
9421876d | 822 | ;; Any corner is outside the screen. |
76c5e5ab | 823 | (:all (or (< fr-bottom top) (> fr-bottom bottom) |
9421876d JB |
824 | (< fr-left left) (> fr-left right) |
825 | (< fr-right left) (> fr-right right) | |
76c5e5ab | 826 | (< fr-top top) (> fr-top bottom))) |
9421876d | 827 | ;; Displaced to the left, right, above or below the screen. |
76c5e5ab | 828 | (`t (or (> fr-left right) |
9421876d | 829 | (< fr-right left) |
76c5e5ab | 830 | (> fr-top bottom) |
9421876d JB |
831 | (< fr-bottom top))) |
832 | ;; Fully inside, no need to do anything. | |
833 | (_ nil)) | |
834 | (let ((fullwidth (> fr-width width)) | |
835 | (fullheight (> fr-height height)) | |
836 | (params nil)) | |
837 | ;; Position frame horizontally. | |
838 | (cond (fullwidth | |
839 | (push `(left . ,left) params)) | |
840 | ((> fr-right right) | |
841 | (push `(left . ,(+ left (- width fr-width))) params)) | |
842 | ((< fr-left left) | |
843 | (push `(left . ,left) params))) | |
844 | ;; Position frame vertically. | |
845 | (cond (fullheight | |
846 | (push `(top . ,top) params)) | |
847 | ((> fr-bottom bottom) | |
848 | (push `(top . ,(+ top (- height fr-height))) params)) | |
849 | ((< fr-top top) | |
850 | (push `(top . ,top) params))) | |
851 | ;; Compute fullscreen state, if required. | |
852 | (when (or fullwidth fullheight) | |
853 | (push (cons 'fullscreen | |
854 | (cond ((not fullwidth) 'fullheight) | |
855 | ((not fullheight) 'fullwidth) | |
856 | (t 'maximized))) | |
857 | params)) | |
858 | ;; Finally, move the frame back onscreen. | |
859 | (when params | |
860 | (modify-frame-parameters frame params)))))) | |
861 | ||
a912c016 | 862 | (defun frameset--find-frame-if (predicate display &rest args) |
9421876d JB |
863 | "Find a frame in `frameset--reuse-list' satisfying PREDICATE. |
864 | Look through available frames whose display property matches DISPLAY | |
865 | and return the first one for which (PREDICATE frame ARGS) returns t. | |
866 | If PREDICATE is nil, it is always satisfied. Internal use only." | |
867 | (cl-find-if (lambda (frame) | |
868 | (and (equal (frame-parameter frame 'display) display) | |
869 | (or (null predicate) | |
870 | (apply predicate frame args)))) | |
871 | frameset--reuse-list)) | |
872 | ||
a912c016 JB |
873 | (defun frameset--reuse-frame (display parameters) |
874 | "Return an existing frame to reuse, or nil if none found. | |
875 | DISPLAY is the display where the frame will be shown, and PARAMETERS | |
024b38fc | 876 | is the parameter alist of the frame being restored. Internal use only." |
9421876d JB |
877 | (let ((frame nil) |
878 | mini) | |
879 | ;; There are no fancy heuristics there. We could implement some | |
880 | ;; based on frame size and/or position, etc., but it is not clear | |
881 | ;; that any "gain" (in the sense of reduced flickering, etc.) is | |
882 | ;; worth the added complexity. In fact, the code below mainly | |
883 | ;; tries to work nicely when M-x desktop-read is used after a | |
884 | ;; desktop session has already been loaded. The other main use | |
885 | ;; case, which is the initial desktop-read upon starting Emacs, | |
886 | ;; will usually have only one frame, and should already work. | |
887 | (cond ((null display) | |
888 | ;; When the target is tty, every existing frame is reusable. | |
a912c016 JB |
889 | (setq frame (frameset--find-frame-if nil display))) |
890 | ((car (setq mini (cdr (assq 'frameset--mini parameters)))) | |
9421876d JB |
891 | ;; If the frame has its own minibuffer, let's see whether |
892 | ;; that frame has already been loaded (which can happen after | |
893 | ;; M-x desktop-read). | |
a912c016 | 894 | (setq frame (frameset--find-frame-if |
9421876d | 895 | (lambda (f id) |
38276e01 | 896 | (frameset-frame-id-equal-p f id)) |
a912c016 | 897 | display (cdr (assq 'frameset--id parameters)))) |
9421876d JB |
898 | ;; If it has not been loaded, and it is not a minibuffer-only frame, |
899 | ;; let's look for an existing non-minibuffer-only frame to reuse. | |
a912c016 JB |
900 | (unless (or frame (eq (cdr (assq 'minibuffer parameters)) 'only)) |
901 | (setq frame (frameset--find-frame-if | |
9421876d JB |
902 | (lambda (f) |
903 | (let ((w (frame-parameter f 'minibuffer))) | |
904 | (and (window-live-p w) | |
905 | (window-minibuffer-p w) | |
906 | (eq (window-frame w) f)))) | |
907 | display)))) | |
908 | (mini | |
909 | ;; For minibufferless frames, check whether they already exist, | |
910 | ;; and that they are linked to the right minibuffer frame. | |
a912c016 | 911 | (setq frame (frameset--find-frame-if |
a04d36a0 | 912 | (lambda (f id mini-id) |
38276e01 JB |
913 | (and (frameset-frame-id-equal-p f id) |
914 | (frameset-frame-id-equal-p (window-frame | |
915 | (minibuffer-window f)) | |
916 | mini-id))) | |
a912c016 | 917 | display (cdr (assq 'frameset--id parameters)) (cdr mini)))) |
9421876d JB |
918 | (t |
919 | ;; Default to just finding a frame in the same display. | |
a912c016 | 920 | (setq frame (frameset--find-frame-if nil display)))) |
9421876d JB |
921 | ;; If found, remove from the list. |
922 | (when frame | |
923 | (setq frameset--reuse-list (delq frame frameset--reuse-list))) | |
924 | frame)) | |
925 | ||
a912c016 JB |
926 | (defun frameset--initial-params (parameters) |
927 | "Return a list of PARAMETERS that must be set when creating the frame. | |
d5671a82 | 928 | Setting position and size parameters as soon as possible helps reducing |
a912c016 JB |
929 | flickering; other parameters, like `minibuffer' and `border-width', can |
930 | not be changed once the frame has been created. Internal use only." | |
d5671a82 | 931 | (cl-loop for param in '(left top with height border-width minibuffer) |
a912c016 | 932 | collect (assq param parameters))) |
d5671a82 | 933 | |
a912c016 | 934 | (defun frameset--restore-frame (parameters window-state filters force-onscreen) |
9421876d JB |
935 | "Set up and return a frame according to its saved state. |
936 | That means either reusing an existing frame or creating one anew. | |
a912c016 | 937 | PARAMETERS is the frame's parameter alist; WINDOW-STATE is its window state. |
063233c3 | 938 | For the meaning of FILTERS and FORCE-ONSCREEN, see `frameset-restore'. |
d5671a82 | 939 | Internal use only." |
a912c016 JB |
940 | (let* ((fullscreen (cdr (assq 'fullscreen parameters))) |
941 | (lines (assq 'tool-bar-lines parameters)) | |
942 | (filtered-cfg (frameset-filter-params parameters filters nil)) | |
9421876d JB |
943 | (display (cdr (assq 'display filtered-cfg))) ;; post-filtering |
944 | alt-cfg frame) | |
945 | ||
946 | ;; This works around bug#14795 (or feature#14795, if not a bug :-) | |
947 | (setq filtered-cfg (assq-delete-all 'tool-bar-lines filtered-cfg)) | |
948 | (push '(tool-bar-lines . 0) filtered-cfg) | |
949 | ||
950 | (when fullscreen | |
951 | ;; Currently Emacs has the limitation that it does not record the size | |
952 | ;; and position of a frame before maximizing it, so we cannot save & | |
953 | ;; restore that info. Instead, when restoring, we resort to creating | |
954 | ;; invisible "fullscreen" frames of default size and then maximizing them | |
955 | ;; (and making them visible) which at least is somewhat user-friendly | |
956 | ;; when these frames are later de-maximized. | |
957 | (let ((width (and (eq fullscreen 'fullheight) (cdr (assq 'width filtered-cfg)))) | |
958 | (height (and (eq fullscreen 'fullwidth) (cdr (assq 'height filtered-cfg)))) | |
959 | (visible (assq 'visibility filtered-cfg))) | |
960 | (setq filtered-cfg (cl-delete-if (lambda (p) | |
961 | (memq p '(visibility fullscreen width height))) | |
962 | filtered-cfg :key #'car)) | |
963 | (when width | |
964 | (setq filtered-cfg (append `((user-size . t) (width . ,width)) | |
965 | filtered-cfg))) | |
966 | (when height | |
967 | (setq filtered-cfg (append `((user-size . t) (height . ,height)) | |
968 | filtered-cfg))) | |
969 | ;; These are parameters to apply after creating/setting the frame. | |
970 | (push visible alt-cfg) | |
971 | (push (cons 'fullscreen fullscreen) alt-cfg))) | |
972 | ||
973 | ;; Time to find or create a frame an apply the big bunch of parameters. | |
974 | ;; If a frame needs to be created and it falls partially or fully offscreen, | |
975 | ;; sometimes it gets "pushed back" onscreen; however, moving it afterwards is | |
976 | ;; allowed. So we create the frame as invisible and then reapply the full | |
024b38fc | 977 | ;; parameter alist (including position and size parameters). |
9421876d JB |
978 | (setq frame (or (and frameset--reuse-list |
979 | (frameset--reuse-frame display filtered-cfg)) | |
980 | (make-frame-on-display display | |
981 | (cons '(visibility) | |
d5671a82 | 982 | (frameset--initial-params filtered-cfg))))) |
9421876d JB |
983 | (modify-frame-parameters frame |
984 | (if (eq (frame-parameter frame 'fullscreen) fullscreen) | |
985 | ;; Workaround for bug#14949 | |
986 | (assq-delete-all 'fullscreen filtered-cfg) | |
987 | filtered-cfg)) | |
988 | ||
989 | ;; If requested, force frames to be onscreen. | |
990 | (when (and force-onscreen | |
991 | ;; FIXME: iconified frames should be checked too, | |
992 | ;; but it is impossible without deiconifying them. | |
993 | (not (eq (frame-parameter frame 'visibility) 'icon))) | |
307764cc | 994 | (frameset-move-onscreen frame force-onscreen)) |
9421876d JB |
995 | |
996 | ;; Let's give the finishing touches (visibility, tool-bar, maximization). | |
997 | (when lines (push lines alt-cfg)) | |
998 | (when alt-cfg (modify-frame-parameters frame alt-cfg)) | |
999 | ;; Now restore window state. | |
a912c016 | 1000 | (window-state-put window-state (frame-root-window frame) 'safe) |
9421876d JB |
1001 | frame)) |
1002 | ||
063233c3 | 1003 | (defun frameset--minibufferless-last-p (state1 state2) |
307764cc | 1004 | "Predicate to sort frame states in an order suitable for creating frames. |
024b38fc JB |
1005 | It sorts minibuffer-owning frames before minibufferless ones. |
1006 | Internal use only." | |
9421876d JB |
1007 | (pcase-let ((`(,hasmini1 ,id-def1) (assq 'frameset--mini (car state1))) |
1008 | (`(,hasmini2 ,id-def2) (assq 'frameset--mini (car state2)))) | |
1009 | (cond ((eq id-def1 t) t) | |
1010 | ((eq id-def2 t) nil) | |
1011 | ((not (eq hasmini1 hasmini2)) (eq hasmini1 t)) | |
1012 | ((eq hasmini1 nil) (string< id-def1 id-def2)) | |
1013 | (t t)))) | |
1014 | ||
d5671a82 | 1015 | (defun frameset-keep-original-display-p (force-display) |
a912c016 JB |
1016 | "True if saved frames' displays should be honored. |
1017 | For the meaning of FORCE-DISPLAY, see `frameset-restore'." | |
d5671a82 | 1018 | (cond ((daemonp) t) |
307764cc | 1019 | ((eq system-type 'windows-nt) nil) ;; Does ns support more than one display? |
d5671a82 JB |
1020 | (t (not force-display)))) |
1021 | ||
063233c3 JB |
1022 | (defun frameset-minibufferless-first-p (frame1 _frame2) |
1023 | "Predicate to sort minibufferless frames before other frames." | |
9421876d JB |
1024 | (not (frame-parameter frame1 'minibuffer))) |
1025 | ||
51d30f2c | 1026 | ;;;###autoload |
d5671a82 | 1027 | (cl-defun frameset-restore (frameset |
a912c016 | 1028 | &key predicate filters reuse-frames |
3677ffeb | 1029 | force-display force-onscreen) |
9421876d JB |
1030 | "Restore a FRAMESET into the current display(s). |
1031 | ||
a912c016 JB |
1032 | PREDICATE is a function called with two arguments, the parameter alist |
1033 | and the window-state of the frame being restored, in that order (see | |
1034 | the docstring of the `frameset' defstruct for additional details). | |
1035 | If PREDICATE returns nil, the frame described by that parameter alist | |
1036 | and window-state is not restored. | |
1037 | ||
1038 | FILTERS is an alist of parameter filters; if nil, the value of | |
1039 | `frameset-filter-alist' is used instead. | |
9421876d | 1040 | |
063233c3 | 1041 | REUSE-FRAMES selects the policy to use to reuse frames when restoring: |
76c5e5ab JB |
1042 | t Reuse existing frames if possible, and delete those not reused. |
1043 | nil Restore frameset in new frames and delete existing frames. | |
1044 | :keep Restore frameset in new frames and keep the existing ones. | |
1045 | LIST A list of frames to reuse; only these are reused (if possible). | |
a912c016 JB |
1046 | Remaining frames in this list are deleted; other frames not |
1047 | included on the list are left untouched. | |
9421876d JB |
1048 | |
1049 | FORCE-DISPLAY can be: | |
76c5e5ab JB |
1050 | t Frames are restored in the current display. |
1051 | nil Frames are restored, if possible, in their original displays. | |
063233c3 | 1052 | :delete Frames in other displays are deleted instead of restored. |
76c5e5ab | 1053 | PRED A function called with two arguments, the parameter alist and |
a912c016 JB |
1054 | the window state (in that order). It must return t, nil or |
1055 | `:delete', as above but affecting only the frame that will | |
1056 | be created from that parameter alist. | |
9421876d JB |
1057 | |
1058 | FORCE-ONSCREEN can be: | |
76c5e5ab JB |
1059 | t Force onscreen only those frames that are fully offscreen. |
1060 | nil Do not force any frame back onscreen. | |
1061 | :all Force onscreen any frame fully or partially offscreen. | |
1062 | PRED A function called with three arguments, | |
d5671a82 JB |
1063 | - the live frame just restored, |
1064 | - a list (LEFT TOP WIDTH HEIGHT), describing the frame, | |
063233c3 JB |
1065 | - a list (LEFT TOP WIDTH HEIGHT), describing the workarea. |
1066 | It must return non-nil to force the frame onscreen, nil otherwise. | |
d5671a82 JB |
1067 | |
1068 | Note the timing and scope of the operations described above: REUSE-FRAMES | |
3677ffeb JB |
1069 | affects existing frames; PREDICATE, FILTERS and FORCE-DISPLAY affect the frame |
1070 | being restored before that happens; and FORCE-ONSCREEN affects the frame once | |
d5671a82 | 1071 | it has been restored. |
9421876d | 1072 | |
a912c016 | 1073 | All keyword parameters default to nil." |
9421876d | 1074 | |
76c5e5ab | 1075 | (cl-assert (frameset-valid-p frameset)) |
9421876d | 1076 | |
d5671a82 | 1077 | (let (other-frames) |
9421876d JB |
1078 | |
1079 | ;; frameset--reuse-list is a list of frames potentially reusable. Later we | |
1080 | ;; will decide which ones can be reused, and how to deal with any leftover. | |
1081 | (pcase reuse-frames | |
d5671a82 | 1082 | ((or `nil `:keep) |
9421876d JB |
1083 | (setq frameset--reuse-list nil |
1084 | other-frames (frame-list))) | |
1085 | ((pred consp) | |
1086 | (setq frameset--reuse-list (copy-sequence reuse-frames) | |
1087 | other-frames (cl-delete-if (lambda (frame) | |
a912c016 JB |
1088 | (memq frame frameset--reuse-list)) |
1089 | (frame-list)))) | |
9421876d JB |
1090 | (_ |
1091 | (setq frameset--reuse-list (frame-list) | |
1092 | other-frames nil))) | |
1093 | ||
1094 | ;; Sort saved states to guarantee that minibufferless frames will be created | |
1095 | ;; after the frames that contain their minibuffer windows. | |
1096 | (dolist (state (sort (copy-sequence (frameset-states frameset)) | |
063233c3 | 1097 | #'frameset--minibufferless-last-p)) |
a912c016 JB |
1098 | (pcase-let ((`(,frame-cfg . ,window-cfg) state)) |
1099 | (when (or (null predicate) (funcall predicate frame-cfg window-cfg)) | |
1100 | (condition-case-unless-debug err | |
1101 | (let* ((d-mini (cdr (assq 'frameset--mini frame-cfg))) | |
1102 | (mb-id (cdr d-mini)) | |
1103 | (default (and (booleanp mb-id) mb-id)) | |
1104 | (force-display (if (functionp force-display) | |
1105 | (funcall force-display frame-cfg window-cfg) | |
1106 | force-display)) | |
1107 | frame to-tty) | |
1108 | ;; Only set target if forcing displays and the target display is different. | |
1109 | (cond ((frameset-keep-original-display-p force-display) | |
1110 | (setq frameset--target-display nil)) | |
1111 | ((eq (frame-parameter nil 'display) (cdr (assq 'display frame-cfg))) | |
1112 | (setq frameset--target-display nil)) | |
1113 | (t | |
1114 | (setq frameset--target-display (cons 'display | |
1115 | (frame-parameter nil 'display)) | |
1116 | to-tty (null (cdr frameset--target-display))))) | |
1117 | ;; Time to restore frames and set up their minibuffers as they were. | |
1118 | ;; We only skip a frame (thus deleting it) if either: | |
1119 | ;; - we're switching displays, and the user chose the option to delete, or | |
1120 | ;; - we're switching to tty, and the frame to restore is minibuffer-only. | |
1121 | (unless (and frameset--target-display | |
1122 | (or (eq force-display :delete) | |
1123 | (and to-tty | |
1124 | (eq (cdr (assq 'minibuffer frame-cfg)) 'only)))) | |
1125 | ;; If keeping non-reusable frames, and the frameset--id of one of them | |
1126 | ;; matches the id of a frame being restored (because, for example, the | |
1127 | ;; frameset has already been read in the same session), remove the | |
1128 | ;; frameset--id from the non-reusable frame, which is not useful anymore. | |
1129 | (when (and other-frames | |
1130 | (or (eq reuse-frames :keep) (consp reuse-frames))) | |
1131 | (let ((dup (frameset-frame-with-id (cdr (assq 'frameset--id frame-cfg)) | |
1132 | other-frames))) | |
1133 | (when dup | |
1134 | (set-frame-parameter dup 'frameset--id nil)))) | |
1135 | ;; Restore minibuffers. Some of this stuff could be done in a filter | |
1136 | ;; function, but it would be messy because restoring minibuffers affects | |
1137 | ;; global state; it's best to do it here than add a bunch of global | |
1138 | ;; variables to pass info back-and-forth to/from the filter function. | |
1139 | (cond | |
1140 | ((null d-mini)) ;; No frameset--mini. Process as normal frame. | |
1141 | (to-tty) ;; Ignore minibuffer stuff and process as normal frame. | |
1142 | ((car d-mini) ;; Frame has minibuffer (or it is minibuffer-only). | |
1143 | (when (eq (cdr (assq 'minibuffer frame-cfg)) 'only) | |
1144 | (setq frame-cfg (append '((tool-bar-lines . 0) (menu-bar-lines . 0)) | |
1145 | frame-cfg)))) | |
1146 | (t ;; Frame depends on other frame's minibuffer window. | |
1147 | (let* ((mb-frame (or (frameset-frame-with-id mb-id) | |
1148 | (error "Minibuffer frame %S not found" mb-id))) | |
1149 | (mb-param (assq 'minibuffer frame-cfg)) | |
1150 | (mb-window (minibuffer-window mb-frame))) | |
1151 | (unless (and (window-live-p mb-window) | |
1152 | (window-minibuffer-p mb-window)) | |
1153 | (error "Not a minibuffer window %s" mb-window)) | |
1154 | (if mb-param | |
1155 | (setcdr mb-param mb-window) | |
1156 | (push (cons 'minibuffer mb-window) frame-cfg))))) | |
1157 | ;; OK, we're ready at last to create (or reuse) a frame and | |
1158 | ;; restore the window config. | |
1159 | (setq frame (frameset--restore-frame frame-cfg window-cfg | |
1160 | (or filters frameset-filter-alist) | |
1161 | force-onscreen)) | |
1162 | ;; Set default-minibuffer if required. | |
1163 | (when default (setq default-minibuffer-frame frame)))) | |
1164 | (error | |
1165 | (delay-warning 'frameset (error-message-string err) :error)))))) | |
9421876d JB |
1166 | |
1167 | ;; In case we try to delete the initial frame, we want to make sure that | |
1168 | ;; other frames are already visible (discussed in thread for bug#14841). | |
1169 | (sit-for 0 t) | |
1170 | ||
1171 | ;; Delete remaining frames, but do not fail if some resist being deleted. | |
d5671a82 | 1172 | (unless (eq reuse-frames :keep) |
9421876d JB |
1173 | (dolist (frame (sort (nconc (if (listp reuse-frames) nil other-frames) |
1174 | frameset--reuse-list) | |
063233c3 JB |
1175 | ;; Minibufferless frames must go first to avoid |
1176 | ;; errors when attempting to delete a frame whose | |
1177 | ;; minibuffer window is used by another frame. | |
1178 | #'frameset-minibufferless-first-p)) | |
9421876d JB |
1179 | (condition-case err |
1180 | (delete-frame frame) | |
1181 | (error | |
1182 | (delay-warning 'frameset (error-message-string err)))))) | |
a912c016 JB |
1183 | (setq frameset--reuse-list nil |
1184 | frameset--target-display nil) | |
9421876d JB |
1185 | |
1186 | ;; Make sure there's at least one visible frame. | |
1187 | (unless (or (daemonp) (visible-frame-list)) | |
1188 | (make-frame-visible (car (frame-list)))))) | |
1189 | ||
77187e6f JB |
1190 | \f |
1191 | ;; Register support | |
1192 | ||
1193 | (defun frameset--jump-to-register (data) | |
1194 | "Restore frameset from DATA stored in register. | |
1195 | Called from `jump-to-register'. Internal use only." | |
1196 | (let* ((delete (and current-prefix-arg t)) | |
1197 | (iconify-list (if delete nil (frame-list)))) | |
1198 | (frameset-restore (aref data 0) | |
1199 | :filters frameset-session-filter-alist | |
1200 | :reuse-frames (if delete t :keep)) | |
1201 | (mapc #'iconify-frame iconify-list) | |
1202 | (let ((frame (frameset-frame-with-id (aref data 1)))) | |
1203 | (when frame | |
1204 | (select-frame-set-input-focus frame) | |
1205 | (goto-char (aref data 2)))))) | |
1206 | ||
1207 | ;;;###autoload | |
1208 | (defun frameset-to-register (register &optional _arg) | |
1209 | "Store the current frameset in register REGISTER. | |
1210 | Use \\[jump-to-register] to restore the frameset. | |
1211 | Argument is a character, naming the register." | |
1212 | (interactive "cFrameset to register: \nP") | |
1213 | (set-register register | |
1214 | (registerv-make | |
1215 | (vector (frameset-save nil | |
1216 | :app 'register | |
1217 | :filters frameset-session-filter-alist) | |
1218 | ;; frameset-save does not include the value of point | |
1219 | ;; in the current buffer, so record that separately. | |
1220 | (frameset-frame-id nil) | |
1221 | (point-marker)) | |
1222 | :print-func (lambda (_data) (princ "a frameset.")) | |
1223 | :jump-func #'frameset--jump-to-register))) | |
1224 | ||
9421876d JB |
1225 | (provide 'frameset) |
1226 | ||
1227 | ;;; frameset.el ends here |