Welcome, guest | Sign In | My Account | Store | Cart

After writing the limited program shown in recipe 577637, the following program was written with a better yet incompatible I/O system designed not to fill up a file share with many, separate files. This program had five revisions as outlined within the source code. To access settings within this program, use the "F2" key. Documentation may be accessed via the "F1" key (with future plans cut).

If anyone wishes to comment or vote this recipe down, please provide your insight into the fault(s) of the program and provide a suggestion as to what solution you would implement to fix the problems.

Python, 1681 lines
   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
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
#! /usr/bin/env python
import tkinter.ttk
import tkinter.messagebox
import tkinter.font
import idlelib.textView

import datetime
import getpass
import os
import string
import random
import colorsys
import urllib.parse
import webbrowser
import pickle
import traceback
import sys
import contextlib
import io

################################################################################

class Color:

    HTML = dict(reversed(color.split(' ')) for color in '''\
#F0F8FF AliceBlue
#FAEBD7 AntiqueWhite
#00FFFF Aqua
#7FFFD4 Aquamarine
#F0FFFF Azure
#F5F5DC Beige
#FFE4C4 Bisque
#000000 Black
#FFEBCD BlanchedAlmond
#0000FF Blue
#8A2BE2 BlueViolet
#A52A2A Brown
#DEB887 BurlyWood
#5F9EA0 CadetBlue
#7FFF00 Chartreuse
#D2691E Chocolate
#FF7F50 Coral
#6495ED CornflowerBlue
#FFF8DC Cornsilk
#DC143C Crimson
#00FFFF Cyan
#00008B DarkBlue
#008B8B DarkCyan
#B8860B DarkGoldenRod
#A9A9A9 DarkGray
#A9A9A9 DarkGrey
#006400 DarkGreen
#BDB76B DarkKhaki
#8B008B DarkMagenta
#556B2F DarkOliveGreen
#FF8C00 Darkorange
#9932CC DarkOrchid
#8B0000 DarkRed
#E9967A DarkSalmon
#8FBC8F DarkSeaGreen
#483D8B DarkSlateBlue
#2F4F4F DarkSlateGray
#2F4F4F DarkSlateGrey
#00CED1 DarkTurquoise
#9400D3 DarkViolet
#FF1493 DeepPink
#00BFFF DeepSkyBlue
#696969 DimGray
#696969 DimGrey
#1E90FF DodgerBlue
#B22222 FireBrick
#FFFAF0 FloralWhite
#228B22 ForestGreen
#FF00FF Fuchsia
#DCDCDC Gainsboro
#F8F8FF GhostWhite
#FFD700 Gold
#DAA520 GoldenRod
#808080 Gray
#808080 Grey
#008000 Green
#ADFF2F GreenYellow
#F0FFF0 HoneyDew
#FF69B4 HotPink
#CD5C5C IndianRed
#4B0082 Indigo
#FFFFF0 Ivory
#F0E68C Khaki
#E6E6FA Lavender
#FFF0F5 LavenderBlush
#7CFC00 LawnGreen
#FFFACD LemonChiffon
#ADD8E6 LightBlue
#F08080 LightCoral
#E0FFFF LightCyan
#FAFAD2 LightGoldenRodYellow
#D3D3D3 LightGray
#D3D3D3 LightGrey
#90EE90 LightGreen
#FFB6C1 LightPink
#FFA07A LightSalmon
#20B2AA LightSeaGreen
#87CEFA LightSkyBlue
#778899 LightSlateGray
#778899 LightSlateGrey
#B0C4DE LightSteelBlue
#FFFFE0 LightYellow
#00FF00 Lime
#32CD32 LimeGreen
#FAF0E6 Linen
#FF00FF Magenta
#800000 Maroon
#66CDAA MediumAquaMarine
#0000CD MediumBlue
#BA55D3 MediumOrchid
#9370D8 MediumPurple
#3CB371 MediumSeaGreen
#7B68EE MediumSlateBlue
#00FA9A MediumSpringGreen
#48D1CC MediumTurquoise
#C71585 MediumVioletRed
#191970 MidnightBlue
#F5FFFA MintCream
#FFE4E1 MistyRose
#FFE4B5 Moccasin
#FFDEAD NavajoWhite
#000080 Navy
#FDF5E6 OldLace
#808000 Olive
#6B8E23 OliveDrab
#FFA500 Orange
#FF4500 OrangeRed
#DA70D6 Orchid
#EEE8AA PaleGoldenRod
#98FB98 PaleGreen
#AFEEEE PaleTurquoise
#D87093 PaleVioletRed
#FFEFD5 PapayaWhip
#FFDAB9 PeachPuff
#CD853F Peru
#FFC0CB Pink
#DDA0DD Plum
#B0E0E6 PowderBlue
#800080 Purple
#FF0000 Red
#BC8F8F RosyBrown
#4169E1 RoyalBlue
#8B4513 SaddleBrown
#FA8072 Salmon
#F4A460 SandyBrown
#2E8B57 SeaGreen
#FFF5EE SeaShell
#A0522D Sienna
#C0C0C0 Silver
#87CEEB SkyBlue
#6A5ACD SlateBlue
#708090 SlateGray
#708090 SlateGrey
#FFFAFA Snow
#00FF7F SpringGreen
#4682B4 SteelBlue
#D2B48C Tan
#008080 Teal
#D8BFD8 Thistle
#FF6347 Tomato
#40E0D0 Turquoise
#EE82EE Violet
#F5DEB3 Wheat
#FFFFFF White
#F5F5F5 WhiteSmoke
#FFFF00 Yellow
#9ACD32 YellowGreen'''.split('\n'))

    @property
    def best_name(self):
        diffs = []
        for name in self.HTML.keys():
            diffs.append((name, self.diff(getattr(self, name))))
        error = min(diffs, key=lambda pair: pair[1])[1]
        return tuple(pair[0] for pair in diffs if pair[1] == error)

    ########################################################################

    @classmethod
    def hsv(cls, hue, saturation, value):
        assert 0 <= hue <= 1 and 0 <= saturation <= 1 and 0 <= value <= 1
        r, g, b = colorsys.hsv_to_rgb(hue, saturation, value)
        return cls(round(r * 0xFF), round(g * 0xFF), round(b * 0xFF))

    @classmethod
    def parse(cls, string):
        assert len(string) == 7 and string[0] == '#'
        return cls(int(string[1:3], 16),
                   int(string[3:5], 16),
                   int(string[5:7], 16))

    ########################################################################

    def __init__(self, red, green, blue):
        self.__rgb = bytes((red, green, blue))

    def __str__(self):
        return '#{:02X}{:02X}{:02X}'.format(*self.__rgb)

    def __repr__(self):
        return '{}({}, {}, {})'.format(self.__class__.__name__, *self.__rgb)

    def __hash__(self):
        return hash(self.__rgb)

    def __eq__(self, other):
        return self.__rgb == other.__rgb

    ########################################################################

    @property
    def red(self):
        return self.__rgb[0]

    @property
    def green(self):
        return self.__rgb[1]

    @property
    def blue(self):
        return self.__rgb[2]

    r, g, b = red, green, blue

    def set_red(self, value):
        return self.__class__(value, self.g, self.b)

    def set_green(self, value):
        return self.__class__(self.r, value, self.b)

    def set_blue(self, value):
        return self.__class__(self.r, self.g, value)

    def add_red(self, value):
        return self.__class__(self.r + value & 0xFF, self.g, self.b)

    def add_green(self, value):
        return self.__class__(self.r, self.g + value & 0xFF, self.b)

    def add_blue(self, value):
        return self.__class__(self.r, self.g, self.b + value & 0xFF)

    ########################################################################

    @property
    def hue(self):
        return colorsys.rgb_to_hsv(self.__rgb[0] / 0xFF,
                                   self.__rgb[1] / 0xFF,
                                   self.__rgb[2] / 0xFF)[0]

    @property
    def saturation(self):
        return colorsys.rgb_to_hsv(self.__rgb[0] / 0xFF,
                                   self.__rgb[1] / 0xFF,
                                   self.__rgb[2] / 0xFF)[1]

    @property
    def value(self):
        return colorsys.rgb_to_hsv(self.__rgb[0] / 0xFF,
                                   self.__rgb[1] / 0xFF,
                                   self.__rgb[2] / 0xFF)[2]

    h, s, v = hue, saturation, value

    def set_hue(self, value):
        return self.hsv(value, self.s, self.v)

    def set_saturation(self, value):
        return self.hsv(self.h, value, self.v)

    def set_value(self, value):
        return self.hsv(self.h, self.s, value)

    def add_hue(self, value):
        return self.hsv(self.__mod(self.h + value), self.s, self.v)

    def add_saturation(self, value):
        return self.hsv(self.h, self.__mod(self.s + value), self.v)

    def add_value(self, value):
        return self.hsv(self.h, self.s, self.__mod(self.v + value))

    ########################################################################

    def invert(self):
        return self.__class__(0xFF - self.r, 0xFF - self.g, 0xFF - self.b)

    def rotate(self, value):
        return self.__class__(self.r + value & 0xFF,
                              self.g + value & 0xFF,
                              self.b + value & 0xFF)

    def diff(self, other):
        r = (self.r - other.r) ** 2
        g = (self.g - other.g) ** 2
        b = (self.b - other.b) ** 2
        return r + g + b

    def mix(self, bias, other):
        assert 0 <= bias <= 1
        alpha = 1 - bias
        return self.__class__(round(self.r * alpha + other.r * bias),
                              round(self.g * alpha + other.g * bias),
                              round(self.b * alpha + other.b * bias))

    @staticmethod
    def get(bias, *colors):
        assert 0 <= bias <= 1
        ranges = len(colors) - 1
        assert ranges > 0
        length = 1 / ranges
        index = int(bias / length)
        if index == ranges:
            return colors[-1]
        first, second = colors[index:index+2]
        return first.mix(bias % length / length, second)

    ########################################################################

    @staticmethod
    def __mod(value):
        div, mod = divmod(value, 1.0)
        if div > 0.0 and not mod:
            return 1.0
        return mod

for key, value in Color.HTML.items():
    setattr(Color, key, Color.parse(value))

################################################################################

class ColorOptions(tkinter.Toplevel):

    LABEL = dict(width=9, anchor=tkinter.CENTER)
    SCALE = dict(orient=tkinter.HORIZONTAL, length=256, from_=0.0, to=1.0)
    VALUE = dict(text='0.0', width=5, relief=tkinter.GROOVE)
    BYTE = dict(text='00', width=3, relief=tkinter.GROOVE,
                anchor=tkinter.CENTER)
    PADDING = dict(padx=2, pady=2)

    ########################################################################

    OPEN = False

    @classmethod
    def open_window(cls, root, color):
        # Only open if not already open and return selection.
        if not cls.OPEN:
            cls.OPEN = True
            window = cls(root, color)
            window.mainloop()
            return window.color
        return ''

    ########################################################################

    def __init__(self, master, color):
        super().__init__(master)
        self.transient(master)
        self.geometry('+{}+{}'.format(master.winfo_rootx(),
                                      master.winfo_rooty()))
        # Build all the widgets that will in the window.
        self.create_interface()
        # Populate the widgets with the correct settings.
        self.load_widget_settings(color)
        # Override the closing of this window to keep track of its state.
        self.protocol('WM_DELETE_WINDOW', self.ask_destroy)
        # Prepare the window for general display.
        self.title('Colors')
        self.resizable(False, False)
        # Create a message box to warn about closing.
        options = dict(title='Warning?', icon=tkinter.messagebox.QUESTION,
                       type=tkinter.messagebox.YESNO, message='''\
Are you sure you want to close?
You will lose all your changes.''')
        self.__cancel_warning = tkinter.messagebox.Message(self, **options)

    def load_widget_settings(self, color):
        # Set the colors.
        color = Color.parse(color)
        self.update_hsv(color.h, color.s, color.v)
        self.hsv_updated(color)

    @property
    def color(self):
        # Return the color of the canvas.
        return self.__color

    def ask_destroy(self):
        # Only close if user wants to lose settings.
        if self.__cancel_warning.show() == tkinter.messagebox.YES:
            self.destroy()
        else:
            self.focus_set()

    def destroy(self):
        # Destroy this window and unset the OPEN flag.
        super().destroy()
        self.quit()
        self.__class__.OPEN = False

    def create_interface(self):
        # Create all the widgets.
        self.rgb_scales = self.create_rgb_scales()
        self.hsv_scales = self.create_hsv_scales()
        self.color_area = self.create_color_area()
        self.input_buttons = self.create_buttons()
        # Place them on the grid.
        self.rgb_scales.grid(row=0, column=0)
        self.hsv_scales.grid(row=1, column=0)
        self.color_area.grid(row=2, column=0, sticky=tkinter.EW)
        self.input_buttons.grid(row=3, column=0, sticky=tkinter.EW)

    def create_rgb_scales(self):
        rgb_scales = tkinter.ttk.Labelframe(self, text='RGB Scales')
        # Create the inner widget.
        self.r_label = tkinter.ttk.Label(rgb_scales, text='Red', **self.LABEL)
        self.g_label = tkinter.ttk.Label(rgb_scales, text='Green', **self.LABEL)
        self.b_label = tkinter.ttk.Label(rgb_scales, text='Blue', **self.LABEL)
        self.r_scale = tkinter.ttk.Scale(rgb_scales, command=self.rgb_updated,
                                         **self.SCALE)
        self.g_scale = tkinter.ttk.Scale(rgb_scales, command=self.rgb_updated,
                                         **self.SCALE)
        self.b_scale = tkinter.ttk.Scale(rgb_scales, command=self.rgb_updated,
                                         **self.SCALE)
        self.r_value = tkinter.ttk.Label(rgb_scales, **self.VALUE)
        self.g_value = tkinter.ttk.Label(rgb_scales, **self.VALUE)
        self.b_value = tkinter.ttk.Label(rgb_scales, **self.VALUE)
        self.r_byte = tkinter.ttk.Label(rgb_scales, **self.BYTE)
        self.g_byte = tkinter.ttk.Label(rgb_scales, **self.BYTE)
        self.b_byte = tkinter.ttk.Label(rgb_scales, **self.BYTE)
        # Place widgets on grid.
        self.r_label.grid(row=0, column=0, **self.PADDING)
        self.g_label.grid(row=1, column=0, **self.PADDING)
        self.b_label.grid(row=2, column=0, **self.PADDING)
        self.r_scale.grid(row=0, column=1, **self.PADDING)
        self.g_scale.grid(row=1, column=1, **self.PADDING)
        self.b_scale.grid(row=2, column=1, **self.PADDING)
        self.r_value.grid(row=0, column=2, **self.PADDING)
        self.g_value.grid(row=1, column=2, **self.PADDING)
        self.b_value.grid(row=2, column=2, **self.PADDING)
        self.r_byte.grid(row=0, column=3, **self.PADDING)
        self.g_byte.grid(row=1, column=3, **self.PADDING)
        self.b_byte.grid(row=2, column=3, **self.PADDING)
        # Return the label frame.
        return rgb_scales

    def create_hsv_scales(self):
        hsv_scales = tkinter.ttk.Labelframe(self, text='HSV Scales')
        # Create the inner widget.
        self.h_label = tkinter.ttk.Label(hsv_scales, text='Hue', **self.LABEL)
        self.s_label = tkinter.ttk.Label(hsv_scales, text='Saturation',
                                         **self.LABEL)
        self.v_label = tkinter.ttk.Label(hsv_scales, text='Value', **self.LABEL)
        self.h_scale = tkinter.ttk.Scale(hsv_scales, command=self.hsv_updated,
                                         **self.SCALE)
        self.s_scale = tkinter.ttk.Scale(hsv_scales, command=self.hsv_updated,
                                         **self.SCALE)
        self.v_scale = tkinter.ttk.Scale(hsv_scales, command=self.hsv_updated,
                                         **self.SCALE)
        self.h_value = tkinter.ttk.Label(hsv_scales, **self.VALUE)
        self.s_value = tkinter.ttk.Label(hsv_scales, **self.VALUE)
        self.v_value = tkinter.ttk.Label(hsv_scales, **self.VALUE)
        self.h_byte = tkinter.ttk.Label(hsv_scales, **self.BYTE)
        self.s_byte = tkinter.ttk.Label(hsv_scales, **self.BYTE)
        self.v_byte = tkinter.ttk.Label(hsv_scales, **self.BYTE)
        # Place widgets on grid.
        self.h_label.grid(row=0, column=0, **self.PADDING)
        self.s_label.grid(row=1, column=0, **self.PADDING)
        self.v_label.grid(row=2, column=0, **self.PADDING)
        self.h_scale.grid(row=0, column=1, **self.PADDING)
        self.s_scale.grid(row=1, column=1, **self.PADDING)
        self.v_scale.grid(row=2, column=1, **self.PADDING)
        self.h_value.grid(row=0, column=2, **self.PADDING)
        self.s_value.grid(row=1, column=2, **self.PADDING)
        self.v_value.grid(row=2, column=2, **self.PADDING)
        self.h_byte.grid(row=0, column=3, **self.PADDING)
        self.s_byte.grid(row=1, column=3, **self.PADDING)
        self.v_byte.grid(row=2, column=3, **self.PADDING)
        # Return the label frame.
        return hsv_scales

    def create_color_area(self):
        # Create a display area set to black to begin with.
        color_area = tkinter.ttk.Labelframe(self, text='Color Sample')
        self.canvas = tkinter.Canvas(color_area, height=70,
                                     background='#000000')
        self.canvas.grid(row=0, column=0)
        return color_area

    def create_buttons(self):
        # Create a frame for the buttons.
        input_buttons = tkinter.ttk.Frame(self)
        # Create the buttons.
        self.empty_space = tkinter.ttk.Label(input_buttons, width=38)
        self.okay_button = tkinter.ttk.Button(input_buttons, text='Accept',
                                              command=self.accept)
        self.cancel_button = tkinter.ttk.Button(input_buttons, text='Cancel',
                                                command=self.cancel)
        # Place them on the grid.
        self.empty_space.grid(row=0, column=0, sticky=tkinter.EW)
        self.okay_button.grid(row=0, column=1, sticky=tkinter.EW)
        self.cancel_button.grid(row=0, column=2, sticky=tkinter.EW)
        # Return the containing frame.
        return input_buttons

    def accept(self):
        # Close the window and allow color to be returned.
        self.destroy()

    def cancel(self):
        # Cancel the color before closing window.
        self.__color = ''
        self.destroy()

    def rgb_updated(self, value):
        # Update the interface after RBG change.
        r = self.r_scale['value']
        g = self.g_scale['value']
        b = self.b_scale['value']
        self.update_rgb(r, g, b)
        h, s, v = colorsys.rgb_to_hsv(r, g, b)
        self.update_hsv(h, s, v)
        self.update_color_area()

    def hsv_updated(self, value):
        # Update the interface after HSV change.
        h = self.h_scale['value']
        s = self.s_scale['value']
        v = self.v_scale['value']
        self.update_hsv(h, s, v)
        r, g, b = colorsys.hsv_to_rgb(h, s, v)
        self.update_rgb(r, g, b)
        self.update_color_area()

    def update_rgb(self, r, g, b):
        # Update RGB values to those given.
        self.r_scale['value'] = r
        self.g_scale['value'] = g
        self.b_scale['value'] = b
        self.r_value['text'] = str(r)[:5]
        self.g_value['text'] = str(g)[:5]
        self.b_value['text'] = str(b)[:5]
        self.r_byte['text'] = '{:02X}'.format(round(r * 255))
        self.g_byte['text'] = '{:02X}'.format(round(g * 255))
        self.b_byte['text'] = '{:02X}'.format(round(b * 255))
        
    def update_hsv(self, h, s, v):
        # Update HSV values to those given.
        self.h_scale['value'] = h
        self.s_scale['value'] = s
        self.v_scale['value'] = v
        self.h_value['text'] = str(h)[:5]
        self.s_value['text'] = str(s)[:5]
        self.v_value['text'] = str(v)[:5]
        self.h_byte['text'] = '{:02X}'.format(round(h * 255))
        self.s_byte['text'] = '{:02X}'.format(round(s * 255))
        self.v_byte['text'] = '{:02X}'.format(round(v * 255))

    def update_color_area(self):
        # Change the color of preview area based on RGB.
        color = '#{}{}{}'.format(self.r_byte['text'],
                                 self.g_byte['text'],
                                 self.b_byte['text'])
        self.canvas['background'] = color
        self.__color = color

################################################################################

class AboutFSM(tkinter.Toplevel):

    NEW_FEATURES = '''\
What's New in FSM 2.5?
=========================

- Timestamps are still encoded in GMT but are automatically converted
  to local time when displayed. Program will not need to be restarted
  if daylight savings time changes while program is running.

- Errors will be recorded to the "FSM Settings" folder if any occur
  during execution. Once the program closes, the file will be created
  with a record of your name, the time, and a stack trace taken from
  the exceptions.

- Links are automatically created to files referenced in relative to
  the program's root folder. If FSM is running on Windows, clicking
  on those links will open the file. See General Help for more info.


What's New in FSM 2.4?
=========================

- Pressing F1 now brings up an "About FSM" box that allows access to
  various documentation regarding the program.

- Menus have been slightly modified in how they come up and close
  down. Fewer errors should be generated in the background when
  closing dialogs that own open child windows (though some may exist).


What's New in FSM 2.3?
=========================

- Pressing F2 allows access to user-settable options in the program.
  Reasonable defaults are provided, and the settings can easily be
  reset by deleting the settings file in the settings folder.

- Clicking on buttons brings up a custom color picker. The only way to
  set the color is by clicking on the "Okay" button at the bottom.

- Ten settings are supplied, but some do not take effect until restart
  while others only apply to new messages. To get the most current
  view according to the settings, the program must be restarted.


What's New in FSM 2.2?
=========================

- Wispering and reverse wispering is now possible. Writing "@[name]"
  before a message should allow only the intended recipient to view
  the message.

- Reverse wispering is accomplished by placing a "!" mark before the
  wispered message ("!@[name] message"). The person named should not
  receive the message.


What's New in FSM 2.1?
=========================

- Links are automatically recognized now when entered into messages.
  Clicking on them should open them up in your default browser.


What's New in FSM 2.0?
=========================

- Entire program was written from scratch. FSM 1 has been canceled
  and is not able to work with the new I/O system. Major version
  changes will probably continue based on changes to the I/O system
  that would not be compatible with older designs.'''

    GENERAL_HELP = '''\
Automatic Links
=========================

If FSM detects a special attribute of the text as described in the
following sections, it will create a "link" that highlights and
possibly reformats that text. Clicking on links is system dependent.

URL - If the text appears to be a URL, it will be highlighted and
      changed into a link that can be clicked on. Clicking on the link
      should try opening the URL in the system's default browser. As
      of right now, only HTTP, HTTPS, and FTP links can be recognized.

PATH - If the text has been formatted to reference a file relative to
       the program's root folder, then a link will be created will the
       file's name highlighted. If the OS is Windows, clicking on that
       link will open that file as though it have been double-clicked.

       Note: the syntax of the command is <path>. As an easy example:
       Has anyone checked out <Stephen Chappell\My Files.txt> yet?


Function Keys
=========================

F1 - Opens "About FSM" and displays a menu to open various bodies of
     documentation. You may browse the history of changes to this
     program, find out different features and how to use this program,
     and find a list of things yet to be accomplished in FSM.

F2 - Brings up a list of options that can be set to change the
     operation of FSM. Colors can be set by clicking on the buttons
     and using the color picker to select a new color. Some options do
     not take effect until restarting the program and cannot be
     changed by others. Options are saved to disk on program exit.


Writing Messages
=========================

Normal Wispering - If you want to write a message to one person, then
     you have the option of wispering to that person. Messages are
     always displayed will the origin's name in brackets beside it. To
     wisper to someone, write "@[name] message" where "name" is the
     person's name and the message follows special wisper syntax.

Reversed Wispering - When you want to send a message to everyone
     except someone in particular, you can reverse wisper by adding a
     "!" to the front of your wispered message. The full syntax for
     the command is "!@[name] message" and is simple to remember since
     "!" and "@" are right beside each other on the keyboard.


Effects of Settings
=========================

Message Settings - Different colors may be selected for highlighting
     messenger names. By default, normal text messages show up light
     blue, wispered text messages show up light red, and reverse
     wispers show up light green. Only names are actually colorized.

Timestamp Settings - If you want to see when a message was written,
     you can turn on timestamps. You have the ability of toggling if
     they are displayed along with the background and foreground color
     of the text.

Hyperlink Settings - When the program identifies possible links to web
     sites, they are changed into clickable text to open up the link
     in your default browser. You may change whether or not links are
     underlines along with the color they show up in.

Display Settings - Normally, only the past day's worth of messages are
     shown when the FSM opens. You can change this in the options to
     be up to ten days. You may also test your ability to read text
     that has been modified to test if spelling is as important as
     your English teachers says it is. You might be surprised.'''

    FUTURE_PLANS = '''\
There are no future plans for FSM at this time.'''

    ########################################################################

    STYLE = dict(padx=5, pady=5, sticky=tkinter.EW)

    OPEN = False

    @classmethod
    def open_window(cls, root):
        # Only open window if not already open.
        if not cls.OPEN:
            cls.OPEN = True
            window = cls(root)
            window.mainloop()
            cls.OPEN = False

    ########################################################################

    def __init__(self, master):
        super().__init__(master)
        self.geometry('+{}+{}'.format(master.winfo_rootx(),
                                      master.winfo_rooty()))
        self.transient(master)
        self.protocol('WM_DELETE_WINDOW', self.close)
        # Create the interface for this window.
        self.resizable(False, False)
        self.create_widgets()

    def create_widgets(self):
        self.title('About FSM')
        # Create a header for the buttons.
        self.font = tkinter.font.Font(self, family='arial', size=24,
                                      weight=tkinter.font.NORMAL)
        self.name = tkinter.ttk.Label(self, text=self.master.title(), width=20,
                                      font=self.font, anchor=tkinter.CENTER)
        self.name.grid(column=0, row=0, columnspan=3)
        # Separate head from the options.
        self.divide = tkinter.Frame(self, borderwidth=1, height=2,
                                    relief=tkinter.SUNKEN, bg='#777')
        self.divide.grid(column=0, row=1, columnspan=3, **self.STYLE)
        # Create buttons to open various informational dialogs.
        # ============
        # New Features
        # General Help
        # Future Plans
        self.new_features = \
            tkinter.ttk.Button(self, text='New Features',
                command=lambda: idlelib.textView.TextViewer(self.master,
                    'New Features', self.NEW_FEATURES))
        self.general_help = \
            tkinter.ttk.Button(self, text='General Help',
                command=lambda: idlelib.textView.TextViewer(self.master,
                    'General Help', self.GENERAL_HELP))
        self.future_plans = \
            tkinter.ttk.Button(self, text='Future Plans',
                command=lambda: idlelib.textView.TextViewer(self.master,
                    'Future Plans', self.FUTURE_PLANS))
        # Place the button on the window.
        self.new_features.grid(column=0, row=2, **self.STYLE)
        self.general_help.grid(column=1, row=2, **self.STYLE)
        self.future_plans.grid(column=2, row=2, **self.STYLE)

    def close(self):
        # Cancel execution of this widnow.
        self.destroy()
        self.quit()        

################################################################################

class SettingsDialog(tkinter.Toplevel):

    FRAME = dict(sticky=tkinter.EW, padx=4, pady=2)
    LABEL = dict(width=19)
    BUTTON = dict(width=5)
    BUTTON_GRID = dict(padx=1, pady=1)

    MIN_CUTOFF = 1
    MAX_CUTOFF = 240

    ########################################################################

    OPEN = False

    @classmethod
    def open_window(cls, root):
        # Only open settings if not open.
        if not cls.OPEN:
            cls.OPEN = True
            window = cls(root)
            window.mainloop()

    ########################################################################

    def __init__(self, master):
        super().__init__(master)
        self.geometry('+{}+{}'.format(master.winfo_rootx(),
                                      master.winfo_rooty()))
        self.transient(master)
        # Build all the widgets that will be in the window.
        self.create_interface()
        # Populate the widgets with the correct settings.
        self.load_widget_settings()
        # Override the closing of this window to keep track of its state.
        self.protocol('WM_DELETE_WINDOW', self.ask_destroy)
        # Prepare the window for general display.
        self.title('Settings')
        self.resizable(False, False)
        # Create a message box to warn about closing.
        options = dict(title='Warning?', icon=tkinter.messagebox.QUESTION,
                       type=tkinter.messagebox.YESNO, message='''\
Are you sure you want to close?
You will lose all your changes.''')
        self.__cancel_warning = tkinter.messagebox.Message(self, **options)

    def ask_destroy(self):
        # Only close if user wants to lose settings.
        if self.__cancel_warning.show() == tkinter.messagebox.YES:
            self.destroy()
        else:
            self.focus_set()

    def destroy(self):
        # Destroy this window and unset the OPEN flag.
        super().destroy()
        self.quit()
        self.__class__.OPEN = False

    def create_interface(self):
        # Create label frames for the different settings.
        self.message_settings()
        self.timestamp_settings()
        self.hyperlink_settings()
        self.display_settings()
        # Create buttons for accepting or cancelling changes.
        self.create_ok_cancel()

    def bind_color_button(self, button):
        # Setup a command for changing the color.
        button['command'] = lambda: self.get_new_color(button)

    def message_settings(self):
        # Create frame for widgets.
        m = self.message = tkinter.ttk.Labelframe(self, text='Message Settings')
        # Create the widgets.
        m.normal_label = tkinter.ttk.Label(m, text='Normal Text:', **self.LABEL)
        m.wisper_label = tkinter.ttk.Label(m, text='Wisper Text:', **self.LABEL)
        m.reverse_label = tkinter.ttk.Label(m, text='Reversed Text:',
                                            **self.LABEL)
        m.normal_button = tkinter.Button(m, **self.BUTTON)
        m.wisper_button = tkinter.Button(m, **self.BUTTON)
        m.reverse_button = tkinter.Button(m, **self.BUTTON)
        # Position the widgets.
        m.normal_label.grid(row=0, column=0)
        m.wisper_label.grid(row=1, column=0)
        m.reverse_label.grid(row=2, column=0)
        m.normal_button.grid(row=0, column=1, **self.BUTTON_GRID)
        m.wisper_button.grid(row=1, column=1, **self.BUTTON_GRID)
        m.reverse_button.grid(row=2, column=1, **self.BUTTON_GRID)
        # Configure the buttons.
        self.bind_color_button(m.normal_button)
        self.bind_color_button(m.wisper_button)
        self.bind_color_button(m.reverse_button)
        # Position the frame.
        m.grid(row=0, column=0, **self.FRAME)

    def timestamp_settings(self):
        # Create frame for widgets.
        t = self.timestamp = tkinter.ttk.Labelframe(self,
                                                    text='Timestamp Settings')
        # Create the widgets.
        t.show_string = tkinter.StringVar(t)
        t.show_checkbutton = tkinter.ttk.Checkbutton(t, text='Show Timestamp',
                                                     variable=t.show_string,
                                                     onvalue='True',
                                                     offvalue='False')
        t.background_label = tkinter.ttk.Label(t, text='Background Color:',
                                               **self.LABEL)
        t.foreground_label = tkinter.ttk.Label(t, text='Foreground Color:',
                                               **self.LABEL)
        t.background_button = tkinter.Button(t, **self.BUTTON)
        t.foreground_button = tkinter.Button(t, **self.BUTTON)
        # Position the widets.
        t.show_checkbutton.grid(row=0, column=0, columnspan=2)
        t.background_label.grid(row=1, column=0)
        t.foreground_label.grid(row=2, column=0)
        t.background_button.grid(row=1, column=1, **self.BUTTON_GRID)
        t.foreground_button.grid(row=2, column=1, **self.BUTTON_GRID)
        # Configure the buttons.
        self.bind_color_button(t.background_button)
        self.bind_color_button(t.foreground_button)
        # Position the frame.
        t.grid(row=1, column=0, **self.FRAME)

    def hyperlink_settings(self):
        # Create frame for widgets.
        h = self.hyperlink = tkinter.ttk.Labelframe(self,
                                                    text='Hyperlink Settings')
        # Create the widgets.
        h.underline_string = tkinter.StringVar(h)
        h.underline_checkbutton = \
            tkinter.ttk.Checkbutton(h, text='Underline Link',
                                    variable=h.underline_string,
                                    onvalue='True', offvalue='False')
        h.foreground_label = tkinter.ttk.Label(h, text='Foreground Color:',
                                               **self.LABEL)
        h.foreground_button = tkinter.Button(h, **self.BUTTON)
        # Position the widgets.
        h.underline_checkbutton.grid(row=0, column=0, columnspan=2)
        h.foreground_label.grid(row=1, column=0)
        h.foreground_button.grid(row=1, column=1, **self.BUTTON_GRID)
        # Configure the button.
        self.bind_color_button(h.foreground_button)
        # Position the frame.
        h.grid(row=2, column=0, **self.FRAME)

    def display_settings(self):
        # Create frame for widgets.
        d = self.display = tkinter.ttk.Labelframe(self, text='Display Settings')
        # Create the widgets.
        d.cutoff_label = tkinter.ttk.Label(d, text='Text Cutoff (hours):',
                                           **self.LABEL)
        d.cutoff_string = tkinter.StringVar(d)
        d.cutoff_spinbox = tkinter.Spinbox(d, from_=self.MIN_CUTOFF,
                                           to=self.MAX_CUTOFF,
                                           textvariable=d.cutoff_string,
                                           **self.BUTTON)
        d.confuse_string = tkinter.StringVar(d)
        d.confuse_checkbutton = tkinter.ttk.\
                                Checkbutton(d, text='Scramble Text',
                                            variable=d.confuse_string,
                                            onvalue='True', offvalue='False')
        # Position the widgets.
        d.cutoff_label.grid(row=0, column=0)
        d.cutoff_spinbox.grid(row=0, column=1)
        d.confuse_checkbutton.grid(row=1, column=0, columnspan=2)
        # Position the frame.
        d.grid(row=3, column=0, **self.FRAME)

    def create_ok_cancel(self):
        # Create frame for widgets.
        b = self.buttons = tkinter.ttk.Frame(self)
        # Create the widgets.
        b.accept = tkinter.ttk.Button(b, text='Accept', command=self.accept)
        b.label = tkinter.ttk.Label(b, width=3)
        b.cancel = tkinter.ttk.Button(b, text='Cancel', command=self.cancel)
        # Position the widgets.
        b.accept.grid(row=0, column=0)
        b.label.grid(row=0, column=1)
        b.cancel.grid(row=0, column=2)
        # Position the frame.
        b.grid(row=4, column=0, **self.FRAME)

    def accept(self):
        # Close window after changing settings.
        self.save_widget_settings()
        self.destroy()

    def cancel(self):
        # Close the window without changing anything.
        self.destroy()

    def save_widget_settings(self):
        # Save settings by their catagories.
        self.save_message_settings()
        self.save_timestamp_settings()
        self.save_hyperlink_settings()
        self.save_display_settings()

    def load_widget_settings(self):
        # Have the widgets display the correct information.
        self.load_message_settings()
        self.load_timestamp_settings()
        self.load_hyperlink_settings()
        self.load_display_settings()

    def load_message_settings(self):
        # Set the color for the name background.
        self.message.normal_button['background'] = SETTINGS.normal_message
        self.message.wisper_button['background'] = SETTINGS.wisper_message
        self.message.reverse_button['background'] = SETTINGS.reverse_wisper

    def save_message_settings(self):
        # Copy current settings back out to global settings.
        SETTINGS.normal_message = Color.parse(self.message.normal_button['bg'])
        SETTINGS.wisper_message = Color.parse(self.message.wisper_button['bg'])
        SETTINGS.reverse_wisper = Color.parse(self.message.reverse_button['bg'])

    def load_timestamp_settings(self):
        # Get timstamp settings and load them in the GUI.
        boolean = ('False', 'True')[SETTINGS.show_timestamp]
        self.timestamp.show_string.set(boolean)
        self.timestamp.background_button['bg'] = SETTINGS.time_background
        self.timestamp.foreground_button['bg'] = SETTINGS.time_foreground

    def save_timestamp_settings(self):
        # Take timestamp options and save in global settings.
        SETTINGS.show_timestamp = self.timestamp.show_string.get() == 'True'
        SETTINGS.time_background = \
            Color.parse(self.timestamp.background_button['bg'])
        SETTINGS.time_foreground = \
            Color.parse(self.timestamp.foreground_button['bg'])

    def load_hyperlink_settings(self):
        # Update the GUI according to the hyperlink settings.
        boolean = ('False', 'True')[SETTINGS.link_underline]
        self.hyperlink.underline_string.set(boolean)
        self.hyperlink.foreground_button['bg'] = SETTINGS.link_foreground

    def save_hyperlink_settings(self):
        # Save the hyperlink settings in the global settings object.
        SETTINGS.link_underline = \
            self.hyperlink.underline_string.get() == 'True'
        SETTINGS.link_foreground = \
            Color.parse(self.hyperlink.foreground_button['bg'])

    def load_display_settings(self):
        # Load the display settings into the GUI.
        self.display.cutoff_string.set(SETTINGS.message_cutoff)
        boolean = ('False', 'True')[SETTINGS.message_confuser]
        self.display.confuse_string.set(boolean)

    def save_display_settings(self):
        # Save user's setting for display for use in program.
        try:
            cutoff = int(self.display.cutoff_string.get())
        except ValueError:
            pass
        else:
            if self.MIN_CUTOFF <= cutoff <= self.MAX_CUTOFF:
                SETTINGS.message_cutoff = cutoff
        SETTINGS.message_confuser = self.display.confuse_string.get() == 'True'

    def get_new_color(self, button):
        # Try changing the color of the button.
        color = ColorOptions.open_window(self.master, button['background'])
        if color:
            button['background'] = color
        self.focus_force()

################################################################################

class Settings:

    FILENAME = 'settings.pickle'
    SLOTS = {'_Settings__path', '_Settings__data'}
    DEFAULT = {'normal_message': Color.LightSteelBlue,
               'wisper_message': Color.LightSteelBlue.set_hue(0),
               'reverse_wisper': Color.LightSteelBlue.set_hue(1 / 3),
               'show_timestamp': False,
               'time_background': Color.Black,
               'time_foreground': Color.White,
               'link_foreground': Color.Blue,
               'link_underline': True,
               'message_cutoff': 24,
               'message_confuser': False}
    
    def __init__(self, path):
        # Save the path and load settings from file.
        self.__path = path
        new, self.__data = self.get_settings()
        # If these the settings did not exist, create and save them.
        if new:
            self.save_settings()

    def get_settings(self):
        # Try opening and loading the settings from file.
        filename = os.path.join(self.__path, self.FILENAME)
        try:
            with open(filename, 'rb') as file:
                settings = pickle.load(file)
            # Test the pickle and check each setting inside it.
            assert isinstance(settings, dict)
            key_list = list(self.DEFAULT)
            for key in settings:
                assert isinstance(key, str)
                assert key in self.DEFAULT
                key_list.remove(key)
            # Add new settings as needed (when new ones are created).
            for key in key_list:
                settings[key] = self.DEFAULT[key]
            # Return old settings, or on error, the default settings.
            return False, settings
        except (IOError, pickle.UnpicklingError, AssertionError):
            return True, self.DEFAULT

    def save_settings(self):
        # Make the directory if it does not exist or check its type.
        if not os.path.exists(self.__path):
            os.makedirs(self.__path)
        elif os.path.isfile(self.__path):
            raise IOError('Directory cannot be created!')
        # Pickle and save the settings in the specified path (filename).
        filename = os.path.join(self.__path, self.FILENAME)
        with open(filename, 'wb') as file:
            pickle.dump(self.__data, file, pickle.HIGHEST_PROTOCOL)

    def __getattr__(self, name):        # Get an attribute.
        # If the name is an instance variable, return it.
        if name in self.SLOTS:
            return vars(self)[name]
        # Otherwise, get it from the settings stored in __data.
        return self.__data[name]

    def __setattr__(self, name, value): # Set an attribute.
        # If the name is an instance variable, go ahead and set it.
        if name in self.SLOTS:
            vars(self)[name] = value
        else:
            # Otherwise, store the setting in the __data attribute.
            self.__data[name] = value

################################################################################

random = random.SystemRandom().sample
string = string.digits + string.ascii_uppercase
# For version 3, use all ASCII letter (uppercase and lowercase).

uuid = lambda: ''.join(random(string, len(string)))

################################################################################

class DirectoryMonitor:

    def __init__(self, path):
        # Save directory path and file monitors (by path).
        self.__path = path
        self.__files = {}

    def update(self, callback):
        # Discover any files are new to the path.
        for name in os.listdir(self.__path):
            if self.valid_name(name) and name not in self.__files:
                path_name = os.path.join(self.__path, name)
                self.__files[name] = FileMonitor(path_name)
        errors = set()
        # Try updating each file monitor with reference to callback.
        for name, monitor in self.__files.items():
            try:
                monitor.update(callback)
            except OSError:
                errors.add(name)
        # Remove any problem files from the list.
        for name in errors:
            del self.__files[name]

    @staticmethod
    def valid_name(name):
        # There should be 36 characters in a valid name.
        if len(name) != len(string):
            return False
        # Every single character should be there (from the template).
        expected_characters = set(string)
        in_both = set(name) & expected_characters
        return in_both == expected_characters
        

################################################################################

class FileMonitor:

    def __init__(self, path):
        # Track mondification is a file and present position within file.
        self.__path = path
        self.__modified = 0
        self.__position = 0

    def update(self, callback):
        # Find out if the file has been modified.
        modified = os.path.getmtime(self.__path)
        if modified != self.__modified:
            # Remember the present time (we are getting an update).
            self.__modified = modified
            with open(self.__path, 'r') as file:
                # Go to present location, read to EOF, and remember position.
                file.seek(self.__position)
                try:
                    text = file.read()
                except UnicodeError:
                    print('Please report problem with:', repr(self.__path))
                    traceback.print_exc()
                    print('-' * 80)
                self.__position = file.tell()
            # Execute callback with file ID and new text update.
            callback(self.__path, text)

################################################################################

class Aggregator:

    def __init__(self):
        # Keep track of message streams.
        self.__streams = {}

    def update(self, path, text):
        # Create a new MessageStream if the path is not recognized.
        if path not in self.__streams:
            self.__streams[path] = MessageStream()
        # Split text on NULL and check that there is nothing following.
        parts = text.split('\0')
        if parts[-1]:
            raise IOError('Text is not properly terminated!')
        # Update stream with all message parts except the last empty one.
        self.__streams[path].update(parts[:-1])

    def get_messages(self):
        all_messages = []
        # Get all new messages waiting in the streams.
        for stream in self.__streams.values():
            all_messages.extend(stream.get_messages())
        # Return them sorted by the timestamps.
        return sorted(all_messages, key=lambda message: message.time)

################################################################################

class MessageStream:

    def __init__(self):
        # Save name, buffered tail, and any waiting messages.
        self.__name = None
        self.__buffer = None
        self.__waiting = []

    def update(self, parts):
        # If there is no name, assume the first part is the name.
        if self.__name is None:
            self.__name = parts.pop(0)
        # If something is in the buffer, add it to front of parts and clear.
        if self.__buffer is not None:
            parts.insert(0, self.__buffer)
            self.__buffer = None
        # If the parts length is odd, save tail in the buffer.
        if len(parts) & 1:
            self.__buffer = parts.pop()
        # Append new, waiting messages to the list.
        for index in range(0, len(parts), 2):
            self.__waiting.append(Message(self.__name, *parts[index:index+2]))

    def get_messages(self):
        # Return the messages and clear the list.
        messages = self.__waiting
        self.__waiting = []
        return messages

################################################################################

class Message:

    def __init__(self, name, timestamp, text):
        self.name = name
        try:
            # Try to parse the timestamp.
            self.time = datetime.datetime.strptime(timestamp,
                                                   '%Y-%m-%dT%H:%M:%SZ')
            self.text = text.strip()
        except ValueError:
            # The messages appear corrupt.
            self.time = datetime.datetime.utcnow()
            self.text = '[STREAM IS CORRUPT]'
        # Assume this is a normal message (for name color).
        self.tag = 'name'

################################################################################

class MessageWriter:

    def __init__(self, path, name):
        # Check the name, save it, and set a couple other variables.
        assert '\0' not in name, 'Name may not have null characters!'
        self.__name = name
        self.__primed = False
        self.__path = os.path.join(path, self.__find(path))

    def __find(self, path):
        # For each file in the directory ...
        for name in os.listdir(path):
            full_path = os.path.join(path, name)
            if os.path.isfile(full_path):
                # Check the first 256 bytes for a name.
                with open(full_path, 'r') as file:
                    data = file.read(256).split('\0', 1)[0]
                # If (your) name was found, file exists.
                if data == self.__name:
                    self.__primed = True
                    return name
        # A new file will need to be created with a unique identifier.
        return uuid()

    def write(self, text):
        # Check the message for invalid characters and try priming the file.
        assert '\0' not in text, 'Text may not have null characters!'
        self.prime()
        # Save the message as (timestamp, null, text, null) in the file.
        timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
        with open(self.__path, 'a') as file:
            file.write(timestamp + '\0' + text + '\0')

    def prime(self):
        if not self.__primed:
            # Write name to file followed by a null.
            with open(self.__path, 'w') as file:
                file.write(self.__name + '\0')
            self.__primed = True
                
################################################################################

# This code provides error logging facilities.

def main():
    # Figure out where files should be stored.
    public_path = os.path.join('Message Storage', 'V2.5')
    private_path = os.path.join('..', 'FSM Settings')
    # Execute the main class (static) function of FSM.
    with capture_stderr() as stderr:
        FSM.main(public_path, private_path)
    # Cleanup stderr and save and errors to file.
    record(stderr, private_path, 'errorlog.pickle')

@contextlib.contextmanager
def capture_stderr():
    # Provide a context manager that captures standard error.
    orig_stderr = sys.stderr
    sys.stderr = io.StringIO()
    try:
        yield sys.stderr
    finally:
        sys.stderr = orig_stderr

def record(stream, path, filename):
    # Find out if there were any errors during execution.
    errors = stream.getvalue()
    if errors:
        # Save them to a pickled file with a timestamp.
        with open(os.path.join(path, filename), 'ab', 0) as file:
            problem = getpass.getuser(), datetime.datetime.utcnow(), errors
            pickle.dump(problem, file)

################################################################################

class FSM(tkinter.ttk.Frame):

    @classmethod
    def main(cls, log_path, settings_path):
        # Create a global settings object for the application.
        global SETTINGS
        SETTINGS = Settings(settings_path)
        # Create the root GUI object.
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        # Bind an event handler for closing the program.
        def on_close():
             SETTINGS.save_settings()
             root.destroy()
             root.quit()
        root.protocol('WM_DELETE_WINDOW', on_close)
        # Set the window title and minimum size for the window.
        name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
        root.title(name)
        root.minsize(320, 240)  # QVGA
        # Create, position, and setup FSM widget for resizing.
        view = cls(root, log_path)
        view.grid(row=0, column=0, sticky=tkinter.NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        # Bind buttons to access the application's menus.
        root.bind_all('<F2>', lambda event: SettingsDialog.open_window(root))
        root.bind_all('<F1>', lambda event: AboutFSM.open_window(root))
        # Start the application's event loop.
        root.mainloop()

    def __init__(self, master, log_path, **kw):
        super().__init__(master, **kw)
        self.configure_widgets()
        # Save username and prepare for program I/O.
        self.__username = getpass.getuser()
        self.__writer = MessageWriter(log_path, self.__username)
        self.__monitor = DirectoryMonitor(log_path)
        self.__messages = Aggregator()
        # Start looking for updates to the files.
        self.after_idle(self.update)

    def configure_widgets(self):
        # Create widgets.
        self.__text = tkinter.Text(self, state=tkinter.DISABLED,
                                   wrap=tkinter.WORD, cursor='arrow')
        self.__scroll = tkinter.ttk.Scrollbar(self, orient=tkinter.VERTICAL,
                                              command=self.__text.yview)
        self.__entry = tkinter.ttk.Entry(self, cursor='xterm')
        # Alter their settings.
        self.__text.configure(yscrollcommand=self.__scroll.set)
        self.__text.tag_configure('name', background=SETTINGS.normal_message)
        self.__text.tag_configure('high', background=SETTINGS.wisper_message)
        self.__text.tag_configure('mess', background=SETTINGS.reverse_wisper)
        self.__text.tag_configure('time', background=SETTINGS.time_background,
                                  foreground=SETTINGS.time_foreground)
        # Configure settings for hyperlinks.
        self.__text.tag_configure('dynamic_link',
                                  foreground=SETTINGS.link_foreground,
                                  underline=SETTINGS.link_underline)
        self.__text.tag_bind('dynamic_link', '<Enter>',
                      lambda event: self.__text.configure(cursor='hand2'))
        self.__text.tag_bind('dynamic_link', '<Leave>',
                      lambda event: self.__text.configure(cursor='arrow'))
        # Configure settings for static links.
        self.__text.tag_configure('static_link',
                                  foreground=SETTINGS.link_foreground,
                                  underline=SETTINGS.link_underline)
        # Place everything on the grid.
        self.__text.grid(row=0, column=0, sticky=tkinter.NSEW)
        self.__scroll.grid(row=0, column=1, sticky=tkinter.NS)
        self.__entry.grid(row=1, column=0, columnspan=2, sticky=tkinter.EW)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        # Setup box for typing.
        self.__entry.bind('<Control-Key-a>', self.select_all)
        self.__entry.bind('<Control-Key-/>', lambda event: 'break')
        self.__entry.bind('<Return>', self.send_message)
        self.__entry.focus_set()
        # Save first status and link counts.
        self.__first_line = True
        self.__url_id = 0
        self.__path_id = 0

    def select_all(self, event):
        # Select everything in the widget.
        event.widget.selection_range(0, tkinter.END)
        return 'break'

    def send_message(self, event):
        # Cut everything from the entry and write to file.
        text = self.__entry.get()
        self.__entry.delete(0, tkinter.END)
        self.__writer.write(text)

    def update(self):
        # Update the directory monitor once a second.
        self.after(1000, self.update)
        self.__monitor.update(self.__messages.update)
        # For each message, show those less than a day old.
        utcnow = datetime.datetime.utcnow()
        for message in self.__messages.get_messages():
            hours = (utcnow - message.time).total_seconds() / 3600
            if hours < SETTINGS.message_cutoff and self.allowed(message):
                self.display(message)

    def allowed(self, message):
        # If there is no text, it is not allowed.
        if not message.text:
            return False
        # Extract some information about the text.
        dest, text, reverse = self.get_wisper(message.text)
        # If there is no destination, it is allowed.
        if dest is None:
            return True
        # Change the message's color.
        message.tag = 'mess' if reverse else 'high'
        if self.__username == message.name:
            # ... unless the source really wants to ignore himself.
            if reverse:
                return False
            # Reformat the text and allow message.
            form = '![{}] {}' if reverse else '-> [{}] {}'
            message.text = form.format(dest, text)
            return True
        # If this is not a reversed whisper ...
        if not reverse:
            # It is only allowed for the destination.
            if dest == self.__username:
                message.text = text
                return True
            return False
        # Otherwise, it is not allowed to anyone else.
        if dest != self.__username:
            message.text = '![{}] {}'.format(dest, text)
            return True
        return False

    def get_wisper(self, message):
        # Note to self: start wispers as "@[" and reversals as "!["
        # next time you implement this system for version 3 of FSM.
        reverse, cleaned = False, message
        # If the messages starts with a '!', it should be cleaned.
        if message[0] == '!':
            reverse, cleaned = True, message[1:]
        # If the message starts with the proper prefix ...
        if cleaned[:2] == '@[':
            try:
                # Find the "end of name" marker.
                index = cleaned.index(']')
            except ValueError:
                pass    # Not wispered.
            else:
                # Return name, cleaned text, and reversal flag.
                dest = cleaned[2:index]
                text = cleaned[index+1:].strip()
                return dest, text, reverse
        # It was not wispered.
        return None, message, False

    def display(self, message):
        # Enable changes and take first line into account.
        self.__text['state'] = tkinter.NORMAL
        if self.__first_line:
            self.__first_line = False
        else:
            self.__text.insert(tkinter.END, '\n')
        # Show the timestamp if requested.
        if SETTINGS.show_timestamp:
            diff = datetime.datetime.now() - datetime.datetime.utcnow()
            time = message.time + diff
            # Display string that has been corrected for local time zone.
            self.__text.insert(tkinter.END, time.strftime('%I:%M %p'), 'time')
            self.__text.insert(tkinter.END, ' ')
        # Show the name with the proper color (message.tag).
        self.__text.insert(tkinter.END, '[' + message.name + ']', message.tag)
        # Add text with formatting, scroll to botton, and disable changes.
        self.add_text_with_URLs(' ' + message.text)
        self.__text.see(tkinter.END)
        self.__text['state'] = tkinter.DISABLED

    def add_text_with_URLs(self, message):
        url_list = self.find_urls(message)
        # Split on each URL, prefix, and create URL.
        for url in url_list:
            head, message = message.split(url, 1)
            self.add_text_with_PATHs(head)
            self.create_url(url)
        # Display whatever may be left.
        self.add_text_with_PATHs(message)

    def add_text_with_PATHs(self, message):
        path_list = self.find_paths(message)
        # Split on each path markup and create path links.
        for markup, path, name in path_list:
            head, message = message.split(markup, 1)
            self.add_plain_text(head)
            self.create_path(path, name)
        # Finish displaying any trailing text.
        self.add_plain_text(message)

    def create_url(self, url):
        # Create a new, incremented URL tag for text.
        self.__url_id += 1
        tag = 'url' + str(self.__url_id)
        # Insert the text and bind a command to open a webbrowser.
        self.__text.insert(tkinter.END, url, ('dynamic_link', tag))
        self.__text.tag_bind(tag, '<1>', lambda event: webbrowser.open(url))

    def create_path(self, path, name):
        # If the user is running Windows ...
        if hasattr(os, 'startfile'):
            # Create a new tag for the path.
            self.__path_id += 1
            tag = 'path' + str(self.__path_id)
            # Add the text and create an opening command.
            self.__text.insert(tkinter.END, name, ('dynamic_link', tag))
            self.__text.tag_bind(tag, '<1>', lambda event: os.startfile(path))
        else:
            # Insert a link that does not do anything.
            self.__text.insert(tkinter.END, name, 'static_link')

    def add_plain_text(self, message):
        # Confuse text if needed before adding text to display.
        if SETTINGS.message_confuser:
            message = confuse(message)
        self.__text.insert(tkinter.END, message)

    def find_paths(self, message):
        # Track found paths and current search positions.
        paths = []
        index_a = index_b = 0
        # While we are still searching the message's end ...
        while index_a > -1 and index_b > -1:
            index_a = message.find('<', index_b)
            # If the less than symbol has been found ...
            if index_a > -1:
                index_b = message.find('>', index_a)
                # If the greater than symbol has been found ...
                if index_b > -1:
                    path_markup = message[index_a:index_b+1]
                    # Add path to list if it exists.
                    self.test_and_add_path(path_markup, paths)
        return paths

    def test_and_add_path(self, markup, paths):
        # Extract the path and create an "absolute" path.
        pulled = markup[1:-1].strip()
        program = os.path.dirname(sys.argv[0])
        absolute = os.path.join(program, pulled)
        # Turn the path into a normal path and test for existence.
        normal = os.path.normpath(absolute)
        if os.path.exists(normal):
            # Record the markup, normal path, and filename.
            base = os.path.basename(normal)
            file = os.path.splitext(base)[0]
            paths.append((markup, normal, file))

    def find_urls(self, message):
        urls = []
        # Split text on whitespace.
        for text in message.split():
            result = urllib.parse.urlparse(text)
            # It is a URL if the protocol is correct and there is a location.
            if result.scheme in {'http', 'https', 'ftp'} and result.netloc:
                urls.append(text)
        # Return the list of found URLs.
        return urls

################################################################################

def confuse(text):
    # Collect all the words in a buffer after processing.
    buffer = []
    for data in words(text):
        if isinstance(data, str):
            buffer.append(data)             # Normal Text
        elif len(data) < 4:
            buffer.append(''.join(data))    # Short Text
        else:
            buffer.append(scramble(data))   # Confused Text
    # Return the processed string.
    return ''.join(buffer)

def words(string):
    # Prepare to process a string.
    data = str(string)
    if data:
        # Collect words and non-words and determine starting state.
        buffer = ''
        mode = 'A' <= data[0] <= 'Z' or 'a' <= data[0] <= 'z'
        for character in data:
            # Add characters to buffer until a mode change.
            if mode == ('A' <= character <= 'Z' or 'a' <= character <= 'z'):
                buffer += character
            else:
                # Yield a data type indicating what has been found.
                yield tuple(buffer) if mode else buffer
                buffer = character
                mode = not mode
        # Yield any remaining data in the buffer.
        yield tuple(buffer) if mode else buffer
    else:
        yield data

def scramble(data):
    # Get the first letter and scramble the middle letters.
    array = [data[0]]
    array.extend(random(data[1:-1], len(data) - 2))
    # Append the last letter and return the final string.
    array.append(data[-1])
    return ''.join(array)

################################################################################

if __name__ == '__main__':
    main()