Add frame identifiers (bug#80138)

A unique frame id is assigned to a new or cloned frame, and
reused on an undeleted frame.

The id facilitates unambiguous identification among frames that
share identical names or titles, deleted frames where a live
frame object no longer exists that we can resurrect by id, for
example via 'tab-bar-undo-close-tab'.  It also aids debugging at
the C level using the frame struct member id.

Rewrite 'clone-frame' and 'undelete-frame' to not let bind
variables that 'make-frame' uses to avoid conflicts with nested
'make-frame' calls, for example via
'after-make-frame-functions'.

* lisp/frame.el (clone-frame, undelete-frame): Use
'frame--purify-parameters' to supply parameters explicitly.
(undelete-frame--save-deleted-frame): Save frame id for
restoration.
(undelete-frame): Restore frame id.
(frame--purify-parameters): New defun.
(make-frame): Assign a new id for a new or cloned frame, reuse
for undeleted frame.
* src/frame.h (struct frame): Add id member.
(frame_next_id): New extern.
* src/frame.c (frame_next_id): New global counter.
(frame_set_id, frame_set_id_from_params): New function.
(Fframe_id): New DEFUN.
(syms_of_frame <Sframe_id>): New defsubr.
(syms_of_frame <Qinternal_id>): New DEFSYM.
(syms_of_frame <frame_internal_parameters>): Add 'Qinternal_id'.
* src/androidfns.c (Fx_create_frame):
* src/haikufns.c (Fx_create_frame):
* src/nsfns.m (Fx_create_frame):
* src/pgtkfns.c (Fx_create_frame):
* src/w32fns.c (Fx_create_frame):
* src/xfns.c (Fx_create_frame): Call 'frame_set_id_from_params'.
* doc/lispref/frames.texi: Add documentation.
* etc/NEWS: Announce frame id.
This commit is contained in:
Stéphane Marks 2026-01-13 09:29:44 +01:00 committed by Martin Rudalics
parent 52875b51bf
commit 785059a1f7
11 changed files with 171 additions and 16 deletions

View file

@ -109,6 +109,24 @@ must be a root frame, which means it cannot be a child frame itself
descending from it.
@end defun
@defun frame-id &optional frame
This function returns the unique identifier of a frame, an integer,
assigned to @var{frame}. If @var{frame} is @code{nil} or unspecified,
it defaults to the selected frame (@pxref{Input Focus}). This can be
used to unambiguously identify a frame in a context where you do not or
cannot use a frame object.
A frame undeleted using @command{undelete-frame} will retain its
identifier. A frame cloned using @command{clone-frame} will not retain
its original identifier. @xref{Frame Commands,,,emacs, the Emacs
Manual}.
Frame identifiers are not persisted using the desktop library
(@pxref{Desktop Save Mode}), @command{frameset-to-register}, or
@code{frameset-save}, and each of their restored frames will bear a new
unique id.
@end defun
@menu
* Creating Frames:: Creating additional frames.
* Multiple Terminals:: Displaying on several different devices.

View file

@ -500,6 +500,14 @@ These are useful if you need to detect a cloned frame or undeleted frame
in hooks like 'after-make-frame-functions' and
'server-after-make-frame-hook'.
---
*** Frames now have unique ids and the new function 'frame-id'.
Each non-tooltip frame is assigned a unique integer id. This allows you
to unambiguously identify frames even if they share the same name or
title. When 'undelete-frame-mode' is enabled, each deleted frame's id
is stored for resurrection. The function 'frame-id' returns a frame's
id (in C, use the frame struct member id).
** Mode Line
+++

View file

@ -951,24 +951,24 @@ and lines for the clone.
FRAME defaults to the selected frame. The frame is created on the
same terminal as FRAME. If the terminal is a text-only terminal then
also select the new frame."
also select the new frame.
A cloned frame is assigned a new frame ID. See `frame-id'."
(interactive (list (selected-frame) current-prefix-arg))
(let* ((frame (or frame (selected-frame)))
(windows (unless no-windows
(window-state-get (frame-root-window frame))))
(default-frame-alist
(parameters
(append `((cloned-from . ,frame))
(seq-remove (lambda (elem)
(memq (car elem) frame-internal-parameters))
(frame-parameters frame))))
(frame--purify-parameters (frame-parameters frame))))
new-frame)
(when (and frame-resize-pixelwise
(display-graphic-p frame))
(push (cons 'width (cons 'text-pixels (frame-text-width frame)))
default-frame-alist)
parameters)
(push (cons 'height (cons 'text-pixels (frame-text-height frame)))
default-frame-alist))
(setq new-frame (make-frame))
parameters))
(setq new-frame (make-frame parameters))
(when windows
(window-state-put windows (frame-root-window new-frame) 'safe))
(unless (display-graphic-p frame)
@ -995,6 +995,24 @@ frame, unless you add them to the hook in your early-init file.")
(defvar x-display-name)
(defun frame--purify-parameters (parameters)
"Return PARAMETERS without internals and ignoring unset parameters.
Use this helper function so that `make-frame' does not override any
parameters.
In the return value, assign nil to each parameter in
`default-frame-alist', `window-system-default-frame-alist',
`frame-inherited-parameters', which is not in PARAMETERS, and remove all
parameters in `frame-internal-parameters' from PARAMETERS."
(dolist (p (append default-frame-alist
window-system-default-frame-alist
frame-inherited-parameters))
(unless (assq (car p) parameters)
(push (cons (car p) nil) parameters)))
(seq-remove (lambda (elem)
(memq (car elem) frame-internal-parameters))
parameters))
(defun make-frame (&optional parameters)
"Return a newly created frame displaying the current buffer.
Optional argument PARAMETERS is an alist of frame parameters for
@ -1094,6 +1112,12 @@ current buffer even if it is hidden."
(setq params (cons '(minibuffer)
(delq (assq 'minibuffer params) params))))
;; Let the `frame-creation-function' apparatus assign a new frame id
;; for a new or cloned frame. For an undeleted frame, send the old
;; id via a frame parameter.
(when-let* ((id (cdr (assq 'undeleted params))))
(push (cons 'frame-id id) params))
;; Now make the frame.
(run-hooks 'before-make-frame-hook)
@ -1125,7 +1149,7 @@ current buffer even if it is hidden."
;; buffers for these windows were set (Bug#79606).
(let* ((root (frame-root-window frame))
(buffer (window-buffer root)))
(with-current-buffer buffer
(with-current-buffer buffer
(set-window-fringes
root left-fringe-width right-fringe-width fringes-outside-margins)
(set-window-scroll-bars
@ -1135,7 +1159,7 @@ current buffer even if it is hidden."
root left-margin-width right-margin-width)))
(let* ((mini (minibuffer-window frame))
(buffer (window-buffer mini)))
(when (eq (window-frame mini) frame)
(when (eq (window-frame mini) frame)
(with-current-buffer buffer
(set-window-fringes
mini left-fringe-width right-fringe-width fringes-outside-margins)
@ -3126,7 +3150,8 @@ Only the 16 most recently deleted frames are saved."
;; to restore a graphical frame.
(and (eq (car elem) 'display) (not (display-graphic-p)))))
(frame-parameters frame))
(window-state-get (frame-root-window frame)))
(window-state-get (frame-root-window frame))
(frame-id frame))
undelete-frame--deleted-frames))
(if (> (length undelete-frame--deleted-frames) 16)
(setq undelete-frame--deleted-frames
@ -3149,7 +3174,9 @@ Without a prefix argument, undelete the most recently deleted
frame.
With a numerical prefix argument ARG between 1 and 16, where 1 is
most recently deleted frame, undelete the ARGth deleted frame.
When called from Lisp, returns the new frame."
When called from Lisp, returns the new frame.
An undeleted frame retains its original frame ID. See `frame-id'."
(interactive "P")
(if (not undelete-frame-mode)
(user-error "Undelete-Frame mode is disabled")
@ -3170,10 +3197,11 @@ When called from Lisp, returns the new frame."
(if graphic "graphic" "non-graphic"))
(setq undelete-frame--deleted-frames
(delq frame-data undelete-frame--deleted-frames))
(let* ((default-frame-alist
(append `((undeleted . t))
(nth 1 frame-data)))
(frame (make-frame)))
(let* ((parameters
;; `undeleted' signals to `make-frame' to reuse its id.
(append `((undeleted . ,(nth 3 frame-data)))
(frame--purify-parameters (nth 1 frame-data))))
(frame (make-frame parameters)))
(window-state-put (nth 2 frame-data) (frame-root-window frame) 'safe)
(select-frame-set-input-focus frame)
frame))))))))

View file

@ -858,6 +858,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame,
XSETFRAME (frame, f);
frame_set_id_from_params (f, parms);
f->terminal = dpyinfo->terminal;
f->output_method = output_android;

View file

@ -337,6 +337,83 @@ return values. */)
: Qnil);
}
/* Frame id. */
EMACS_UINT frame_next_id = 1; /* 0 indicates no id (yet) set. */
DEFUN ("frame-id", Fframe_id, Sframe_id, 0, 1, 0,
doc: /* Return FRAME's id.
If FRAME is nil, use the selected frame.
Return nil if the id has not been set. */)
(Lisp_Object frame)
{
if (NILP (frame))
frame = selected_frame;
struct frame *f = decode_live_frame (frame);
if (f->id == 0)
return Qnil;
else
return make_fixnum (f->id);
}
/** frame_set_id: Set frame F's id to ID.
If ID is 0 and F's ID is 0, use frame_next_id and increment it,
otherwise, use ID.
Signal an error if ID >= frame_next_id.
Signal an error if ID is in use on another live frame.
Return ID if it was used, 0 otherwise. */
EMACS_UINT
frame_set_id (struct frame *f, EMACS_UINT id)
{
if (id >= frame_next_id)
error ("Specified frame ID unassigned");
if (id > 0)
{
eassume (CONSP (Vframe_list));
Lisp_Object frame, tail = Qnil;
FOR_EACH_FRAME (tail, frame)
{
if (id == XFRAME (frame)->id)
error ("Specified frame ID already in use");
}
}
if (id == 0)
if (f->id != 0)
return 0;
else
f->id = frame_next_id++;
else
f->id = id;
return f->id;
}
/** frame_set_id_from_params: Set frame F's id from params, if present.
Call frame_set_id to using the frame parameter 'frame-id, if present
and a valid positive integer greater than 0, otherwise use 0.
Return frame_set_id's return value. */
EMACS_UINT
frame_set_id_from_params (struct frame *f, Lisp_Object params)
{
EMACS_UINT id = 0;
Lisp_Object param_id = Fcdr (Fassq (Qframe_id, params));
if (TYPE_RANGED_FIXNUMP (int, param_id))
{
EMACS_INT id_1 = XFIXNUM (param_id);
if (id_1 > 0)
id = (EMACS_UINT) id_1;
}
return frame_set_id (f, id);
}
DEFUN ("window-system", Fwindow_system, Swindow_system, 0, 1, 0,
doc: /* The name of the window system that FRAME is displaying through.
The value is a symbol:
@ -1358,6 +1435,7 @@ make_initial_frame (void)
f = make_frame (true);
XSETFRAME (frame, f);
frame_set_id (f, 0);
Vframe_list = Fcons (frame, Vframe_list);
@ -1742,6 +1820,7 @@ affects all frames on the same terminal device. */)
frames don't obscure other frames. */
Lisp_Object parent = Fcdr (Fassq (Qparent_frame, parms));
struct frame *f = make_terminal_frame (t, parent, parms);
frame_set_id_from_params (f, parms);
if (!noninteractive)
init_frame_faces (f);
@ -7195,6 +7274,7 @@ syms_of_frame (void)
DEFSYM (Qfont_parameter, "font-parameter");
DEFSYM (Qforce, "force");
DEFSYM (Qinhibit, "inhibit");
DEFSYM (Qframe_id, "frame-id");
DEFSYM (Qcloned_from, "cloned-from");
DEFSYM (Qundeleted, "undeleted");
@ -7581,6 +7661,7 @@ allow `make-frame' to show the current buffer even if its hidden. */);
#else
frame_internal_parameters = list3 (Qname, Qparent_id, Qwindow_id);
#endif
frame_internal_parameters = Fcons (Qframe_id, frame_internal_parameters);
frame_internal_parameters = Fcons (Qcloned_from, frame_internal_parameters);
frame_internal_parameters = Fcons (Qundeleted, frame_internal_parameters);
@ -7607,6 +7688,7 @@ The default is \\+`inhibit' in NS builds and nil everywhere else. */);
alter_fullscreen_frames = Qnil;
#endif
defsubr (&Sframe_id);
defsubr (&Sframep);
defsubr (&Sframe_live_p);
defsubr (&Swindow_system);

View file

@ -292,6 +292,9 @@ struct frame
struct image_cache *image_cache;
#endif /* HAVE_WINDOW_SYSTEM */
/* Unique frame id. */
EMACS_UINT id;
/* Tab-bar item index of the item on which a mouse button was pressed. */
int last_tab_bar_item;
@ -1415,6 +1418,10 @@ FRAME_PARENT_FRAME (struct frame *f)
#define AUTO_FRAME_ARG(name, parameter, value) \
AUTO_LIST1 (name, AUTO_CONS_EXPR (parameter, value))
extern EMACS_UINT frame_next_id;
extern EMACS_UINT frame_set_id (struct frame *f, EMACS_UINT id);
extern EMACS_UINT frame_set_id_from_params (struct frame *f, Lisp_Object params);
/* False means there are no visible garbaged frames. */
extern bool frame_garbaged;

View file

@ -750,6 +750,8 @@ haiku_create_frame (Lisp_Object parms)
XSETFRAME (frame, f);
frame_set_id_from_params (f, parms);
f->terminal = dpyinfo->terminal;
f->output_method = output_haiku;

View file

@ -1262,6 +1262,8 @@ Turn the input menu (an NSMenu) into a lisp list for tracking on lisp side.
XSETFRAME (frame, f);
frame_set_id_from_params (f, parms);
f->terminal = dpyinfo->terminal;
f->output_method = output_ns;

View file

@ -1283,6 +1283,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, 1, 1, 0,
XSETFRAME (frame, f);
frame_set_id_from_params (f, parms);
f->terminal = dpyinfo->terminal;
f->output_method = output_pgtk;

View file

@ -6315,6 +6315,8 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame,
XSETFRAME (frame, f);
frame_set_id_from_params (f, parameters);
parent_frame = gui_display_get_arg (dpyinfo, parameters, Qparent_frame,
NULL, NULL,
RES_TYPE_SYMBOL);

View file

@ -5020,6 +5020,8 @@ This function is an internal primitive--use `make-frame' instead. */)
XSETFRAME (frame, f);
frame_set_id_from_params (f, parms);
f->terminal = dpyinfo->terminal;
f->output_method = output_x_window;