wl-seat: split focus navigation
This commit is contained in:
parent
c4e9011714
commit
c5dd462a6e
2 changed files with 477 additions and 461 deletions
|
|
@ -2,6 +2,7 @@ mod event_handling;
|
||||||
mod device_handler;
|
mod device_handler;
|
||||||
pub mod ext_transient_seat_manager_v1;
|
pub mod ext_transient_seat_manager_v1;
|
||||||
pub mod ext_transient_seat_v1;
|
pub mod ext_transient_seat_v1;
|
||||||
|
mod focus;
|
||||||
mod gesture_owner;
|
mod gesture_owner;
|
||||||
mod kb_owner;
|
mod kb_owner;
|
||||||
mod pointer_owner;
|
mod pointer_owner;
|
||||||
|
|
@ -73,7 +74,6 @@ use {
|
||||||
dnd_icon::DndIcon,
|
dnd_icon::DndIcon,
|
||||||
tray::{DynTrayItem, TrayItemId},
|
tray::{DynTrayItem, TrayItemId},
|
||||||
xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges},
|
xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges},
|
||||||
zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
|
||||||
},
|
},
|
||||||
xdg_toplevel_drag_v1::XdgToplevelDragV1,
|
xdg_toplevel_drag_v1::XdgToplevelDragV1,
|
||||||
},
|
},
|
||||||
|
|
@ -84,9 +84,8 @@ use {
|
||||||
rect::Rect,
|
rect::Rect,
|
||||||
state::{DeviceHandlerData, State},
|
state::{DeviceHandlerData, State},
|
||||||
tree::{
|
tree::{
|
||||||
Direction, FoundNode, Node, NodeId, NodeLayer, NodeLayerLink, NodeLocation, OutputNode,
|
FoundNode, Node, NodeId, NodeLocation, OutputNode, ToplevelNode, WorkspaceNode,
|
||||||
StackedNode, ToplevelNode, WorkspaceNode, generic_node_visitor,
|
generic_node_visitor, toplevel_set_workspace,
|
||||||
toplevel_set_workspace,
|
|
||||||
},
|
},
|
||||||
utils::{
|
utils::{
|
||||||
asyncevent::AsyncEvent,
|
asyncevent::AsyncEvent,
|
||||||
|
|
@ -94,9 +93,9 @@ use {
|
||||||
clonecell::CloneCell,
|
clonecell::CloneCell,
|
||||||
copyhashmap::CopyHashMap,
|
copyhashmap::CopyHashMap,
|
||||||
event_listener::{EventListener, EventSource},
|
event_listener::{EventListener, EventSource},
|
||||||
linkedlist::{LinkedList, LinkedNode, NodeRef},
|
linkedlist::{LinkedList, LinkedNode},
|
||||||
numcell::NumCell,
|
numcell::NumCell,
|
||||||
rc_eq::{rc_eq, rc_weak_eq},
|
rc_eq::rc_eq,
|
||||||
smallmap::SmallMap,
|
smallmap::SmallMap,
|
||||||
static_text::StaticText,
|
static_text::StaticText,
|
||||||
},
|
},
|
||||||
|
|
@ -114,13 +113,12 @@ use {
|
||||||
},
|
},
|
||||||
kbvm::Keycode,
|
kbvm::Keycode,
|
||||||
linearize::Linearize,
|
linearize::Linearize,
|
||||||
run_on_drop::on_drop,
|
|
||||||
smallvec::SmallVec,
|
smallvec::SmallVec,
|
||||||
std::{
|
std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::hash_map::Entry,
|
collections::hash_map::Entry,
|
||||||
mem,
|
mem,
|
||||||
ops::{Deref, DerefMut},
|
ops::DerefMut,
|
||||||
rc::{Rc, Weak},
|
rc::{Rc, Weak},
|
||||||
},
|
},
|
||||||
thiserror::Error,
|
thiserror::Error,
|
||||||
|
|
@ -733,459 +731,6 @@ impl WlSeatGlobal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(self: &Rc<Self>) {
|
|
||||||
let kb_node = self.keyboard_node.get();
|
|
||||||
if let Some(tl) = kb_node.node_toplevel() {
|
|
||||||
tl.tl_close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn move_focus(self: &Rc<Self>, direction: Direction) {
|
|
||||||
let tl = match self.keyboard_node.get().node_toplevel() {
|
|
||||||
Some(tl) => tl,
|
|
||||||
_ => {
|
|
||||||
if let Some(ws) = self.keyboard_node.get().node_into_workspace()
|
|
||||||
&& let Some(target) = self
|
|
||||||
.state
|
|
||||||
.find_output_in_direction(&ws.output.get(), direction)
|
|
||||||
{
|
|
||||||
target.take_keyboard_navigation_focus(self, direction);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if direction == Direction::Down && tl.node_is_container() {
|
|
||||||
tl.node_do_focus(self, direction);
|
|
||||||
} else {
|
|
||||||
let data = tl.tl_data();
|
|
||||||
if data.is_fullscreen.get()
|
|
||||||
&& let Some(output) = data.output_opt()
|
|
||||||
&& let Some(target) = self.state.find_output_in_direction(&output, direction)
|
|
||||||
{
|
|
||||||
target.take_keyboard_navigation_focus(self, direction);
|
|
||||||
} else if let Some(p) = data.parent.get()
|
|
||||||
&& let Some(c) = p.node_into_container()
|
|
||||||
{
|
|
||||||
c.move_focus_from_child(self, tl.deref(), direction);
|
|
||||||
} else if let Some(float) = data.float.get() {
|
|
||||||
let ws = float.workspace.get();
|
|
||||||
let floats: Vec<_> = ws
|
|
||||||
.stacked
|
|
||||||
.iter()
|
|
||||||
.filter_map(|node| (*node).clone().node_into_float())
|
|
||||||
.filter(|f| f.child.get().is_some())
|
|
||||||
.collect();
|
|
||||||
if let Some(pos) = floats.iter().position(|f| f.id == float.id) {
|
|
||||||
let target = match direction {
|
|
||||||
Direction::Left | Direction::Down => {
|
|
||||||
if pos == 0 {
|
|
||||||
floats.last()
|
|
||||||
} else {
|
|
||||||
floats.get(pos - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if pos + 1 >= floats.len() {
|
|
||||||
floats.first()
|
|
||||||
} else {
|
|
||||||
floats.get(pos + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(f) = target
|
|
||||||
&& f.id != float.id
|
|
||||||
{
|
|
||||||
f.clone().node_do_focus(self, Direction::Unspecified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn maybe_schedule_warp_mouse_to_focus(self: &Rc<Self>) {
|
|
||||||
if self.mouse_follows_focus() {
|
|
||||||
self.warp_mouse_to_focus_skip_target_check.set(true);
|
|
||||||
self.schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn schedule_warp_mouse_to_focus(self: &Rc<Self>) {
|
|
||||||
if !self.warp_mouse_to_focus_scheduled.replace(true) {
|
|
||||||
self.state.pending_warp_mouse_to_focus.push(self.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn move_focused(self: &Rc<Self>, direction: Direction) {
|
|
||||||
let kb_node = self.keyboard_node.get();
|
|
||||||
let Some(tl) = kb_node.node_toplevel() else {
|
|
||||||
if let Some(ws) = self.keyboard_node.get().node_into_workspace()
|
|
||||||
&& let Some(target) = self
|
|
||||||
.state
|
|
||||||
.find_output_in_direction(&ws.output.get(), direction)
|
|
||||||
{
|
|
||||||
self.state.move_ws_to_output(&ws, &target);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let data = tl.tl_data();
|
|
||||||
if data.is_fullscreen.get()
|
|
||||||
&& let Some(output) = data.output_opt()
|
|
||||||
&& let Some(target) = self.state.find_output_in_direction(&output, direction)
|
|
||||||
{
|
|
||||||
let ws = target.ensure_workspace();
|
|
||||||
toplevel_set_workspace(&self.state, tl, &ws);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
} else if let Some(parent) = data.parent.get()
|
|
||||||
&& let Some(c) = parent.node_into_container()
|
|
||||||
{
|
|
||||||
c.move_child(tl, direction);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
} else if let Some(float) = data.float.get() {
|
|
||||||
float.move_by_direction(direction);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_focus_on_workspace(&self, ws: &WorkspaceNode) -> Option<Rc<dyn Node>> {
|
|
||||||
let mut node = self.focus_history.last()?;
|
|
||||||
loop {
|
|
||||||
if let Some(node) = node.node.upgrade()
|
|
||||||
&& let Some(NodeLocation::Workspace(_, new)) = node.node_location()
|
|
||||||
&& new == ws.id
|
|
||||||
{
|
|
||||||
return Some(node);
|
|
||||||
}
|
|
||||||
node = node.prev()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_focus_history(
|
|
||||||
&self,
|
|
||||||
next: impl Fn(&NodeRef<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
|
||||||
first: impl FnOnce(&LinkedList<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
|
||||||
) -> Option<(Rc<dyn Node>, bool)> {
|
|
||||||
let original = self.keyboard_node.get();
|
|
||||||
let mut output = None;
|
|
||||||
let mut workspace = None;
|
|
||||||
if let Some(old) = original.node_location() {
|
|
||||||
match old {
|
|
||||||
NodeLocation::Workspace(o, w) => {
|
|
||||||
workspace = Some(w);
|
|
||||||
output = Some(o);
|
|
||||||
}
|
|
||||||
NodeLocation::Output(o) => {
|
|
||||||
output = Some(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (output.is_none() || workspace.is_none())
|
|
||||||
&& let Some(old) = self.last_focus_location.get()
|
|
||||||
{
|
|
||||||
match old {
|
|
||||||
NodeLocation::Workspace(o, w) => {
|
|
||||||
workspace = workspace.or(Some(w));
|
|
||||||
output = output.or(Some(o));
|
|
||||||
}
|
|
||||||
NodeLocation::Output(o) => {
|
|
||||||
output = output.or(Some(o));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if workspace.is_none()
|
|
||||||
&& let Some(output) = original.node_output()
|
|
||||||
&& let Some(ws) = output.workspace.get()
|
|
||||||
{
|
|
||||||
workspace = Some(ws.id);
|
|
||||||
}
|
|
||||||
let matches = |node: &FocusHistoryData| {
|
|
||||||
let visible = node.visible.get();
|
|
||||||
if self.focus_history_visible_only.get() && !visible {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let node = node.node.upgrade()?;
|
|
||||||
if self.focus_history_same_workspace.get() {
|
|
||||||
let new = node.node_location()?;
|
|
||||||
let o = match new {
|
|
||||||
NodeLocation::Workspace(o, w) => {
|
|
||||||
if workspace != Some(w) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
o
|
|
||||||
}
|
|
||||||
NodeLocation::Output(o) => o,
|
|
||||||
};
|
|
||||||
if output != Some(o) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((node, visible))
|
|
||||||
};
|
|
||||||
let node = original.node_seat_state().get_focus_history(self);
|
|
||||||
if let Some(mut node) = node {
|
|
||||||
loop {
|
|
||||||
node = match next(&node) {
|
|
||||||
Some(n) => n,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
if let Some(matches) = matches(&node) {
|
|
||||||
return Some(matches);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut node = first(&self.focus_history)?;
|
|
||||||
loop {
|
|
||||||
if rc_weak_eq(&original, &node.node) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if let Some(matches) = matches(&node) {
|
|
||||||
return Some(matches);
|
|
||||||
}
|
|
||||||
node = next(&node)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_history(
|
|
||||||
self: &Rc<Self>,
|
|
||||||
next: impl Fn(&NodeRef<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
|
||||||
first: impl FnOnce(&LinkedList<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
|
||||||
) {
|
|
||||||
let Some((node, visible)) = self.get_focus_history(next, first) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.focus_history_rotate.fetch_add(1);
|
|
||||||
let _reset = on_drop(|| {
|
|
||||||
self.focus_history_rotate.fetch_sub(1);
|
|
||||||
});
|
|
||||||
if !visible {
|
|
||||||
node.clone().node_make_visible();
|
|
||||||
if !node.node_visible() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.focus_node(node);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_prev(self: &Rc<Self>) {
|
|
||||||
self.focus_history(|s| s.prev(), |l| l.last());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_next(self: &Rc<Self>) {
|
|
||||||
self.focus_history(|s| s.next(), |l| l.first());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_history_set_visible(&self, visible: bool) {
|
|
||||||
self.focus_history_visible_only.set(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_history_set_same_workspace(&self, same_workspace: bool) {
|
|
||||||
self.focus_history_same_workspace.set(same_workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_layer_rel<LI, SI>(
|
|
||||||
self: &Rc<Self>,
|
|
||||||
next_layer: impl Fn(NodeLayer) -> NodeLayer,
|
|
||||||
layer_node_next: impl Fn(
|
|
||||||
&NodeRef<Rc<ZwlrLayerSurfaceV1>>,
|
|
||||||
) -> Option<NodeRef<Rc<ZwlrLayerSurfaceV1>>>,
|
|
||||||
stacked_node_next: impl Fn(
|
|
||||||
&NodeRef<Rc<dyn StackedNode>>,
|
|
||||||
) -> Option<NodeRef<Rc<dyn StackedNode>>>,
|
|
||||||
layer_list_iter: impl Fn(&LinkedList<Rc<ZwlrLayerSurfaceV1>>) -> LI,
|
|
||||||
stacked_list_iter: impl Fn(&LinkedList<Rc<dyn StackedNode>>) -> SI,
|
|
||||||
) where
|
|
||||||
LI: Iterator<Item = NodeRef<Rc<ZwlrLayerSurfaceV1>>>,
|
|
||||||
SI: Iterator<Item = NodeRef<Rc<dyn StackedNode>>>,
|
|
||||||
{
|
|
||||||
fn node_viable(n: &(impl Node + ?Sized)) -> bool {
|
|
||||||
n.node_visible() && n.node_accepts_focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
let current = self.keyboard_node.get();
|
|
||||||
let Some(output) = current.node_output() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let current_layer = current.node_layer();
|
|
||||||
match ¤t_layer {
|
|
||||||
NodeLayerLink::Layer0(l)
|
|
||||||
| NodeLayerLink::Layer1(l)
|
|
||||||
| NodeLayerLink::Layer2(l)
|
|
||||||
| NodeLayerLink::Layer3(l) => {
|
|
||||||
if let Some(n) = layer_node_next(l)
|
|
||||||
&& node_viable(&**n)
|
|
||||||
{
|
|
||||||
n.deref()
|
|
||||||
.clone()
|
|
||||||
.node_do_focus(self, Direction::Unspecified);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NodeLayerLink::Stacked(l) | NodeLayerLink::StackedAboveLayers(l) => {
|
|
||||||
if let Some(n) = stacked_node_next(l)
|
|
||||||
&& node_viable(&**n)
|
|
||||||
&& n.node_output().map(|o| o.id) == Some(output.id)
|
|
||||||
{
|
|
||||||
n.deref()
|
|
||||||
.clone()
|
|
||||||
.node_do_focus(self, Direction::Unspecified);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NodeLayerLink::Display => {}
|
|
||||||
NodeLayerLink::Output => {}
|
|
||||||
NodeLayerLink::Workspace => {}
|
|
||||||
NodeLayerLink::Tiled => {}
|
|
||||||
NodeLayerLink::Fullscreen => {}
|
|
||||||
NodeLayerLink::Lock => {}
|
|
||||||
NodeLayerLink::InputMethod => {}
|
|
||||||
}
|
|
||||||
let handle_layer_shell = |l: &LinkedList<Rc<ZwlrLayerSurfaceV1>>| {
|
|
||||||
for n in layer_list_iter(l) {
|
|
||||||
if node_viable(&**n) {
|
|
||||||
return Some(n.deref().clone() as Rc<dyn Node>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let handle_stacked = |l: &LinkedList<Rc<dyn StackedNode>>| {
|
|
||||||
for n in stacked_list_iter(l) {
|
|
||||||
if node_viable(&**n) && n.node_output().map(|o| o.id) == Some(output.id) {
|
|
||||||
return Some(n.deref().clone() as Rc<dyn Node>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let ws = output.workspace.get();
|
|
||||||
let first = next_layer(current_layer.layer());
|
|
||||||
let mut layer = first;
|
|
||||||
loop {
|
|
||||||
let node = match layer {
|
|
||||||
NodeLayer::Display => None,
|
|
||||||
NodeLayer::Layer0 => handle_layer_shell(&output.layers[0]),
|
|
||||||
NodeLayer::Layer1 => handle_layer_shell(&output.layers[1]),
|
|
||||||
NodeLayer::Output => None,
|
|
||||||
NodeLayer::Workspace => {
|
|
||||||
if let Some(ws) = &ws
|
|
||||||
&& ws.container_visible()
|
|
||||||
{
|
|
||||||
self.focus_node(ws.clone());
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
NodeLayer::Tiled => ws
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|w| w.container.get())
|
|
||||||
.map(|n| n as Rc<dyn Node>),
|
|
||||||
NodeLayer::Fullscreen => ws
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|w| w.fullscreen.get())
|
|
||||||
.map(|n| n as Rc<dyn Node>),
|
|
||||||
NodeLayer::Stacked => handle_stacked(&self.state.root.stacked),
|
|
||||||
NodeLayer::Layer2 => handle_layer_shell(&output.layers[2]),
|
|
||||||
NodeLayer::Layer3 => handle_layer_shell(&output.layers[3]),
|
|
||||||
NodeLayer::StackedAboveLayers => {
|
|
||||||
handle_stacked(&self.state.root.stacked_above_layers)
|
|
||||||
}
|
|
||||||
NodeLayer::Lock => None,
|
|
||||||
NodeLayer::InputMethod => None,
|
|
||||||
};
|
|
||||||
if let Some(n) = node {
|
|
||||||
if node_viable(&*n) {
|
|
||||||
n.node_do_focus(self, Direction::Unspecified);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layer = next_layer(layer);
|
|
||||||
if layer == first {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_layer_below(self: &Rc<Self>) {
|
|
||||||
self.focus_layer_rel(
|
|
||||||
|l| l.prev(),
|
|
||||||
|n| n.prev(),
|
|
||||||
|n| n.prev(),
|
|
||||||
|l| l.rev_iter(),
|
|
||||||
|l| l.rev_iter(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_layer_above(self: &Rc<Self>) {
|
|
||||||
self.focus_layer_rel(
|
|
||||||
|l| l.next(),
|
|
||||||
|n| n.next(),
|
|
||||||
|n| n.next(),
|
|
||||||
|l| l.iter(),
|
|
||||||
|l| l.iter(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_focus_float_tiled(self: &Rc<Self>) {
|
|
||||||
let current = self.keyboard_node.get();
|
|
||||||
match current.node_layer().layer() {
|
|
||||||
NodeLayer::Tiled | NodeLayer::Fullscreen => self.focus_floats(),
|
|
||||||
_ => self.focus_tiles(),
|
|
||||||
}
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_floats(self: &Rc<Self>) {
|
|
||||||
let current = self.keyboard_node.get();
|
|
||||||
if current.node_layer().layer() == NodeLayer::Stacked {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(output) = current.node_output() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(ws) = output.workspace.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if let Some(child) = ws
|
|
||||||
.stacked
|
|
||||||
.rev_iter()
|
|
||||||
.filter_map(|node| (*node).clone().node_into_float())
|
|
||||||
.find_map(|float| float.child.get())
|
|
||||||
{
|
|
||||||
child.node_do_focus(self, Direction::Unspecified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus_tiles(self: &Rc<Self>) {
|
|
||||||
let current = self.keyboard_node.get();
|
|
||||||
if matches!(
|
|
||||||
current.node_layer().layer(),
|
|
||||||
NodeLayer::Tiled | NodeLayer::Fullscreen,
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(output) = current.node_output() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(ws) = output.workspace.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let node = match ws.fullscreen.get() {
|
|
||||||
Some(fs) => fs as Rc<dyn Node>,
|
|
||||||
_ => match ws.container.get() {
|
|
||||||
Some(c) => c,
|
|
||||||
_ => return,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if node.node_visible() && node.node_accepts_focus() {
|
|
||||||
node.node_do_focus(self, Direction::Unspecified);
|
|
||||||
self.maybe_schedule_warp_mouse_to_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_drag(
|
pub fn start_drag(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
origin: &Rc<WlSurface>,
|
origin: &Rc<WlSurface>,
|
||||||
|
|
|
||||||
471
src/ifs/wl_seat/focus.rs
Normal file
471
src/ifs/wl_seat/focus.rs
Normal file
|
|
@ -0,0 +1,471 @@
|
||||||
|
use {
|
||||||
|
super::{WlSeatGlobal, event_handling::FocusHistoryData},
|
||||||
|
crate::{
|
||||||
|
ifs::wl_surface::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||||
|
tree::{
|
||||||
|
Direction, Node, NodeLayer, NodeLayerLink, NodeLocation, StackedNode, WorkspaceNode,
|
||||||
|
toplevel_set_workspace,
|
||||||
|
},
|
||||||
|
utils::{
|
||||||
|
linkedlist::{LinkedList, NodeRef},
|
||||||
|
rc_eq::rc_weak_eq,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run_on_drop::on_drop,
|
||||||
|
std::{ops::Deref, rc::Rc},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl WlSeatGlobal {
|
||||||
|
pub fn close(self: &Rc<Self>) {
|
||||||
|
let kb_node = self.keyboard_node.get();
|
||||||
|
if let Some(tl) = kb_node.node_toplevel() {
|
||||||
|
tl.tl_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_focus(self: &Rc<Self>, direction: Direction) {
|
||||||
|
let tl = match self.keyboard_node.get().node_toplevel() {
|
||||||
|
Some(tl) => tl,
|
||||||
|
_ => {
|
||||||
|
if let Some(ws) = self.keyboard_node.get().node_into_workspace()
|
||||||
|
&& let Some(target) = self
|
||||||
|
.state
|
||||||
|
.find_output_in_direction(&ws.output.get(), direction)
|
||||||
|
{
|
||||||
|
target.take_keyboard_navigation_focus(self, direction);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if direction == Direction::Down && tl.node_is_container() {
|
||||||
|
tl.node_do_focus(self, direction);
|
||||||
|
} else {
|
||||||
|
let data = tl.tl_data();
|
||||||
|
if data.is_fullscreen.get()
|
||||||
|
&& let Some(output) = data.output_opt()
|
||||||
|
&& let Some(target) = self.state.find_output_in_direction(&output, direction)
|
||||||
|
{
|
||||||
|
target.take_keyboard_navigation_focus(self, direction);
|
||||||
|
} else if let Some(p) = data.parent.get()
|
||||||
|
&& let Some(c) = p.node_into_container()
|
||||||
|
{
|
||||||
|
c.move_focus_from_child(self, tl.deref(), direction);
|
||||||
|
} else if let Some(float) = data.float.get() {
|
||||||
|
let ws = float.workspace.get();
|
||||||
|
let floats: Vec<_> = ws
|
||||||
|
.stacked
|
||||||
|
.iter()
|
||||||
|
.filter_map(|node| (*node).clone().node_into_float())
|
||||||
|
.filter(|f| f.child.get().is_some())
|
||||||
|
.collect();
|
||||||
|
if let Some(pos) = floats.iter().position(|f| f.id == float.id) {
|
||||||
|
let target = match direction {
|
||||||
|
Direction::Left | Direction::Down => {
|
||||||
|
if pos == 0 {
|
||||||
|
floats.last()
|
||||||
|
} else {
|
||||||
|
floats.get(pos - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if pos + 1 >= floats.len() {
|
||||||
|
floats.first()
|
||||||
|
} else {
|
||||||
|
floats.get(pos + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(f) = target
|
||||||
|
&& f.id != float.id
|
||||||
|
{
|
||||||
|
f.clone().node_do_focus(self, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn maybe_schedule_warp_mouse_to_focus(self: &Rc<Self>) {
|
||||||
|
if self.mouse_follows_focus() {
|
||||||
|
self.warp_mouse_to_focus_skip_target_check.set(true);
|
||||||
|
self.schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_warp_mouse_to_focus(self: &Rc<Self>) {
|
||||||
|
if !self.warp_mouse_to_focus_scheduled.replace(true) {
|
||||||
|
self.state.pending_warp_mouse_to_focus.push(self.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_focused(self: &Rc<Self>, direction: Direction) {
|
||||||
|
let kb_node = self.keyboard_node.get();
|
||||||
|
let Some(tl) = kb_node.node_toplevel() else {
|
||||||
|
if let Some(ws) = self.keyboard_node.get().node_into_workspace()
|
||||||
|
&& let Some(target) = self
|
||||||
|
.state
|
||||||
|
.find_output_in_direction(&ws.output.get(), direction)
|
||||||
|
{
|
||||||
|
self.state.move_ws_to_output(&ws, &target);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let data = tl.tl_data();
|
||||||
|
if data.is_fullscreen.get()
|
||||||
|
&& let Some(output) = data.output_opt()
|
||||||
|
&& let Some(target) = self.state.find_output_in_direction(&output, direction)
|
||||||
|
{
|
||||||
|
let ws = target.ensure_workspace();
|
||||||
|
toplevel_set_workspace(&self.state, tl, &ws);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
} else if let Some(parent) = data.parent.get()
|
||||||
|
&& let Some(c) = parent.node_into_container()
|
||||||
|
{
|
||||||
|
c.move_child(tl, direction);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
} else if let Some(float) = data.float.get() {
|
||||||
|
float.move_by_direction(direction);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last_focus_on_workspace(&self, ws: &WorkspaceNode) -> Option<Rc<dyn Node>> {
|
||||||
|
let mut node = self.focus_history.last()?;
|
||||||
|
loop {
|
||||||
|
if let Some(node) = node.node.upgrade()
|
||||||
|
&& let Some(NodeLocation::Workspace(_, new)) = node.node_location()
|
||||||
|
&& new == ws.id
|
||||||
|
{
|
||||||
|
return Some(node);
|
||||||
|
}
|
||||||
|
node = node.prev()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_focus_history(
|
||||||
|
&self,
|
||||||
|
next: impl Fn(&NodeRef<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
||||||
|
first: impl FnOnce(&LinkedList<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
||||||
|
) -> Option<(Rc<dyn Node>, bool)> {
|
||||||
|
let original = self.keyboard_node.get();
|
||||||
|
let mut output = None;
|
||||||
|
let mut workspace = None;
|
||||||
|
if let Some(old) = original.node_location() {
|
||||||
|
match old {
|
||||||
|
NodeLocation::Workspace(o, w) => {
|
||||||
|
workspace = Some(w);
|
||||||
|
output = Some(o);
|
||||||
|
}
|
||||||
|
NodeLocation::Output(o) => {
|
||||||
|
output = Some(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (output.is_none() || workspace.is_none())
|
||||||
|
&& let Some(old) = self.last_focus_location.get()
|
||||||
|
{
|
||||||
|
match old {
|
||||||
|
NodeLocation::Workspace(o, w) => {
|
||||||
|
workspace = workspace.or(Some(w));
|
||||||
|
output = output.or(Some(o));
|
||||||
|
}
|
||||||
|
NodeLocation::Output(o) => {
|
||||||
|
output = output.or(Some(o));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if workspace.is_none()
|
||||||
|
&& let Some(output) = original.node_output()
|
||||||
|
&& let Some(ws) = output.workspace.get()
|
||||||
|
{
|
||||||
|
workspace = Some(ws.id);
|
||||||
|
}
|
||||||
|
let matches = |node: &FocusHistoryData| {
|
||||||
|
let visible = node.visible.get();
|
||||||
|
if self.focus_history_visible_only.get() && !visible {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let node = node.node.upgrade()?;
|
||||||
|
if self.focus_history_same_workspace.get() {
|
||||||
|
let new = node.node_location()?;
|
||||||
|
let o = match new {
|
||||||
|
NodeLocation::Workspace(o, w) => {
|
||||||
|
if workspace != Some(w) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
|
NodeLocation::Output(o) => o,
|
||||||
|
};
|
||||||
|
if output != Some(o) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((node, visible))
|
||||||
|
};
|
||||||
|
let node = original.node_seat_state().get_focus_history(self);
|
||||||
|
if let Some(mut node) = node {
|
||||||
|
loop {
|
||||||
|
node = match next(&node) {
|
||||||
|
Some(n) => n,
|
||||||
|
_ => break,
|
||||||
|
};
|
||||||
|
if let Some(matches) = matches(&node) {
|
||||||
|
return Some(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut node = first(&self.focus_history)?;
|
||||||
|
loop {
|
||||||
|
if rc_weak_eq(&original, &node.node) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Some(matches) = matches(&node) {
|
||||||
|
return Some(matches);
|
||||||
|
}
|
||||||
|
node = next(&node)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_history(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
next: impl Fn(&NodeRef<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
||||||
|
first: impl FnOnce(&LinkedList<FocusHistoryData>) -> Option<NodeRef<FocusHistoryData>>,
|
||||||
|
) {
|
||||||
|
let Some((node, visible)) = self.get_focus_history(next, first) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.focus_history_rotate.fetch_add(1);
|
||||||
|
let _reset = on_drop(|| {
|
||||||
|
self.focus_history_rotate.fetch_sub(1);
|
||||||
|
});
|
||||||
|
if !visible {
|
||||||
|
node.clone().node_make_visible();
|
||||||
|
if !node.node_visible() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.focus_node(node);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_prev(self: &Rc<Self>) {
|
||||||
|
self.focus_history(|s| s.prev(), |l| l.last());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_next(self: &Rc<Self>) {
|
||||||
|
self.focus_history(|s| s.next(), |l| l.first());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_history_set_visible(&self, visible: bool) {
|
||||||
|
self.focus_history_visible_only.set(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_history_set_same_workspace(&self, same_workspace: bool) {
|
||||||
|
self.focus_history_same_workspace.set(same_workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_layer_rel<LI, SI>(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
next_layer: impl Fn(NodeLayer) -> NodeLayer,
|
||||||
|
layer_node_next: impl Fn(
|
||||||
|
&NodeRef<Rc<ZwlrLayerSurfaceV1>>,
|
||||||
|
) -> Option<NodeRef<Rc<ZwlrLayerSurfaceV1>>>,
|
||||||
|
stacked_node_next: impl Fn(
|
||||||
|
&NodeRef<Rc<dyn StackedNode>>,
|
||||||
|
) -> Option<NodeRef<Rc<dyn StackedNode>>>,
|
||||||
|
layer_list_iter: impl Fn(&LinkedList<Rc<ZwlrLayerSurfaceV1>>) -> LI,
|
||||||
|
stacked_list_iter: impl Fn(&LinkedList<Rc<dyn StackedNode>>) -> SI,
|
||||||
|
) where
|
||||||
|
LI: Iterator<Item = NodeRef<Rc<ZwlrLayerSurfaceV1>>>,
|
||||||
|
SI: Iterator<Item = NodeRef<Rc<dyn StackedNode>>>,
|
||||||
|
{
|
||||||
|
fn node_viable(n: &(impl Node + ?Sized)) -> bool {
|
||||||
|
n.node_visible() && n.node_accepts_focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = self.keyboard_node.get();
|
||||||
|
let Some(output) = current.node_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let current_layer = current.node_layer();
|
||||||
|
match ¤t_layer {
|
||||||
|
NodeLayerLink::Layer0(l)
|
||||||
|
| NodeLayerLink::Layer1(l)
|
||||||
|
| NodeLayerLink::Layer2(l)
|
||||||
|
| NodeLayerLink::Layer3(l) => {
|
||||||
|
if let Some(n) = layer_node_next(l)
|
||||||
|
&& node_viable(&**n)
|
||||||
|
{
|
||||||
|
n.deref()
|
||||||
|
.clone()
|
||||||
|
.node_do_focus(self, Direction::Unspecified);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeLayerLink::Stacked(l) | NodeLayerLink::StackedAboveLayers(l) => {
|
||||||
|
if let Some(n) = stacked_node_next(l)
|
||||||
|
&& node_viable(&**n)
|
||||||
|
&& n.node_output().map(|o| o.id) == Some(output.id)
|
||||||
|
{
|
||||||
|
n.deref()
|
||||||
|
.clone()
|
||||||
|
.node_do_focus(self, Direction::Unspecified);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeLayerLink::Display => {}
|
||||||
|
NodeLayerLink::Output => {}
|
||||||
|
NodeLayerLink::Workspace => {}
|
||||||
|
NodeLayerLink::Tiled => {}
|
||||||
|
NodeLayerLink::Fullscreen => {}
|
||||||
|
NodeLayerLink::Lock => {}
|
||||||
|
NodeLayerLink::InputMethod => {}
|
||||||
|
}
|
||||||
|
let handle_layer_shell = |l: &LinkedList<Rc<ZwlrLayerSurfaceV1>>| {
|
||||||
|
for n in layer_list_iter(l) {
|
||||||
|
if node_viable(&**n) {
|
||||||
|
return Some(n.deref().clone() as Rc<dyn Node>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let handle_stacked = |l: &LinkedList<Rc<dyn StackedNode>>| {
|
||||||
|
for n in stacked_list_iter(l) {
|
||||||
|
if node_viable(&**n) && n.node_output().map(|o| o.id) == Some(output.id) {
|
||||||
|
return Some(n.deref().clone() as Rc<dyn Node>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let ws = output.workspace.get();
|
||||||
|
let first = next_layer(current_layer.layer());
|
||||||
|
let mut layer = first;
|
||||||
|
loop {
|
||||||
|
let node = match layer {
|
||||||
|
NodeLayer::Display => None,
|
||||||
|
NodeLayer::Layer0 => handle_layer_shell(&output.layers[0]),
|
||||||
|
NodeLayer::Layer1 => handle_layer_shell(&output.layers[1]),
|
||||||
|
NodeLayer::Output => None,
|
||||||
|
NodeLayer::Workspace => {
|
||||||
|
if let Some(ws) = &ws
|
||||||
|
&& ws.container_visible()
|
||||||
|
{
|
||||||
|
self.focus_node(ws.clone());
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
NodeLayer::Tiled => ws
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|w| w.container.get())
|
||||||
|
.map(|n| n as Rc<dyn Node>),
|
||||||
|
NodeLayer::Fullscreen => ws
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|w| w.fullscreen.get())
|
||||||
|
.map(|n| n as Rc<dyn Node>),
|
||||||
|
NodeLayer::Stacked => handle_stacked(&self.state.root.stacked),
|
||||||
|
NodeLayer::Layer2 => handle_layer_shell(&output.layers[2]),
|
||||||
|
NodeLayer::Layer3 => handle_layer_shell(&output.layers[3]),
|
||||||
|
NodeLayer::StackedAboveLayers => {
|
||||||
|
handle_stacked(&self.state.root.stacked_above_layers)
|
||||||
|
}
|
||||||
|
NodeLayer::Lock => None,
|
||||||
|
NodeLayer::InputMethod => None,
|
||||||
|
};
|
||||||
|
if let Some(n) = node {
|
||||||
|
if node_viable(&*n) {
|
||||||
|
n.node_do_focus(self, Direction::Unspecified);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer = next_layer(layer);
|
||||||
|
if layer == first {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_layer_below(self: &Rc<Self>) {
|
||||||
|
self.focus_layer_rel(
|
||||||
|
|l| l.prev(),
|
||||||
|
|n| n.prev(),
|
||||||
|
|n| n.prev(),
|
||||||
|
|l| l.rev_iter(),
|
||||||
|
|l| l.rev_iter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_layer_above(self: &Rc<Self>) {
|
||||||
|
self.focus_layer_rel(
|
||||||
|
|l| l.next(),
|
||||||
|
|n| n.next(),
|
||||||
|
|n| n.next(),
|
||||||
|
|l| l.iter(),
|
||||||
|
|l| l.iter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_focus_float_tiled(self: &Rc<Self>) {
|
||||||
|
let current = self.keyboard_node.get();
|
||||||
|
match current.node_layer().layer() {
|
||||||
|
NodeLayer::Tiled | NodeLayer::Fullscreen => self.focus_floats(),
|
||||||
|
_ => self.focus_tiles(),
|
||||||
|
}
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_floats(self: &Rc<Self>) {
|
||||||
|
let current = self.keyboard_node.get();
|
||||||
|
if current.node_layer().layer() == NodeLayer::Stacked {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(output) = current.node_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ws) = output.workspace.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(child) = ws
|
||||||
|
.stacked
|
||||||
|
.rev_iter()
|
||||||
|
.filter_map(|node| (*node).clone().node_into_float())
|
||||||
|
.find_map(|float| float.child.get())
|
||||||
|
{
|
||||||
|
child.node_do_focus(self, Direction::Unspecified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_tiles(self: &Rc<Self>) {
|
||||||
|
let current = self.keyboard_node.get();
|
||||||
|
if matches!(
|
||||||
|
current.node_layer().layer(),
|
||||||
|
NodeLayer::Tiled | NodeLayer::Fullscreen,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(output) = current.node_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ws) = output.workspace.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let node = match ws.fullscreen.get() {
|
||||||
|
Some(fs) => fs as Rc<dyn Node>,
|
||||||
|
_ => match ws.container.get() {
|
||||||
|
Some(c) => c,
|
||||||
|
_ => return,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if node.node_visible() && node.node_accepts_focus() {
|
||||||
|
node.node_do_focus(self, Direction::Unspecified);
|
||||||
|
self.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue