| 1 | # Copyright (c) 2008-2009, samurai-x.org |
|---|
| 2 | # All rights reserved. |
|---|
| 3 | # |
|---|
| 4 | # Redistribution and use in source and binary forms, with or without |
|---|
| 5 | # modification, are permitted provided that the following conditions are met: |
|---|
| 6 | # * Redistributions of source code must retain the above copyright |
|---|
| 7 | # notice, this list of conditions and the following disclaimer. |
|---|
| 8 | # * Redistributions in binary form must reproduce the above copyright |
|---|
| 9 | # notice, this list of conditions and the following disclaimer in the |
|---|
| 10 | # documentation and/or other materials provided with the distribution. |
|---|
| 11 | # * Neither the name of the samurai-x.org nor the |
|---|
| 12 | # names of its contributors may be used to endorse or promote products |
|---|
| 13 | # derived from this software without specific prior written permission. |
|---|
| 14 | # |
|---|
| 15 | # THIS SOFTWARE IS PROVIDED BY SAMURAI-X.ORG ``AS IS'' AND ANY |
|---|
| 16 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|---|
| 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|---|
| 18 | # DISCLAIMED. IN NO EVENT SHALL SAMURAI-X.ORG BE LIABLE FOR ANY |
|---|
| 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|---|
| 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|---|
| 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|---|
| 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|---|
| 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|---|
| 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|---|
| 25 | |
|---|
| 26 | """ |
|---|
| 27 | sx-layoutmgr is a plugin that adds layout algorithms to desktops. |
|---|
| 28 | |
|---|
| 29 | Dependencies |
|---|
| 30 | ------------ |
|---|
| 31 | |
|---|
| 32 | sx-layoutmgr depends on :ref:`sx-actions` and :ref:`sx-desktops`. |
|---|
| 33 | |
|---|
| 34 | Configuration |
|---|
| 35 | ------------- |
|---|
| 36 | |
|---|
| 37 | sx-layoutmgr reads the desired layout for each desktop from the |
|---|
| 38 | desktop information key "layout". |
|---|
| 39 | |
|---|
| 40 | Builtin layouts: |
|---|
| 41 | |
|---|
| 42 | `floating` |
|---|
| 43 | The user is free to arrange the windows. |
|---|
| 44 | `max` |
|---|
| 45 | Maximize the currently focused window |
|---|
| 46 | `vert` |
|---|
| 47 | Stack the windows vertically |
|---|
| 48 | `horiz` |
|---|
| 49 | Stack the windows horizontally |
|---|
| 50 | |
|---|
| 51 | Example:: |
|---|
| 52 | |
|---|
| 53 | 'desktops.desktops': [ |
|---|
| 54 | ('desktop one', {'layout': 'vert'}), |
|---|
| 55 | ('just another desktop', {'layout': 'floating'}), |
|---|
| 56 | ('the third one!', {'layout': 'max'}), |
|---|
| 57 | ] |
|---|
| 58 | |
|---|
| 59 | Actions |
|---|
| 60 | ------- |
|---|
| 61 | |
|---|
| 62 | .. function:: layoutmgr.set_layout(name) |
|---|
| 63 | :module: |
|---|
| 64 | |
|---|
| 65 | sets the layout of the currently active desktop to the layout *name*. |
|---|
| 66 | |
|---|
| 67 | Required parameters: |
|---|
| 68 | * `screen` |
|---|
| 69 | * `name` |
|---|
| 70 | |
|---|
| 71 | .. function:: layoutmgr.cycle([offset=+1]) |
|---|
| 72 | :module: |
|---|
| 73 | |
|---|
| 74 | cycles through the available desktop layouts for the currently |
|---|
| 75 | active desktop. |
|---|
| 76 | |
|---|
| 77 | Required parameters: |
|---|
| 78 | * `screen` |
|---|
| 79 | |
|---|
| 80 | """ |
|---|
| 81 | |
|---|
| 82 | from samuraix.plugin import Plugin |
|---|
| 83 | from samuraix.util import OrderedDict |
|---|
| 84 | from samuraix import app |
|---|
| 85 | |
|---|
| 86 | from ooxcb.protocol import xproto |
|---|
| 87 | |
|---|
| 88 | import logging |
|---|
| 89 | log = logging.getLogger(__name__) |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | class Layout(object): |
|---|
| 93 | def __init__(self, desktop): |
|---|
| 94 | self.desktop = desktop |
|---|
| 95 | desktop.push_handlers(self) |
|---|
| 96 | |
|---|
| 97 | def detach(self): |
|---|
| 98 | self.desktop.remove_handlers(self) |
|---|
| 99 | |
|---|
| 100 | def on_rearrange(self, desktop): |
|---|
| 101 | self.layout() |
|---|
| 102 | |
|---|
| 103 | def layout(self): |
|---|
| 104 | raise NotImplemented() |
|---|
| 105 | |
|---|
| 106 | |
|---|
| 107 | class FloatingLayout(Layout): |
|---|
| 108 | """ do nothing. """ |
|---|
| 109 | name = 'floating' |
|---|
| 110 | |
|---|
| 111 | def layout(self): |
|---|
| 112 | # raise the currently focused window to the top |
|---|
| 113 | if self.desktop.clients: |
|---|
| 114 | self.desktop.clients.current() \ |
|---|
| 115 | .actor.configure(stack_mode=xproto.StackMode.Above) |
|---|
| 116 | app.conn.flush() |
|---|
| 117 | |
|---|
| 118 | |
|---|
| 119 | class MaxLayout(Layout): |
|---|
| 120 | """ make the focused window fill the screen """ |
|---|
| 121 | |
|---|
| 122 | name = 'max' |
|---|
| 123 | |
|---|
| 124 | def layout(self): |
|---|
| 125 | geom = self.desktop.screen.get_geometry() |
|---|
| 126 | client = self.desktop.clients.current() |
|---|
| 127 | client.actor.configure(x=geom.x, y=geom.y, width=geom.width, height=geom.height) |
|---|
| 128 | app.conn.flush() |
|---|
| 129 | |
|---|
| 130 | |
|---|
| 131 | class VertLayout(Layout): |
|---|
| 132 | """ arrange windows vertically with equal height """ |
|---|
| 133 | |
|---|
| 134 | name = 'vert' |
|---|
| 135 | |
|---|
| 136 | def layout(self): |
|---|
| 137 | if self.desktop.clients: |
|---|
| 138 | geom = self.desktop.screen.get_geometry() |
|---|
| 139 | h = geom.height / len(self.desktop.clients) |
|---|
| 140 | t = 0 |
|---|
| 141 | for client in self.desktop.clients: |
|---|
| 142 | client.actor.configure(x=0, y=t, width=geom.width, height=h) |
|---|
| 143 | t += h |
|---|
| 144 | app.conn.flush() |
|---|
| 145 | |
|---|
| 146 | |
|---|
| 147 | class HorizLayout(Layout): |
|---|
| 148 | """ arrange windows horizontally with equal width """ |
|---|
| 149 | |
|---|
| 150 | name = 'horiz' |
|---|
| 151 | |
|---|
| 152 | def layout(self): |
|---|
| 153 | if self.desktop.clients: |
|---|
| 154 | geom = self.desktop.screen.get_geometry() |
|---|
| 155 | w = geom.width / len(self.desktop.clients) |
|---|
| 156 | t = 0 |
|---|
| 157 | for client in self.desktop.clients: |
|---|
| 158 | client.actor.configure(x=t, y=0, width=w, height=geom.height) |
|---|
| 159 | t += w |
|---|
| 160 | app.conn.flush() |
|---|
| 161 | |
|---|
| 162 | |
|---|
| 163 | class SXLayoutMgr(Plugin): |
|---|
| 164 | key = 'layoutmgr' |
|---|
| 165 | |
|---|
| 166 | def __init__(self, app): |
|---|
| 167 | self.layouters = OrderedDict() |
|---|
| 168 | self.register(FloatingLayout) |
|---|
| 169 | self.register(MaxLayout) |
|---|
| 170 | self.register(VertLayout) |
|---|
| 171 | self.register(HorizLayout) |
|---|
| 172 | |
|---|
| 173 | self.app = app |
|---|
| 174 | app.push_handlers(self) |
|---|
| 175 | |
|---|
| 176 | # register actions |
|---|
| 177 | app.plugins['actions'].register('layoutmgr.set_layout', |
|---|
| 178 | self.action_set_layout) |
|---|
| 179 | app.plugins['actions'].register('layoutmgr.cycle', |
|---|
| 180 | self.action_cycle) |
|---|
| 181 | |
|---|
| 182 | def action_set_layout(self, info): |
|---|
| 183 | """ |
|---|
| 184 | set the layout of the current desktop. |
|---|
| 185 | |
|---|
| 186 | parameters: |
|---|
| 187 | `name`: str |
|---|
| 188 | identifier of the layout |
|---|
| 189 | |
|---|
| 190 | needed: |
|---|
| 191 | `screen` |
|---|
| 192 | """ |
|---|
| 193 | self.attach_layouter( |
|---|
| 194 | info['screen'].data['desktops'].active_desktop, |
|---|
| 195 | self.layouters[info['name']] |
|---|
| 196 | ) |
|---|
| 197 | |
|---|
| 198 | def action_cycle(self, info): |
|---|
| 199 | """ |
|---|
| 200 | cycle the layouts for the current desktop |
|---|
| 201 | |
|---|
| 202 | parameters: |
|---|
| 203 | `offset`: int |
|---|
| 204 | offset to cycle, defaults to +1 |
|---|
| 205 | |
|---|
| 206 | needed: |
|---|
| 207 | `screen` |
|---|
| 208 | """ |
|---|
| 209 | offset = info.get('offset', 1) |
|---|
| 210 | desktop = info['screen'].data['desktops'].active_desktop |
|---|
| 211 | layouter_cls = self.get_data(desktop).__class__ |
|---|
| 212 | current_name = getattr(layouter_cls, 'name', layouter_cls.__name__) |
|---|
| 213 | |
|---|
| 214 | keys = self.layouters.keys() |
|---|
| 215 | length = len(keys) |
|---|
| 216 | index = keys.index(current_name) |
|---|
| 217 | new_index = ((index or length) + offset) % length |
|---|
| 218 | new_layouter_cls = self.layouters[keys[new_index]] |
|---|
| 219 | self.attach_layouter(desktop, new_layouter_cls) |
|---|
| 220 | |
|---|
| 221 | def register(self, layouter_cls): |
|---|
| 222 | name = getattr(layouter_cls, 'name', layouter_cls.__name__) |
|---|
| 223 | log.info('registering layouter %s = %s', name, layouter_cls) |
|---|
| 224 | self.layouters[name] = layouter_cls |
|---|
| 225 | |
|---|
| 226 | def on_ready(self, app): |
|---|
| 227 | # should potentially do this in on_create_screen but |
|---|
| 228 | # that means making sure desktop.on_create_screen is called |
|---|
| 229 | # first... |
|---|
| 230 | for screen in app.screens: |
|---|
| 231 | screendata = screen.data['desktops'] |
|---|
| 232 | screendata.push_handlers(self) |
|---|
| 233 | for desktop in screendata.desktops: |
|---|
| 234 | log.debug('trying to attach to %s...', desktop) |
|---|
| 235 | layouter_name = desktop.config.get('layout') |
|---|
| 236 | if not layouter_name: |
|---|
| 237 | log.debug('no layouter_name') |
|---|
| 238 | continue |
|---|
| 239 | layouter_cls = self.layouters.get(layouter_name) |
|---|
| 240 | if not layouter_cls: |
|---|
| 241 | log.error('cant find layouter %s', name) |
|---|
| 242 | return |
|---|
| 243 | self.attach_layouter(desktop, layouter_cls) |
|---|
| 244 | |
|---|
| 245 | def attach_layouter(self, desktop, layouter_cls): |
|---|
| 246 | """ |
|---|
| 247 | attach the layouter class *layouter_cls* to *desktop*. |
|---|
| 248 | That will overwrite any layouter that was |
|---|
| 249 | attached before. |
|---|
| 250 | """ |
|---|
| 251 | if self.has_data(desktop): |
|---|
| 252 | self.get_data(desktop).detach() |
|---|
| 253 | self.remove_data(desktop) |
|---|
| 254 | log.debug('attached %s', layouter_cls) |
|---|
| 255 | layouter = layouter_cls(desktop) |
|---|
| 256 | self.attach_data_to(desktop, layouter) |
|---|
| 257 | #desktop.push_handlers(on_rearrange=self.on_rearrange) |
|---|
| 258 | # rearrange it automatically |
|---|
| 259 | layouter.layout() |
|---|