1# Copyright (c) 2010 matt 2# Copyright (c) 2010-2011 Paul Colomiets 3# Copyright (c) 2011 Mounier Florian 4# Copyright (c) 2012 Craig Barnes 5# Copyright (c) 2012, 2014-2015 Tycho Andersen 6# Copyright (c) 2013 Tao Sauvage 7# Copyright (c) 2013 Julien Iguchi-Cartigny 8# Copyright (c) 2014 ramnes 9# Copyright (c) 2014 Sean Vig 10# Copyright (c) 2014 dequis 11# Copyright (c) 2018 Nazar Mokrynskyi 12# 13# Permission is hereby granted, free of charge, to any person obtaining a copy 14# of this software and associated documentation files (the "Software"), to deal 15# in the Software without restriction, including without limitation the rights 16# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17# copies of the Software, and to permit persons to whom the Software is 18# furnished to do so, subject to the following conditions: 19# 20# The above copyright notice and this permission notice shall be included in 21# all copies or substantial portions of the Software. 22# 23# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29# SOFTWARE. 30 31from typing import List, Optional 32 33from libqtile.config import Match 34from libqtile.layout.base import Layout 35from libqtile.backend.base import Window 36 37 38class Floating(Layout): 39 """ 40 Floating layout, which does nothing with windows but handles focus order 41 """ 42 default_float_rules = [ 43 Match(wm_type='utility'), 44 Match(wm_type='notification'), 45 Match(wm_type='toolbar'), 46 Match(wm_type='splash'), 47 Match(wm_type='dialog'), 48 Match(wm_class='file_progress'), 49 Match(wm_class='confirm'), 50 Match(wm_class='dialog'), 51 Match(wm_class='download'), 52 Match(wm_class='error'), 53 Match(wm_class='notification'), 54 Match(wm_class='splash'), 55 Match(wm_class='toolbar'), 56 Match(func=lambda c: c.has_fixed_size()), 57 Match(func=lambda c: c.has_fixed_ratio()) 58 ] 59 60 defaults = [ 61 ("border_focus", "#0000ff", "Border colour(s) for the focused window."), 62 ("border_normal", "#000000", "Border colour(s) for un-focused windows."), 63 ("border_width", 1, "Border width."), 64 ("max_border_width", 0, "Border width for maximize."), 65 ("fullscreen_border_width", 0, "Border width for fullscreen."), 66 ] 67 68 def __init__(self, float_rules: Optional[List[Match]] = None, no_reposition_rules=None, **config): 69 """ 70 If you have certain apps that you always want to float you can provide 71 ``float_rules`` to do so. ``float_rules`` are a list of 72 Match objects:: 73 74 from libqtile.config import Match 75 Match(title=WM_NAME, wm_class=WM_CLASS, role=WM_WINDOW_ROLE) 76 77 When a new window is opened its ``match`` method is called with each of 78 these rules. If one matches, the window will float. The following 79 will float GIMP and Skype:: 80 81 from libqtile.config import Match 82 float_rules=[Match(wm_class="skype"), Match(wm_class="gimp")] 83 84 The following ``Match`` will float all windows that are transient windows for a 85 parent window: 86 87 Match(func=lambda c: bool(c.is_transient_for())) 88 89 Specify these in the ``floating_layout`` in your config. 90 91 Floating layout will try to center most of floating windows by default, 92 but if you don't want this to happen for certain windows that are 93 centered by mistake, you can use ``no_reposition_rules`` option to 94 specify them and layout will rely on windows to position themselves in 95 correct location on the screen. 96 """ 97 Layout.__init__(self, **config) 98 self.clients: List[Window] = [] 99 self.focused = None 100 self.group = None 101 102 if float_rules is None: 103 float_rules = self.default_float_rules 104 105 self.float_rules = float_rules 106 self.no_reposition_rules = no_reposition_rules or [] 107 self.add_defaults(Floating.defaults) 108 109 def match(self, win): 110 """Used to default float some windows""" 111 return any(win.match(rule) for rule in self.float_rules) 112 113 def find_clients(self, group): 114 """Find all clients belonging to a given group""" 115 return [c for c in self.clients if c.group is group] 116 117 def to_screen(self, group, new_screen): 118 """Adjust offsets of clients within current screen""" 119 for win in self.find_clients(group): 120 if win.maximized: 121 win.maximized = True 122 elif win.fullscreen: 123 win.fullscreen = True 124 else: 125 # If the window hasn't been floated before, it will be configured in 126 # .configure() 127 if win.float_x is not None and win.float_y is not None: 128 # By default, place window at same offset from top corner 129 new_x = new_screen.x + win.float_x 130 new_y = new_screen.y + win.float_y 131 # make sure window isn't off screen left/right... 132 new_x = min(new_x, new_screen.x + new_screen.width - win.width) 133 new_x = max(new_x, new_screen.x) 134 # and up/down 135 new_y = min(new_y, new_screen.y + new_screen.height - win.height) 136 new_y = max(new_y, new_screen.y) 137 138 win.x = new_x 139 win.y = new_y 140 win.group = new_screen.group 141 142 def focus_first(self, group=None): 143 if group is None: 144 clients = self.clients 145 else: 146 clients = self.find_clients(group) 147 148 if clients: 149 return clients[0] 150 151 def focus_next(self, win): 152 if win not in self.clients or win.group is None: 153 return 154 155 clients = self.find_clients(win.group) 156 idx = clients.index(win) 157 if len(clients) > idx + 1: 158 return clients[idx + 1] 159 160 def focus_last(self, group=None): 161 if group is None: 162 clients = self.clients 163 else: 164 clients = self.find_clients(group) 165 166 if clients: 167 return clients[-1] 168 169 def focus_previous(self, win): 170 if win not in self.clients or win.group is None: 171 return 172 173 clients = self.find_clients(win.group) 174 idx = clients.index(win) 175 if idx > 0: 176 return clients[idx - 1] 177 178 def focus(self, client): 179 self.focused = client 180 181 def blur(self): 182 self.focused = None 183 184 def on_screen(self, client, screen_rect): 185 if client.x < screen_rect.x: # client's left edge 186 return False 187 if screen_rect.x + screen_rect.width < client.x + client.width: # right 188 return False 189 if client.y < screen_rect.y: # top 190 return False 191 if screen_rect.y + screen_rect.width < client.y + client.height: # bottom 192 return False 193 return True 194 195 def compute_client_position(self, client, screen_rect): 196 """ recompute client.x and client.y, returning whether or not to place 197 this client above other windows or not """ 198 above = True 199 200 if client.has_user_set_position() and not self.on_screen(client, screen_rect): 201 # move to screen 202 client.x = screen_rect.x + client.x 203 client.y = screen_rect.y + client.y 204 if not client.has_user_set_position() or not self.on_screen(client, screen_rect): 205 # client has not been properly placed before or it is off screen 206 transient_for = client.is_transient_for() 207 if transient_for is not None: 208 # if transient for a window, place in the center of the window 209 center_x = transient_for.x + transient_for.width / 2 210 center_y = transient_for.y + transient_for.height / 2 211 above = False 212 else: 213 center_x = screen_rect.x + screen_rect.width / 2 214 center_y = screen_rect.y + screen_rect.height / 2 215 216 x = center_x - client.width / 2 217 y = center_y - client.height / 2 218 219 # don't go off the right... 220 x = min(x, screen_rect.x + screen_rect.width - client.width) 221 # or left... 222 x = max(x, screen_rect.x) 223 # or bottom... 224 y = min(y, screen_rect.y + screen_rect.height - client.height) 225 # or top 226 y = max(y, screen_rect.y) 227 228 client.x = int(round(x)) 229 client.y = int(round(y)) 230 return above 231 232 def configure(self, client, screen_rect): 233 if client.has_focus: 234 bc = self.border_focus 235 else: 236 bc = self.border_normal 237 238 if client.maximized: 239 bw = self.max_border_width 240 elif client.fullscreen: 241 bw = self.fullscreen_border_width 242 else: 243 bw = self.border_width 244 245 # 'sun-awt-X11-XWindowPeer' is a dropdown used in Java application, 246 # don't reposition it anywhere, let Java app to control it 247 cls = client.get_wm_class() or '' 248 is_java_dropdown = 'sun-awt-X11-XWindowPeer' in cls 249 if is_java_dropdown: 250 client.paint_borders(bc, bw) 251 client.cmd_bring_to_front() 252 253 # alternatively, users may have asked us explicitly to leave the client alone 254 elif any(m.compare(client) for m in self.no_reposition_rules): 255 client.paint_borders(bc, bw) 256 client.cmd_bring_to_front() 257 258 else: 259 above = False 260 261 # We definitely have a screen here, so let's be sure we'll float on screen 262 if client.float_x is None or client.float_y is None: 263 # this window hasn't been placed before, let's put it in a sensible spot 264 above = self.compute_client_position(client, screen_rect) 265 266 client.place( 267 client.x, 268 client.y, 269 client.width, 270 client.height, 271 bw, 272 bc, 273 above, 274 respect_hints=True, 275 ) 276 client.unhide() 277 278 def add(self, client): 279 self.clients.append(client) 280 self.focused = client 281 282 def remove(self, client): 283 if client not in self.clients: 284 return 285 286 next_focus = self.focus_next(client) 287 if client is self.focused: 288 self.blur() 289 self.clients.remove(client) 290 return next_focus 291 292 def info(self): 293 d = Layout.info(self) 294 d["clients"] = [c.name for c in self.clients] 295 return d 296 297 def cmd_next(self): 298 # This can't ever be called, but implement the abstract method 299 pass 300 301 def cmd_previous(self): 302 # This can't ever be called, but implement the abstract method 303 pass 304