aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/plugin/keybindingchooser/chooser/BindingPanel.java
blob: 2b9c7061440fed48eb1f69fe5dbe7ee16b95ef08 (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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.plugin.keybindingchooser.chooser;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

import net.java.sip.communicator.plugin.desktoputil.*;

/**
 * Panel containing a listing of current keybinding mappings. This contains
 * methods that can be overwritten to provide easy editing functionality and
 * display logic. Note that this does not support the manual addition or removal
 * of BindingEntry components. However this is designed to tolerate the changing
 * of entry visibility (including individual fields) and the manual addition and
 * removal of extra components either to this panel or its BindingEntries.<br>
 * This represents a mapping of keystrokes to strings, and hence duplicate
 * shortcuts aren't supported. An exception is made in the case of disabled
 * shortcuts, but to keep mappings unique duplicate actions among disabled
 * entries are not permitted.
 *
 * @author Damian Johnson (atagar1@gmail.com)
 * @version September 1, 2007
 */
public abstract class BindingPanel
    extends TransparentPanel
{
    /**
     * Serial version UID.
     */
    private static final long serialVersionUID = 0L;

    private ArrayList<BindingEntry> contents = new ArrayList<BindingEntry>();

    /**
     * Method called whenever an entry is either added or shifts in the display.
     * For instance, if the second entry is removed then this is called on the
     * third to last elements.
     *
     * @param index newly assigned index of entry
     * @param entry entry that has been added or shifted
     * @param isNew if true the entry is new to the display, false otherwise
     */
    protected abstract void onUpdate(int index, BindingEntry entry,
        boolean isNew);

    /**
     * Method called upon any mouse clicks within a BindingEntry in the display.
     *
     * @param event fired mouse event that triggered method call
     * @param entry entry on which the click landed
     * @param field field of entry on which the click landed, null if not a
     *            recognized field
     */
    protected abstract void onClick(MouseEvent event, BindingEntry entry,
        BindingEntry.Field field);

    /**
     * Constructor.
     */
    public BindingPanel()
    {
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        addMouseListener(new MouseTracker());
    }

    /**
     * Adds a new key binding mapping to the end of the listing. If this already
     * contains the shortcut then the previous entry is replaced instead (not
     * triggering the onUpdate method). Disabled shortcuts trigger replacement
     * on duplicate actions instead. This uses the normal parameters used to
     * generate key stokes, such as:
     *
     * <pre>
     * bindingPanel.putBinding('Y', 0, &quot;Confirm Selection&quot;);
     * bindingPanel.putBinding(KeyEvent.VK_DELETE, KeyEvent.CTRL_MASK
     *     | KeyEvent.ALT_MASK, &quot;Kill Process&quot;);
     * </pre>
     *
     * @param keyCode key code of keystroke component of mapping
     * @param modifier modifiers of keystroke component of mapping
     * @param action string component of mapping
     * @return true if contents did not already include shortcut
     */
    public boolean putBinding(int keyCode, int modifier, String action)
    {
        return putBinding(KeyStroke.getKeyStroke(keyCode, modifier), action);
    }

    /**
     * Adds a new key binding mapping to the end of the listing. If this already
     * contains the shortcut then the previous entry is replaced instead (not
     * triggering the onUpdate method). Disabled shortcuts trigger replacement
     * on duplicate actions instead.
     *
     * @param shortcut keystroke component of mapping
     * @param action string component of mapping
     * @return true if contents did not already include shortcut
     */
    public boolean putBinding(KeyStroke shortcut, String action)
    {
        return putBinding(shortcut, action, getComponentCount());
    }

    /**
     * Adds a new key binding mapping to a particular index of the listing. If
     * this already contains the shortcut then the previous entry is replaced
     * instead (not triggering the onUpdate method). Disabled shortcuts trigger
     * replacement on duplicate actions instead.
     *
     * @param shortcut keystroke component of mapping
     * @param action string component of mapping
     * @param index location in which to insert mapping
     * @return true if contents did not already include shortcut
     * @throws IndexOutOfBoundsException if index is out of range (index < 0 ||
     *             index > getBindingCount()).
     */
    public boolean putBinding(KeyStroke shortcut, String action, int index)
    {
        return putBinding(new BindingEntry(shortcut, action), index);
    }

    /**
     * Adds a new key binding mapping to a particular index of the listing. If
     * this already contains the shortcut then the previous entry is replaced
     * instead (not triggering the onUpdate method). Disabled shortcuts trigger
     * replacement on duplicate actions instead.
     *
     * @param newEntry entry to add to contents
     * @param index location in which to insert mapping
     * @return true if contents did not already include shortcut
     * @throws IndexOutOfBoundsException if index is out of range (index < 0 ||
     *             index > getBindingCount()).
     */
    public boolean putBinding(BindingEntry newEntry, int index)
    {
        if (index < 0 || index > getBindingCount())
        {
            String message = "Attempting to add to invalid index: " + index;
            throw new IndexOutOfBoundsException(message);
        }

        KeyStroke shortcut = newEntry.getShortcut();
        if (shortcut != BindingEntry.DISABLED)
        {
            // Checks for duplicate shortcut
            for (BindingEntry entry : this.contents)
            {
                if (shortcut.equals(entry.getShortcut()))
                {
                    entry.setAction(newEntry.getAction());
                    return false;
                }
            }
        }
        else
        {
            // Checks if this entry would be a duplicate
            if (this.contents.contains(newEntry))
                return false;
        }

        this.contents.add(index, newEntry);

        // Inserts into display, maintaining ordering of collection
        if (index > 0)
        {
            /*
             * Places the new entry after previously listed one, transversing
             * backward to support common case of adding to the end. This
             * depends on bindings being unique.
             */
            BindingEntry previous = getBinding(index - 1);
            for (int i = getComponentCount() - 1; i >= 0; --i)
            {
                if (getComponent(i).equals(previous))
                {
                    add(newEntry, i + 1);
                    break;
                }
                assert i != 0 : "Listing doesn't contain expected previous entry "
                    + "when adding to index " + index;
            }
        }
        else
        {
            add(newEntry, 0); // Adds to start
        }

        // Calls update on add entry and any shifted contents
        onUpdate(index, newEntry, true);
        for (int i = index + 1; i < getBindingCount(); ++i)
        {
            BindingEntry shifted = getBinding(i);
            onUpdate(i, shifted, false);
        }
        return true;
    }

    /**
     * Adds a collection of new key binding mappings to the end of the listing.
     * If any shortcuts are already contained then the previous entries are
     * replaced (not triggering the onUpdate method). Disabled shortcuts trigger
     * replacement on duplicate actions instead.
     *
     * @param bindings mapping between keystrokes and actions to be added
     */
    public void putAllBindings(Map<KeyStroke, String> bindings)
    {
        for (KeyStroke action : bindings.keySet())
        {
            putBinding(action, bindings.get(action));
        }
    }

    /**
     * Removes a particular binding from the contents.
     *
     * @param entry binding to be removed
     * @return true if binding was in the contents, false otherwise
     */
    public boolean removeBinding(BindingEntry entry)
    {
        int index = getBindingIndex(entry);
        if (index != -1)
            return removeBinding(index) != null;
        else
            return false;
    }

    /**
     * Removes the binding at a particular index of the listing.
     *
     * @param index from which to remove entry
     * @return the entry that was removed from the contents
     * @throws IndexOutOfBoundsException if index is out of range (index < 0 ||
     *             index > getBindingCount()).
     */
    public BindingEntry removeBinding(int index)
    {
        if (index < 0 || index > getBindingCount())
        {
            String message =
                "Attempting to remove from invalid index: " + index;
            throw new IndexOutOfBoundsException(message);
        }

        BindingEntry entry = this.contents.remove(index);
        remove(entry); // Removes from display

        // Calls update on shifted entries
        for (int i = index; i < getBindingCount(); ++i)
        {
            BindingEntry shifted = getBinding(i);
            onUpdate(i, shifted, false);
        }

        return entry;
    }

    /**
     * Removes all bindings from the panel.
     */
    public void clearBindings()
    {
        while (getBindingCount() > 0)
        {
            removeBinding(0);
        }
    }

    /**
     * Returns if a keystroke is in the panel's current contents. This provides
     * a preemptive means of checking if adding a non-disabled shortcut would
     * cause a replacement.
     *
     * @param shortcut keystroke to be checked against contents
     * @return true if contents includes the shortcut, false otherwise
     */
    public boolean contains(KeyStroke shortcut)
    {
        for (BindingEntry entry : this.contents)
        {
            if (shortcut == BindingEntry.DISABLED)
            {
                if (entry.isDisabled())
                    return true;
            }
            else
            {
                if (shortcut.equals(entry.getShortcut()))
                    return true;
            }
        }
        return false;
    }

    /**
     * Provides number of key bindings currently present.
     *
     * @return number of key bindings in the display
     */
    public int getBindingCount()
    {
        return this.contents.size();
    }

    /**
     * Provides the index of a particular entry.
     *
     * @param entry entry for which the index should be returned
     * @return entry index, -1 if not found
     */
    public int getBindingIndex(BindingEntry entry)
    {
        return this.contents.indexOf(entry);
    }

    /**
     * Provides a binding at a particular index.
     *
     * @param index index from which to retrieve binding.
     * @return the entry at the specified position in this list
     */
    public BindingEntry getBinding(int index)
    {
        return this.contents.get(index);
    }

    /**
     * Provides listing of the current keybinding entries.
     *
     * @return list of current entry contents
     */
    public ArrayList<BindingEntry> getBindings()
    {
        return new ArrayList<BindingEntry>(this.contents);
    }

    /**
     * Provides the mapping between keystrokes and actions represented by the
     * contents of the display. Disabled entries aren't included in the mapping.
     *
     * @return mapping between contained keystrokes and their associated actions
     */
    public LinkedHashMap<KeyStroke, String> getBindingMap()
    {
        LinkedHashMap<KeyStroke, String> mapping =
            new LinkedHashMap<KeyStroke, String>();
        for (BindingEntry entry : this.contents)
        {
            if (entry.isDisabled())
                continue;
            else
                mapping.put(entry.getShortcut(), entry.getAction());
        }
        return mapping;
    }

    /**
     * Provides an input map associating keystrokes to actions according to the
     * contents of the display. Disabled entries aren't included in the mapping.
     *
     * @return input mapping between contained keystrokes and their associated
     *         actions
     */
    public InputMap getBindingInputMap()
    {
        InputMap mapping = new InputMap();
        LinkedHashMap<KeyStroke, String> bindingMap = getBindingMap();
        for (KeyStroke keystroke : bindingMap.keySet())
        {
            mapping.put(keystroke, bindingMap.get(keystroke));
        }
        return mapping;
    }

    // Mouse listener for clicks within display
    private class MouseTracker
        extends MouseInputAdapter
    {
        @Override
        public void mousePressed(MouseEvent event)
        {
            Point loc = event.getPoint();
            Component comp = getComponentAt(loc);

            if (comp instanceof BindingEntry)
            {
                BindingEntry entry = (BindingEntry) comp;

                // Gets label within entry
                int x = loc.x - entry.getLocation().x;
                int y = loc.y - entry.getLocation().y;
                Component label = entry.findComponentAt(x, y);

                if (entry.getField(BindingEntry.Field.INDENT).equals(label))
                {
                    onClick(event, entry, BindingEntry.Field.INDENT);
                }
                else if (entry.getField(BindingEntry.Field.ACTION)
                    .equals(label))
                {
                    onClick(event, entry, BindingEntry.Field.ACTION);
                }
                else if (entry.getField(BindingEntry.Field.SHORTCUT).equals(
                    label))
                {
                    onClick(event, entry, BindingEntry.Field.SHORTCUT);
                }
                else
                {
                    onClick(event, entry, null); // Click fell on unrecognized
                                                 // component
                }
            }
        }
    }
}