mirror of
https://github.com/vidarh/rubywm.git
synced 2024-11-15 19:48:30 +01:00
438 lines
14 KiB
Ruby
438 lines
14 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)
|
|
begin
|
|
# 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.is_a?(X11::Form::Error) || 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
|
|
)
|
|
rescue Exception => e
|
|
p [:ZZZZZZZZZZZZZZZZZZZZZZZZZADOPT_FAILED, e]
|
|
# Failure here most likely reflects a window that has "disappeared".
|
|
# We should handle that better, but for now this is fine
|
|
end
|
|
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 rescue nil
|
|
if @attr
|
|
set_focus(w.wid)
|
|
@start = ev
|
|
end
|
|
end
|
|
|
|
def on_motion_notify(ev)
|
|
# @start.button == 1 -> move
|
|
# @start.button == 3 -> resize
|
|
if ev.child != @start.child
|
|
set_focus(ev.child) rescue nil # FIXME
|
|
end
|
|
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
|
|
begin
|
|
return if w.special?
|
|
rescue # FIXME: Why is this here?
|
|
end
|
|
|
|
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?
|
|
# FIXME:
|
|
# Move stepwise instead.
|
|
g = @focus.get_geometry rescue nil
|
|
return if g.nil?
|
|
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
|