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