osc: implement pseudo client side decorations via OSC

Today, if window decorations are not present, either because they were
disabled, or because the platform doesn't support them
(eg: gnome-shell on wayland), there are no window controls, meaning it
is not possible to minimize/maximize/close a window without knowing
keyboard shortcuts.

While you can imagine various ways of offering client side decorations,
it is attractive to consider using OSC because that is functionality
that we already have.

The main work here is defining a separate input area from the main
OSC box with its own buttons, etc.

While we could probably handle auto-detection based on whether
decorations are present or not, it's manually controlled for now.

The window control logic is mostly disconnected from the OSC itself,
except in the case of the `topbar` layout, where there has to be
coordination so that the controls don't get drawn on top of each other.

I had to do fine-positioning of the buttons based on the font on
my system, so don't be surprised if it looks wrong elsewhere.

You could also argue that window controls should be unscaled, even
if the main OSC box is scaled, but I've not tried to do this.
This commit is contained in:
Philip Langdale 2019-11-26 08:50:14 +08:00 committed by Philip Langdale
parent 901b3dddb0
commit a220f08648
2 changed files with 161 additions and 7 deletions

View File

@ -341,6 +341,22 @@ Configurable Options
``--sub-use-margins`` option is set to ``yes``, the default). This may be
fixed later.
``windowcontrols``
Default: no (Do not show window controls)
Whether to show window management controls over the video, and if so,
which side of the window to place them. This may be desirable when the
window has no decorations, either because they have been explicitly
disabled (``border=no``) or because the current platform doesn't support
them (eg: gnome-shell with wayland).
The set of window controls is fixed, offering ``minimze``, ``maximize``,
and ``quit``. Not all platforms implement ``minimize`` and ``maximize``,
but ``quit`` will always work.
Supports ``left`` and ``right`` which will place the controls on those
respective sides.
Script Commands
~~~~~~~~~~~~~~~

View File

@ -45,6 +45,7 @@ local user_opts = {
visibility = "auto", -- only used at init to set visibility_mode(...)
boxmaxchars = 80, -- title crop threshold for box layout
boxvideo = false, -- apply osc_param.video_margins to video
windowcontrols = "no", -- where to show window controls (or not at all)
}
-- read_options may modify hidetimeout, so save the original default value in
@ -85,6 +86,9 @@ local osc_styles = {
timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}",
wcBar = "{\\1c&H000000}",
}
-- internal states, do not touch
@ -115,7 +119,7 @@ local state = {
using_video_margins = false,
}
local window_control_box_width = 80
--
@ -961,6 +965,104 @@ function add_layout(name)
end
end
-- Window Controls
function window_controls(alignment, topbar)
local wc_geo = {
x = 0,
y = 30,
an = 1,
w = osc_param.playresx,
h = 30,
}
local controlbox_w = window_control_box_width
local titlebox_w = wc_geo.w - controlbox_w
-- Default alignment is "right"
local controlbox_left = wc_geo.w - controlbox_w
local titlebox_left = wc_geo.x + 5
if alignment == "left" then
controlbox_left = wc_geo.x
titlebox_left = wc_geo.x + controlbox_w + 5
elseif alignment == "right" then
-- Already default
else
msg.error("Invalid setting \""..alignment.."\" for windowcontrols")
-- Falls back to "right"
end
add_area("window-controls",
get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
controlbox_w, wc_geo.h))
local lo
-- Background Bar
new_element("wcbar", "box")
lo = add_layout("wcbar")
lo.geometry = wc_geo
lo.layer = 10
lo.style = osc_styles.wcBar
lo.alpha[1] = user_opts.boxalpha
local first_geo =
{x = controlbox_left + 5, y = 15, an = 4, w = 25, h = 25}
local second_geo =
{x = controlbox_left + 30, y = 15, an = 4, w = 25, h = 25}
local third_geo =
{x = controlbox_left + 55, y = 15, an = 4, w = 25, h = 25}
-- Close
ne = new_element("close", "button")
ne.content = "\226\152\146"
ne.eventresponder["mbtn_left_up"] =
function () mp.commandv("quit") end
lo = add_layout("close")
lo.geometry = alignment == "left" and first_geo or third_geo
lo.style = osc_styles.wcButtons
-- Minimize
ne = new_element("minimize", "button")
ne.content = "\226\154\128"
ne.eventresponder["mbtn_left_up"] =
function () mp.commandv("cycle", "window-minimized") end
lo = add_layout("minimize")
lo.geometry = alignment == "left" and second_geo or first_geo
lo.style = osc_styles.wcButtons
-- Maximize
ne = new_element("maximize", "button")
ne.content = "\226\150\163"
ne.eventresponder["mbtn_left_up"] =
function () mp.commandv("cycle", "window-maximized") end
lo = add_layout("maximize")
lo.geometry = alignment == "left" and third_geo or second_geo
-- At least with default Ubuntu fonts, this symbol is differently aligned
lo.geometry.y = 13
lo.style = osc_styles.wcButtons
if topbar then
-- The title is already there as part of the top bar
return
end
-- Window Title
ne = new_element("wctitle", "button")
ne.content = function ()
local title = mp.command_native({"expand-text", user_opts.title})
-- escape ASS, and strip newlines and trailing slashes
title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
return not (title == "") and title or "mpv"
end
lo = add_layout("wctitle")
lo.geometry =
{ x = titlebox_left, y = wc_geo.y - 3, an = 1, w = titlebox_w, h = wc_geo.h }
lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
osc_styles.wcButtons,
titlebox_left, wc_geo.y - wc_geo.h, titlebox_w, wc_geo.y + wc_geo.h)
end
--
-- Layouts
--
@ -1252,7 +1354,7 @@ layouts["slimbox"] = function ()
end
function bar_layout(direction)
function bar_layout(direction, windowcontrols)
local osc_geo = {
x = -2,
y,
@ -1268,6 +1370,20 @@ function bar_layout(direction)
local tsW = 90
local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
-- Special topbar handling when window controls are present
local padwc_l
local padwc_r
if direction < 0 or windowcontrols == "no" then
padwc_l = 0
padwc_r = 0
elseif windowcontrols == "left" then
padwc_l = window_control_box_width
padwc_r = 0
else
padwc_l = 0
padwc_r = window_control_box_width
end
if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
osc_param.playresy = minW / osc_param.display_aspect
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
@ -1348,7 +1464,7 @@ function bar_layout(direction)
-- Playback control buttons
geo = { x = osc_geo.x + padX, y = line2, an = 4,
geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4,
w = buttonW, h = 36 - padY*2}
lo = add_layout("playpause")
lo.geometry = geo
@ -1374,7 +1490,7 @@ function bar_layout(direction)
local sb_l = geo.x + padX
-- Fullscreen button
geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4,
w = buttonW, h = geo.h }
lo = add_layout("tog_fs")
lo.geometry = geo
@ -1442,11 +1558,11 @@ function bar_layout(direction)
end
layouts["bottombar"] = function()
bar_layout(-1)
bar_layout(-1, false)
end
layouts["topbar"] = function()
bar_layout(1)
bar_layout(1, user_opts.windowcontrols)
end
-- Validate string type user options
@ -1704,7 +1820,6 @@ function osc_init()
ne.eventresponder["mbtn_left_up"] =
function () mp.commandv("cycle", "fullscreen") end
--seekbar
ne = new_element("seekbar", "slider")
@ -1863,6 +1978,12 @@ function osc_init()
-- load layout
layouts[user_opts.layout]()
-- load window controls
if user_opts.windowcontrols ~= "no" then
window_controls(user_opts.windowcontrols,
user_opts.layout == "topbar")
end
--do something with the elements
prepare_elements()
@ -2103,6 +2224,18 @@ function render()
end
end
if user_opts.windowcontrols ~= "no" then
for _,cords in ipairs(osc_param.areas["window-controls"]) do
if state.osc_visible then -- activate only when OSC is actually visible
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
end
if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
mouse_over_osc = true
end
end
end
-- autohide
if not (state.showtime == nil) and (user_opts.hidetimeout >= 0)
and (state.showtime + (user_opts.hidetimeout/1000) < now)
@ -2353,6 +2486,11 @@ mp.set_key_bindings({
}, "input", "force")
mp.enable_key_bindings("input")
mp.set_key_bindings({
{"mbtn_left", function(e) process_event("mbtn_left", "up") end,
function(e) process_event("mbtn_left", "down") end},
}, "window-controls", "force")
mp.enable_key_bindings("window-controls")
user_opts.hidetimeout_orig = user_opts.hidetimeout