├── README.md ├── noframe.patch └── steamwm.cpp /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Various window management fixes for the Linux Steam client 3 | 4 | **DISCLAIMER: Use at your own risk! This is in no way endorsed by VALVE.** 5 | 6 | These files are mirrored on both [GitHub](https://github.com/dscharrer/steamwm) and [Gist](https://gist.github.com/06d6b6a5370c4f6979f3) - use whichever you prefer. 7 | 8 | Feature request for these changes to be implemented in Steam: [issue #1040](https://github.com/ValveSoftware/steam-for-linux/issues/1040) 9 | 10 | ## steamwm.cpp 11 | 12 | The window management fixes: 13 | 14 | * Force borders on non-menu windows. 15 | * Let the WM position non-menu/tooltip windows. 16 | * Group all steam windows. 17 | This helps WMs with their focus stealing preventions, 18 | and also prevents all Steam windows from being dimmed 19 | (by KWin) if any Steam window has focus (is a KWin setting). 20 | * Tell the WM which Steam windows are dialogs. 21 | This lets the window manager place them more intelligently. 22 | For example, the WM might center dialogs. 23 | * Steam sets error dialogs as unmanaged windows - fix that. 24 | 25 | 26 | Obsolete fixes (now disabled by default): 27 | 28 | * Set `_NET_WM_NAME` to the `WM_NAME` value to get better window titles. 29 | * Set fixed size hints for windows with a fixed layout. 30 | 31 | Fixes can be individually enabled or disabled - for details see the comments in the source file. 32 | 33 | This file compiles to a library that can be `LD_PRELOAD`ed into the Steam process. For your convenience it is also its own build and wrapper script. 34 | 35 | Requires: `g++` with support for x86 targets, `Xlib` + headers 36 | 37 | Use: 38 | 39 | $ chmod +x steamwm.cpp 40 | $ ./steamwm.cpp steam 41 | 42 | 43 | ## noframe.patch 44 | 45 | This is a Steam skin that complements `steamwm.cpp`: It is exactly the same as the default skin, but with the window borders and controls removed. 46 | 47 | To install it use: 48 | 49 | $ chmod +x noframe.patch 50 | $ ./noframe.patch 51 | 52 | and then select the `noframe` skin in the Steam settings. 53 | -------------------------------------------------------------------------------- /noframe.patch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Steam skin without any window decorations. 4 | # Same as the default skin, but with the window borders and controls removed. 5 | # To in stall it just run ./noframe.patch 6 | 7 | self="$(readlink -f "$(which "$0")")" 8 | name="$(basename "$self" .patch)" 9 | skin="skins/$name" 10 | 11 | cd ~/.steam/root/ 12 | 13 | if [ -e "$skin" ] ; then 14 | echo "Skin '$name' already exists!" 15 | echo "Remove \"$(readlink -f "$skin")\" first." 16 | exit 1 17 | fi 18 | 19 | mkdir -p "$skin/resource/styles" 20 | cp resource/styles/steam.styles "$skin/resource/styles/" 21 | cp resource/steamscheme.res "$skin/resource/" 22 | dos2unix -f "$skin/resource/styles/steam.styles" 23 | dos2unix -f "$skin/resource/steamscheme.res" 24 | patch -ld "$skin" -p0 < "$self" 25 | 26 | exit 27 | 28 | diff -ru resource/steamscheme.res skins/noframe/resource/steamscheme.res 29 | --- resource/steamscheme.res 2011-08-02 14:14:04.000000000 +0200 30 | +++ skins/noframe/resource/steamscheme.res 2012-11-17 06:01:22.762356738 +0100 31 | @@ -94,56 +94,27 @@ 32 | 33 | frame_title 34 | { 35 | - xpos 0 36 | - ypos 1 37 | - wide max 38 | - tall 28 39 | - AutoResize 1 40 | + visible 0 // hidden 41 | } 42 | 43 | frame_captiongrip 44 | { 45 | - xpos 4 46 | - ypos 4 47 | - wide r20 48 | - tall 60 49 | - AutoResize 1 50 | + visible 0 // hidden 51 | } 52 | 53 | frame_minimize 54 | { 55 | - xpos r45 56 | - xpos 22 [$OSX] 57 | - ypos 7 58 | - ypos 3 [$OSX] 59 | - wide 20 60 | - tall 20 61 | - PinCorner 1 62 | - PinCorner 0 [$OSX] 63 | + visible 0 // hidden 64 | } 65 | 66 | frame_maximize 67 | { 68 | - xpos r59 69 | - xpos 43 [$OSX] 70 | - ypos 7 71 | - ypos 3 [$OSX] 72 | - wide 20 73 | - tall 20 74 | - visible 0 [!$OSX] 75 | - PinCorner 1 76 | - PinCorner 0 [$OSX] 77 | - } 78 | + visible 0 // hidden 79 | + } 80 | + 81 | frame_close 82 | { 83 | - xpos r29 84 | - xpos 1 [$OSX] 85 | - ypos 7 86 | - ypos 3 [$OSX] 87 | - wide 20 88 | - tall 20 89 | - PinCorner 1 90 | - PinCorner 0 [$OSX] 91 | + visible 0 // hidden 92 | } 93 | 94 | frame_brGrip 95 | @@ -161,7 +132,7 @@ 96 | sheet 97 | { 98 | xpos 9 99 | - ypos 26 100 | + ypos 9 101 | wide r9 102 | tall r48 103 | } 104 | @@ -198,7 +169,7 @@ 105 | subpanel 106 | { 107 | xpos 10 108 | - ypos 28 109 | + ypos 10 110 | wide r10 111 | tall r48 112 | AutoResize 3 113 | diff -ru resource/styles/steam.styles skins/noframe/resource/styles/steam.styles 114 | --- resource/styles/steam.styles 2015-07-29 10:23:54.131952610 +0200 115 | +++ skins/noframe/resource/styles/steam.styles 2015-07-29 10:25:08.610957742 +0200 116 | @@ -806,25 +806,11 @@ 117 | Frame 118 | { 119 | bgcolor="DialogBG" 120 | - render 121 | - { 122 | - 0="gradient_horizontal( x0, y1 - 1, x1 + 1, y1, ClientBG, focus )" 123 | - 1="gradient( x1 - 1, y0, x1, y1, ClientBG, focus )" 124 | - 2="gradient_horizontal( x0, y0, x1, y0 + 1, focus0, ClientBG )" 125 | - 3="gradient( x0, y0, x0 + 1, y1, focus0, ClientBG )" 126 | - } 127 | } 128 | 129 | Frame:FrameFocus 130 | { 131 | bgcolor="DialogBG" 132 | - render 133 | - { 134 | - 0="gradient_horizontal( x0, y1 - 1, x1 + 1, y1, ClientBG, focus2 )" 135 | - 1="gradient( x1 - 1, y0, x1, y1, ClientBG, focus2 )" 136 | - 2="gradient_horizontal( x0, y0, x1, y0 + 1, focus, ClientBG )" 137 | - 3="gradient( x0, y0, x0 + 1, y1, focus, ClientBG )" 138 | - } 139 | } 140 | 141 | FrameBRGripPanel 142 | @@ -839,161 +825,88 @@ 143 | 144 | FrameMinimizeButton 145 | { 146 | - render_bg {} 147 | - image="graphics/win32_win_min" 148 | - image="graphics/osx_win_dis" [$OSX] 149 | } 150 | 151 | FrameMinimizeButton:hover 152 | { 153 | - render_bg {} 154 | - bgcolor="none" 155 | - image="graphics/win32_win_min_hover" 156 | - image="graphics/osx_min_hov" [$OSX] 157 | } 158 | 159 | FrameMinimizeButton:framefocus [$OSX] 160 | { 161 | - render_bg {} 162 | - bgcolor="none" 163 | - image="graphics/osx_min_def" 164 | } 165 | 166 | FrameMinimizeButton:framefocus:hover [$OSX] 167 | { 168 | - render_bg {} 169 | - bgcolor="none" 170 | - image="graphics/osx_min_hov" 171 | } 172 | 173 | FrameMinimizeButton:active [$OSX] 174 | { 175 | - render_bg {} 176 | - bgcolor="none" 177 | - image="graphics/osx_min_down" 178 | } 179 | 180 | // need the maximize button to have different styles for OSX & win32 181 | FrameMaximizeButton 182 | { 183 | - render_bg {} 184 | - image="graphics/win32_win_max" 185 | - image="graphics/osx_win_dis" [$OSX] 186 | } 187 | 188 | FrameMaximizeButton:hover 189 | { 190 | - render_bg {} 191 | - bgcolor="none" 192 | - image="graphics/win32_win_max_hover" 193 | - image="graphics/osx_max_hov" [$OSX] 194 | } 195 | 196 | FrameMaximizeButton:framefocus 197 | { 198 | - render_bg {} 199 | - bgcolor="none" 200 | - image="graphics/win32_win_max" 201 | - image="graphics/osx_max_def" [$OSX] 202 | } 203 | 204 | FrameMaximizeButton:framefocus:hover 205 | { 206 | - render_bg {} 207 | - bgcolor="none" 208 | - image="graphics/win32_win_max_hover" 209 | - image="graphics/osx_max_hov" [$OSX] 210 | } 211 | 212 | FrameMaximizeButton:active 213 | { 214 | - render_bg {} 215 | - bgcolor="none" 216 | - image="graphics/win32_win_max_hover" 217 | - image="graphics/osx_max_down" [$OSX] 218 | } 219 | 220 | // these are for when the maximize button becomes the restore button 221 | FrameRestoreButton 222 | { 223 | - render_bg {} 224 | - image="graphics/win32_win_restore" 225 | - image="graphics/osx_win_dis" [$OSX] 226 | } 227 | 228 | FrameRestoreButton:hover 229 | { 230 | - render_bg {} 231 | - bgcolor="none" 232 | - image="graphics/win32_win_restore_hover" 233 | - image="graphics/osx_max_hov" [$OSX] 234 | } 235 | 236 | FrameRestoreButton:framefocus 237 | { 238 | - render_bg {} 239 | - bgcolor="none" 240 | - image="graphics/win32_win_restore" 241 | - image="graphics/osx_max_def" [$OSX] 242 | } 243 | 244 | FrameRestoreButton:framefocus:hover 245 | { 246 | - render_bg {} 247 | - bgcolor="none" 248 | - image="graphics/win32_win_restore_hover" 249 | - image="graphics/osx_max_hov" [$OSX] 250 | } 251 | 252 | FrameRestoreButton:active 253 | { 254 | - render_bg {} 255 | - bgcolor="none" 256 | - image="graphics/win32_win_restore_hover" 257 | - image="graphics/osx_max_down" [$OSX] 258 | } 259 | 260 | FrameCloseButton 261 | { 262 | - render_bg {} 263 | - image="graphics/win32_win_close" 264 | - image="graphics/osx_win_dis" [$OSX] 265 | } 266 | 267 | FrameCloseButton:hover 268 | { 269 | - render_bg {} 270 | - image="graphics/win32_win_close_hover" 271 | - image="graphics/osx_close_hov" [$OSX] 272 | } 273 | 274 | FrameCloseButton:framefocus [$OSX] 275 | { 276 | - render_bg {} 277 | - bgcolor="none" 278 | - image="graphics/osx_close_def" 279 | } 280 | 281 | FrameCloseButton:framefocus:hover [$OSX] 282 | { 283 | - render_bg {} 284 | - bgcolor="none" 285 | - image="graphics/osx_close_hov" 286 | } 287 | 288 | FrameCloseButton:active [$OSX] 289 | { 290 | - render_bg {} 291 | - bgcolor="none" 292 | - image="graphics/osx_close_down" 293 | } 294 | 295 | FrameCloseButton:disabled 296 | { 297 | - render_bg {} 298 | - bgcolor="none" 299 | - image="graphics/win32_win_close_disabled" 300 | - image="graphics/osx_win_dis" [$OSX] 301 | } 302 | 303 | FrameTitle 304 | -------------------------------------------------------------------------------- /steamwm.cpp: -------------------------------------------------------------------------------- 1 | #if 0 2 | 3 | #// Various window management fixes for the Linux Steam client. 4 | #// 5 | #// You can set the following environment variables to 0 to disable individual features: 6 | #// 7 | #// STEAMWM_FORCE_BORDERS Force borders on non-menu windows. 8 | #// 9 | #// STEAMWM_PREVENT_MOVE Let the WM position non-menu/tooltip windows. 10 | #// 11 | #// STEAMWM_GROUP_WINDOWS Group all steam windows. 12 | #// This helps WMs with their focus stealing preventions, 13 | #// and also prevents all Steam windows from being dimmed 14 | #// (by KWin) if any Steam window has focus (is a KWin setting). 15 | #// NOTE: Window is still dimmed when showing menus/tooltips :( 16 | #// 17 | #// STEAMWM_SET_WINDOW_TYPE Tell the WM which Steam windows are dialogs. 18 | #// This lets the window manager place them more intelligently. 19 | #// For example, the WM might center dialogs. 20 | #// NOTE: We simply treat every window with a title other than 21 | #// "Steam" or "Friends" as a dialog window. 22 | #// The startup window is also marked as a dialog. 23 | #// 24 | #// STEAMWM_MANAGE_ERRORS Steam sets error dialogs as unmanaged windows - fix that. 25 | #// 26 | #// 27 | #// Obsolete fixes (now disabled by default): 28 | #// 29 | #// STEAMWM_FIX_NET_WM_NAME Set _NET_WM_NAME to the WM_NAME value to get better window 30 | #// titles (and add " - Steam" suffix if needed). 31 | #// Steam now doesn't set WM_ICON_NAME, _NET_WM_NAME or 32 | #// _NET_WM_ICON_NAME anymore - while it would be better to set 33 | #// them, their absence is unlikely to cause problems. 34 | #// 35 | #// STEAMWM_SET_FIXED_SIZE Set fixed size hints for windows with a fixed layout. 36 | #// 37 | #// 38 | #// Requires: g++ with support for x86 targets, Xlib + headers 39 | #// 40 | #// 41 | #// Use: 42 | #// $ chmod +x steamwm.cpp 43 | #// and then 44 | #// 45 | #// 46 | #// $ DEBUGGER="$(pwd)/steamwm.cpp" steam 47 | #// 48 | #// *or* 49 | #// 50 | #// $ ./steamwm.cpp steam // Prints ld.so errors on 64-bit systems 51 | #// 52 | #// *or* 53 | #// 54 | #// $ ./steamwm.cpp // Compile 55 | #// $ LD_PRELOAD="$(pwd)/steamwm.so" steam // Prints ld.so errors on 64-bit systems 56 | #// 57 | #// 58 | #// DISCLAIMER: Use at your own risk! This is in no way endorsed by VALVE. 59 | #// 60 | #// This program is free software. It comes without any warranty, to 61 | #// the extent permitted by applicable law. You can redistribute it 62 | #// and/or modify it under the terms of the Do What The Fuck You Want 63 | #// To Public License, Version 2, as published by Sam Hocevar. See 64 | #// http://sam.zoy.org/wtfpl/COPYING for more details. 65 | #// 66 | 67 | 68 | [ -z $STEAMWM_FORCE_BORDERS ] && export STEAMWM_FORCE_BORDERS=1 69 | [ -z $STEAMWM_PREVENT_MOVE ] && export STEAMWM_PREVENT_MOVE=1 70 | [ -z $STEAMWM_FIX_NET_WM_NAME ] && export STEAMWM_FIX_NET_WM_NAME=0 71 | [ -z $STEAMWM_GROUP_WINDOWS ] && export STEAMWM_GROUP_WINDOWS=1 72 | [ -z $STEAMWM_SET_WINDOW_TYPE ] && export STEAMWM_SET_WINDOW_TYPE=1 73 | [ -z $STEAMWM_SET_FIXED_SIZE ] && export STEAMWM_SET_FIXED_SIZE=0 74 | [ -z $STEAMWM_MANAGE_ERRORS ] && export STEAMWM_MANAGE_ERRORS=1 75 | 76 | 77 | self="$(readlink -f "$(which "$0")")" 78 | name="$(basename "$self" .cpp)" 79 | out="$(dirname "$self")/$name" 80 | soname="$name.so" 81 | 82 | 83 | #// On amd64 platforms, compile a dummy 64-bit steamwm.so, 84 | #// so that native 64-bit tools invoked by Steam and its 85 | #// launch script won't spam (harmless) ld.so errors. 86 | if [ -f '/lib64/ld-linux-x86-64.so.2' ] ; then 87 | dout="$out/64" 88 | mkdir -p "$dout" 89 | if ! [ -f "$dout/$soname" ] ; then 90 | echo -e "\n" | gcc -shared -fPIC -m64 -x c - -o "$dout/$soname" &> /dev/null 91 | strip "$dout/$soname" &> /dev/null 92 | #// ignore all errors - this may at worst cause warnings later 93 | fi 94 | export LD_LIBRARY_PATH="$dout:$LD_LIBRARY_PATH" 95 | fi 96 | 97 | 98 | #// Compile the LD_PRELOAD library 99 | mkdir -p "$out" 100 | if [ "$self" -nt "$out/$soname" ] ; then 101 | echo "Compiling $soname..." 102 | g++ -shared -fPIC -m32 -x c++ "$self" -o "$out/$soname" \ 103 | -lX11 -static-libgcc -static-libstdc++ \ 104 | -O3 -Wall -Wextra -DSONAME="$soname" \ 105 | || exit 1 106 | fi 107 | 108 | 109 | #// Run the executable 110 | export LD_PRELOAD="$soname:$LD_PRELOAD" 111 | export LD_LIBRARY_PATH="$out:$LD_LIBRARY_PATH" 112 | [ -z "$1" ] || exec "$@" 113 | 114 | 115 | exit 116 | 117 | #endif // 0 118 | 119 | 120 | #ifndef _GNU_SOURCE 121 | #define _GNU_SOURCE 122 | #endif 123 | 124 | #include 125 | #include 126 | #include 127 | #include 128 | #include 129 | #include 130 | #include 131 | 132 | #define STR_(x) # x 133 | #define STR(x) STR_(x) 134 | 135 | 136 | // List of window titles for windows that should not be marked as dialogs 137 | static const char * main_windows[] = { 138 | "Steam", 139 | "Friends", 140 | }; 141 | // List of window titles for windows that should be dialogs even if unmanaged 142 | static const char * dialog_windows[] = { 143 | "Untitled", 144 | "Steam - Go Online", 145 | }; 146 | static const char * fixed_size_windows[] = { 147 | "Settings", 148 | "About Steam", 149 | "Backup and Restore Programs", 150 | "Steam - Error", 151 | "Steam - Self Updater", 152 | }; 153 | static const char * fixed_size_suffixes[] = { 154 | " - Properties", 155 | " - Category", 156 | }; 157 | 158 | 159 | static bool force_borders = false; 160 | static bool prevent_move = false; 161 | static bool fix_net_wm_name = false; 162 | static bool group_windows = false; 163 | static bool set_window_type = false; 164 | static bool set_fixed_size = false; 165 | static bool manage_errors = false; 166 | 167 | extern "C" { 168 | extern char * program_invocation_short_name; // provided by glibc 169 | } 170 | 171 | static bool get_setting(const char * name) { 172 | char * setting = getenv(name); 173 | return (setting && setting[0] != '\0' && setting[0] != '0'); 174 | } 175 | 176 | void steamwm_init(void) __attribute__((constructor)); 177 | void steamwm_init(void) { 178 | 179 | // Only attach to steam! 180 | if(strcmp(program_invocation_short_name, "steam") != 0) { 181 | return; 182 | } 183 | 184 | // Prevent steamwm.so from being attached to processes started by steam 185 | const char * envname = "LD_PRELOAD"; 186 | const char * oldenv = getenv(envname); 187 | if(oldenv) { 188 | char * env = strdup(oldenv); 189 | char * pos = strstr(env, STR(SONAME)); 190 | if(pos) { 191 | size_t len1 = strlen(STR(SONAME)); 192 | size_t len2 = strlen(pos + len1); 193 | memmove(pos, pos + len1, len2); 194 | *(pos + len2) = '\0'; 195 | setenv(envname, env, 1); 196 | } 197 | free(env); 198 | } 199 | 200 | force_borders = get_setting("STEAMWM_FORCE_BORDERS"); 201 | prevent_move = get_setting("STEAMWM_PREVENT_MOVE"); 202 | fix_net_wm_name = get_setting("STEAMWM_FIX_NET_WM_NAME"); 203 | group_windows = get_setting("STEAMWM_GROUP_WINDOWS"); 204 | set_window_type = get_setting("STEAMWM_SET_WINDOW_TYPE"); 205 | set_fixed_size = get_setting("STEAMWM_SET_FIXED_SIZE"); 206 | manage_errors = get_setting("STEAMWM_MANAGE_ERRORS"); 207 | 208 | 209 | fprintf(stderr, "\n[steamwm] attached to steam:\n force_borders %d\n" 210 | " prevent_move %d\n fix_net_wm_name %d\n" 211 | " group_windows %d\n set_window_type %d\n" 212 | " set_fixed_size %d\n manage_errors %d\n\n", 213 | int(force_borders), int(prevent_move), int(fix_net_wm_name), 214 | int(group_windows), int(set_window_type), int(set_fixed_size), 215 | int(manage_errors)); 216 | 217 | } 218 | 219 | 220 | /* helper functions */ 221 | 222 | #define BASE_NAME(SymbolName) base_ ## SymbolName 223 | #define TYPE_NAME(SymbolName) SymbolName ## _t 224 | #define INTERCEPT(ReturnType, SymbolName, ...) \ 225 | typedef ReturnType (*TYPE_NAME(SymbolName))(__VA_ARGS__); \ 226 | static void * const BASE_NAME(SymbolName) = dlsym(RTLD_NEXT, STR(SymbolName)); \ 227 | ReturnType SymbolName(__VA_ARGS__) 228 | #define BASE(SymbolName) ((TYPE_NAME(SymbolName))BASE_NAME(SymbolName)) 229 | 230 | static bool is_unmanaged_window(Display * dpy, Window w); 231 | static void set_is_unmanaged_window(Display * dpy, Window w, bool is_unmanaged); 232 | static bool is_fixed_size_window_name(const char * name); 233 | static bool is_main_window_name(const char * name); 234 | static bool is_dialog_window_name(const char * name); 235 | static void set_window_desired_size(Display * dpy, Window w, int width, int height, 236 | bool set_fixed); 237 | static void set_window_property(Display * dpy, Window w, Atom property, Atom type, 238 | long value); 239 | static void set_window_group_hint(Display * dpy, Window w, XID window_group); 240 | static void set_window_is_dialog(Display * dpy, Window w, bool is_dialog); 241 | static void set_window_modal(Display * dpy, Window w); 242 | 243 | 244 | /* fix window titles and types, and add window borders & title bars */ 245 | 246 | static Window first_window = None, second_window = None; 247 | 248 | static void name_changed(Display * dpy, Window w, const unsigned char * data, int n); 249 | 250 | INTERCEPT(void, XSetWMName, 251 | Display * dpy, 252 | Window w, 253 | XTextProperty * prop 254 | ) { 255 | 256 | if(prop->format == 8) { 257 | // The libX11 pulled in with STEAM_RUNTIME=1 has XSetWMName (or XSetTextProperty?) 258 | // liked/optimized to use internal functions and not the global XChangeProperty, 259 | // so our override below won't work -> also intercept XSetWMName(). 260 | name_changed(dpy, w, prop->value, prop->nitems); 261 | } 262 | 263 | return BASE(XSetWMName)(dpy, w, prop); 264 | } 265 | 266 | INTERCEPT(int, XChangeProperty, 267 | Display * dpy, 268 | Window w, 269 | Atom property, 270 | Atom type, 271 | int format, 272 | int mode, 273 | const unsigned char * data, 274 | int n 275 | ) { 276 | 277 | if(property == XA_WM_NAME && format == 8) { 278 | name_changed(dpy, w, data, n); 279 | } 280 | 281 | if(manage_errors && property == XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False)) { 282 | if(!is_unmanaged_window(dpy, w) && type == XA_ATOM && format == 32 && n > 0) { 283 | Atom menu = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_MENU", False); 284 | Atom type = reinterpret_cast(data)[0]; 285 | if(type == menu) { 286 | // Ignore the window type Steam sets on error dialogs. 287 | return 1; 288 | } 289 | } 290 | } 291 | 292 | if(force_borders && property == XInternAtom(dpy, "_MOTIF_WM_HINTS", False)) { 293 | // Don't suppress window borders! 294 | return 1; 295 | } 296 | 297 | return BASE(XChangeProperty)(dpy, w, property, type, format, mode, data, n); 298 | } 299 | 300 | static void name_changed(Display * dpy, Window w, const unsigned char * data, int n) { 301 | 302 | char * value = (char *)data; 303 | 304 | if(fix_net_wm_name) { 305 | // Use the XA_WM_NAME as both XA_WM_NAME and _NET_WM_NAME. 306 | // Steam sets _NET_WM_NAME to just "Steam" for all windows. 307 | const unsigned char * name = data; 308 | unsigned char * buffer = NULL; 309 | int nn = n; 310 | if(n > 0 && strstr((char *)data, "Steam") == 0) { 311 | // Make sure "Steam" is in all window titles. 312 | char suffix[] = " - Steam"; 313 | nn = n + sizeof(suffix) - 1; 314 | name = buffer = (unsigned char *)malloc(nn + 1); 315 | memcpy(buffer, data, n); 316 | memcpy(buffer + n, suffix, sizeof(suffix)); 317 | } 318 | Atom net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); 319 | Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); 320 | BASE(XChangeProperty)(dpy, w, net_wm_name, utf8_string, 8, PropModeReplace, name, nn); 321 | if(buffer) { 322 | free(buffer); 323 | } 324 | } 325 | 326 | if(manage_errors && is_unmanaged_window(dpy, w) && is_dialog_window_name(value)) { 327 | // Error dialogs should be managed by the window manager. 328 | set_is_unmanaged_window(dpy, w, false); 329 | set_window_modal(dpy, w); 330 | set_window_desired_size(dpy, w, -1, -1, true); 331 | } 332 | 333 | if(set_window_type && !is_unmanaged_window(dpy, w) 334 | && w != first_window && w != second_window) { 335 | // Set the window type for non-menu windows. 336 | // This should probably be done *before* mapping the windows, 337 | // but then we don't have a title yet. 338 | // Try to guess the window type from the title. 339 | set_window_is_dialog(dpy, w, !is_main_window_name(value)); 340 | } 341 | 342 | if(set_fixed_size && is_fixed_size_window_name(value)) { 343 | // Set fixed size hints for windows with static layouts. 344 | set_window_desired_size(dpy, w, -1, -1, true); 345 | } 346 | 347 | } 348 | 349 | 350 | /* ignore window move requests for non-menu windows */ 351 | 352 | INTERCEPT(int, XResizeWindow, 353 | Display * dpy, 354 | Window w, 355 | unsigned int width, 356 | unsigned int height 357 | ) { 358 | 359 | if(set_fixed_size || manage_errors) { 360 | // Set fixed size hints for windows with static layouts. 361 | set_window_desired_size(dpy, w, width, height, false); 362 | } 363 | 364 | return BASE(XResizeWindow)(dpy, w, width, height); 365 | } 366 | 367 | INTERCEPT(int, XMoveResizeWindow, 368 | Display * dpy, 369 | Window w, 370 | int x, 371 | int y, 372 | unsigned int width, 373 | unsigned int height 374 | ) { 375 | 376 | if(set_fixed_size || manage_errors) { 377 | // Set fixed size hints for windows with static layouts. 378 | set_window_desired_size(dpy, w, width, height, false); 379 | } 380 | 381 | if(prevent_move && !is_unmanaged_window(dpy, w)) { 382 | // Ignore the position request for non-menu windows. 383 | return BASE(XResizeWindow)(dpy, w, width, height); 384 | } 385 | 386 | return BASE(XMoveResizeWindow)(dpy, w, x, y, width, height); 387 | } 388 | 389 | INTERCEPT(int, XMoveWindow, 390 | Display * dpy, 391 | Window w, 392 | int x, 393 | int y 394 | ) { 395 | 396 | if(prevent_move && !is_unmanaged_window(dpy, w)) { 397 | // Ignore the position request for non-menu windows. 398 | return 1; 399 | } 400 | 401 | return BASE(XMoveWindow)(dpy, w, x, y); 402 | } 403 | 404 | 405 | /* group windows and force the first and second window to be dialogs */ 406 | 407 | INTERCEPT(int, XMapWindow, 408 | Display * dpy, 409 | Window w 410 | ) { 411 | 412 | if(first_window == None) { 413 | first_window = w; 414 | } 415 | 416 | if(group_windows) { 417 | // Group all steam windows. 418 | Atom leader = XInternAtom(dpy, "WM_CLIENT_LEADER", False); 419 | set_window_property(dpy, w, leader, XA_WINDOW, first_window); 420 | set_window_group_hint(dpy, w, first_window); 421 | } 422 | 423 | if(set_window_type && (w == first_window || second_window == None)) { 424 | // Force the first and second windows to be marked as dialogs. 425 | set_window_is_dialog(dpy, w, true); 426 | if(w != first_window) { 427 | // Give the second window a proper size *now* so that the WM can center it. 428 | second_window = w; 429 | XResizeWindow(dpy, w, 384, 107); 430 | } 431 | } 432 | 433 | return BASE(XMapWindow)(dpy, w); 434 | } 435 | 436 | INTERCEPT(void, XSetWMNormalHints, 437 | Display * dpy, 438 | Window w, 439 | XSizeHints * hints 440 | ) { 441 | XSizeHints old_hints; 442 | long supplied; 443 | // Prevent steam from overriding the max size hints 444 | if((set_fixed_size || manage_errors) 445 | && XGetWMNormalHints(dpy, w, &old_hints, &supplied)) { 446 | if(!(hints->flags & PSize) && (old_hints.flags & PSize)) { 447 | hints->flags |= PSize; 448 | hints->width = old_hints.width; 449 | hints->height = old_hints.height; 450 | } 451 | if((old_hints.flags & PMinSize) && (old_hints.flags & PMaxSize) 452 | && old_hints.min_width == old_hints.max_width 453 | && old_hints.min_height == old_hints.max_height) { 454 | hints->flags |= PMinSize | PMaxSize; 455 | hints->min_width = hints->max_width = old_hints.max_width; 456 | hints->min_height = hints->max_height = old_hints.max_height; 457 | } 458 | } 459 | BASE(XSetWMNormalHints)(dpy, w, hints); 460 | } 461 | 462 | 463 | /* helper function implementations */ 464 | 465 | static bool is_unmanaged_window(Display * dpy, Window w) { 466 | XWindowAttributes xwa; 467 | if(!XGetWindowAttributes(dpy, w, &xwa)) { 468 | return false; 469 | } 470 | return xwa.override_redirect; 471 | } 472 | 473 | static void set_is_unmanaged_window(Display * dpy, Window w, bool is_unmanaged) { 474 | XSetWindowAttributes xswa; 475 | xswa.override_redirect = is_unmanaged; 476 | XChangeWindowAttributes(dpy, w, CWOverrideRedirect, &xswa); 477 | } 478 | 479 | template 480 | static bool is_in_array(const char * (&array)[N], const char * needle) { 481 | 482 | for(unsigned i = 0; i < N; i++) { 483 | if(strcmp(needle, array[i]) == 0) { 484 | return true; 485 | } 486 | } 487 | 488 | return false; 489 | } 490 | 491 | static bool is_main_window_name(const char * name) { 492 | return is_in_array(main_windows, name); 493 | } 494 | 495 | static bool is_dialog_window_name(const char * name) { 496 | return is_in_array(dialog_windows, name); 497 | } 498 | 499 | static bool is_fixed_size_window_name(const char * name) { 500 | 501 | if(is_in_array(fixed_size_windows, name)) { 502 | return true; 503 | } 504 | 505 | int len = strlen(name); 506 | for(unsigned i = 0; i < sizeof(fixed_size_suffixes)/sizeof(*fixed_size_suffixes); i++) { 507 | int plen = strlen(fixed_size_suffixes[i]); 508 | if(len > plen && strcmp(name + len - plen, fixed_size_suffixes[i]) == 0) { 509 | return true; 510 | } 511 | } 512 | 513 | return false; 514 | } 515 | 516 | static void set_window_desired_size(Display * dpy, Window w, int width, int height, 517 | bool set_fixed) { 518 | XSizeHints xsh; 519 | long supplied; 520 | if(!XGetWMNormalHints(dpy, w, &xsh, &supplied)) { 521 | xsh.flags = 0; 522 | } 523 | if(width > 0 && height > 0) { 524 | // Store the desired size. 525 | // PBaseSize (base_width and base_height) sounds more appropriate 526 | // (and is not obsolete), but some window managers won't let the window 527 | // get smaller than the base size. 528 | xsh.flags |= PSize; 529 | xsh.width = width, xsh.height = height; 530 | } else if(xsh.flags & PSize) { 531 | // Retrieve the desired size. 532 | width = xsh.width, height = xsh.height; 533 | } else { 534 | Window root; 535 | int x, y; 536 | unsigned int cwidth, cheight, border_width, depth; 537 | if(!XGetGeometry(dpy, w, &root, &x, &y, &cwidth, &cheight, &border_width, &depth)) { 538 | return; 539 | } 540 | width = cwidth, height = cheight; 541 | } 542 | if(set_fixed || ((xsh.flags & PMinSize) && (xsh.flags & PMaxSize) 543 | && xsh.min_width == xsh.max_width && xsh.min_height == xsh.max_height)) { 544 | xsh.flags |= PMinSize | PMaxSize; 545 | xsh.min_width = xsh.max_width = width; 546 | xsh.min_height = xsh.max_height = height; 547 | } 548 | BASE(XSetWMNormalHints)(dpy, w, &xsh); 549 | } 550 | 551 | static void set_window_property(Display * dpy, Window w, Atom property, Atom type, 552 | long value) { 553 | unsigned char data[sizeof(long)]; 554 | memcpy(&data, &value, sizeof(long)); 555 | BASE(XChangeProperty)(dpy, w, property, type, 32, PropModeReplace, data, 1); 556 | } 557 | 558 | static void set_window_group_hint(Display * dpy, Window w, XID window_group) { 559 | XWMHints base_hints; 560 | XWMHints * h = XGetWMHints(dpy, w); 561 | if(!h) { 562 | h = &base_hints; 563 | h->flags = 0; 564 | } 565 | h->flags |= WindowGroupHint; 566 | h->window_group = window_group; 567 | XSetWMHints(dpy, w, h); 568 | if(h != &base_hints) { 569 | XFree(h); 570 | } 571 | } 572 | 573 | static void set_window_is_dialog(Display * dpy, Window w, bool is_dialog) { 574 | Atom window_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); 575 | if(is_dialog) { 576 | Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); 577 | set_window_property(dpy, w, window_type, XA_ATOM, dialog); 578 | } else { 579 | Atom normal = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_NORMAL", False); 580 | set_window_property(dpy, w, window_type, XA_ATOM, normal); 581 | } 582 | } 583 | 584 | static void set_window_modal(Display * dpy, Window w) { 585 | XWindowAttributes xwa; 586 | if(!XGetWindowAttributes(dpy, w, &xwa)) { 587 | return; 588 | } 589 | Atom state = XInternAtom(dpy, "_NET_WM_STATE", False); 590 | Atom state_modal = XInternAtom(dpy, "_NET_WM_STATE_MODAL", False); 591 | if(xwa.map_state == IsUnmapped) { 592 | set_window_property(dpy, w, state, XA_ATOM, state_modal); 593 | } else { 594 | XEvent event; 595 | event.type = ClientMessage; 596 | event.xclient.message_type = state; 597 | event.xclient.window = w; 598 | event.xclient.format = 32; 599 | event.xclient.data.l[0] = 1; // add 600 | event.xclient.data.l[1] = state_modal; 601 | event.xclient.data.l[2] = 0; 602 | event.xclient.data.l[3] = 1; 603 | event.xclient.data.l[4] = 0; 604 | XSendEvent(dpy, DefaultRootWindow(dpy), False, 605 | (SubstructureNotifyMask | SubstructureRedirectMask), &event); 606 | } 607 | } 608 | --------------------------------------------------------------------------------