diff --git a/build.zig b/build.zig index 9e6de6b..bc405f3 100644 --- a/build.zig +++ b/build.zig @@ -80,6 +80,7 @@ fn addProtocolDeps(exe: *std.build.LibExeObjStep, protocol_step: *std.build.Step exe.step.dependOn(protocol_step); exe.addIncludeDir("protocol"); exe.addCSourceFile("protocol/river-control-unstable-v1-protocol.c", &[_][]const u8{"-std=c99"}); + exe.addCSourceFile("protocol/river-status-unstable-v1-protocol.c", &[_][]const u8{"-std=c99"}); } const ScanProtocolsStep = struct { diff --git a/river/Control.zig b/river/Control.zig index 1c12b42..89b009f 100644 --- a/river/Control.zig +++ b/river/Control.zig @@ -44,7 +44,7 @@ pub fn init(self: *Self, server: *Server) !void { protocol_version, self, bind, - ) orelse return error.CantCreateRiverWindowManagementGlobal; + ) orelse return error.CantCreateWlGlobal; self.listen_display_destroy.notify = handleDisplayDestroy; c.wl_display_add_destroy_listener(server.wl_display, &self.listen_display_destroy); @@ -67,11 +67,7 @@ fn bind(wl_client: ?*c.wl_client, data: ?*c_void, version: u32, id: u32) callcon c.wl_client_post_no_memory(wl_client); return; }; - c.wl_resource_set_implementation(wl_resource, &implementation, self, resourceDestroy); -} - -fn resourceDestroy(wl_resource: ?*c.wl_resource) callconv(.C) void { - // TODO + c.wl_resource_set_implementation(wl_resource, &implementation, self, null); } fn runCommand( diff --git a/river/Output.zig b/river/Output.zig index 2dae989..09e1168 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -28,6 +28,7 @@ const Log = @import("log.zig").Log; const Root = @import("Root.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; +const OutputStatus = @import("OutputStatus.zig"); root: *Root, wlr_output: *c.wlr_output, @@ -55,6 +56,9 @@ master_factor: f64, /// Current layout of the output. layout: Layout, +/// List of status tracking objects relaying changes to this output to clients. +status_trackers: std.SinglyLinkedList(OutputStatus), + // All listeners for this output, in alphabetical order listen_destroy: c.wl_listener, listen_frame: c.wl_listener, @@ -133,6 +137,8 @@ pub fn init(self: *Self, root: *Root, wlr_output: *c.wlr_output) !void { // LeftMaster is the default layout for all outputs self.layout = Layout.LeftMaster; + self.status_trackers = std.SinglyLinkedList(OutputStatus).init(); + // Set up listeners self.listen_destroy.notify = handleDestroy; c.wl_signal_add(&wlr_output.events.destroy, &self.listen_destroy); @@ -170,20 +176,6 @@ pub fn init(self: *Self, root: *Root, wlr_output: *c.wlr_output) !void { } } -pub fn deinit(self: *Self) void { - for (self.layers) |*layer| { - while (layer.pop()) |layer_surface_node| { - self.root.server.allocator.destroy(layer_surface_node); - } - } - - while (self.views.first) |node| { - node.view.deinit(); - self.views.remove(node); - self.root.server.allocator.destroy(node); - } -} - pub fn getRenderer(self: Self) *c.wlr_renderer { return c.river_wlr_backend_get_renderer(self.wlr_output.backend); } @@ -708,7 +700,7 @@ fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { while (seat_it) |seat_node| : (seat_it = seat_node.next) { const seat = &seat_node.data; if (seat.focused_output == self) { - seat.focused_output = fallback_output; + seat.focusOutput(self); seat.focus(null); } } diff --git a/river/OutputStatus.zig b/river/OutputStatus.zig new file mode 100644 index 0000000..b08c195 --- /dev/null +++ b/river/OutputStatus.zig @@ -0,0 +1,77 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; + +const implementation = c.struct_zriver_output_status_v1_interface{ + .destroy = destroy, +}; + +output: *Output, +wl_resource: *c.wl_resource, + +pub fn init(self: *Self, output: *Output, wl_resource: *c.wl_resource) void { + self.output = output; + self.wl_resource = wl_resource; + + c.wl_resource_set_implementation(wl_resource, &implementation, self, handleResourceDestroy); + + // Send view/focused tags once on bind. + self.sendViewTags(); + self.sendFocusedTags(); +} + +fn handleResourceDestroy(wl_resource: ?*c.wl_resource) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); + const node = @fieldParentPtr(std.SinglyLinkedList(Self).Node, "data", self); + self.output.status_trackers.remove(node); +} + +fn destroy(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource) callconv(.C) void { + c.wl_resource_destroy(wl_resource); +} + +/// Send the current tags of each view on the output to the client. +pub fn sendViewTags(self: Self) void { + var view_tags: c.wl_array = undefined; + c.wl_array_init(&view_tags); + var it = ViewStack(View).iterator(self.output.views.first, std.math.maxInt(u32)); + while (it.next()) |node| { + const ptr = c.wl_array_add(&view_tags, @sizeOf(u32)) orelse { + c.wl_resource_post_no_memory(self.wl_resource); + Log.Error.log("out of memory", .{}); + return; + }; + const ptr_u32 = @ptrCast(*u32, @alignCast(@alignOf(u32), ptr)); + ptr_u32.* = node.view.current_tags; + } + c.zriver_output_status_v1_send_view_tags(self.wl_resource, &view_tags); +} + +/// Send the currently focused tags of the output to the client. +pub fn sendFocusedTags(self: Self) void { + c.zriver_output_status_v1_send_focused_tags(self.wl_resource, self.output.current_focused_tags); +} diff --git a/river/Root.zig b/river/Root.zig index d411739..2b229b0 100644 --- a/river/Root.zig +++ b/river/Root.zig @@ -104,7 +104,7 @@ pub fn addOutput(self: *Self, wlr_output: *c.wlr_output) void { // TODO: move views from the noop output to the new one and focus(null) var it = self.server.input_manager.seats.first; while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focused_output = &self.outputs.first.?.data; + seat_node.data.focusOutput(&self.outputs.first.?.data); } } } @@ -220,8 +220,12 @@ fn commitTransaction(self: *Self) void { ); output.current_focused_tags = tags; output.pending_focused_tags = null; + var it = output.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendFocusedTags(); } + var view_tags_changed = false; + var view_it = ViewStack(View).iterator(output.views.first, std.math.maxInt(u32)); while (view_it.next()) |view_node| { const view = &view_node.view; @@ -236,10 +240,16 @@ fn commitTransaction(self: *Self) void { if (view.pending_tags) |tags| { view.current_tags = tags; view.pending_tags = null; + view_tags_changed = true; } view.dropStashedBuffer(); } + + if (view_tags_changed) { + var it = output.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendViewTags(); + } } // Iterate over all seats and update focus diff --git a/river/Seat.zig b/river/Seat.zig index 82979fc..5fc1320 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -27,9 +27,12 @@ const InputManager = @import("InputManager.zig"); const Keyboard = @import("Keyboard.zig"); const LayerSurface = @import("LayerSurface.zig"); const Output = @import("Output.zig"); +const SeatStatus = @import("SeatStatus.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; +// TODO: remove none variant, unify focused_view and focused_layer fields +// with type ?FocusTarget const FocusTarget = union(enum) { view: *View, layer: *LayerSurface, @@ -62,6 +65,9 @@ focus_stack: ViewStack(*View), /// recieve focus. focused_layer: ?*LayerSurface, +/// List of status tracking objects relaying changes to this seat to clients. +status_trackers: std.SinglyLinkedList(SeatStatus), + listen_request_set_selection: c.wl_listener, pub fn init(self: *Self, input_manager: *InputManager, name: []const u8) !void { @@ -70,6 +76,7 @@ pub fn init(self: *Self, input_manager: *InputManager, name: []const u8) !void { // This will be automatically destroyed when the display is destroyed self.wlr_seat = c.wlr_seat_create(input_manager.server.wl_display, name.ptr) orelse return error.CantCreateWlrSeat; + self.wlr_seat.data = self; try self.cursor.init(self); errdefer self.cursor.destroy(); @@ -86,6 +93,8 @@ pub fn init(self: *Self, input_manager: *InputManager, name: []const u8) !void { self.focused_layer = null; + self.status_trackers = std.SinglyLinkedList(SeatStatus).init(); + self.listen_request_set_selection.notify = handleRequestSetSelection; c.wl_signal_add(&self.wlr_seat.events.request_set_selection, &self.listen_request_set_selection); } @@ -167,9 +176,7 @@ pub fn setFocusRaw(self: *Self, focus_target: FocusTarget) void { .view => |target_view| target_view == self.focused_view, .layer => |target_layer| target_layer == self.focused_layer, .none => false, - }) { - return; - } + }) return; // Obtain the target wlr_surface const target_wlr_surface = switch (focus_target) { @@ -223,6 +230,23 @@ pub fn setFocusRaw(self: *Self, focus_target: FocusTarget) void { ); } } + + // Inform any clients tracking status of the change + var it = self.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendFocusedView(); +} + +/// Focus the given output, notifying any listening clients of the change. +pub fn focusOutput(self: *Self, output: *Output) void { + const root = &self.input_manager.server.root; + + var it = self.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendOutput(.unfocused); + + self.focused_output = output; + + it = self.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendOutput(.focused); } /// Handle the unmapping of a view, removing it from the focus stack and diff --git a/river/SeatStatus.zig b/river/SeatStatus.zig new file mode 100644 index 0000000..3f50f90 --- /dev/null +++ b/river/SeatStatus.zig @@ -0,0 +1,81 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Seat = @import("Seat.zig"); +const Output = @import("Output.zig"); +const View = @import("View.zig"); + +const FocusState = enum { + focused, + unfocused, +}; + +const implementation = c.struct_zriver_seat_status_v1_interface{ + .destroy = destroy, +}; + +seat: *Seat, +wl_resource: *c.wl_resource, + +pub fn init(self: *Self, seat: *Seat, wl_resource: *c.wl_resource) void { + self.seat = seat; + self.wl_resource = wl_resource; + + c.wl_resource_set_implementation(wl_resource, &implementation, self, handleResourceDestroy); + + // Send focused output/view once on bind + self.sendOutput(.focused); + self.sendFocusedView(); +} + +fn handleResourceDestroy(wl_resource: ?*c.wl_resource) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); + const node = @fieldParentPtr(std.SinglyLinkedList(Self).Node, "data", self); + self.seat.status_trackers.remove(node); +} + +fn destroy(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource) callconv(.C) void { + c.wl_resource_destroy(wl_resource); +} + +pub fn sendOutput(self: Self, state: FocusState) void { + const wl_client = c.wl_resource_get_client(self.wl_resource); + const output_resources = &self.seat.focused_output.wlr_output.resources; + var output_resource = c.wl_resource_from_link(output_resources.next); + while (c.wl_resource_get_link(output_resource) != output_resources) : (output_resource = + c.wl_resource_from_link(c.wl_resource_get_link(output_resource).*.next)) + { + if (c.wl_resource_get_client(output_resource) == wl_client) switch (state) { + .focused => c.zriver_seat_status_v1_send_focused_output(self.wl_resource, output_resource), + .unfocused => c.zriver_seat_status_v1_send_unfocused_output(self.wl_resource, output_resource), + }; + } +} + +pub fn sendFocusedView(self: Self) void { + c.zriver_seat_status_v1_send_focused_view(self.wl_resource, if (self.seat.focused_view) |v| + v.getTitle() + else + ""); +} diff --git a/river/Server.zig b/river/Server.zig index f700746..b10918a 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -23,15 +23,16 @@ const std = @import("std"); const c = @import("c.zig"); const Config = @import("Config.zig"); +const Control = @import("Control.zig"); const DecorationManager = @import("DecorationManager.zig"); const InputManager = @import("InputManager.zig"); const LayerSurface = @import("LayerSurface.zig"); const Log = @import("log.zig").Log; const Output = @import("Output.zig"); const Root = @import("Root.zig"); +const StatusManager = @import("StatusManager.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; -const Control = @import("Control.zig"); const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); allocator: *std.mem.Allocator, @@ -57,6 +58,7 @@ input_manager: InputManager, root: Root, config: Config, control: Control, +status_manager: StatusManager, pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { self.allocator = allocator; @@ -123,6 +125,7 @@ pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { // Must be called after root is initialized try self.input_manager.init(self); try self.control.init(self); + try self.status_manager.init(self); // These all free themselves when the wl_display is destroyed _ = c.wlr_data_device_manager_create(self.wl_display) orelse diff --git a/river/StatusManager.zig b/river/StatusManager.zig new file mode 100644 index 0000000..3ea4207 --- /dev/null +++ b/river/StatusManager.zig @@ -0,0 +1,146 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Log = @import("log.zig").Log; +const Output = @import("Output.zig"); +const OutputStatus = @import("OutputStatus.zig"); +const Seat = @import("Seat.zig"); +const SeatStatus = @import("SeatStatus.zig"); +const Server = @import("Server.zig"); + +const protocol_version = 1; + +const implementation = c.struct_zriver_status_manager_v1_interface{ + .destroy = destroy, + .get_river_output_status = getRiverOutputStatus, + .get_river_seat_status = getRiverSeatStatus, +}; + +// TODO: remove this field, move allocator to util or something +server: *Server, +wl_global: *c.wl_global, + +listen_display_destroy: c.wl_listener, + +pub fn init(self: *Self, server: *Server) !void { + self.server = server; + self.wl_global = c.wl_global_create( + server.wl_display, + &c.zriver_status_manager_v1_interface, + protocol_version, + self, + bind, + ) orelse return error.CantCreateWlGlobal; + + self.listen_display_destroy.notify = handleDisplayDestroy; + c.wl_display_add_destroy_listener(server.wl_display, &self.listen_display_destroy); +} + +fn handleDisplayDestroy(wl_listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_display_destroy", wl_listener.?); + c.wl_global_destroy(self.wl_global); +} + +/// Called when a client binds our global +fn bind(wl_client: ?*c.wl_client, data: ?*c_void, version: u32, id: u32) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), data)); + const wl_resource = c.wl_resource_create( + wl_client, + &c.zriver_status_manager_v1_interface, + @intCast(c_int, version), + id, + ) orelse { + c.wl_client_post_no_memory(wl_client); + Log.Error.log("out of memory\n", .{}); + return; + }; + c.wl_resource_set_implementation(wl_resource, &implementation, self, null); +} + +fn destroy(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource) callconv(.C) void {} + +fn getRiverOutputStatus( + wl_client: ?*c.wl_client, + wl_resource: ?*c.wl_resource, + new_id: u32, + output_wl_resource: ?*c.wl_resource, +) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); + // This can be null if the output is inert, in which case we ignore the request + const wlr_output = c.wlr_output_from_resource(output_wl_resource) orelse return; + const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_output.*.data)); + const allocator = self.server.allocator; + + const node = allocator.create(std.SinglyLinkedList(OutputStatus).Node) catch { + c.wl_client_post_no_memory(wl_client); + Log.Error.log("out of memory\n", .{}); + return; + }; + + const output_status_resource = c.wl_resource_create( + wl_client, + &c.zriver_output_status_v1_interface, + protocol_version, + new_id, + ) orelse { + c.wl_client_post_no_memory(wl_client); + Log.Error.log("out of memory\n", .{}); + return; + }; + + node.data.init(output, output_status_resource); + output.status_trackers.prepend(node); +} + +fn getRiverSeatStatus( + wl_client: ?*c.wl_client, + wl_resource: ?*c.wl_resource, + new_id: u32, + seat_wl_resource: ?*c.wl_resource, +) callconv(.C) void { + const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource))); + // This can be null if the seat is inert, in which case we ignore the request + const wlr_seat_client = c.wlr_seat_client_from_resource(wl_resource) orelse return; + const seat = @ptrCast(*Seat, @alignCast(@alignOf(*Seat), wlr_seat_client.*.seat.*.data)); + const allocator = self.server.allocator; + + const node = allocator.create(std.SinglyLinkedList(SeatStatus).Node) catch { + c.wl_client_post_no_memory(wl_client); + Log.Error.log("out of memory\n", .{}); + return; + }; + + const seat_status_resource = c.wl_resource_create( + wl_client, + &c.zriver_seat_status_v1_interface, + protocol_version, + new_id, + ) orelse { + c.wl_client_post_no_memory(wl_client); + Log.Error.log("out of memory\n", .{}); + return; + }; + + node.data.init(seat, seat_status_resource); + seat.status_trackers.prepend(node); +} diff --git a/river/View.zig b/river/View.zig index 8ab32cc..7929673 100644 --- a/river/View.zig +++ b/river/View.zig @@ -223,6 +223,14 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surfa }; } +/// Return the current title of the view. May be an empty string. +pub fn getTitle(self: Self) [*:0]const u8 { + return switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(), + .xwayland_view => |xwayland_view| xwayland_view.getTitle(), + }; +} + /// Called by the impl when the surface is ready to be displayed pub fn map(self: *Self) void { const root = self.output.root; diff --git a/river/VoidView.zig b/river/VoidView.zig index 72c57a7..fca2321 100644 --- a/river/VoidView.zig +++ b/river/VoidView.zig @@ -46,3 +46,7 @@ pub fn forEachSurface( pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { unreachable; } + +pub fn getTitle(self: Self) [*:0]const u8 { + unreachable; +} diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index b3806db..88e7dce 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -100,6 +100,15 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surfa ); } +/// Return the current title of the toplevel. May be an empty string. +pub fn getTitle(self: Self) [*:0]const u8 { + const wlr_xdg_toplevel: *c.wlr_xdg_toplevel = @field( + self.wlr_xdg_surface, + c.wlr_xdg_surface_union, + ).toplevel; + return wlr_xdg_toplevel.title; +} + /// Called when the xdg surface is destroyed fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { const self = @fieldParentPtr(Self, "listen_destroy", listener.?); diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig index dbd35e4..681d9d1 100644 --- a/river/XwaylandView.zig +++ b/river/XwaylandView.zig @@ -105,6 +105,11 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surfa ); } +/// Get the current title of the xwayland surface. May be an empty string +pub fn getTitle(self: Self) [*:0]const u8 { + return self.wlr_xwayland_surface.title; +} + /// Called when the xwayland surface is destroyed fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { const self = @fieldParentPtr(Self, "listen_destroy", listener.?); diff --git a/river/c.zig b/river/c.zig index df4bcee..d5b5ddf 100644 --- a/river/c.zig +++ b/river/c.zig @@ -51,6 +51,7 @@ pub usingnamespace @cImport({ @cInclude("include/bindings.h"); @cInclude("river-control-unstable-v1-protocol.h"); + @cInclude("river-status-unstable-v1-protocol.h"); }); // These are needed because zig currently names translated anonymous unions diff --git a/river/command/focus_output.zig b/river/command/focus_output.zig index b11664f..fdb7095 100644 --- a/river/command/focus_output.zig +++ b/river/command/focus_output.zig @@ -45,10 +45,10 @@ pub fn focusOutput( // Focus the next/prev output in the list if there is one, else wrap const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output); - seat.focused_output = switch (direction) { + seat.focusOutput(switch (direction) { .Next => if (focused_node.next) |node| &node.data else &root.outputs.first.?.data, .Prev => if (focused_node.prev) |node| &node.data else &root.outputs.last.?.data, - }; + }); seat.focus(null); }