mirror of
https://github.com/vidarh/rubywm.git
synced 2024-09-28 03:21:18 +02:00
3889d7a5f5
Some of these may possibly need to be reintroduced, but in most cases bailing out from handling the current event is the right choice in any case.
424 lines
13 KiB
Ruby
424 lines
13 KiB
Ruby
|
|
require_relative 'floating'
|
|
|
|
class WindowManager
|
|
attr_reader :dpy, :desktops, :windows, :focus
|
|
|
|
def inspect = "<WindowManager>"
|
|
|
|
def initialize dpy, config
|
|
@dpy = dpy
|
|
@windows = {}
|
|
|
|
@border_normal = 0x88666666
|
|
@border_focus = 0xffff66ff
|
|
|
|
@floating = FloatingLayout.new(rootgeom)
|
|
|
|
process_config(config)
|
|
|
|
change_property(:_NET_NUMBER_OF_DESKTOPS, :cardinal, @desktops.count)
|
|
|
|
mask = X11::Form::ButtonPressMask|X11::Form::ButtonReleaseMask|X11::Form::PointerMotionMask
|
|
root.grab_button(true, mask, :async, :async, 0, 0, 1, X11::Form::Mod3)
|
|
root.grab_button(true, mask, :async, :async, 0, 0, 3, X11::Form::Mod3)
|
|
root.grab_button(true, mask, :async, :async, 0, 0, 1, X11::Form::Mod4)
|
|
root.grab_button(true, mask, :async, :async, 0, 0, 3, X11::Form::Mod4)
|
|
|
|
eventmask = (X11::Form::SubstructureNotifyMask |
|
|
X11::Form::SubstructureRedirectMask |
|
|
X11::Form::StructureNotifyMask |
|
|
X11::Form::EnterWindowMask |
|
|
X11::Form::LeaveWindowMask |
|
|
X11::Form::ButtonPressMask |
|
|
# X11::Form::ExposureMask |
|
|
X11::Form::KeyPressMask |
|
|
X11::Form::FocusChangeMask
|
|
)
|
|
|
|
root.select_input(eventmask)
|
|
at_exit { root.set_input_focus(:parent) }
|
|
|
|
children = root.query_tree.children
|
|
children.each { |wid| window(wid) }
|
|
|
|
desktops.each(&:hide)
|
|
change_desktop(current_desktop_id)
|
|
|
|
end
|
|
|
|
def process_node_child(spec, n)
|
|
if spec[:type] != :node && !spec[:nodes]
|
|
return Leaf.new(iclass: spec[:iclass], parent: n)
|
|
end
|
|
cur = Node.new(parent: n)
|
|
process_node_config(cur, spec, n)
|
|
return cur
|
|
end
|
|
|
|
def process_node_config(n, spec, parent=nil)
|
|
n.ratio = spec[:ratio] if spec&.dig(:ratio)
|
|
n.dir = spec[:dir].to_sym if spec&.dig(:dir)
|
|
Array(spec&.dig(:nodes)).each do |sub|
|
|
n.nodes << process_node_child(sub, n)
|
|
end
|
|
end
|
|
|
|
# FIXME: I'm not particlarly happy about building this in.
|
|
# I prefer the bspwm approach of externalising it, because
|
|
# I need/want an API to change it dynamically anyway, so
|
|
# this is likely to change.
|
|
def process_config(config)
|
|
num_desktops = config.dig(:desktops, :number) || 10
|
|
@desktops ||= num_desktops.times.map do |num|
|
|
c = config.dig(:desktops, num+1)
|
|
name = c&.dig(:name) || (num+1).to_s
|
|
Desktop.new(self, num, name).tap do |d|
|
|
if c&.dig(:layout) == "floating"
|
|
# FIXME: Should be ok to set this to @floating
|
|
# but some logic checks for a nil layout
|
|
d.layout = nil
|
|
else
|
|
d.layout = TiledLayout.new(d, rootgeom)
|
|
process_node_config(d.layout.root,c)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def change_property(atom, type, data, mode: :replace, format: 32)
|
|
root.change_property(mode, atom, type, format, Array(data).pack("V*").unpack("C*"))
|
|
end
|
|
|
|
def current_desktop_id = (@current_desktop_id ||= root.get_property(:_NET_CURRENT_DESKTOP, :cardinal)&.value.to_i)
|
|
def current_desktop = desktops[current_desktop_id] || desktops[0]
|
|
def root_id = (@root_id ||= @dpy.screens.first.root)
|
|
def root = (@root ||= X11::Window.new(@dpy, root_id))
|
|
def layout = current_desktop&.layout || @floating
|
|
def layout_for(w) = (w.floating? ? @floating : layout)
|
|
def update_layout = layout.call(@focus)
|
|
|
|
# FIXME: Does not take into account panels
|
|
def rootgeom = (@rootgeom ||= root.get_geometry)
|
|
def window(wid)
|
|
return root if (wid == root.wid)
|
|
return @windows[wid] if @windows[wid]
|
|
adopt(wid)
|
|
end
|
|
|
|
def update_client_list = change_property(:_NET_CLIENT_LIST, :window, @windows.keys)
|
|
|
|
# If we don't already know about this window, we "adopt" it.
|
|
def adopt(wid, desktop=nil)
|
|
return if wid.nil?
|
|
w = @windows[wid] # To avoid infinite recursion, this *must not* use #window
|
|
return w if w
|
|
w = Window.new(self, wid)
|
|
|
|
# FIXME: At least some of these ought to "adopted" but set as
|
|
# floating/non-layout so they stay on a single desktop.
|
|
#
|
|
if w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_POPUP) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_NOTIFICATION) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_POPUP_MENU) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_MENU) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_DOCK) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_TOOLTIP) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_DIALOG) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_SPLASH) ||
|
|
w.type == dpy.atom(:_NET_WM_WINDOW_TYPE_UTILITY)
|
|
w.floating = true
|
|
w.stack
|
|
return w
|
|
end
|
|
if w.desktop?
|
|
w.floating = true
|
|
end
|
|
attr = w.get_window_attributes
|
|
return w if attr.wclass == 2 # InputOnly
|
|
return w if attr.override_redirect
|
|
w.mapped = attr.map_state != 0
|
|
geom = w.get_geometry
|
|
return w if geom.width < 2 || geom.height < 2
|
|
@windows[wid] = w
|
|
|
|
wms = w.get_property(:_NET_WM_STATE, :atom)&.value
|
|
if wms == dpy.atom(:_NET_WM_STATE_ABOVE)
|
|
# This seems like it's probably not a good idea.
|
|
return w
|
|
end
|
|
|
|
w.set_border(@border_normal)
|
|
|
|
desktop = dpy.get_property(wid, :_NET_WM_DESKTOP, :cardinal)&.value
|
|
desktop ||= current_desktop_id
|
|
move_to_desktop(wid, desktop)
|
|
w.select_input(
|
|
X11::Form::FocusChangeMask |
|
|
X11::Form::PropertyChangeMask |
|
|
X11::Form::EnterWindowMask |
|
|
X11::Form::LeaveWindowMask
|
|
)
|
|
update_client_list
|
|
return w
|
|
end
|
|
|
|
def map_window(wid)
|
|
w = window(wid)
|
|
w.mapped = true
|
|
layout_for(w).place(w, @focus) unless layout_for(w).find(w)
|
|
w.map
|
|
set_focus(wid) unless w.special?
|
|
end
|
|
|
|
def move_to_desktop(wid,desktop)
|
|
return if wid == root_id
|
|
d = desktops[desktop] || desktops[0]
|
|
w = window(wid)
|
|
old = w.desktop
|
|
w.desktop = d
|
|
d.update_layout if d.active?
|
|
old&.update_layout
|
|
d.active? ? w.show : w.hide
|
|
end
|
|
|
|
def change_desktop(d)
|
|
if current_desktop_id == d
|
|
update_layout
|
|
return current_desktop.show
|
|
end
|
|
old = current_desktop
|
|
@current_desktop_id = d
|
|
current_desktop.show
|
|
update_layout
|
|
# FIXME: Switch focus (keep focus stack per desktop)
|
|
old.hide
|
|
change_property(:_NET_CURRENT_DESKTOP, :cardinal, d)
|
|
f = current_desktop&.mapped_regular_children&.first
|
|
set_focus(f.wid) if f
|
|
end
|
|
|
|
|
|
def set_focus(wid)
|
|
return if wid == root_id
|
|
w = window(wid)
|
|
|
|
# FIXME: This may be a bit brutal, in that it prevents keyboard control of the desktop or dock.
|
|
return if w.special?
|
|
|
|
@focus&.set_border(@border_normal)
|
|
@focus = w
|
|
@focus.set_input_focus(:parent)
|
|
@focus.set_border(@border_focus)
|
|
change_property(:_NET_ACTIVE_WINDOW, :window, wid)
|
|
end
|
|
|
|
def destroy_window(wid)
|
|
if w = @windows[wid]
|
|
@windows.delete(wid)
|
|
update_layout
|
|
update_client_list
|
|
end
|
|
end
|
|
|
|
# # X Event-handlers
|
|
|
|
def on_error(ev) = destroy_window(ev.bad_resource_id)
|
|
|
|
def on_map_notify(ev) = (window(ev.window).mapped = true)
|
|
def on_unmap_notify(ev) = (window(ev.window).mapped = false)
|
|
def on_map_request(ev) = map_window(ev.window)
|
|
def on_property_notify(ev) = (p dpy.get_atom_name(ev.atom) rescue nil)
|
|
|
|
def on_button_press(ev)
|
|
return if !ev.child
|
|
w = window(ev.child)
|
|
@attr = w.get_geometry
|
|
set_focus(w.wid)
|
|
@start = ev
|
|
end
|
|
|
|
def on_motion_notify(ev)
|
|
# @start.button == 1 -> move
|
|
# @start.button == 3 -> resize
|
|
set_focus(ev.child) if ev.child != @start.child
|
|
return if !@start&.child || !@attr
|
|
|
|
xdiff = ev.root_x - @start.root_x;
|
|
ydiff = ev.root_y - @start.root_y;
|
|
|
|
w = window(@start.child)
|
|
|
|
# FIXME: Any other types we don't want to allow moving or resizing
|
|
return if w.special?
|
|
|
|
if @start.detail == 1 # Move
|
|
if w.floating?
|
|
w.configure(x: @attr.x + xdiff, y: @attr.y + ydiff)
|
|
end
|
|
elsif @start.detail == 3 # Resize
|
|
lr = (ev.event_x-@attr.x < @attr.width / 2)
|
|
tb = (ev.event_y-@attr.y < @attr.height/ 2)
|
|
if w.floating?
|
|
# If left/above the centre point, we grow/shrink the window to the left/top
|
|
# otherwise to the right/bottom. Doing it to the left/top requires
|
|
# moving it at the same time.
|
|
@attr.x = @attr.x + (lr ? xdiff : 0)
|
|
@attr.y = @attr.y + (tb ? ydiff : 0)
|
|
@attr.width = @attr.width + (lr ? -xdiff : xdiff)
|
|
@attr.height = @attr.height+ (tb ? -ydiff : ydiff)
|
|
w.configure(x: @attr.x, y: @attr.y, width: @attr.width, height: @attr.height)
|
|
else
|
|
ancestors = ->(first,dir,flag, &block) do
|
|
first&.ancestors&.each_cons(2) do |prev, node|
|
|
if node.dir == dir &&
|
|
((node.nodes[0] == prev && !flag) ||
|
|
(node.nodes[1] == prev))
|
|
node.ratio += node.geom ? block.call(prev,node,flag) : 0.0
|
|
node.ratio = node.ratio.clamp(0.1,0.9)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
ancestors.call(w.layout_leaf,:lr,lr) do |prev, node, flag|
|
|
(((node.geom.width * node.ratio) + xdiff)/node.geom.width) - node.ratio
|
|
end
|
|
|
|
ancestors.call(w.layout_leaf, :tb, tb) do |prev,node, flag|
|
|
(((node.geom.height * node.ratio) + ydiff)/node.geom.height) - node.ratio
|
|
end
|
|
update_layout
|
|
end
|
|
@start.root_x = ev.root_x
|
|
@start.root_y = ev.root_y
|
|
end
|
|
end
|
|
|
|
def on_button_release(ev) = (@start.child = nil if @start)
|
|
def on_focus_in(ev) = focus || set_focus(ev.event)
|
|
def on_enter_notify(ev) = set_focus(ev.event)
|
|
def on_unmap_notify(ev) = window(ev.window)&.desktop&.update_layout
|
|
def on_destroy_notify(ev) = destroy_window(ev.window)
|
|
|
|
# # Client Messages
|
|
|
|
def on_net_active_window(wid, ...) = map_window(wid)
|
|
|
|
def on_net_restack_window(wid,source, sibling_wid, detail)
|
|
w = window(wid)
|
|
|
|
# FIXME: Handle sibling.
|
|
detail = case detail
|
|
when 0 then :above
|
|
when 1 then :below
|
|
else detail
|
|
end
|
|
w.configure(stack_mode: detail)
|
|
end
|
|
|
|
def on_net_current_desktop(_, d) = change_desktop(d)
|
|
|
|
def on_net_wm_state(wid, action, prop1, prop2, source)
|
|
w = window(wid)
|
|
p [:got_wm_state_for, w, prop1 == 0 ? "None" : dpy.get_atom_name(prop1),
|
|
prop2 == 0 ? "None" : dpy.get_atom_name(prop2)]
|
|
# FIXME: Need to check if "action" for toggle vs set/clear
|
|
[prop1, prop2].each do |prop|
|
|
case prop
|
|
when dpy.atom(:_NET_WM_STATE_FULLSCREEN)
|
|
w.toggle_maximize
|
|
end
|
|
end
|
|
# For the time being, we recognize two things only:
|
|
# NET_WM_STATE_FULLSCREEN and NET_WM_STATE_MAXIMIZED_{VERT,HORZ}
|
|
end
|
|
|
|
# FIXME: This should be _NET_CLOSE_WINDOW
|
|
# and _NET_CLOSE_WINDOW should initiate a WM_DELETE_WINDOW
|
|
# *to the client* if they support it, w/fallback to destroyf
|
|
def on_net_wm_desktop(wid, d) = move_to_desktop(wid, d)
|
|
|
|
def on_wm_delete_window(*args)
|
|
# FIXME: Include id in args
|
|
@focus.destroy if @focus
|
|
end
|
|
|
|
# # RWM specific ClientMessages
|
|
|
|
# Move focus to the "nearest" window in `dir` direction
|
|
def on_rwm_focus(_, dir)
|
|
dir = dpy.get_atom_name(dir).downcase.to_sym
|
|
return if !@focus || @focus.special?
|
|
w = find_closest(@focus, dir, @focus.desktop.mapped_regular_children)
|
|
set_focus(w.wid) if w
|
|
end
|
|
|
|
# Toggle the direction of the node split.
|
|
def on_rwm_shift_direction(_,dir)
|
|
# FIXME: Respect the window passed instead of doing it to @focus
|
|
return if !@focus || @focus.special?
|
|
if node = layout.find(@focus)
|
|
node = node.parent if node.is_a?(Leaf)
|
|
node.dir = node.dir == :lr ? :tb : :lr
|
|
current_desktop&.update_layout
|
|
end
|
|
end
|
|
|
|
# Swap nodes in the nearest parent node of the focused window
|
|
def on_rwm_swap_nodes(_)
|
|
# FIXME: Respect the window passed instead of doing it to @focus
|
|
# no matter what
|
|
return if !@focus || @focus.special?
|
|
# FIXME: Move to layout?
|
|
if node = layout.find(@focus)
|
|
node = node.parent if node.is_a?(Leaf)
|
|
tmp = node.nodes[0]
|
|
node.nodes[0] = node.nodes[1]
|
|
node.nodes[1] = tmp
|
|
update_layout
|
|
end
|
|
end
|
|
|
|
# Move the focused window, either swapping it into the container
|
|
# of the nearest leaf (if tiled), or moving it stepwise if floating
|
|
# FIXME: Just have rwm move specify x/y *offsets* instead? Would
|
|
# save an (admittedly cached) get_atom_name
|
|
def on_rwm_move(_,dir)
|
|
return if !@focus || @focus.special?
|
|
dir = dpy.get_atom_name(dir).downcase.to_sym
|
|
|
|
if @focus.floating?
|
|
g = @focus.get_geometry
|
|
|
|
case dir
|
|
when :left then @focus.configure(x: g.x - 20)
|
|
when :right then @focus.configure(x: g.x + 20)
|
|
when :down then @focus.configure(y: g.y + 20)
|
|
when :up then @focus.configure(y: g.y - 20)
|
|
end
|
|
return
|
|
end
|
|
|
|
w = find_closest(@focus, dir, @focus.desktop.mapped_regular_children)
|
|
|
|
l1 = @focus.desktop&.layout&.find(@focus)
|
|
l2 = w&.desktop&.layout&.find(w)
|
|
if l1 && l2
|
|
l2.window = @focus
|
|
l1.window = w
|
|
@focus.desktop.update_layout
|
|
|
|
# FIXME: We want to ensure focus stays in @focus
|
|
# here. Not sure how. We get enter/leave/focus in/out
|
|
# events. How can we get button/motion events for individual windows
|
|
# Maybe I have to do
|
|
# https://stackoverflow.com/questions/62448181/how-do-i-monitor-mouse-movement-events-in-all-windows-not-just-one-on-x11
|
|
# XInput v2.0
|
|
# And then ignore enter/leave events. Seems stupid
|
|
# Investigate what Katriawm does?
|
|
set_focus(@focus.wid)
|
|
end
|
|
end
|
|
end
|