~bioxy/awn-extras/sabnzbd-applet

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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
# Awn Applet Library - Simplified APIs for programming applets for Awn.
#
# Copyright (C) 2007 - 2008  Pavel Panchekha <pavpanchekha@gmail.com>
#               2008 - 2010  onox <denkpadje@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys

import pygtk
pygtk.require("2.0")
import gtk

from desktopagnostic import config, Color
import awn
from awn.extras import _, configbinder, __version__

import cairo
try:
    import cPickle as pickle
except ImportError:
    import pickle
import glib
import gobject

___file___ = sys.argv[0]
# Basically, __file__ = current file location
# sys.argv[0] = file name or called file
# Since awnlib is in site-packages, __file__ refers to something there
# For relative paths to work, we need a way of determining where the
# User applet is. So this bit of magic works.

bug_report_link = "https://launchpad.net/awn-extras/+filebug"


def create_frame(parent, label):
    """Create a frame with a bold title. To be used in a preferences window.

    """
    vbox = gtk.VBox(spacing=6)
    parent.add(vbox)

    label = gtk.Label("<b>" + label + "</b>")
    label.set_use_markup(True)
    label.props.xalign = 0.0
    vbox.add(label)

    alignment = gtk.Alignment()
    alignment.set_padding(0, 0, 12, 0)
    vbox.add(alignment)

    frame_vbox = gtk.VBox(spacing=6)
    alignment.add(frame_vbox)

    return frame_vbox


def add_cell_renderer_text(combobox):
    """Add a gtk.CellRendererText to the combobox. To be used if the combobox
    has a gtk.ListStore model with a string as the first column.

    """
    text = gtk.CellRendererText()
    combobox.pack_start(text, True)
    combobox.add_attribute(text, "text", 0)


def is_required_version(version, required_version):
    """Return True if version is higher than or equal to
    required_version, False otherwise.

    """
    for i, j in zip(version, required_version):
        if i > j:
            return True
        elif i < j:
            return False
    return True


class KeyringError(Exception):
    pass


class KeyringCancelledError(KeyringError):
    pass


class KeyringNoMatchError(KeyringError):
    pass


class Dialogs:

    __special_dialogs = ("menu", "about", "preferences")

    __gtk_show_image_ok = is_required_version(gtk.gtk_version, (2, 16, 0))

    def __init__(self, parent):
        """Create an instance of Dialogs. Creates a context menu,
        and an About dialog, which is added to the menu.

        @param parent: The parent applet of the dialogs instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        self.__register = {}
        self.__current = None

        self.menu = self.new("menu")

        meta_keys = self.__parent.meta.keys()

        # Create the About dialog if the applet provides the necessary metadata
        if all([key in meta_keys for key in ("name", "author", "copyright-year")]):
            about_dialog = self.new("about")

            about_item = gtk.ImageMenuItem(_("_About %s") % self.__parent.meta["name"])
            if self.__gtk_show_image_ok:
                about_item.props.always_show_image = True
            about_item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
            self.menu.append(about_item)
            about_item.connect("activate", lambda w: self.toggle("about"))

    def connect_signals(self, parent):
        def popup_menu_cb(widget, event):
            self.toggle("menu", once=True, event=event)
        parent.connect("context-menu-popup", popup_menu_cb)

        def clicked_cb(widget, dialog_name):
            if dialog_name in self.__register:
                self.toggle(dialog_name)
        parent.connect("clicked", clicked_cb, "main")
        parent.connect("middle-clicked", clicked_cb, "secondary")

    def new(self, dialog, title=None, focus=True):
        """Create a new AWN dialog.

        @param dialog: The name to register the dialog under.
        @type dialog: C{string}
        @param title: The title of the new dialog
        @type title: C{string}
        @param focus: Whether to force the focus
        @type focus: C{bool}
        @return: The new menu or dialog
        @rtype: C{gtk.Menu}, C{function}, or C{awn.AppletDialog}

        """
        if dialog == "menu":
            dlog = self.__parent.create_default_menu()
        elif dialog == "about":
            dlog = self.AboutDialog(self.__parent)
        elif dialog == "preferences":
            dlog = self.PreferencesDialog(self.__parent)

            position = len(self.menu)
            if "about" in self.__register:
                position = position - 1

            prefs_item = gtk.ImageMenuItem(stock_id=gtk.STOCK_PREFERENCES)
            if self.__gtk_show_image_ok:
                prefs_item.props.always_show_image = True
            self.menu.insert(prefs_item, position)
            prefs_item.connect("activate", lambda w: self.toggle(
               "preferences", "show"))
        else:
            dlog = awn.Dialog(self.__parent)

        self.register(dialog, dlog, focus)

        if dialog not in self.__special_dialogs and title:
            dlog.set_title(" " + title + " ")

        return dlog

    def register(self, dialog, dlog, focus=True):
        """Register a dialog.

        Once a name has been registered, it cannot be registered again
        until the dialog is explicitly unregistered.

        @param dialog: The name to use for the dialog.
        @type dialog: C{string}
        @param dlog: The actual dialog or menu or function.
        @type dlog: C{function}, C{gtk.Menu}, or C{awn.AppletDialog}
        @param focus: True if the dialog should be hidden when focus is lost, False otherwise.
        @type focus: C{bool}

        """
        if dialog in self.__register:
            raise RuntimeError("Dialog '%s' already registered" % dialog)

        if focus and dialog not in self.__special_dialogs and isinstance(dlog, awn.Dialog):
            dlog.props.hide_on_unfocus = focus

        self.__register[dialog] = dlog

    def unregister(self, dialog):
        """Unregister a dialog.

        @param dialog: The name to use for the dialog. Must not be equal
            to the name of any of the special dialogs.
        @type dialog: C{string}

        """
        if dialog not in self.__register:
            raise RuntimeError("Dialog '%s' not registered" % dialog)
        if dialog in self.__special_dialogs:
            raise RuntimeError("Unregistering special dialog '%s' is forbidden" % dialog)

        if dialog == self.__current:
            self.__register[dialog].hide()
            self.__current = None
        del self.__register[dialog]

    def toggle(self, dialog, force="", once=False, event=None):
        """Show or hide a dialog.

        @param dialog: The dialog that should be shown.
        @type dialog: C{string}
        @param force: "Hide" or "Show". Whether to force the hiding or showing
                      of the dialog in question.
        @type force: C{string}
        @param once: Only show or hide one dialog. If a dialog is already
            opened, and you request that another dialog be toggled, only the
            open one is hidden. False by default.
        @type once: C{bool}
        @param event: The event that triggered the toggle.
        @type event: C{gdk.Event}

        """
        force = force.lower()

        assert force in ("hide", "show", ""), "Force must be \"hide\", \"show\", or \"\""
        assert dialog in self.__register, "Dialog '%s' must be registered" % dialog

        if dialog == "menu":
            self.show_menu(self.__parent, event)
        elif dialog == "about":
            self.__register["about"].show()
            self.__register["about"].present()
        else:
            if force == "hide" or (self.__register[dialog].is_active() and force != "show"):
                self.__register[dialog].hide()
                self.__current = None

                # Because the dialog is now hidden, show the tooltip again
                self.__parent.tooltip.show()
            else:
                self.__parent.tooltip.hide()

                if self.__current is not None and self.__current not in self.__special_dialogs:
                    current = self.__register[self.__current]
                    current_was_active = current.is_active()

                    current.hide()

                    if current_was_active and once:
                        self.__current = None
                        return

                self.__register[dialog].show_all()
                self.__current = dialog
                if dialog == "preferences":
                    self.__register[dialog].present()

    def show_menu(self, parent, event):
        self.__register["menu"].show_all()
        parent.popup_gtk_menu(self.__register["menu"], event.button, event.time)

    def hide(self):
        """Hide the currently visible dialog.

        """
        if self.__current is not None:
            self.__register[self.__current].hide()
            self.__current = None

    def is_visible(self, dialog):
        """Return True if the specified dialog is visible, False otherwise.

        """
        assert dialog in self.__register, "Dialog '%s' must be registered" % dialog

        return self.__register[dialog].is_active()

    class BaseDialog:

        """Base class for dialogs. Sets and updates the icon and hides
        the dialog instead of letting it being destroyed.

        """

        def __init__(self, parent):
            self.__parent = parent

            if "logo" in parent.meta:
                self.update_logo_icon()
                parent.connect_size_changed(self.update_logo_icon)
            elif "theme" in parent.meta:
                self.set_icon_name(parent.meta["theme"])

            # Connect some signals to be able to hide the window
            self.connect("response", self.response_event)
            self.connect("delete_event", gtk.Widget.hide_on_delete)

        def response_event(self, widget, response):
            if response < 0:
                self.hide()

        def update_logo_icon(self):
            """Update the logo to be of the same height as the panel.

            """
            self.set_icon_from_file(self.__parent.meta["logo"])

    class AboutDialog(BaseDialog, gtk.AboutDialog):

        """Applet's About dialog.

        """

        def __init__(self, parent):
            gtk.AboutDialog.__init__(self)
            Dialogs.BaseDialog.__init__(self, parent)

            self.__parent = parent

            self.set_name(parent.meta["name"])

            if "version" in parent.meta:
                self.set_version(parent.meta["version"])
            if "description" in parent.meta:
                self.set_comments(parent.meta["description"])

            copyright_info = (parent.meta["copyright-year"], parent.meta["author"])
            self.set_copyright("Copyright \xc2\xa9 %s %s" % copyright_info)

            if "authors" in parent.meta:
                self.set_authors(parent.meta["authors"])
            if "artists" in parent.meta:
                self.set_artists(parent.meta["artists"])

            if "logo" in parent.meta:
                self.set_logo(gtk.gdk.pixbuf_new_from_file_at_size( \
                    parent.meta["logo"], 48, 48))
            elif "theme" in parent.meta:
                self.set_logo_icon_name(parent.meta["theme"])

    class PreferencesDialog(BaseDialog, gtk.Dialog):

        """A Dialog window that has the title "<applet's name> Preferences",
        uses the applet's logo as its icon and has a Close button.

        """

        def __init__(self, parent):
            gtk.Dialog.__init__(self, flags=gtk.DIALOG_NO_SEPARATOR)
            Dialogs.BaseDialog.__init__(self, parent)

            self.__parent = parent

            self.set_resizable(False)
            self.set_border_width(5)

            # This is a window title, %s is an applet's name.
            self.set_title(_("%s Preferences") % parent.meta["name"])
            self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)


class Tooltip:

    def __init__(self, parent):
        """Create a new Tooltip object.

        @param parent: The parent applet of the tooltip instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        self.__tooltip = parent.get_icon().get_tooltip()
        self.set(parent.meta["name"])

        self.disable_toggle_on_click()
        if parent.meta.has_option("no-tooltip"):
            self.__tooltip.props.smart_behavior = False

    def disable_toggle_on_click(self):
        self.__tooltip.props.toggle_on_click = False

    def is_visible(self):
        return (self.__tooltip.flags() & gtk.VISIBLE) != 0

    def show(self):
        """Show the applet tooltip.

        """
        self.__tooltip.show()

    def hide(self):
        """Hide the applet tooltip.

        """
        self.__tooltip.hide()

    def set(self, text):
        """Set the applet tooltip.

        @param text: The new tooltip text.
        @type text: C{string}

        """
        self.__parent.set_tooltip_text(text)

    def connect_becomes_visible(self, callback):
        assert callable(callback)
        self.__tooltip.connect("map-event", lambda w, e: callback())


class Icon:

    APPLET_SIZE = "applet-size"

    def __init__(self, parent):
        """Create a new Icon object.

        @param parent: The parent applet of the icon instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        self.__previous_context = None
        self.__has_remove_custom_icon_item = False

        # Set the themed icon to set the C{awn.Icons} object
        if "theme" in parent.meta:
            self.theme(parent.meta["theme"])

    def file(self, file, set=True, size=None):
        """Get an icon from a file location.

        @param file: The path to the file. Can be relative or absolute.
        @type file: C{string}
        @param set: Whether to also set the icon. True by default.
        @type set: C{bool}
        @param size: Width and height of icon.
        @type size: C{int}
        @return: The resultant pixbuf or None (if C{set} is C{True})
        @rtype: C{gtk.gdk.Pixbuf} or C{None}

        """
        if file[0] != "/":
            file = os.path.join(os.path.abspath(os.path.dirname(___file___)), file)

        if size is None:
            icon = gtk.gdk.pixbuf_new_from_file(file)
        else:
            if size is self.__class__.APPLET_SIZE:
                size = self.__parent.get_size()
            icon = gtk.gdk.pixbuf_new_from_file_at_size(file, size, size)

        if set:
            self.set(icon)
        else:
            return icon

    def theme(self, name):
        """Set an icon from the default icon theme. The resultant
        pixbuf will be returned.

        @param name: The name of the theme icon.
        @type name: C{string}

        """
        self.__parent.set_icon_name(name)

        if not self.__has_remove_custom_icon_item:
            self.__has_remove_custom_icon_item = True

            icon = self.__parent.get_icon()
            assert isinstance(icon, awn.ThemedIcon)

            item = icon.create_remove_custom_icon_item()
            self.__parent.dialog.menu.insert(item, 1)

    def set(self, icon):
        """Set a C{gtk.gdk.pixbuf} or C{cairo.Context} as your applet icon.

        @param icon: The icon to set your applet icon to.
        @type icon: C{gtk.gdk.Pixbuf} or C{cairo.Context}

        """
        if isinstance(icon, cairo.Context):
            self.__parent.set_icon_context(icon)

            if self.__previous_context != icon:
                del self.__previous_context
                self.__previous_context = icon
        else:
            self.__parent.set_icon_pixbuf(icon)

    def hide(self):
        """Hide the applet's icon.

        """
        self.__parent.hide()


class Theme:

    def __init__(self, parent):
        """Create a new Theme object.

        @param parent: The parent applet of the theme instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        self.__states = None
        self.__icon_state = None

    def set_states(self, states_icons):
        self.__states, icons = zip(*states_icons.items())
        self.__icon_state = None
        self.__parent.set_icon_info(self.__states, icons)

    def icon(self, state):
        if self.__states is None or state not in self.__states:
            raise RuntimeError("invalid state")

        if state != self.__icon_state:
            self.__icon_state = state
            self.__parent.set_icon_state(state)

    def theme(self, theme):
        self.__parent.get_icon().override_gtk_theme(theme)


class Icons:

    def __init__(self, parent):
        """Create a new Icons object.

        @param parent: The parent applet of the icons instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        self.__icon_box = awn.IconBox(parent)
        parent.add(self.__icon_box)

        def update_size():
            size = self.__parent.get_size()
            for icon in self.__icon_box.get_children():
                icon.set_size(size)

        parent.connect_size_changed(update_size)

    def add(self, icon_name, tooltip_text, context_menu=None):
        """Set an icon from the default icon theme and set the applet
        tooltip. Optionally provide a context menu that should be
        displayed instead of the applet's standard context menu. The
        resultant themed icon will be returned.

        @param icon_name: The name of the theme icon.
        @type icon_name: C{string}
        @param tooltip_text: The new tooltip text.
        @type tooltip_text: C{string}
        @param context_menu: Optional context menu.
        @type context_menu: C{gtk.Menu} or C{None}
        @return: The resultant themed icon
        @rtype: C{awn.ThemedIcon}

        """
        icon = awn.ThemedIcon()
        icon.set_info_simple(self.__parent.meta["short"], self.__parent.get_uid(), icon_name)
        icon.set_tooltip_text(tooltip_text)
        icon.set_size(self.__parent.get_size())

        # Callback context menu
        if context_menu is None:

            def popup_menu_cb(widget, event):
                self.__parent.dialog.show_menu(widget, event)
            icon.connect("context-menu-popup", popup_menu_cb)
        else:
            assert isinstance(context_menu, gtk.Menu)

            def popup_menu_cb(widget, event, menu):
                menu.show_all()
                widget.popup_gtk_menu(menu, event.button, event.time)
            icon.connect("context-menu-popup", popup_menu_cb, context_menu)

        icon.show_all()
        self.__icon_box.add(icon)
        return icon

    def remove(self, icon):
        """Remove the specified icon from the applet. The icon will not
        be destroyed.

        @param icon: The icon to be removed.
        @type icon: C{awn.ThemedIcon}

        """
        assert isinstance(icon, awn.ThemedIcon)

        self.__icon_box.remove(icon)

    def destroy_all(self):
        """Remove and destroy all icons in the applet.

        """
        for icon in self.__icon_box.get_children():
            icon.destroy()


class Errors:

    def __init__(self, parent):
        """Create a new Modules object.

        @param parent: The parent applet of the icon instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

    def module(self, scope, name):
        """Tell the user that they need to install a module to use your applet.
        This function will attempts to import the module, and if this is not
        possible, alert the user. Otherwise, it will call your callback with
        the module as the first (and only) argument

        @param scope: The dictionary that contains the globals to
                      import the module into
        @type scope: C{dict}
        @param name: the name of the module that must be installed.
        @type name: C{string}

        """
        try:
            """ Do not add the module to globals[name], otherwise
            awn.check_dependencies() will not show an error dialog. """
            scope[name] = __import__(name, scope)
        except ImportError:
            self.__parent.icon.theme("dialog-error")
            self.__parent.tooltip.set("Python module %s not found" % name)

            awn.check_dependencies(scope, name)

    def set_error_icon_and_click_to_restart(self):
        self.__parent.icon.theme("dialog-error")

        def crash_applet(widget=None, event=None):
            gtk.main_quit()
        self.__parent.connect("clicked", crash_applet)

    def general(self, error, callback=None, traceback=None):
        """Tell the user that an error has occured.

        @param error: the error itself.
        @type error: C{string} or C{Exception}
        @param callback: The function called when the user closes the dialog
        @type callback: C{function}
        @param traceback: Formatted traceback, can be copied to clipboard
        via button in dialog.
        @type traceback: C{str}

        """
        assert isinstance(error, Exception) or type(error) in (str, tuple)

        if traceback is not None:
            traceback = "".join(traceback)[:-1]

        args = {"message": "", "url": None}
        if isinstance(error, Exception):
            error_type = type(error).__name__
            error = str(error)
            if traceback is not None:
                print "\n".join(["-" * 80, traceback, "-" * 80])
                summary = "%s in %s: %s" % (error_type, self.__parent.meta["name"], error)
                if self.__parent.meta["version"] == __version__:
                    args["message"] = "If you speak English and know how a bug tracker works, then visit Launchpad and report the bug by following these steps:\n\n" \
                                    + "1) Paste the error summary text in the 'summary' field\n" \
                                    + "2) Press Continue and then check whether the bug has already been reported or not. Do NOT add duplicates. Instead comment on the bug report that already exists.\n" \
                                    + "3) If you continue and report the bug, put the following in the big textarea:\n" \
                                    + "    - exact version of awn-extras\n" \
                                    + "    - operating system name and version\n" \
                                    + "    - the traceback\n" \
                                    + "    - other info requested by the guidelines found below the big textarea\n\n" \
                                    + "Remember: you must be able to speak English and check regularly whether the developers ask you questions. Do NOT add duplicates, but comment on the existing bug report. You cannot expect a bug to be fixed if you don't provide information.\n\n" \
                                    + "If you don't think you can meet these conditions, then don't file a bug report."
                    args["url"] = bug_report_link
                else:
                    args["message"] = "Report this bug at the bug tracker of the %s applet." % self.__parent.meta["name"]
                    if "bug-report-url" in self.__parent.meta:
                        args["url"] = self.__parent.meta["bug-report-url"]
        else:
            error_type = "Error"
            if isinstance(error, tuple):
                args["message"] = error[1]
                error = error[0]

        dialog = self.ErrorDialog(self.__parent, error_type, error, **args)

        if traceback is not None:
            copy_traceback_button = gtk.Button("Copy traceback to clipboard")
            copy_traceback_button.set_image(gtk.image_new_from_stock(gtk.STOCK_COPY, gtk.ICON_SIZE_MENU))
            dialog.hbox.pack_start(copy_traceback_button, expand=False)

            copy_summary_button = gtk.Button("Copy summary to clipboard")
            copy_summary_button.set_image(gtk.image_new_from_stock(gtk.STOCK_COPY, gtk.ICON_SIZE_MENU))
            dialog.hbox.pack_start(copy_summary_button, expand=False)

            dialog.hbox.reorder_child(copy_traceback_button, 0)
            dialog.hbox.reorder_child(copy_summary_button, 0)

            def clicked_cb(widget, text):
                clipboard = gtk.clipboard_get()
                clipboard.set_text(text)
                clipboard.store()
            copy_traceback_button.connect("clicked", clicked_cb, traceback)
            copy_summary_button.connect("clicked", clicked_cb, summary)

        if callable(callback):

            def response_cb(widget, response):
                if response < 0:
                    callback()
            dialog.connect("response", response_cb)

        dialog.show_all()

    class ErrorDialog(Dialogs.BaseDialog, gtk.MessageDialog):

        """A MessageDialog window that shows an error.

        """

        def __init__(self, parent, error_type, title, message="", url=None):
            gtk.MessageDialog.__init__(self, type=gtk.MESSAGE_ERROR, message_format=title)
            Dialogs.BaseDialog.__init__(self, parent)

            self.__parent = parent

            self.set_skip_taskbar_hint(False)
            self.set_title("%s in %s" % (error_type, parent.meta["name"]))

            self.hbox = gtk.HBox(spacing=6)
            self.action_area.add(self.hbox)

            close_button = gtk.Button(stock=gtk.STOCK_CLOSE)
            close_button.connect("clicked", lambda w: self.response(gtk.RESPONSE_CLOSE))
            self.hbox.add(close_button)

            if len(message) > 0:
                self.format_secondary_markup(message)

            # Make texts non-selectable to stop unhelpful bug reports from stupid users
            for i in self.get_message_area().get_children():
                i.set_selectable(False)

            if url is not None:
                alignment = gtk.Alignment(xalign=0.5, xscale=0.0)
                alignment.add(gtk.LinkButton(url, url))
                self.vbox.pack_start(alignment, expand=False)


class Settings:

    __setting_types = (bool, int, long, float, str, list, Color)

    def __init__(self, parent):
        """Create a new Settings object. This object
        can be used as a dictionary to retrieve and set values of
        configuration keys. More importantly, this object provides
        the methods get_binder() and load_bindings(), which should
        be used to bind keys to their corresponding Gtk+ widgets,
        and to make the keys available as GObject properties.

        @param parent: The parent applet of the settings instance.
        @type parent: L{Applet}

        """
        type_parent = type(parent)
        if type_parent in (AppletSimple, AppletMultiple, config.Client):
            self.__folder = config.GROUP_DEFAULT
        elif type_parent is str:
            self.__folder = parent
            parent = None

        self.__client = self.ConfigClient(self.__folder, parent)

    def get_binder(self, builder):
        """Return an object that can be used to bind keys to their
        corresponding Gtk+ widgets, which are to be retrieved
        via the given C{gtk.Builder} instance.

        @param key: Instance of C{gtk.Builder}, used to retrieve Gtk+ widgets
        @type key: C{gtk.Builder}
        @return: An object that provides the method bind() to bind keys
        @rtype: C{object}

        """
        return self.__client.get_config_binder(builder)

    def load_bindings(self, object):
        """Load the bindings by creating a C{gobject.GObject} from the
        descriptions given by the given binder object. This object
        should be an object that was returned by get_binder(). The
        "props" value (instance of C{gobject.GProps}) of the GObject will
        be returned.

        @param key: An object returned by get_binder()
        @type key: C{object}
        @return: The "props" value of the created GObject
        @rtype: C{gobject.GProps}

        """
        return self.__client.load_bindings(object)

    def __getitem__(self, key):
        """Get a key from the currect directory.

        @param key: A relative path to the correct key
        @type key: C{string}
        @return: The value of the key
        @rtype: C{object}

        """
        value = self.__client.get(key)

        if type(value) is str and value[:9] == "!pickle;\n":
            value = pickle.loads(value[9:])
        return value

    def __setitem__(self, key, value):
        """Set or create a key from the currect directory.

        @param key: A relative path to the correct key
        @type key: C{string}

        """
        unpickled_value = value

        if type(value) not in self.__setting_types:
            value = "!pickle;\n%s" % pickle.dumps(value)
        elif type(value) is long:
            value = int(value)

        self.__client.set(key, value)

    def __contains__(self, key):
        """Test if a key exists in the current directory.

        @param key: A relative path to the correct key
        @type key: C{string}

        """
        return self.__client.contains(key)

    class ConfigClient:

        def __init__(self, folder, client=None):
            """Create a new config client.

            If the client is an C{Applet}, config instances will
            automatically be removed if the applet is deleted.

            @param folder: Folder to start with.
            @type folder: C{string}
            @param client: Applet used to construct a corresponding
            config.Client or a preconstructed config.Client
            @type client: C{None,Applet,config.Client}

            """
            self.__config_object = None
            self.__parent = None

            type_client = type(client)
            if client is None:
                self.__client = awn.config_get_default(awn.PANEL_ID_DEFAULT)
            elif type_client in (AppletSimple, AppletMultiple):
                self.__client = awn.config_get_default_for_applet(client)

                def applet_deleted_cb(applet):
                    self.__client.remove_instance()
                client.connect("applet-deleted", applet_deleted_cb)
                
                self.__parent = client
            elif type_client is config.Client:
                self.__client = client
            else:
                raise RuntimeError("Parameter 'client' must be None, an Applet, or a config.Client")

            self.__folder = folder

        def get_config_binder(self, builder):
            if not isinstance(builder, gtk.Builder):
                raise RuntimeError("Builder must be an instance of gtk.Builder")
            return configbinder.get_config_binder(self.__client, self.__folder, builder)

        def load_bindings(self, binder):
            if self.__config_object is not None:
                raise RuntimeError("Configuration object already set")

            self.__config_object = binder.create_gobject()
            return self.__config_object.props

        def set(self, key, value):
            """Set an existing key's value.

            @param key: The name of the key, relative to the current folder.
            @type key: C{string}
            @param value: The value to set the key to.
            @type value: C{bool}, C{int}, C{float}, or C{string}

            """
            try:
                self.__config_object.set_property(key, value)
            except:
                try:
                    self.__client.set_value(self.__folder, key, value)
                except:
                    name = self.__parent.meta["name"] if self.__parent is not None else "UNKNOWN" 
                    print "%s: Could not set new value for key '%s'" % (name, key)
                    raise

        def get(self, key):
            """Get an existing key's value.

            @param key: The name of the key, relative to the current folder.
            @type key: C{string}
            @return: The value of the key
            @rtype: C{object}

            """
            try:
                return self.__config_object.get_property(key)
            except:
                try:
                    return self.__client.get_value(self.__folder, key)
                except:
                    name = self.__parent.meta["name"] if self.__parent is not None else "UNKNOWN"
                    print "%s: key '%s' does not exist" % (name, key)
                    raise

        def contains(self, key):
            """Test if the key maps to a value.

            @param key: The name of the key, relative to the current folder.
            @type key: C{string}
            @return: True if the key maps to a value, False otherwise
            @rtype: C{bool}

            """
            r = False
            if self.__config_object is not None:
                r = key in gobject.list_properties(self.__config_object)
            if r:
                return r
            try:
                self.__client.get_value(self.__folder, key)
            except Exception, e:
                if str(e).split(":", 1)[0] == "Could not find the key specified":
                    return False
            return True


class Keyring:

    def __init__(self, parent=None):
        """Create a new Keyring object. This includes importing the keyring
        module and connecting to the daemon.

        @param parent: The parent applet of the keyring instance.
        @type parent: L{Applet}

        """
        if parent is not None:
            self.__parent = parent

            self.__parent.errors.module(globals(), "gnomekeyring")
        else:
            awn.check_dependencies(globals(), "gnomekeyring")

        if not gnomekeyring.is_available():
            raise KeyringError("Keyring not available")

        keyring_list = gnomekeyring.list_keyring_names_sync()

        if len(keyring_list) == 0:
            raise KeyringError("No keyrings available")

        try:
            gnomekeyring.get_default_keyring_sync()
        except gnomekeyring.NoKeyringDaemonError:
            raise KeyringError("Had trouble connecting to daemon")

    def new(self, keyring=None, name=None, pwd=None, attrs={}, type="generic"):
        """Create a new keyring key.

        @param keyring: The keyring holding the key. If omitted, the default
            keyring is returned.
        @type keyring: C{string}
        @param name: The display name of the key. If omitted, an empty key is
            returned.
        @type name: C{string}
        @param pwd: The password stored in the key. If omitted, empty key is
            returned.
        @type pwd: C{string}
        @param attrs: Other attributes stored in the key. By default: {}
        @type attrs: C{dict}
        @param type: The type of key. By default: "generic"
        @type type: C{string}; "generic", "network", or "note"
        @return: A new L{Key} object
        @rtype: L{Key}

        """
        k = self.Key(keyring)
        if name and pwd:
            k.set(k.keyring, name, pwd, attrs, type)
        return k

    def from_token(self, keyring, token):
        """Load the key with the given token. Note: If keyring is None, the
        default keyring is used. However, this is not recommended.

        @param keyring: The keyring holding the key. If omitted, the default
            keyring is used.
        @type keyring: C{string}
        @param token: The password token of the key
        @type token: C{int} or C{long}
        @return: A new L{Key} object
        @rtype: L{Key}

        """
        k = self.Key(keyring, token)

        keys = gnomekeyring.list_item_ids_sync(k.keyring)
        if k.token not in keys:
            raise KeyringNoMatchError("Token does not exist")

        return k

    class Key(object):

        def __init__(self, keyring=None, token=0):
            """Create a new key. If keyring is None, the default keyring or
            "login" keyring is used. Note: self.keyring will hold the name of
            the keyring eventually used. To identify a key unambiguously
            keyring name and token are needed.

            @param keyring: The keyring holding the key. If omitted, the
                default keyring is used.
            @type keyring: C{string}
            @param token: The token of an already-existing key. Optional.
            @type token: C{long}

            """
            keyring_list = gnomekeyring.list_keyring_names_sync()
            if keyring is None:
                keyring = gnomekeyring.get_default_keyring_sync()
                if keyring is None:
                    if "login" in keyring_list:
                        keyring = "login"
                    else:
                        raise KeyringError("No default keyring set")
            if keyring not in keyring_list:
                raise KeyringNoMatchError("Keyring does not exist")

            self.keyring = keyring
            self.token = token

        def set(self, keyring, name, pwd, attrs={}, type="generic"):
            """Create a new keyring key. Note that if another key
            exists with the same name, it will be overwritten.

            @param keyring: The keyring holding the key.
            @type keyring: C{string}
            @param name: The display name of the key.
            @type name: C{string}
            @param pwd: The password stored in the key.
            @type pwd: C{string}
            @param attrs: Other attributes stored in the key. By default: {}
            @type attrs: C{dict}
            @param type: The type of key. By default: "generic"
            @type type: C{string}; "generic", "network", or "note"

            """
            if type == "network":
                type = gnomekeyring.ITEM_NETWORK_PASSWORD
            elif type == "note":
                type = gnomekeyring.ITEM_NOTE
            else:  # Generic included
                type = gnomekeyring.ITEM_GENERIC_SECRET

            try:
                self.token = gnomekeyring.item_create_sync(keyring, type, \
                    name, attrs, pwd, True)
                self.keyring = keyring
            except gnomekeyring.CancelledError:
                self.token = 0

        def __unlock(self):
            """Unlock the key's keyring."""

            info = gnomekeyring.get_info_sync(self.keyring)
            if not info.get_is_locked():
                return

            # The straight way would be:
            # gnomekeyring.unlock_sync(self.keyring, None)
            # But this results in a type error, see launchpad bugs #432882.
            # We create a dummy key instead, this triggers a user dialog to
            # unlock the keyring. We delete the dummy key then immediately.
            try:
                tmp = gnomekeyring.item_create_sync(self.keyring, \
                      gnomekeyring.ITEM_GENERIC_SECRET, "awn-extras dummy", \
                      {"dummy_attr": "none"}, "dummy_pwd", True)
            except gnomekeyring.CancelledError:
                raise KeyringCancelledError("Operation cancelled by user")
            try:
                gnomekeyring.item_delete_sync(self.keyring, tmp)
            except gnomekeyring.BadArgumentsError:
                # Race condition if several applets use this method at once
                pass

        def delete(self):
            """Delete the current key. Will also reset the token. Note that
            "del [Key]" will not delete the key itself; that would be too
            destructive. delete() MUST be called manually.

            """
            self.__unlock()
            gnomekeyring.item_delete_sync(self.keyring, self.token)
            self.token = 0

        def __get(self):
            self.__unlock()
            return gnomekeyring.item_get_info_sync(self.keyring, self.token)

        def __getAttrs(self):
            self.__unlock()
            return gnomekeyring.item_get_attributes_sync(self.keyring, self.token)

        def __setAttrs(self, a):
            self.__unlock()
            return gnomekeyring.item_set_attributes_sync(self.keyring, self.token, a)

        def __getName(self):
            return self.__get().get_display_name()

        def __setName(self, name):
            info = self.__get()
            info.set_display_name(name)
            return gnomekeyring.item_set_info_sync(self.keyring, self.token, info)

        def __getPass(self):
            return self.__get().get_secret()

        def __setPass(self, passwd):
            info = self.__get()
            info.set_secret(passwd)
            return gnomekeyring.item_set_info_sync(self.keyring, self.token, info)

        attrs = property(__getAttrs, __setAttrs)
        """
        @ivar: The other attributes stored in the Key. Can be used like any
        property.
        """

        name = property(__getName, __setName)
        """
        @ivar: The display name of the Key. Can be used like any property
        """

        password = property(__getPass, __setPass)
        """
        @ivar: The password stored in the Key. Can be used like any property.
        """


class Timing:

    """Provides utilities to register a function to be called periodically
    or once after a specified delay.

    """

    def __init__(self, parent):
        """Create a new Timing object.

        @param parent: The parent applet of the timing instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

    def register(self, callback, seconds, start=True):
        """Register a function to be called periodically.

        @param callback: Function to be called.
        @type callback: C{function}
        @param seconds: Number of seconds within each call.
        @type seconds: C{float} or C{int}
        @param start: Whether to start the callback automatically
        @type start: C{bool}
        @return: A L{Callback} object for the C{callback} parameter
        @rtype: L{Callback}

        """
        def callback_wrapper():
            callback()
            return True
        cb = self.Callback(callback_wrapper, seconds)
        if start:
            cb.start()
        return cb

    def delay(self, callback, seconds, start=True):
        """Delay the execution of the given callback.

        @param callback: Function
        @type callback: C{function}
        @param seconds: Number of seconds to delay function call
        @type seconds: C{float} or C{int}
        @return: A L{Callback} object for the C{callback} parameter
        @rtype: L{Callback}

        """
        def callback_wrapper():
            callback()
            return False
        cb = self.Callback(callback_wrapper, seconds)
        if start:
            cb.start()
        return cb

    class Callback:

        """Wrapper around a callback function to provide ways to start and
        stop the function, to change the interval or to test if the callback
        is scheduled to run.

        """

        def __init__(self, callback, seconds):
            """Create a new C{Callback} object.

            @param callback: The function to wrap the Callback around.
            @type callback: C{function}
            @param seconds: Number of seconds within each call.
            @type seconds: C{float} or C{int}

            """
            assert seconds > 0.0

            self.__callback = callback
            self.__seconds = seconds
            self.__timer_id = None

        def is_started(self):
            """Return True if the callback has been scheduled to run after
            each interval, False if the callback is stopped.

            @return: True if the callback has been scheduled, False otherwise
            @rtype: L{bool}

            """
            return self.__timer_id is not None

        def start(self):
            """Start executing the callback periodically.

            @return: True if the callback was started, False otherwise
            @rtype: L{bool}

            """
            if self.__timer_id is not None:
                return False

            if int(self.__seconds) == self.__seconds:
                self.__timer_id = glib.timeout_add_seconds(int(self.__seconds), self.__callback)
            else:
                self.__timer_id = glib.timeout_add(int(self.__seconds * 1000), self.__callback)
            return True

        def stop(self):
            """Stop the callback from running again if it was scheduled
            to run.

            @return: True if the callback was stopped, False otherwise
            @rtype: L{bool}

            """
            if self.__timer_id is None:
                return False

            glib.source_remove(self.__timer_id)
            self.__timer_id = None
            return True

        def change_interval(self, seconds):
            """Change the interval and restart the callback if it was scheduled
            to run.

            @param seconds: Number of seconds within each call.
            @type seconds: C{float} or C{int}

            """
            assert seconds > 0.0

            self.__seconds = seconds

            # Restart if the callback was scheduled to run
            if self.stop():
                self.start()


class Notify:

    def __init__(self, parent):
        """Create a new Notify object.

        @param parent: The parent applet of the notify instance.
        @type parent: L{Applet}

        """
        self.__parent = parent

        awn.check_dependencies(globals(), "pynotify")

        pynotify.init(parent.meta["short"])

    def __del__(self):
        pynotify.uninit()

    def send(self, *args, **kwargs):
        """Show a new notification via libnotify.

        @param subject: The subject of your message. If blank, "Message from
            [applet name]" is used.
        @type subject: C{string}
        @param body: The main body of your message. Blank by default.
        @type body: C{string}
        @param icon: The full absolute path to the name of the icon to use.
        @type icon: C{string}
        @param timeout: Timeout in seconds after which the message closes
        @type timeout: C{int}

        """
        notification = self.Notification(self.__parent, *args, **kwargs)
        notification.show()

    def create(self, *args, **kwargs):
        """Return a notification that can be shown via show().

        @param subject: The subject of your message. If blank, "Message from
            [applet name]" is used.
        @type subject: C{string}
        @param body: The main body of your message. Blank by default.
        @type body: C{string}
        @param icon: The full absolute path to the name of the icon to use.
        @type icon: C{string}
        @param timeout: Timeout in seconds after which the message closes
        @type timeout: C{int}
        @return: a notification object
        @rtype: C{self.Notification}

        """
        return self.Notification(self.__parent, *args, **kwargs)

    class Notification:

        """An object that manages a libnotify notification.

        """

        def __init__(self, parent, subject=None, body="", icon="", timeout=0):
            if subject is None:
                subject = '"Message From %s"' % parent.meta["name"]
            self.__notification = pynotify.Notification(subject, body, icon)
            if timeout > 0:
                self.__notification.set_timeout(timeout * 1000)

        def show(self):
            try:
                self.__notification.show()
            except glib.GError:
                pass  # Ignore error when no reply has been received


class Meta:

    def __init__(self, parent, info={}, options=()):
        """Create a new Meta object.

        @param parent: The parent applet of the meta instance.
        @type parent: L{Applet}
        @param info: Values for the meta dictionary
        @type info: C{dict}
        @param options: Options to set. Format:
            (option", "option", ("option": True|False), ("option":
                ("suboption", "suboption", ("suboption": True|False), ...)))

        """
        assert "name" in info

        self.__parent = parent

        self.__info = info
        self.__options = options

    def has_option(self, option):
        """Check if the applet has set a specific option.

        @param option: Option to check
        @type option: C{str}

        """
        return option in self.__options

    def __getitem__(self, key):
        """Get a key from the dictionary.

        @param key: The key
        @type key: C{string}

        """
        return self.__info[key]

    def keys(self):
        """Return a list of keys from the dictionary.

        """
        return self.__info.keys()

    def __contains__(self, key):
        """Return True if the dictionary contains the key, False otherwise.

        @param key: The key
        @type key: C{string}

        """
        return key in self.__info


def _getmodule(module):
    """Return a getter that lazy-loads a module, represented by a
    single instantiated class.

    @param module: The class of the module to initialize and get
    @type module: C{class}

    """
    instance = {}

    def getter(self):
        key = (self, module)
        if key not in instance:
            instance[key] = module(self)
        return instance[key]
    return property(getter)


class Applet(object):

    def __init__(self, meta, options):
        """Create a new instance of the Applet object.

        @param meta: The meta information to be passed to the Meta constructor
        @type meta: C{dict}

        """
        # Create all required child-objects, others will be lazy-loaded
        self.meta = Meta(self, meta, options)

    def connect_size_changed(self, callback):
        self.connect("size-changed", lambda w, e: callback())

    settings = _getmodule(Settings)
    timing = _getmodule(Timing)
    keyring = _getmodule(Keyring)
    notification = _getmodule(Notify)


class AppletSimple(awn.AppletSimple, Applet):

    def __init__(self, uid, panel_id, meta={}, options=[]):
        """Create a new instance of the AppletSimple object.

        @param uid: The unique identifier of the applet
        @type uid: C{string}
        @param panel_id: Identifier of the panel in which the applet resides.
        @type panel_id: C{int}

        """
        awn.AppletSimple.__init__(self, meta["short"], uid, panel_id)
        Applet.__init__(self, meta, options)

        # Create all required child-objects, others will be lazy-loaded
        self.tooltip = Tooltip(self)
        self.dialog = Dialogs(self)
        self.icon = Icon(self)

        self.dialog.connect_signals(self)

    theme = _getmodule(Theme)
    errors = _getmodule(Errors)


class AppletMultiple(awn.Applet, Applet):

    def __init__(self, uid, panel_id, meta={}, options=[]):
        """Create a new instance of the AppletMultiple object.

        @param uid: The unique identifier of the applet
        @type uid: C{string}
        @param panel_id: Identifier of the panel in which the applet resides.
        @type panel_id: C{int}

        """
        awn.Applet.__init__(self, meta["short"], uid, panel_id)
        Applet.__init__(self, meta, options)

        # Create all required child-objects, others will be lazy-loaded
        self.icons = Icons(self)
        self.dialog = Dialogs(self)


def init_start(applet_class, meta={}, options=[]):
    """Do the work to create a new applet, and then start the applet.
    This makes the icon appear on the bar and starts GTK+.

    The callable applet_class parameter is called and given an instance of
    C{Applet}. It can then set an icon, tooltip, dialogs, and other things,
    before GTK+ starts, which makes the icon appear on the AWN panel.

    @param applet_class A callable, used to do some initialization
    @type applet_class: C{callable}
    @param meta: The meta-information to pass to the constructor
    @type meta: C{dict}
    @param options: Options to set for the new applet
    @type options: C{list} or C{tuple}
    @return: The newly created applet.
    @rtype: L{Applet}

    """
    assert callable(applet_class)

    glib.threads_init()

    awn.init(sys.argv[1:])
    if "multiple-icons" in options:
        applet = AppletMultiple(awn.uid, awn.panel_id, meta, options)
    else:
        applet = AppletSimple(awn.uid, awn.panel_id, meta, options)

    try:
        applet_class(applet)
    except Exception, e:
        # TODO don't know what to do for multiple-icons applets
        if "multiple-icons" not in options:
            applet.errors.set_error_icon_and_click_to_restart()
            import traceback
            traceback = traceback.format_exception(type(e), e, sys.exc_traceback)
            applet.errors.general(e, traceback=traceback, callback=gtk.main_quit)
        else:
            raise

    awn.embed_applet(applet)
    gtk.main()