summaryrefslogtreecommitdiffstats
path: root/extra/arandr/arandr-0.1.3/screenlayout/metacity.py
blob: a6cac05d6ae9fce8609e46a9d9e6e097a9965c18 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import os
import gobject
import gtk

try:
    import gconf
except ImportError:
    gconf = None

import gettext
gettext.install('arandr')

SCRIPTSDIR = os.path.expanduser('~/.screenlayout/') # must end in /

# cycling template:
# sh -c 'COUNT=`cat /tmp/counter 2>/dev/null`; LENGTH=3; COUNT=$(expr $(expr $COUNT + 1) % $LENGTH); echo $COUNT > /tmp/counter; case "$COUNT" in 0) echo zero;; 1) echo uno;; 2) echo dos;; esac'
# countfile must be a harmless string
CYCLINGPATTERN = """sh -c 'COUNTFILE=%(countfile)s; COUNT=`cat $COUNTFILE 2>/dev/null`; LENGTH=%(length)d; COUNT=$(expr $(expr $COUNT + 1) %% $LENGTH); echo $COUNT > $COUNTFILE; case "$COUNT" in %(cases)s;; esac'"""
CYCLINGPATTERN_RECOGNITION = [
        """sh -c 'COUNTFILE=""",
        """; COUNT=`cat $COUNTFILE 2>/dev/null`; LENGTH=""",
        """; COUNT=$(expr $(expr $COUNT + 1) % $LENGTH); echo $COUNT > $COUNTFILE; case "$COUNT" in """,
        """;; esac'""",
        ]

class MetacityWidget(gtk.Table):
    """Widget that manages bindings of screenlayout scripts to metacity keybindings.

    Not related to ARandR except that ARandR scripts are bound."""
    def __init__(self):
        gtk.Table.__init__(self, rows=13, columns=2)

        c = gconf.client_get_default()
        c.add_dir('/apps/metacity/global_keybindings', gconf.CLIENT_PRELOAD_NONE)
        c.add_dir('/apps/metacity/keybinding_commands', gconf.CLIENT_PRELOAD_NONE)

        self.attach(gtk.Label(_("Accelerator")), 0,1,0,1)
        self.attach(gtk.Label(_("Action")), 1,2,0,1)

        self.lines = []
        for i in range(1,13):
            k = KeyBindingButton(c, '/apps/metacity/global_keybindings/run_command_%d'%i)
            a = ActionWidget(c, '/apps/metacity/keybinding_commands/command_%d'%i)
            self.attach(k, 0, 1, i, i+1)
            self.attach(a, 1, 2, i, i+1)
            self.lines.append((k,a))
            k.connect('notify::bound', lambda *args: self._update())
            a.connect('notify::editable', lambda *args: self._update())
        self._update()

    def _update(self):
        for i,(k,a) in enumerate(self.lines):
            enable = (i==0 or k.props.bound or self.lines[i-1][0].props.bound) and a.props.editable
            k.props.sensitive = enable
            a.props.sensitive = enable and k.props.bound


class GConfButton(gtk.Button):
    """Button connected to a gconfkey via a gconf client c.

    Will call self._update when the key is changed; use self.set(value) to change the key's value."""
    def __init__(self, c, gconfkey):
        self._properties = {}
        super(GConfButton, self).__init__()

        self.gconf = c
        self.gconfkey = gconfkey
        self._id = c.notify_add(gconfkey, self._update)
        c.notify(gconfkey)

    def __del__(self):
        self.gconf.notify_remove(self._id)
        #print "del" # FIXME: not called!

    def do_get_property(self, key):
        return self._properties[key]
    def do_set_property(self, key, value):
        self._properties[key] = value

    def set(self, value):
        self.gconf.set_string(self.gconfkey, value)

    def _update(self, *args):
        """Called when the value of the key is changed (hooked into GConf); overwrite this."""
        pass


class KeyBindingButton(GConfButton):
    """GConfButton that will interpret the value as a keybinding and ask for a new keybinding when pressed."""
    __gproperties__ = {
            'bound': (gobject.TYPE_BOOLEAN, 'bound', 'slot is bound to a key', False, gobject.PARAM_READWRITE),
            }

    def __init__(self, *args, **kwords):
        super(KeyBindingButton, self).__init__(*args, **kwords)

        self.connect('clicked', self.on_clicked)
        self.connect('key-press-event', self.on_keypress)

    def _update(self, *args):
        s = self.gconf.get_string(self.gconfkey)

        if s == "disabled":
            self.props.label = _("disabled")
            self.props.bound = False
        else:
            self.props.label = s
            self.props.bound = True

        self.editing = False

    def abort_editing(self):
        self.editing = False
        self._update()

    def on_clicked(self, widget):
        if not self.editing:
            self.editing = True
            self.props.label = _("New accelerator...")
        else:
            self.abort_editing()

    def on_keypress(self, widget, event): # modified from gnome deskbar-applet's DeskbarPreferencesUI.py
        if not self.editing:
            return

        keymap = gtk.gdk.keymap_get_default()
        translation = keymap.translate_keyboard_state(event.hardware_keycode, event.state, event.group)
        if translation == None: # FIXME: metacity can also handle raw keycodes with modifiers (but can compiz?)
            accel_name = "%#x"%event.hardware_keycode
        else:
            (keyval, egroup, level, consumed_modifiers) = translation
            upper = event.keyval
            accel_keyval = gtk.gdk.keyval_to_lower(upper)

            # Put shift back if it changed the case of the key, not otherwise.
            if upper != accel_keyval and (consumed_modifiers & gtk.gdk.SHIFT_MASK):
                consumed_modifiers &= ~(gtk.gdk.SHIFT_MASK)

            # filter consumed/ignored modifiers
            ignored_modifiers = gtk.gdk.MOD2_MASK | gtk.gdk.MOD5_MASK
            accel_mods = event.state & gtk.gdk.MODIFIER_MASK & ~(consumed_modifiers | ignored_modifiers)

            if accel_mods == 0 and accel_keyval == gtk.keysyms.Escape:
                self.abort_editing()
                return
            if accel_mods == 0 and accel_keyval == gtk.keysyms.BackSpace:
                self.set('disabled')
                return

            if not gtk.accelerator_valid(accel_keyval, accel_mods):
                return # just modifiers

            accel_name = gtk.accelerator_name(accel_keyval, accel_mods)
            #self.set_accelerator(accel_keyval, event.hardware_keycode, accel_mods)
            #self.__old_value = None
            #self.emit('accel-edited', accel_name, accel_keyval, accel_mods, event.hardware_keycode)

        self.set(accel_name)

class ActionWidget(GConfButton):
    """GConfButton that will interpret the value as a command and allow changing it if it is a screenlayout script or a collection thereof."""
    __gproperties__ = {
            'editable': (gobject.TYPE_BOOLEAN, 'editable', 'true if property can be managed by MetacityWidget', False, gobject.PARAM_READWRITE),
            }

    def __init__(self, *args, **kwords):
        super(ActionWidget, self).__init__(*args, **kwords)

        self.connect('clicked', self.on_clicked)

    def _update(self, *args):
        s = self.gconf.get_string(self.gconfkey)

        if s.startswith('"'+SCRIPTSDIR) and s.endswith('.sh"'):
            text = s[len(SCRIPTSDIR)+1:-4]

            self.props.label = text
            self.props.editable = True
            self.items = [text]
        elif s == "":
            self.props.label = _("no action")
            self.props.editable = True
            self.items = []
        elif s.startswith(CYCLINGPATTERN_RECOGNITION[0]):
            try:
                left = s[len(CYCLINGPATTERN_RECOGNITION[0]):]
                index = left.index(CYCLINGPATTERN_RECOGNITION[1])
                # countfile = left[:index] # not needed because not configurable. differing count files will be reset to the global one on change.
                left = left[index+len(CYCLINGPATTERN_RECOGNITION[1]):]
                index = left.index(CYCLINGPATTERN_RECOGNITION[2])
                length = int(left[:index])
                left = left[index+len(CYCLINGPATTERN_RECOGNITION[2]):]
                index = left.index(CYCLINGPATTERN_RECOGNITION[3])
                cases = left[:index]
                left = left[index+len(CYCLINGPATTERN_RECOGNITION[3]):]
                if left!="":
                    raise ValueError("Not my syntax.")

                # countfile, length, cases
                counter, scripts = zip(*[part.split(") ") for part in cases.split(" ;; ")])
                if counter != tuple(str(i) for i in range(length)):
                    raise ValueError("Not my syntax.")

                self.items = []
                for s in scripts:
                    if s.startswith('"'+SCRIPTSDIR) and s.endswith('.sh"'):
                        self.items.append(s[len(SCRIPTSDIR)+1:-4])
                    else:
                        raise ValueError("Not my syntax.");
            except (ValueError, ):
                self.props.label = _("incompatible configuration")
                self.props.editable = False
                self.items = None
                raise
            self.props.label = ", ".join(self.items)
            self.props.editable = True
        else:
            self.props.label = _("other application")
            self.props.editable = False
            self.items = None

    def on_clicked(self, widget):
        m = gtk.Menu()
        try:
            for f in os.listdir(SCRIPTSDIR):
                if not f.endswith('.sh'):
                    continue
                text = f[:-3]
                i = gtk.CheckMenuItem(text)
                if text in self.items:
                    i.props.active = True
                i.connect('activate', lambda menuitem, script: self.toggle(script), text)
                m.add(i)
        except OSError: # no such directory
            pass

        if not m.get_children():
            i = gtk.MenuItem(_("No files in %(folder)r. Save a layout first.")%{'folder':SCRIPTSDIR})
            i.props.sensitive = False
            m.add(i)
        else:
            m.add(gtk.MenuItem())

            i = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
            i.connect('activate', lambda menuitem: self.set(""))
            m.add(i)

        m.show_all()
        m.popup(None, None, None, 1, 0)

    def toggle(self, item):
        if item in self.items:
            self.items.remove(item)
        else:
            self.items.append(item)
        if len(self.items) == 0:
            self.set("")
        elif len(self.items) == 1:
            self.set('"%s%s.sh"'%(SCRIPTSDIR, self.items[0]))
        else:
            self.set(CYCLINGPATTERN%{'length':len(self.items), 'countfile':'/tmp/screenlayout_count.%s'%os.environ['USER'], 'cases':" ;; ".join('%d) "%s.sh"'%(i,SCRIPTSDIR+script) for (i,script) in enumerate(self.items))})


def show_keybinder():
    if not gconf:
        d = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE)
        d.props.text = _("gconf not available.")
        d.props.secondary_text = _("In order to configure metacity, you need to have the python gconf module installed.")
        d.run()
        d.destroy()
        return

    d = gtk.Window()
    d.props.modal = True
    d.props.title = _("Keybindings (via Metacity)")

    close = gtk.Button(gtk.STOCK_CLOSE)
    close.props.use_stock = True
    close.connect('clicked', lambda *args: d.destroy())
    buttons = gtk.HBox() # FIXME: use HButtonBox
    buttons.props.border_width = 5
    buttons.pack_end(close, expand=False)

    t = MetacityWidget()

    contents = gtk.VBox()
    contents.pack_start(t)
    l = gtk.Label(_('Click on a button in the left column and press a key combination you want to bind to a certain screen layout. (Use backspace to clear accelerators, escape to abort editing.) Then, select one or more layouts in the right column.\n\nThis will only work if you use metacity or another program reading its configuration.'))
    l.props.wrap = True
    contents.pack_start(l)
    contents.pack_end(buttons, expand=False)
    d.add(contents)
    d.show_all()