mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-31 04:17:36 +00:00
Compare commits
1241
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0178206a92 | ||
|
|
46989b1ea2 | ||
|
|
f9f5656218 | ||
|
|
09c855daca | ||
|
|
3bb8693e8e | ||
|
|
baa354c1d4 | ||
|
|
444d1d620c | ||
|
|
05bc99690d | ||
|
|
0ba4fdb43f | ||
|
|
2aeed8c80d | ||
|
|
dc6bebd7eb | ||
|
|
a7ac7fd3b1 | ||
|
|
9b5a0fbbfb | ||
|
|
b05c912109 | ||
|
|
91cd92f92f | ||
|
|
ec357f1a77 | ||
|
|
2b77b98163 | ||
|
|
3ad080b788 | ||
|
|
3282323053 | ||
|
|
29715948cc | ||
|
|
69a8d95fbb | ||
|
|
befbbb9c3b | ||
|
|
8c30016f3d | ||
|
|
e8eaed2e1f | ||
|
|
df78974183 | ||
|
|
96cf42c763 | ||
|
|
8a70822a2f | ||
|
|
a06ad56545 | ||
|
|
dc074c89dd | ||
|
|
115aae74fe | ||
|
|
178a7d2aec | ||
|
|
9d1504704f | ||
|
|
bb1cd85466 | ||
|
|
f01d83204e | ||
|
|
cc120e514a | ||
|
|
92900159cc | ||
|
|
62f699e376 | ||
|
|
8e4742a250 | ||
|
|
40460f8d95 | ||
|
|
e88313e34f | ||
|
|
d605ee9d50 | ||
|
|
b222ee0320 | ||
|
|
1622990436 | ||
|
|
7cd6295359 | ||
|
|
b4bff7ffe1 | ||
|
|
00851abfa9 | ||
|
|
b810afdd69 | ||
|
|
66ea130c8d | ||
|
|
92b8039c71 | ||
|
|
947fbaddfa | ||
|
|
bf4181dc76 | ||
|
|
055ae6d04e | ||
|
|
56aa68f832 | ||
|
|
263f7a4038 | ||
|
|
41969c7f55 | ||
|
|
c4a90c16a7 | ||
|
|
c1f8c8ceb3 | ||
|
|
dec05699f3 | ||
|
|
41a7004a53 | ||
|
|
98c3611ade | ||
|
|
e3a3cf6bcd | ||
|
|
1944382e0e | ||
|
|
24b760b306 | ||
|
|
f4109c0ee4 | ||
|
|
459a61af58 | ||
|
|
080d9e723e | ||
|
|
b58be16e8a | ||
|
|
71baddb0be | ||
|
|
d2a687d158 | ||
|
|
fc960c62e3 | ||
|
|
66e4d3fef6 | ||
|
|
7501d86853 | ||
|
|
be8d536565 | ||
|
|
aff19db572 | ||
|
|
ac709c97bf | ||
|
|
203bf38b21 | ||
|
|
99b47c9a29 | ||
|
|
b06c70eb65 | ||
|
|
483fff9305 | ||
|
|
230261485a | ||
|
|
a49a5021df | ||
|
|
53692b1b71 | ||
|
|
850c348cca | ||
|
|
cc5807e568 | ||
|
|
1585123040 | ||
|
|
f10eb2e0a5 | ||
|
|
7b2528f7c2 | ||
|
|
1ae6767971 | ||
|
|
e838f9ab43 | ||
|
|
34b3367468 | ||
|
|
98ba46872d | ||
|
|
f1a8b154fc | ||
|
|
572c872c31 | ||
|
|
8cff67cfed | ||
|
|
2aee03c7fb | ||
|
|
15268c45c4 | ||
|
|
bf19b09ad0 | ||
|
|
7f38fbe7c7 | ||
|
|
7f89b13387 | ||
|
|
f69bd6c4ad | ||
|
|
9891579ac4 | ||
|
|
901241d74e | ||
|
|
2167c1d3b8 | ||
|
|
1f087223a9 | ||
|
|
3fa722fa18 | ||
|
|
2322eb87e3 | ||
|
|
adec5f6c04 | ||
|
|
315ed7f2df | ||
|
|
37d4d5eb96 | ||
|
|
20867949ed | ||
|
|
6199b8f31b | ||
|
|
de0647b299 | ||
|
|
59f07814b1 | ||
|
|
84729ce7d4 | ||
|
|
eb2f2b2fc2 | ||
|
|
a4ad133080 | ||
|
|
236e02911c | ||
|
|
bfbc522721 | ||
|
|
5b852ca765 | ||
|
|
eda11e38ba | ||
|
|
e5f42bd403 | ||
|
|
ed66323b7f | ||
|
|
73bae6df8e | ||
|
|
b8225fde2c | ||
|
|
a4fd136570 | ||
|
|
5ec9a6e76b | ||
|
|
1566e37943 | ||
|
|
66cda41bfc | ||
|
|
b10288bef7 | ||
|
|
e6eb14ef65 | ||
|
|
d72619660f | ||
|
|
8a87077efe | ||
|
|
4183f090e8 | ||
|
|
87fd68504c | ||
|
|
7fc027af42 | ||
|
|
b7fe5c3302 | ||
|
|
55ee6de9cd | ||
|
|
6f1f56205e | ||
|
|
6adb410cc6 | ||
|
|
a808c6fbab | ||
|
|
71c2501ff6 | ||
|
|
f5c735e97c | ||
|
|
f56855010f | ||
|
|
b80713c1e5 | ||
|
|
93a1eea08b | ||
|
|
7393607a52 | ||
|
|
53784f0e10 | ||
|
|
900448528a | ||
|
|
e04e3d9ba5 | ||
|
|
5eaede0622 | ||
|
|
2c06bf566f | ||
|
|
4784a1a102 | ||
|
|
e3cb2cd6db | ||
|
|
dfe14a0f97 | ||
|
|
aa6fce3297 | ||
|
|
06da3164fc | ||
|
|
9cacb134c7 | ||
|
|
17e5d32621 | ||
|
|
f5b305317f | ||
|
|
9677609340 | ||
|
|
ab8e47fd01 | ||
|
|
a2181c88d7 | ||
|
|
1a9ff9ffb7 | ||
|
|
cc4b0e2540 | ||
|
|
fa525e1cee | ||
|
|
7d59762561 | ||
|
|
87e0a6fe34 | ||
|
|
45d9f10a89 | ||
|
|
93937e8b1b | ||
|
|
bdc2b7cd67 | ||
|
|
c2be4f87c5 | ||
|
|
b6ba250f82 | ||
|
|
4a5a25bb65 | ||
|
|
bc1889c7d4 | ||
|
|
5471c38905 | ||
|
|
9866dd49bb | ||
|
|
a6969bc756 | ||
|
|
23dff002a4 | ||
|
|
03b875697e | ||
|
|
d717e07d5c | ||
|
|
c39f03823a | ||
|
|
f3a04f23f8 | ||
|
|
0089785713 | ||
|
|
3a140b07fe | ||
|
|
06ebbfb300 | ||
|
|
74039c799a | ||
|
|
b3e2ed1071 | ||
|
|
1f8e75388d | ||
|
|
ee0c6fdf2d | ||
|
|
54b7d24081 | ||
|
|
bd572d8059 | ||
|
|
bf76c7d2c7 | ||
|
|
61e885db5f | ||
|
|
6001562585 | ||
|
|
bad82ee587 | ||
|
|
1d1b41db83 | ||
|
|
16beb1a86c | ||
|
|
690b79f113 | ||
|
|
4d13785119 | ||
|
|
4f29b77d65 | ||
|
|
be08bed01e | ||
|
|
d64072076d | ||
|
|
ea4fc96df5 | ||
|
|
e107e2d804 | ||
|
|
10351c3d4a | ||
|
|
7e91024670 | ||
|
|
67b4798caa | ||
|
|
301e940236 | ||
|
|
5bdac195fc | ||
|
|
8134b6dff7 | ||
|
|
7083f7b268 | ||
|
|
72d2b22088 | ||
|
|
acf410776d | ||
|
|
715fa606ff | ||
|
|
82f82190a1 | ||
|
|
f85ac1ea10 | ||
|
|
f6926cc2e4 | ||
|
|
b6a245c852 | ||
|
|
874792fa6e | ||
|
|
8312f652a4 | ||
|
|
378f7bbda1 | ||
|
|
ca13595e37 | ||
|
|
22a43bcfc5 | ||
|
|
5dbb553a20 | ||
|
|
4c54e6cd31 | ||
|
|
33397edf9a | ||
|
|
fa748711c4 | ||
|
|
9ffa4decd4 | ||
|
|
6e80d78297 | ||
|
|
8a4213ced1 | ||
|
|
389ad61251 | ||
|
|
910fe051e0 | ||
|
|
d8dd2e8636 | ||
|
|
8d1429d063 | ||
|
|
d1b5c08272 | ||
|
|
6ef3cf776f | ||
|
|
0a6f2e569a | ||
|
|
cdd3801549 | ||
|
|
1f453e70ee | ||
|
|
2bd31a77d3 | ||
|
|
aeb340a4cb | ||
|
|
1ffdcfc89e | ||
|
|
51f4af0c5c | ||
|
|
159a82e077 | ||
|
|
01476c53ac | ||
|
|
bc0bc7c68a | ||
|
|
880f6a3220 | ||
|
|
45fd97d980 | ||
|
|
557e4d7bd2 | ||
|
|
4edcf0fecc | ||
|
|
ae409d05c1 | ||
|
|
6b732da749 | ||
|
|
45741365f4 | ||
|
|
2c86863258 | ||
|
|
8994806789 | ||
|
|
e51f0df1a6 | ||
|
|
a8a0c64a2a | ||
|
|
5449561fe2 | ||
|
|
073fae648e | ||
|
|
6121caf75d | ||
|
|
253d6669eb | ||
|
|
533a1d46eb | ||
|
|
c0b4fbfeb1 | ||
|
|
f6d96bbf17 | ||
|
|
52658b636a | ||
|
|
c32994af66 | ||
|
|
a1ac821678 | ||
|
|
2e9b02b65a | ||
|
|
548ed836a0 | ||
|
|
78a0ebab6e | ||
|
|
fad987641b | ||
|
|
3aa053e7a8 | ||
|
|
7aebf371e9 | ||
|
|
ac8ce3eec3 | ||
|
|
567ec55875 | ||
|
|
59569dcb34 | ||
|
|
3b09a5bde6 | ||
|
|
01f7f9b601 | ||
|
|
907ff374e6 | ||
|
|
a786418e19 | ||
|
|
5c2ef06035 | ||
|
|
6448f7ea12 | ||
|
|
e5c34e3dd7 | ||
|
|
ce2803f354 | ||
|
|
7e3d22b10e | ||
|
|
84dab55993 | ||
|
|
1878967da1 | ||
|
|
60a143eb7c | ||
|
|
aa961ff518 | ||
|
|
40cdc9108a | ||
|
|
bbdc7e1fe1 | ||
|
|
b62721d79a | ||
|
|
450db2678a | ||
|
|
02a5434e4f | ||
|
|
51b10e2bc4 | ||
|
|
8cb3d7771c | ||
|
|
4ef40ff151 | ||
|
|
d7e5c2ccac | ||
|
|
f414fae60a | ||
|
|
a6c57b7128 | ||
|
|
cbf1d35f24 | ||
|
|
ecf36f12cb | ||
|
|
69c8c3d0a2 | ||
|
|
fa79df4309 | ||
|
|
c97839905c | ||
|
|
d34149b236 | ||
|
|
d9d3e5d613 | ||
|
|
a745af8ead | ||
|
|
6e0d2f97cc | ||
|
|
e288ea3690 | ||
|
|
87c1727ea0 | ||
|
|
1a9a42ff54 | ||
|
|
5592056351 | ||
|
|
98bfd87446 | ||
|
|
3f63be7290 | ||
|
|
b23d0cb8eb | ||
|
|
0da009d1b9 | ||
|
|
b1fade86fe | ||
|
|
04bad947e3 | ||
|
|
f2cdb08e76 | ||
|
|
da3b0b5e21 | ||
|
|
a240d312d0 | ||
|
|
bd48e2055e | ||
|
|
42c212df64 | ||
|
|
59e811328f | ||
|
|
317c1a1475 | ||
|
|
a93e67662b | ||
|
|
a42f597463 | ||
|
|
14379b1be9 | ||
|
|
2e8767509e | ||
|
|
429dfd7467 | ||
|
|
a1eddb9e50 | ||
|
|
71a7b54edd | ||
|
|
ad05fbe97b | ||
|
|
0fed962b37 | ||
|
|
29163884ce | ||
|
|
ee36796180 | ||
|
|
cff2fd8ed9 | ||
|
|
344cbd0660 | ||
|
|
e6e676c91e | ||
|
|
5fd2f49266 | ||
|
|
10fb3e6d31 | ||
|
|
535315485a | ||
|
|
aa592b8bc6 | ||
|
|
88146a5389 | ||
|
|
c8789c5450 | ||
|
|
766035f4b7 | ||
|
|
a90b504dc7 | ||
|
|
26b2280fc2 | ||
|
|
15bbca8d77 | ||
|
|
319bcdaba9 | ||
|
|
89445f94a2 | ||
|
|
beb4aef424 | ||
|
|
ef680c080d | ||
|
|
c187f5de1d | ||
|
|
d40a151e05 | ||
|
|
e67c141921 | ||
|
|
de2ffd3aad | ||
|
|
4c8fd58471 | ||
|
|
873a102872 | ||
|
|
6dc11de431 | ||
|
|
672f25ce27 | ||
|
|
9731c4b394 | ||
|
|
b2c5955e7d | ||
|
|
e0ce1603ff | ||
|
|
bc0380e1cb | ||
|
|
1223cfc965 | ||
|
|
2f63a5fd10 | ||
|
|
b827310c75 | ||
|
|
361a4203da | ||
|
|
8de928d905 | ||
|
|
5a3d6e30cb | ||
|
|
227eda9d92 | ||
|
|
9fe0df74b3 | ||
|
|
381c1880c2 | ||
|
|
5439e5d473 | ||
|
|
0597e477ae | ||
|
|
50bac0a470 | ||
|
|
cfff603508 | ||
|
|
b9e734a21b | ||
|
|
45bd822c28 | ||
|
|
894a60ad45 | ||
|
|
acbe5885f1 | ||
|
|
a981b8b04d | ||
|
|
d75c0ac452 | ||
|
|
da061aac1d | ||
|
|
77433a3a1c | ||
|
|
4aaa32833a | ||
|
|
2a0cebb851 | ||
|
|
1b23bb8a67 | ||
|
|
5dc66c5b4e | ||
|
|
49b1800027 | ||
|
|
1cad83aba8 | ||
|
|
e1f5238063 | ||
|
|
5897d76c04 | ||
|
|
a0fc7723fc | ||
|
|
ec4a1d1724 | ||
|
|
6139216146 | ||
|
|
861c504c41 | ||
|
|
c9df6e4d25 | ||
|
|
c9d3ccdf0d | ||
|
|
2c275b279b | ||
|
|
44f6e0fded | ||
|
|
e5278fd17e | ||
|
|
1bd3c93717 | ||
|
|
afc6691f07 | ||
|
|
8c41c7b8ac | ||
|
|
5445d42eba | ||
|
|
d88f2ef7c0 | ||
|
|
14b5a67bf5 | ||
|
|
8e04b172ce | ||
|
|
5fff34be91 | ||
|
|
3467a60215 | ||
|
|
210133d9e2 | ||
|
|
524b1df4c9 | ||
|
|
9115949169 | ||
|
|
0b3aa6aac2 | ||
|
|
e03e0c6fc4 | ||
|
|
613a2722be | ||
|
|
e9b9d9702e | ||
|
|
750c123d1e | ||
|
|
5e0fc1cbb5 | ||
|
|
831e0b4b85 | ||
|
|
d64966594d | ||
|
|
015f027cf2 | ||
|
|
c3d9dead19 | ||
|
|
b296f17d2b | ||
|
|
22fdc971c7 | ||
|
|
291be04dc3 | ||
|
|
5c7215c455 | ||
|
|
2a83dcb64a | ||
|
|
979118a86f | ||
|
|
4fbd50e81f | ||
|
|
3b1c1c0886 | ||
|
|
dbc8bfc7f3 | ||
|
|
10bb6723f3 | ||
|
|
c58a5b520e | ||
|
|
7b6cb51f4d | ||
|
|
c704880e13 | ||
|
|
41c422ed03 | ||
|
|
c94ed00f41 | ||
|
|
1d93cf2b1a | ||
|
|
420f3a538d | ||
|
|
453e8dbd2e | ||
|
|
c6549867bd | ||
|
|
3aecb3ea98 | ||
|
|
2819546716 | ||
|
|
7e0f03a6fc | ||
|
|
6e36fc0b20 | ||
|
|
e856605a09 | ||
|
|
d7315773e1 | ||
|
|
a27aaf8bd0 | ||
|
|
f2c10e9f4c | ||
|
|
c0b875de5d | ||
|
|
141eb7b98e | ||
|
|
6099311cbd | ||
|
|
dcb06fbc4f | ||
|
|
8258c8ef0b | ||
|
|
9312c96154 | ||
|
|
c27b48d27a | ||
|
|
a091a7c5d6 | ||
|
|
f3039fb77c | ||
|
|
abe159eceb | ||
|
|
f06bdbb14f | ||
|
|
ffb6b3e6fe | ||
|
|
7f25de0fad | ||
|
|
d6c5d8b4f2 | ||
|
|
78e1ea43c8 | ||
|
|
9d24c855e2 | ||
|
|
872af9ce2a | ||
|
|
76a3d0b6da | ||
|
|
18b2a668c5 | ||
|
|
72be296713 | ||
|
|
078ab40d99 | ||
|
|
b620991770 | ||
|
|
732fc2d1e1 | ||
|
|
79a9ae31d3 | ||
|
|
e0e16eeca5 | ||
|
|
a784435c2b | ||
|
|
57987b6f67 | ||
|
|
480917eeab | ||
|
|
013e7bdf7e | ||
|
|
c4032ecd1d | ||
|
|
2ea11eb5ab | ||
|
|
7fad67484c | ||
|
|
f34cbbdf29 | ||
|
|
d44dd5da19 | ||
|
|
35132e7f46 | ||
|
|
231b3052fc | ||
|
|
1c0cc2acef | ||
|
|
14a4b04d9b | ||
|
|
8aee2a511f | ||
|
|
9bec13a72b | ||
|
|
65cb048b1b | ||
|
|
fb348ed4f7 | ||
|
|
cf10b826d7 | ||
|
|
b7ac5272d0 | ||
|
|
dc893f7ca3 | ||
|
|
206b1d4a05 | ||
|
|
0bd83b6635 | ||
|
|
4dcfe992de | ||
|
|
ef9d6662cb | ||
|
|
0e5f3b1697 | ||
|
|
7fbea7913a | ||
|
|
4b81001330 | ||
|
|
043029cf41 | ||
|
|
81988f1ec7 | ||
|
|
e49144c80a | ||
|
|
9ecc739e8d | ||
|
|
94799f19dc | ||
|
|
3d5f4eec18 | ||
|
|
c7e89d3018 | ||
|
|
63e3f6c3fc | ||
|
|
d39a583127 | ||
|
|
e1762fdcdd | ||
|
|
f568427dc2 | ||
|
|
fa077e4fc7 | ||
|
|
fd745d37a9 | ||
|
|
2da26b2de2 | ||
|
|
6e486bb30b | ||
|
|
5e297dd293 | ||
|
|
17dc653149 | ||
|
|
63cbb04acc | ||
|
|
6f55c7bbba | ||
|
|
ea4adf1039 | ||
|
|
3ff7818d58 | ||
|
|
e779198a80 | ||
|
|
3e4ebe8e47 | ||
|
|
b77a374f48 | ||
|
|
43936f640c | ||
|
|
6671d371e9 | ||
|
|
4fd99d2c52 | ||
|
|
092550e2a3 | ||
|
|
6563e730a0 | ||
|
|
b39c3e8d4b | ||
|
|
579129b891 | ||
|
|
ec8bb7e14d | ||
|
|
9f092a3c2e | ||
|
|
5b820b270c | ||
|
|
4af4c6706f | ||
|
|
da2c89daf3 | ||
|
|
ea425ae4c4 | ||
|
|
fb9c0715b3 | ||
|
|
8cedfebc07 | ||
|
|
93736b7ba9 | ||
|
|
37a6e8fde5 | ||
|
|
0274d9fee7 | ||
|
|
f223644640 | ||
|
|
0aa4607d8e | ||
|
|
37b68bb166 | ||
|
|
e801064bd9 | ||
|
|
ae4eef5d16 | ||
|
|
fdba5f55c5 | ||
|
|
bfeaabc038 | ||
|
|
6b9bad37c2 | ||
|
|
a32b2d5f73 | ||
|
|
9f4db09bec | ||
|
|
58fe3d3d27 | ||
|
|
28303a7893 | ||
|
|
031e5e7ca3 | ||
|
|
c70079ae71 | ||
|
|
8ac2b94400 | ||
|
|
b1f2fb44a8 | ||
|
|
cd4c3cffe0 | ||
|
|
98c01b88e2 | ||
|
|
4970a2742d | ||
|
|
10d18e35ed | ||
|
|
12960f8160 | ||
|
|
622d738b44 | ||
|
|
4e5c0aed67 | ||
|
|
96d3ad2c47 | ||
|
|
c519784ecc | ||
|
|
d0d7033f63 | ||
|
|
9aaa565657 | ||
|
|
50e98d1335 | ||
|
|
837df80b64 | ||
|
|
f7a27cf905 | ||
|
|
915047b996 | ||
|
|
8231d8ddf1 | ||
|
|
125673880c | ||
|
|
b24cb2c48e | ||
|
|
a56458384c | ||
|
|
063f2f0283 | ||
|
|
da35d6b6ed | ||
|
|
fca30325e4 | ||
|
|
4aac83b50d | ||
|
|
48b4345e80 | ||
|
|
4636c0f3b8 | ||
|
|
db5f5ba332 | ||
|
|
7c093e1a64 | ||
|
|
8f48d01c33 | ||
|
|
89d345dfdc | ||
|
|
1bbf87e2cb | ||
|
|
ec9c658565 | ||
|
|
8db24fcaf6 | ||
|
|
d9859af73c | ||
|
|
bce326de9d | ||
|
|
83bc94d1c3 | ||
|
|
676ddc4601 | ||
|
|
19cceb4dd9 | ||
|
|
3a4b8fe947 | ||
|
|
f31ac7cf21 | ||
|
|
802c644d94 | ||
|
|
5385e63fd2 | ||
|
|
749909b23c | ||
|
|
df594ae48e | ||
|
|
980e2f2c91 | ||
|
|
e1a3f2b64d | ||
|
|
ea47767bb8 | ||
|
|
3d3674a7d8 | ||
|
|
192b6ecfc4 | ||
|
|
79ff197cde | ||
|
|
836698f8d2 | ||
|
|
ac52d32c29 | ||
|
|
e66a0656c8 | ||
|
|
7dcf5629d7 | ||
|
|
facb129183 | ||
|
|
0b83c20648 | ||
|
|
0f9cadf8d3 | ||
|
|
c82e5750f0 | ||
|
|
13557bd13f | ||
|
|
2ff5eb39eb | ||
|
|
dccdfb5635 | ||
|
|
aba4c865e4 | ||
|
|
9709ca8b52 | ||
|
|
ea74a503a3 | ||
|
|
675d1ebd57 | ||
|
|
7eb042d985 | ||
|
|
dc4aa545f0 | ||
|
|
b70abb730d | ||
|
|
36a2cc9a62 | ||
|
|
113be1254e | ||
|
|
674a03dc23 | ||
|
|
9656985dbd | ||
|
|
0b4fe62f89 | ||
|
|
847a9c2b98 | ||
|
|
ce78d098ec | ||
|
|
18c1866db7 | ||
|
|
1ae0566cf1 | ||
|
|
383fb9d704 | ||
|
|
7f281d0442 | ||
|
|
2d76a03b01 | ||
|
|
f96ffde1fb | ||
|
|
e216f0a355 | ||
|
|
0e86264837 | ||
|
|
39fb7c6b5b | ||
|
|
0fa2b62cc9 | ||
|
|
ea69f8b9cc | ||
|
|
b0e88390e5 | ||
|
|
57d0750f3c | ||
|
|
4882f26142 | ||
|
|
3be864e50f | ||
|
|
d583403195 | ||
|
|
373737cfd0 | ||
|
|
946362f0d1 | ||
|
|
7ee4003840 | ||
|
|
475945d329 | ||
|
|
5111a02672 | ||
|
|
f2bebcb2e6 | ||
|
|
e413f3a9fd | ||
|
|
0c4d33dccc | ||
|
|
12477179cb | ||
|
|
ab81b1ad32 | ||
|
|
4af8fcc008 | ||
|
|
6800472725 | ||
|
|
0af674ca74 | ||
|
|
5b95490371 | ||
|
|
e6b9b9cfbd | ||
|
|
9dfaf293f2 | ||
|
|
fffcc85486 | ||
|
|
993e9a9e86 | ||
|
|
b349b23b81 | ||
|
|
886a4a422f | ||
|
|
2f32e5eaef | ||
|
|
d8dd3d0660 | ||
|
|
f19901c214 | ||
|
|
fcd1897883 | ||
|
|
77063efe58 | ||
|
|
e198fbdc40 | ||
|
|
3dd97c0b49 | ||
|
|
57a53abbf7 | ||
|
|
e30380a8f9 | ||
|
|
b93aa732d2 | ||
|
|
16d8780812 | ||
|
|
cdcaf1c4d4 | ||
|
|
febd1260cd | ||
|
|
45162a3fcb | ||
|
|
e62b22da55 | ||
|
|
b598e80c58 | ||
|
|
41648b8605 | ||
|
|
7bd8660f89 | ||
|
|
d0c63345f2 | ||
|
|
6b285cd7f0 | ||
|
|
a5e9d65852 | ||
|
|
1a7b7054fb | ||
|
|
01fc279709 | ||
|
|
5540ef8ca7 | ||
|
|
ef3ee2e660 | ||
|
|
d661c83acc | ||
|
|
437bcc8078 | ||
|
|
94195237d9 | ||
|
|
b8f63166cd | ||
|
|
68316732de | ||
|
|
0a42b39a94 | ||
|
|
fc91326a5f | ||
|
|
0fc2ed7081 | ||
|
|
8433d4a1d1 | ||
|
|
ac1407484a | ||
|
|
a531dc6fc0 | ||
|
|
e42d5b860e | ||
|
|
57858db161 | ||
|
|
f3392c7df1 | ||
|
|
18c2145138 | ||
|
|
ff6981bf10 | ||
|
|
db82911b1f | ||
|
|
768f5121cb | ||
|
|
c2d7a1dc74 | ||
|
|
61efb2fcdf | ||
|
|
b6716f77e0 | ||
|
|
47c4bb957b | ||
|
|
1a64819ab5 | ||
|
|
ff7939df03 | ||
|
|
443ced0dce | ||
|
|
baf505df52 | ||
|
|
90331b075a | ||
|
|
900e3b6ea1 | ||
|
|
74d8463255 | ||
|
|
af6692d616 | ||
|
|
6611b74c19 | ||
|
|
37bff8b70b | ||
|
|
d964b9f10a | ||
|
|
189a2ddc3d | ||
|
|
641b35d965 | ||
|
|
433e6606b6 | ||
|
|
bc88af047e | ||
|
|
b93beb8730 | ||
|
|
24a176c736 | ||
|
|
4d741040b6 | ||
|
|
92a30edf6c | ||
|
|
2615577173 | ||
|
|
031c3acc57 | ||
|
|
500541a785 | ||
|
|
60e01cdb33 | ||
|
|
c0c5325d8a | ||
|
|
fbe4b3d439 | ||
|
|
aa72c9e081 | ||
|
|
91db2f409e | ||
|
|
2c2211026f | ||
|
|
578f00172f | ||
|
|
fb93d3dcf0 | ||
|
|
10989ada31 | ||
|
|
28e9063c04 | ||
|
|
63ec5e78b6 | ||
|
|
52f6503631 | ||
|
|
a5a91d7258 | ||
|
|
a00b94ec33 | ||
|
|
78b1e292f9 | ||
|
|
d9d86df1bd | ||
|
|
b5f9f05f9f | ||
|
|
245561d40f | ||
|
|
9301ba7c25 | ||
|
|
24e9eadfab | ||
|
|
fe8f2deb6e | ||
|
|
5aa315e190 | ||
|
|
d180a25f2e | ||
|
|
1ae19f88b9 | ||
|
|
50702a0560 | ||
|
|
fbee21dad3 | ||
|
|
13c3fe5771 | ||
|
|
fde9f56d4d | ||
|
|
f2ca8ab940 | ||
|
|
3d0b1b7680 | ||
|
|
40172028e6 | ||
|
|
49923fa140 | ||
|
|
b96a4e5b94 | ||
|
|
9a50fc4789 | ||
|
|
601daf9e72 | ||
|
|
2658852ec2 | ||
|
|
737fd115e4 | ||
|
|
461f341abc | ||
|
|
c26feeb73e | ||
|
|
29714ae18f | ||
|
|
496f934adb | ||
|
|
bf15c2bf11 | ||
|
|
07cf8ac9be | ||
|
|
9708f80ad4 | ||
|
|
0bd0dc6216 | ||
|
|
d3e5583070 | ||
|
|
10eedb2bb5 | ||
|
|
d381a602b7 | ||
|
|
266251ffbf | ||
|
|
cd9bb7a86e | ||
|
|
97f98e3c93 | ||
|
|
c72c761a60 | ||
|
|
2fe83853ac | ||
|
|
cd940d64ed | ||
|
|
15d4575b0a | ||
|
|
e21d23e911 | ||
|
|
9e649ce96a | ||
|
|
68526721b7 | ||
|
|
be3c5dac8a | ||
|
|
26cac2b84d | ||
|
|
2f552abf94 | ||
|
|
7129ebe884 | ||
|
|
b9992215f8 | ||
|
|
0c55a5f372 | ||
|
|
8cc33d1e37 | ||
|
|
73169f0be5 | ||
|
|
7f8d3f289b | ||
|
|
d8c74e81b8 | ||
|
|
3000254e67 | ||
|
|
2356340f0b | ||
|
|
227d743666 | ||
|
|
b53feeb04c | ||
|
|
3b1043a6b4 | ||
|
|
ba32f92e69 | ||
|
|
5c7d978245 | ||
|
|
d96b642f4f | ||
|
|
2366c5c1fd | ||
|
|
84a85a767c | ||
|
|
e0c9bd10a4 | ||
|
|
eed6c93349 | ||
|
|
7023bb8b90 | ||
|
|
579814a5de | ||
|
|
a2786a9aee | ||
|
|
bcd0c725ce | ||
|
|
3c1c01fd9f | ||
|
|
3364d1c5cc | ||
|
|
8d61a5fbe1 | ||
|
|
b2cd64b956 | ||
|
|
53488c0adb | ||
|
|
ca9b8a78b2 | ||
|
|
c05b900fe6 | ||
|
|
907450a63d | ||
|
|
6b1e308dab | ||
|
|
e28df7d7d7 | ||
|
|
84d3a820f2 | ||
|
|
dda325b51b | ||
|
|
f0e29069a9 | ||
|
|
4730c23a97 | ||
|
|
593d098cc1 | ||
|
|
2e15f351a9 | ||
|
|
14a195592a | ||
|
|
7c81471a3a | ||
|
|
42ea1a5972 | ||
|
|
1da107c404 | ||
|
|
b2ada8a76f | ||
|
|
5ac90c163a | ||
|
|
53e7d14e1f | ||
|
|
186e2e6130 | ||
|
|
fefa603b45 | ||
|
|
99cf8e6387 | ||
|
|
53b4fe4fdc | ||
|
|
6773df15b9 | ||
|
|
bc64d147a8 | ||
|
|
569f93549e | ||
|
|
2045d1eaeb | ||
|
|
a2595093f8 | ||
|
|
9a9c9b8481 | ||
|
|
99ec1af301 | ||
|
|
f3fe384df1 | ||
|
|
c7f3dd5b0f | ||
|
|
547be633b6 | ||
|
|
a88002861b | ||
|
|
395022d652 | ||
|
|
dd24de5daf | ||
|
|
3c02cf5358 | ||
|
|
bf0f0ab509 | ||
|
|
3d0639b6dc | ||
|
|
ed50f3b654 | ||
|
|
25d186bf5c | ||
|
|
e02107102c | ||
|
|
1376466939 | ||
|
|
f2c05c4841 | ||
|
|
17136d23f9 | ||
|
|
812f69f4a7 | ||
|
|
7f286129de | ||
|
|
b92459ba38 | ||
|
|
51e9737114 | ||
|
|
66051b066f | ||
|
|
a3cb43bad8 | ||
|
|
ba4f0f7e4d | ||
|
|
ada5887deb | ||
|
|
af148b5008 | ||
|
|
205b645de8 | ||
|
|
f1f0703619 | ||
|
|
dc23fe49af | ||
|
|
44de0334b7 | ||
|
|
de96e0c22c | ||
|
|
2f5593390e | ||
|
|
0ef1239283 | ||
|
|
258dd7ba38 | ||
|
|
3e38560620 | ||
|
|
1826a14afe | ||
|
|
e46fc1dc77 | ||
|
|
debb43c1ca | ||
|
|
da8bf0abe1 | ||
|
|
92b7583f9f | ||
|
|
8950c4f0e3 | ||
|
|
9442f6b7d7 | ||
|
|
c14e08633d | ||
|
|
dfbf07b10c | ||
|
|
b2b29c928f | ||
|
|
c56448a29c | ||
|
|
42a00edbd8 | ||
|
|
18bb6daf10 | ||
|
|
799cb189c9 | ||
|
|
1112e658d4 | ||
|
|
23cdee2f22 | ||
|
|
13688507dc | ||
|
|
568d0faef6 | ||
|
|
41035a5b10 | ||
|
|
4cb0a87c68 | ||
|
|
cab5683cd9 | ||
|
|
4d20007a9b | ||
|
|
56eb096903 | ||
|
|
c077863e7a | ||
|
|
49e5cb64ad | ||
|
|
7e99e3ca20 | ||
|
|
0b4d156b5b | ||
|
|
76ba6a4e64 | ||
|
|
46bab97b49 | ||
|
|
2a32934f86 | ||
|
|
bda5594f0c | ||
|
|
134688ef5c | ||
|
|
e3fd7a295c | ||
|
|
c2d07b7b06 | ||
|
|
d9f31e8543 | ||
|
|
38f523b3c2 | ||
|
|
69a353d969 | ||
|
|
945e70c0ff | ||
|
|
277e9bee7b | ||
|
|
95614d567c | ||
|
|
6a488af1ca | ||
|
|
a0f9cb5183 | ||
|
|
1a74b4c692 | ||
|
|
746311c6cf | ||
|
|
876aaa483e | ||
|
|
570c31f248 | ||
|
|
b3695ed710 | ||
|
|
e9dddaad20 | ||
|
|
2b254a6f16 | ||
|
|
e9022d045c | ||
|
|
c70ea739ec | ||
|
|
85da0164ba | ||
|
|
89b262103f | ||
|
|
2c5744be57 | ||
|
|
cfcf8bc567 | ||
|
|
d1acff5b44 | ||
|
|
2add37983d | ||
|
|
ce00fdaae8 | ||
|
|
2fe29d4a26 | ||
|
|
fc582d8125 | ||
|
|
c41307dc37 | ||
|
|
27280f6da1 | ||
|
|
ce7fec170a | ||
|
|
98ef6fd902 | ||
|
|
1c4d9b1d8f | ||
|
|
b8b988c10b | ||
|
|
0eb87f85b4 | ||
|
|
a0136e2144 | ||
|
|
d12770c65d | ||
|
|
b0186bf4f2 | ||
|
|
9e15700f93 | ||
|
|
6b2494ede4 | ||
|
|
266965f506 | ||
|
|
1226f5120c | ||
|
|
f4ac0630b8 | ||
|
|
b3cb22caef | ||
|
|
b1d96dfdee | ||
|
|
25732f2f52 | ||
|
|
5ecdaafadf | ||
|
|
223bbc86e1 | ||
|
|
0867e718d1 | ||
|
|
c814b5daed | ||
|
|
0e8ee2f7d5 | ||
|
|
4b22d67f42 | ||
|
|
edf0a61653 | ||
|
|
e50ede815c | ||
|
|
6d2888e1c6 | ||
|
|
b6461ba4fb | ||
|
|
25708554fd | ||
|
|
74f204e6fa | ||
|
|
263ec36d80 | ||
|
|
bb6b738fc7 | ||
|
|
fb1c2732ac | ||
|
|
1fb42fc119 | ||
|
|
23f9170b90 | ||
|
|
7be2005fae | ||
|
|
afadec517b | ||
|
|
da5a12c938 | ||
|
|
a9aa15d63f | ||
|
|
e6d2d840e9 | ||
|
|
4963127c31 | ||
|
|
62c7ac891f | ||
|
|
e4b594f716 | ||
|
|
0835f18721 | ||
|
|
8231cbc55c | ||
|
|
b63cde48c6 | ||
|
|
30150178db | ||
|
|
61eebfcf46 | ||
|
|
40154ed86e | ||
|
|
23605ff2b0 | ||
|
|
964eb00ce1 | ||
|
|
930c1e874c | ||
|
|
1d96b09129 | ||
|
|
9c6ce6d200 | ||
|
|
a1aad41a6b | ||
|
|
38cb3cb1a2 | ||
|
|
751e1d27a6 | ||
|
|
36d4bd32b5 | ||
|
|
d835cf0c00 | ||
|
|
6ce85bae7e | ||
|
|
7d1df21b0e | ||
|
|
bbb87b4970 | ||
|
|
14d9bb795d | ||
|
|
2eb1d1bfb9 | ||
|
|
bae78b5055 | ||
|
|
c0705448cd | ||
|
|
e0a8fa3fde | ||
|
|
64f4b0fc13 | ||
|
|
c28a05e968 | ||
|
|
170d6ac2e7 | ||
|
|
64cc36a1ac | ||
|
|
af61bcb93f | ||
|
|
1f24e89c1b | ||
|
|
b12aee53d3 | ||
|
|
feb8ea0316 | ||
|
|
db51d5ca54 | ||
|
|
01e9f9181d | ||
|
|
3a9db70426 | ||
|
|
c7e850b95f | ||
|
|
89b3fb003c | ||
|
|
6abb8234fb | ||
|
|
4395663cc9 | ||
|
|
aeb982c616 | ||
|
|
3fd26e681b | ||
|
|
8bcf446187 | ||
|
|
42d51dc808 | ||
|
|
b05aa3dd32 | ||
|
|
98a539657c | ||
|
|
85f7125f22 | ||
|
|
2f8dd8c088 | ||
|
|
86d595b086 | ||
|
|
2f563fb870 | ||
|
|
c78a18dc3e | ||
|
|
0fa636dd79 | ||
|
|
7fc9fa70c1 | ||
|
|
9109cf3b54 | ||
|
|
50076efe1e | ||
|
|
097785fb93 | ||
|
|
7e07deeb23 | ||
|
|
e8c0c680ca | ||
|
|
d9ee7b23c2 | ||
|
|
03339e3c5d | ||
|
|
c58c5554f9 | ||
|
|
16ae7b7e74 | ||
|
|
6d3fec88ec | ||
|
|
4b74e7da31 | ||
|
|
fa70ffc918 | ||
|
|
fd177f4b9a | ||
|
|
08c666a53f | ||
|
|
639a0fe1c2 | ||
|
|
668567d6e2 | ||
|
|
09f92407ec | ||
|
|
9dc12c0142 | ||
|
|
2fd821ca00 | ||
|
|
03195b66bc | ||
|
|
a3d906165a | ||
|
|
4c1ce50dd2 | ||
|
|
b638242e6f | ||
|
|
2349aacc3e | ||
|
|
1be7bbe0dc | ||
|
|
ea332d5492 | ||
|
|
aedede7381 | ||
|
|
d4cd46c331 | ||
|
|
62450c3bb1 | ||
|
|
bafba81f56 | ||
|
|
651b445c75 | ||
|
|
5adc3c6a99 | ||
|
|
4914ff8205 | ||
|
|
783205c731 | ||
|
|
00ab2d1f3a | ||
|
|
98dcbdd87a | ||
|
|
4a936a6eef | ||
|
|
ef7ee1b699 | ||
|
|
4e70c1fb10 | ||
|
|
21d4fa5176 | ||
|
|
f16b2e8243 | ||
|
|
4e550a22b0 | ||
|
|
c2404d3c67 | ||
|
|
f1a5467d04 | ||
|
|
512486f2e8 | ||
|
|
f1d95fa883 | ||
|
|
31cf9b8527 | ||
|
|
e0e8e8c6cc | ||
|
|
6653690d0c | ||
|
|
e63f59d33e | ||
|
|
3702c18df5 | ||
|
|
dfe215f34c | ||
|
|
621a559585 | ||
|
|
61317eff71 | ||
|
|
b314a951da | ||
|
|
1a1c105c71 | ||
|
|
3f8ee35ddb | ||
|
|
d2ea74ea14 | ||
|
|
8e1fb771e0 | ||
|
|
bbe5c82700 | ||
|
|
95e3ff3349 | ||
|
|
00c3b2083b | ||
|
|
8549858951 | ||
|
|
4730c3f4b5 | ||
|
|
19ba66fd9d | ||
|
|
589c26192d | ||
|
|
8471aeea1b | ||
|
|
58c39c14a5 | ||
|
|
2cfe45950b | ||
|
|
688671db6a | ||
|
|
1d2e50dfa2 | ||
|
|
7a70036c8f | ||
|
|
ab88a3b236 | ||
|
|
266e14063e | ||
|
|
3af0296779 | ||
|
|
5ff05503a4 | ||
|
|
13e6782cf1 | ||
|
|
24052e6369 | ||
|
|
9b17ce5524 | ||
|
|
e4c2d62eb0 | ||
|
|
27fa37a7e1 | ||
|
|
987da2b812 | ||
|
|
d1611bae12 | ||
|
|
96a3c4b9dd | ||
|
|
8ef40dced5 | ||
|
|
4a6ddb901f | ||
|
|
a5094e0acc | ||
|
|
966d9fe0d2 | ||
|
|
fffcca592d | ||
|
|
d3cc6c29c9 | ||
|
|
341584c032 | ||
|
|
a1e178d0d7 | ||
|
|
89c00e193b | ||
|
|
b38a5d19b1 | ||
|
|
f7ae515d84 | ||
|
|
c59687b6e2 | ||
|
|
c2bd9f26a7 | ||
|
|
6d78b9b6df | ||
|
|
0669ce06f2 | ||
|
|
767d9378e3 | ||
|
|
c2d62b7a13 | ||
|
|
10e33298a9 | ||
|
|
ecc88112d5 | ||
|
|
b190063264 | ||
|
|
0c9c0c0239 | ||
|
|
ae333ecb77 | ||
|
|
72a85d11df | ||
|
|
f33ba954f5 | ||
|
|
f642330306 | ||
|
|
ae1a39322e | ||
|
|
29502d10eb | ||
|
|
37550b9fb0 | ||
|
|
efd2ed1960 | ||
|
|
150e44dcd5 | ||
|
|
3a164c8080 | ||
|
|
fb9223c6c3 | ||
|
|
6c6f69d4e5 | ||
|
|
e836226174 | ||
|
|
37fedd41e3 | ||
|
|
3711461201 | ||
|
|
4ddd8ac263 | ||
|
|
aa7a96cfb8 | ||
|
|
2c2768a34f | ||
|
|
91c2fd2085 | ||
|
|
9ab63ee854 | ||
|
|
ef1dd16766 | ||
|
|
39f4b573da | ||
|
|
2a0306b14d | ||
|
|
e0735a1d82 | ||
|
|
6f44fe9f93 | ||
|
|
f390e5ed17 | ||
|
|
ede3eb64ae | ||
|
|
d0d4972681 | ||
|
|
4be4a65c72 | ||
|
|
3e009bbf26 | ||
|
|
3db3f03e5d | ||
|
|
698f58eaa0 | ||
|
|
95cf933ab2 | ||
|
|
692f365fed | ||
|
|
96aa310839 | ||
|
|
8d3da8e4fa | ||
|
|
885145d43d | ||
|
|
c893e4a809 | ||
|
|
1536c55b99 | ||
|
|
f3f94eec66 | ||
|
|
51b633f0d7 | ||
|
|
5f1007dcbc | ||
|
|
ae7ce81b8c | ||
|
|
8b72e5db31 | ||
|
|
0d35ceda31 | ||
|
|
ed59003a95 | ||
|
|
94501c4618 | ||
|
|
bf3847ce59 | ||
|
|
c6ea894f10 | ||
|
|
8df4e1ee6e | ||
|
|
b608709014 | ||
|
|
0450136af3 | ||
|
|
29e4695503 | ||
|
|
a7a080f416 | ||
|
|
effb274410 | ||
|
|
45f6f4bc90 | ||
|
|
82b91b2f64 | ||
|
|
59a9d8c093 | ||
|
|
ae42acaef3 | ||
|
|
531be2db49 | ||
|
|
8089973b7b | ||
|
|
1e8cc3101b | ||
|
|
a27a5fbc32 | ||
|
|
fccd76ec4c | ||
|
|
39738b9ded | ||
|
|
3a18574838 | ||
|
|
ec238f2cc3 | ||
|
|
8c08803fd0 | ||
|
|
cc084692ae | ||
|
|
96a25dfcaa | ||
|
|
500237b2c8 | ||
|
|
67a0098c7a | ||
|
|
9f9044b164 | ||
|
|
4fc80f4bbd | ||
|
|
eca41e7474 | ||
|
|
9107322d28 | ||
|
|
88c1846e1a | ||
|
|
be3d559d8a | ||
|
|
f5ca6c39f6 | ||
|
|
653fdb999e | ||
|
|
f25d76d0d6 | ||
|
|
41b0f80360 | ||
|
|
31fe918b3d | ||
|
|
32495792dc | ||
|
|
30bdbb4544 | ||
|
|
c528421a8e | ||
|
|
0cfb53cea4 | ||
|
|
b349f0a2fb |
No files matched your search
@@ -0,0 +1,12 @@
|
||||
# EditorConfig reference: https://editorconfig.org
|
||||
# https://editorconfig.org/#file-format-details
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -5,9 +5,9 @@ name: Node build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ node ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ node ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 17.x, 18.x, 19.x]
|
||||
node-version: [24.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
name: Main branch - Build Testbook with fresh frameworks and manual tests & deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write # required by deploy-pages
|
||||
id-token: write # required by deploy-pages
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout cappuccino (this repo)
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Checkout Cappuccino-Testbook into ./testbook
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ArgosOz/Cappuccino-Testbook
|
||||
path: testbook
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
|
||||
- run: echo "${PWD}/dist/cappuccino/bin" >> $GITHUB_PATH
|
||||
- run: echo "${PWD}/dist/objective-j/bin" >> $GITHUB_PATH
|
||||
- run: npm install
|
||||
- run: npm update
|
||||
- run: jake dist
|
||||
- run: jake test-only
|
||||
|
||||
# Refresh Frameworks from dist → testbook/Frameworks
|
||||
- name: Refresh Frameworks
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ ! -d dist ]; then
|
||||
echo "❌ dist/ not found at repo root" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p testbook/Frameworks
|
||||
rm -rf testbook/Frameworks/*
|
||||
cp -a dist/cappuccino/Frameworks/{Foundation,AppKit} testbook/Frameworks/
|
||||
cp -a dist/objective-j/Frameworks/Objective-J testbook/Frameworks/
|
||||
echo "Frameworks populated:" && ls -la testbook/Frameworks || true
|
||||
|
||||
- name: Generate manifest of manual test directories
|
||||
if: ${{ hashFiles('Tests/Manual/**') != '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p artifacts
|
||||
OUT="testbook/Resources/recipes.txt"
|
||||
: > "$OUT"
|
||||
|
||||
if [ ! -d "Tests/Manual" ]; then
|
||||
echo "❌ Tests/Manual not found at repo root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Iterate over immediate subdirectories of Tests/Manual (null-safe for spaces)
|
||||
while IFS= read -r -d '' d; do
|
||||
name=$(basename "$d")
|
||||
ts=$(date +%s%3N) # milliseconds since epoch
|
||||
printf '%s|%s|\n' "$ts" "$name" >> "$OUT"
|
||||
# Optional tiny sleep to help keep timestamps distinct:
|
||||
sleep 0.01
|
||||
done < <(find "Tests/Manual" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
|
||||
|
||||
echo "Wrote $OUT:"
|
||||
cat "$OUT"
|
||||
|
||||
# Replace subdirectories inside testbook/Resources with Tests/Manual subdirectories
|
||||
- name: Sync Resources from Tests/Manual
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ ! -d "Tests/Manual" ]; then
|
||||
echo "❌ Tests/Manual not found at repo root" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p testbook/Resources
|
||||
# Remove only immediate subdirectories (keep stray files if any)
|
||||
find testbook/Resources -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -r rm -rf
|
||||
# Copy each immediate subdirectory from Tests/Manual → testbook/Resources
|
||||
while IFS= read -r -d '' d; do
|
||||
cp -a "$d" testbook/Resources/
|
||||
done < <(find Tests/Manual -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
echo "Resources now contains:" && ls -la testbook/Resources || true
|
||||
|
||||
# Ensure each test Index.html includes the include path line before OBJJ_MAIN_FILE
|
||||
- name: Inject OBJJ_INCLUDE_PATHS into Index.html files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
found=0
|
||||
while IFS= read -r -d '' f; do
|
||||
found=1
|
||||
if grep -q 'OBJJ_INCLUDE_PATHS' "$f"; then
|
||||
echo "Already updated: $f"
|
||||
continue
|
||||
fi
|
||||
perl -0777 -i -pe 's/OBJJ_MAIN_FILE\s*=\s*"main\.j";/OBJJ_INCLUDE_PATHS = ["..\/..\/Frameworks"];\nOBJJ_MAIN_FILE = "main.j";/i' "$f"
|
||||
perl -0777 -i -pe 's/Frameworks\/Objective-J\/Objective-J.js/\.\.\/\.\.\/Frameworks\/Objective-J\/Objective-J.js/' "$f"
|
||||
echo "Updated: $f"
|
||||
done < <(find testbook/Resources -mindepth 2 -maxdepth 2 -type f -iname "index.html" -print0)
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "⚠️ No index.html files found under testbook/Resources/*/" >&2
|
||||
fi
|
||||
|
||||
# Optional: prune VCS/CI metadata from the served content
|
||||
- name: Clean testbook for Pages (optional)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f testbook/index.html
|
||||
test -d testbook/Resources
|
||||
test -d testbook/Frameworks
|
||||
rm -rf testbook/.git testbook/.github || true
|
||||
find testbook -maxdepth 1 -type f -name ".git*" -delete || true
|
||||
|
||||
- name: Publish to gh-pages (preserve PR previews)
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: testbook
|
||||
clean-exclude: pr-preview/ # keep PR preview folders
|
||||
force: false # avoid force-push so previews survive
|
||||
|
||||
- name: Checkout gh-pages ./gh-pages
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: gh-pages
|
||||
ref: gh-pages
|
||||
|
||||
- name: Clean gh-pages for Pages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -d gh-pages
|
||||
rm -rf gh-pages/.git gh-pages/.github || true
|
||||
find gh-pages -maxdepth 1 -type f -name ".git*" -delete || true
|
||||
find gh-pages -type f -name "*.xib" -delete || true
|
||||
find gh-pages -type f -name "*xcode*" -delete || true
|
||||
find gh-pages -name "Jakefile" -delete || true
|
||||
|
||||
- name: Upload Pages artifact (testbook/)
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: gh-pages
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
- name: Comment Pages URL on PR
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const url = `${{ toJSON(steps.deployment.outputs.page_url) }}`;
|
||||
const body = `📄 GitHub Pages preview for this PR:\n\n${url}`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body
|
||||
});
|
||||
+3
-1
@@ -25,4 +25,6 @@ node_modules
|
||||
/dist/objective-j/lib
|
||||
/dist/cappuccino/package.json
|
||||
/dist/cappuccino/lib
|
||||
|
||||
/dist/cappuccino/bin
|
||||
Tests/Manual/.Frameworks
|
||||
/Tests/Manual/index.html
|
||||
@@ -84,6 +84,8 @@
|
||||
@import "CPRadio.j"
|
||||
@import "CPResponder.j"
|
||||
@import "CPRuleEditor.j"
|
||||
@import "CPRulerMarker.j"
|
||||
@import "CPRulerView.j"
|
||||
@import "CPScroller.j"
|
||||
@import "CPScrollView.j"
|
||||
@import "CPSearchField.j"
|
||||
@@ -93,12 +95,15 @@
|
||||
@import "CPSlider.j"
|
||||
@import "CPSound.j"
|
||||
@import "CPSplitView.j"
|
||||
@import "CPStackView.j"
|
||||
@import "CPStepper.j"
|
||||
@import "CPTableColumn.j"
|
||||
@import "CPTableView.j"
|
||||
@import "CPTabView.j"
|
||||
@import "CPText.j"
|
||||
@import "CPTextAttachment.j"
|
||||
@import "CPTextField.j"
|
||||
@import "CPTextList.j"
|
||||
@import "CPTextView.j"
|
||||
@import "CPTokenField.j"
|
||||
@import "CPToolbar.j"
|
||||
@@ -115,3 +120,5 @@
|
||||
@import "CPWindow.j"
|
||||
@import "CPWindowController.j"
|
||||
@import "CPWorkspace.j"
|
||||
@import "CPFontPanel.j"
|
||||
@import "CPTreeController.j"
|
||||
+92
-41
@@ -137,7 +137,8 @@ var bottomHeight = 71;
|
||||
@delegate -(void)alertDidEnd:(CPAlert)theAlert returnCode:(int)returnCode;
|
||||
Called when the user dismisses the alert by clicking one of the buttons.
|
||||
@param theAlert the alert panel that the user dismissed
|
||||
@param returnCode the index of the button that the user clicked (starting from 0,
|
||||
@param returnCode the tag of the button that the user clicked. Unless a button's
|
||||
tag has been changed, it is the button's index (starting from 0,
|
||||
representing the first button added to the alert which appears on the
|
||||
right, 1 representing the next button to the left and so on)
|
||||
*/
|
||||
@@ -172,7 +173,7 @@ var bottomHeight = 71;
|
||||
BOOL _needsLayout;
|
||||
}
|
||||
|
||||
#pragma mark Creating Alerts
|
||||
// MARK: Creating Alerts
|
||||
|
||||
/*!
|
||||
Returns a CPAlert object with the provided info
|
||||
@@ -249,8 +250,8 @@ var bottomHeight = 71;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate
|
||||
// MARK: -
|
||||
// MARK: Delegate
|
||||
|
||||
/*!
|
||||
Set the delegate of the receiver
|
||||
@@ -272,7 +273,7 @@ var bottomHeight = 71;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Accessors
|
||||
// MARK: Accessors
|
||||
|
||||
- (CPTheme)theme
|
||||
{
|
||||
@@ -402,7 +403,7 @@ var bottomHeight = 71;
|
||||
_needsLayout = YES;
|
||||
}
|
||||
|
||||
#pragma mark Accessing Buttons
|
||||
// MARK: Accessing Buttons
|
||||
|
||||
/*!
|
||||
Adds a button with a given title to the receiver.
|
||||
@@ -418,27 +419,32 @@ var bottomHeight = 71;
|
||||
*/
|
||||
- (void)addButtonWithTitle:(CPString)aTitle
|
||||
{
|
||||
var bounds = [[_window contentView] bounds],
|
||||
count = [_buttons count],
|
||||
|
||||
var count = [_buttons count],
|
||||
button = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[button setTitle:aTitle];
|
||||
[button setTag:count];
|
||||
[button setTarget:self];
|
||||
[button setAction:@selector(_takeReturnCodeFrom:)];
|
||||
[button setBezelStyle:CPSmallSquareBezelStyle];
|
||||
|
||||
[[_window contentView] addSubview:button];
|
||||
// Only add subview if the window has been created.
|
||||
// Otherwise, _createWindowWithStyle will handle adding the buttons from the _buttons array later.
|
||||
if (_window)
|
||||
[[_window contentView] addSubview:button];
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
[button setKeyEquivalent:CPCarriageReturnCharacter];
|
||||
[button setBezelStyle:CPRoundedBezelStyle];
|
||||
}
|
||||
else if ([aTitle lowercaseString] === @"cancel")
|
||||
[button setKeyEquivalent:CPEscapeFunctionKey];
|
||||
|
||||
[_buttons insertObject:button atIndex:0];
|
||||
}
|
||||
|
||||
#pragma mark Layout
|
||||
// MARK: Layout
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -530,6 +536,9 @@ var bottomHeight = 71;
|
||||
[[_window contentView] addSubview:_suppressionButton];
|
||||
}
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
*/
|
||||
/*!
|
||||
@ignore
|
||||
*/
|
||||
@@ -539,25 +548,17 @@ var bottomHeight = 71;
|
||||
minimumSize = [_themeView currentValueForThemeAttribute:@"size"],
|
||||
buttonOffset = [_themeView currentValueForThemeAttribute:@"button-offset"],
|
||||
helpLeftOffset = [_themeView currentValueForThemeAttribute:@"help-image-left-offset"],
|
||||
aRepresentativeButton = [_buttons objectAtIndex:0],
|
||||
defaultElementsMargin = [_themeView currentValueForThemeAttribute:@"default-elements-margin"],
|
||||
panelSize = [[_window contentView] frame].size,
|
||||
|
||||
buttonsOriginY,
|
||||
buttonMarginY,
|
||||
buttonMarginX,
|
||||
theme = [self theme],
|
||||
offsetX;
|
||||
|
||||
[aRepresentativeButton setTheme:[self theme]];
|
||||
[aRepresentativeButton sizeToFit];
|
||||
|
||||
panelSize.height = CGRectGetMaxY([lastView frame]) + defaultElementsMargin + [aRepresentativeButton frameSize].height;
|
||||
if (panelSize.height < minimumSize.height)
|
||||
panelSize.height = minimumSize.height;
|
||||
|
||||
buttonsOriginY = panelSize.height - [aRepresentativeButton frameSize].height + buttonOffset;
|
||||
offsetX = panelSize.width - inset.right;
|
||||
var isHUD = (_defaultWindowStyle & CPHUDBackgroundWindowMask) || (theme === [CPTheme defaultHudTheme]);
|
||||
|
||||
// 1. Determine Margins (Moved up so we can use them in height calculation)
|
||||
switch ([_window styleMask])
|
||||
{
|
||||
case _CPModalWindowMask:
|
||||
@@ -571,27 +572,61 @@ var bottomHeight = 71;
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. Prepare buttons and get row height
|
||||
var maxButtonHeight = 0.0;
|
||||
|
||||
for (var i = 0; i < [_buttons count]; i++)
|
||||
{
|
||||
var btn = _buttons[i];
|
||||
[btn sizeToFit];
|
||||
|
||||
if (isHUD)
|
||||
[btn setThemeState:CPThemeStateHUD];
|
||||
else
|
||||
[btn unsetThemeState:CPThemeStateHUD];
|
||||
|
||||
maxButtonHeight = MAX(maxButtonHeight, CGRectGetHeight([btn frame]));
|
||||
}
|
||||
|
||||
// 3. Calculate Content Height
|
||||
var lastViewMaxY = CGRectGetMaxY([lastView frame]);
|
||||
// Use bottomHeight (71) to reserve space for the footer
|
||||
var requiredContentHeight = lastViewMaxY + bottomHeight;
|
||||
|
||||
var finalContentSize = CGSizeMake(
|
||||
[[_window contentView] frame].size.width,
|
||||
MAX(requiredContentHeight, minimumSize.height)
|
||||
);
|
||||
|
||||
// 4. Position Buttons
|
||||
// Calculate the top Y coordinate to vertically center the button row within the bottomHeight area
|
||||
// Center Y of footer = Height - (bottomHeight / 2.0)
|
||||
// Top Y of button = Center Y - (maxButtonHeight / 2.0)
|
||||
buttonsOriginY = finalContentSize.height - ((bottomHeight + maxButtonHeight) / 2.0) - buttonMarginY;
|
||||
|
||||
offsetX = finalContentSize.width - inset.right;
|
||||
// Loop and set frames
|
||||
for (var i = [_buttons count] - 1; i >= 0 ; i--)
|
||||
{
|
||||
var button = _buttons[i];
|
||||
[button setTheme:[self theme]];
|
||||
[button sizeToFit];
|
||||
|
||||
var buttonFrame = [button frame],
|
||||
var button = _buttons[i],
|
||||
buttonFrame = [button frame],
|
||||
width = MAX(80.0, CGRectGetWidth(buttonFrame)),
|
||||
height = CGRectGetHeight(buttonFrame);
|
||||
height = CGRectGetHeight(buttonFrame),
|
||||
yOffset = FLOOR((maxButtonHeight - height) / 2.0);
|
||||
|
||||
offsetX -= width;
|
||||
[button setFrame:CGRectMake(offsetX + buttonMarginX, buttonsOriginY + buttonMarginY, width, height)];
|
||||
[button setFrame:CGRectMake(offsetX + buttonMarginX, buttonsOriginY + buttonMarginY + yOffset, width, height)];
|
||||
offsetX -= 10;
|
||||
}
|
||||
|
||||
// Position Help Button if needed
|
||||
if (_showHelp)
|
||||
{
|
||||
var helpImage = [_themeView currentValueForThemeAttribute:@"help-image"],
|
||||
helpImagePressed = [_themeView currentValueForThemeAttribute:@"help-image-pressed"],
|
||||
helpImageSize = helpImage ? [helpImage size] : CGSizeMakeZero(),
|
||||
helpFrame = CGRectMake(helpLeftOffset, buttonsOriginY, helpImageSize.width, helpImageSize.height);
|
||||
helpYOffset = FLOOR((maxButtonHeight - helpImageSize.height) / 2.0),
|
||||
helpFrame = CGRectMake(helpLeftOffset, buttonsOriginY + buttonMarginY + helpYOffset, helpImageSize.width, helpImageSize.height);
|
||||
|
||||
[_alertHelpButton setImage:helpImage];
|
||||
[_alertHelpButton setAlternateImage:helpImagePressed];
|
||||
@@ -599,8 +634,7 @@ var bottomHeight = 71;
|
||||
[_alertHelpButton setFrame:helpFrame];
|
||||
}
|
||||
|
||||
panelSize.height += [aRepresentativeButton frameSize].height + inset.bottom + buttonOffset;
|
||||
return panelSize;
|
||||
return finalContentSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -614,9 +648,14 @@ var bottomHeight = 71;
|
||||
if (!_window)
|
||||
[self _createWindowWithStyle:nil];
|
||||
|
||||
// Ensure the theme view knows if we are in HUD mode so it picks up the right specificities
|
||||
if ((_defaultWindowStyle & CPHUDBackgroundWindowMask) || ([self theme] === [CPTheme defaultHudTheme]))
|
||||
[_themeView setThemeState:CPThemeStateHUD];
|
||||
else
|
||||
[_themeView unsetThemeState:CPThemeStateHUD];
|
||||
|
||||
var iconOffset = [_themeView currentValueForThemeAttribute:@"image-offset"],
|
||||
theImage = _icon,
|
||||
finalSize;
|
||||
theImage = _icon;
|
||||
|
||||
if (!theImage)
|
||||
switch (_alertStyle)
|
||||
@@ -648,11 +687,17 @@ var bottomHeight = 71;
|
||||
else if (_accessoryView)
|
||||
lastView = _accessoryView;
|
||||
|
||||
finalSize = [self _layoutButtonsFromView:lastView];
|
||||
if ([_window styleMask] & CPDocModalWindowMask)
|
||||
finalSize.height -= 26; // adjust the absence of title bar
|
||||
|
||||
[_window setFrameSize:finalSize];
|
||||
// 1. Get the size needed for the *content* (text, buttons, padding)
|
||||
var finalContentSize = [self _layoutButtonsFromView:lastView];
|
||||
|
||||
// 2. Convert Content Size -> Frame Size
|
||||
// This accounts for the Title Bar and borders automatically.
|
||||
var contentRect = CGRectMake(0.0, 0.0, finalContentSize.width, finalContentSize.height);
|
||||
var frameRect = [[_window class] frameRectForContentRect:contentRect styleMask:[_window styleMask]];
|
||||
|
||||
// 3. Apply the calculated Frame Size
|
||||
[_window setFrameSize:frameRect.size];
|
||||
|
||||
[_window center];
|
||||
|
||||
if ([_window styleMask] & _CPModalWindowMask || [_window styleMask] & CPHUDBackgroundWindowMask)
|
||||
@@ -664,7 +709,7 @@ var bottomHeight = 71;
|
||||
_needsLayout = NO;
|
||||
}
|
||||
|
||||
#pragma mark Displaying Alerts
|
||||
// MARK: Displaying Alerts
|
||||
|
||||
/*!
|
||||
Displays the \c CPAlert panel as a modal dialog. The user will not be
|
||||
@@ -742,7 +787,7 @@ var bottomHeight = 71;
|
||||
[self beginSheetModalForWindow:aWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
// MARK: Private
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -752,6 +797,12 @@ var bottomHeight = 71;
|
||||
var frame = CGRectMakeZero();
|
||||
frame.size = [_themeView currentValueForThemeAttribute:@"size"];
|
||||
|
||||
// Propagate CPHUDBackgroundWindowMask from _defaultWindowStyle to forceStyle.
|
||||
// This ensures that even if we force CPDocModalWindowMask (for sheets),
|
||||
// the window still knows it should be a HUD.
|
||||
if (_defaultWindowStyle & CPHUDBackgroundWindowMask)
|
||||
forceStyle |= CPHUDBackgroundWindowMask;
|
||||
|
||||
_window = [[CPPanel alloc] initWithContentRect:frame styleMask:forceStyle || _defaultWindowStyle];
|
||||
[_window setLevel:CPStatusWindowLevel];
|
||||
[_window setPlatformWindow:[[CPApp keyWindow] platformWindow]];
|
||||
|
||||
@@ -63,8 +63,8 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class Methods
|
||||
// MARK: -
|
||||
// MARK: Class Methods
|
||||
|
||||
/*! Returns the current default CPAppearance
|
||||
*/
|
||||
@@ -99,8 +99,8 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*! Creates a CPAppearance object initialized to the specified appearance file in the specified bundle
|
||||
This method does actually nothing special. It just creates a default appearance object
|
||||
@@ -122,8 +122,8 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Implementation
|
||||
// MARK: -
|
||||
// MARK: Implementation
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
@@ -139,8 +139,8 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CPCoding
|
||||
// MARK: -
|
||||
// MARK: CPCoding
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
@@ -159,4 +159,4 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
[aCoder encodeBool:_allowsVibrancy forKey:@"_allowsVibrancy"];
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
+332
-26
@@ -37,6 +37,8 @@
|
||||
@import "CPWindowController.j"
|
||||
@import "_CPPopoverWindow.j"
|
||||
|
||||
@global CPWindowObjectList
|
||||
|
||||
@typedef CPModalSession
|
||||
|
||||
var CPMainCibFile = @"CPMainCibFile",
|
||||
@@ -121,7 +123,14 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
|
||||
CPPanel _aboutPanel;
|
||||
|
||||
CPMenu _windowsMenu;
|
||||
CPMenuItem _windowsMenuSeparator;
|
||||
|
||||
CPThemeBlend _themeBlend @accessors(property=themeBlend);
|
||||
|
||||
// OS behavior
|
||||
CPApplicationOSBehavior _OSBehavior;
|
||||
BOOL _simulatesWindows;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -154,6 +163,9 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
_eventListenerInsertionIndex = 0;
|
||||
|
||||
_windows = [[CPNull null]];
|
||||
|
||||
_OSBehavior = CPApplicationLegacyOSBehavior;
|
||||
_simulatesWindows = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -241,9 +253,10 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
// At this point we clear the window.status to eliminate Safari's "Cancelled" error message
|
||||
// The message shouldn't be displayed, because only an XHR is cancelled, but it is a usability issue.
|
||||
// We do it here so that applications can change it in willFinish or didFinishLaunching
|
||||
#if PLATFORM(DOM)
|
||||
window.status = " ";
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
window.status = " ";
|
||||
}
|
||||
|
||||
// We also want to set the default cursor on the body, so that buttons and things don't have an iBeam
|
||||
[[CPCursor arrowCursor] set];
|
||||
@@ -273,9 +286,10 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
var needsUntitled = !!_documentController,
|
||||
URLStrings = nil;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
URLStrings = window.cpOpeningURLStrings && window.cpOpeningURLStrings();
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
URLStrings = window.cpOpeningURLStrings && window.cpOpeningURLStrings();
|
||||
}
|
||||
|
||||
var index = 0,
|
||||
count = [URLStrings count];
|
||||
@@ -368,7 +382,7 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
to the following respective keys in your info.plist file.
|
||||
|
||||
<pre>
|
||||
CPBundleName
|
||||
CPBundleDisplayName, or CPBundleName when there is no display name
|
||||
CPApplicationIcon (through a call to -applicationIconImage, see documentation for that method for more details)
|
||||
CPBundleVersion
|
||||
CPBundleShortVersionString
|
||||
@@ -382,7 +396,7 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
if (!_aboutPanel)
|
||||
{
|
||||
var mainInfo = [[CPBundle mainBundle] infoDictionary],
|
||||
applicationTitle = [options objectForKey:"ApplicationName"] || [mainInfo objectForKey:@"CPBundleName"],
|
||||
applicationTitle = [options objectForKey:"ApplicationName"] || [self _applicationName],
|
||||
applicationIcon = [options objectForKey:@"ApplicationIcon"] || [self applicationIconImage],
|
||||
version = [options objectForKey:@"Version"] || [mainInfo objectForKey:@"CPBundleVersion"],
|
||||
applicationVersion = [options objectForKey:@"ApplicationVersion"] || [mainInfo objectForKey:@"CPBundleShortVersionString"],
|
||||
@@ -727,11 +741,10 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
*/
|
||||
- (CPArray)orderedWindows
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
return CPWindowObjectList();
|
||||
#else
|
||||
if (CPDOMAvailable)
|
||||
return CPWindowObjectList();
|
||||
|
||||
return [];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)hide:(id)aSender
|
||||
@@ -768,11 +781,238 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
|
||||
if ([CPPlatform supportsNativeMainMenu])
|
||||
window.cpSetMainMenu([self menu]);
|
||||
|
||||
// The new main menu may contain a different Windows menu, so find it again and list
|
||||
// the windows that were ordered in before the main menu was set.
|
||||
_windowsMenu = nil;
|
||||
_windowsMenuSeparator = nil;
|
||||
|
||||
for (var i = 0, count = [_windows count]; i < count; i++)
|
||||
{
|
||||
var aWindow = _windows[i];
|
||||
|
||||
// _windows is indexed by window number and holds a placeholder at index 0.
|
||||
if ([aWindow isKindOfClass:[CPWindow class]])
|
||||
[aWindow _updateWindowsMenuItem];
|
||||
}
|
||||
}
|
||||
else
|
||||
[aMenu _setMenuName:@"CPMainMenu"];
|
||||
}
|
||||
|
||||
/*
|
||||
@ignore
|
||||
Returns the application's name as users see it: CPBundleDisplayName from the main bundle's
|
||||
Info.plist, or CPBundleName when there is no display name.
|
||||
*/
|
||||
- (CPString)_applicationName
|
||||
{
|
||||
var mainBundle = [CPBundle mainBundle];
|
||||
|
||||
return [mainBundle objectForInfoDictionaryKey:@"CPBundleDisplayName"] || [mainBundle objectForInfoDictionaryKey:@"CPBundleName"];
|
||||
}
|
||||
|
||||
// MARK: Managing the Windows Menu
|
||||
|
||||
/*!
|
||||
Returns the application's Windows menu, which lists the application's windows.
|
||||
|
||||
Unless a menu has been set with \c -setWindowsMenu:, this is the submenu of the main menu
|
||||
named \c _CPWindowsMenu. nib2cib gives that name to the menu marked as the Windows menu in
|
||||
Interface Builder.
|
||||
@return the Windows menu, or \c nil if the application has none
|
||||
*/
|
||||
- (CPMenu)windowsMenu
|
||||
{
|
||||
if (!_windowsMenu)
|
||||
_windowsMenu = [[self mainMenu] _menuWithName:@"_CPWindowsMenu"];
|
||||
|
||||
return _windowsMenu;
|
||||
}
|
||||
|
||||
/*!
|
||||
Makes \c aMenu the application's Windows menu, and moves the items for the listed windows
|
||||
from the previous Windows menu to it.
|
||||
@param aMenu the new Windows menu
|
||||
*/
|
||||
- (void)setWindowsMenu:(CPMenu)aMenu
|
||||
{
|
||||
var previousMenu = [self windowsMenu];
|
||||
|
||||
if (previousMenu === aMenu)
|
||||
return;
|
||||
|
||||
var listedWindows = [self _windowsInWindowsMenu];
|
||||
|
||||
[listedWindows makeObjectsPerformSelector:@selector(_removeFromWindowsMenu)];
|
||||
|
||||
_windowsMenu = aMenu;
|
||||
_windowsMenuSeparator = nil;
|
||||
|
||||
[listedWindows makeObjectsPerformSelector:@selector(_updateWindowsMenuItem)];
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds an item for \c aWindow to the Windows menu. The item makes the window key and orders it
|
||||
front when chosen. Does nothing if the menu already has an item for \c aWindow.
|
||||
@param aWindow the window to list
|
||||
@param aString the item's title
|
||||
@param isFilename if \c YES, \c aString is a file path, shown as the file name followed by its
|
||||
directory
|
||||
*/
|
||||
- (void)addWindowsItem:(CPWindow)aWindow title:(CPString)aString filename:(BOOL)isFilename
|
||||
{
|
||||
var menu = [self windowsMenu];
|
||||
|
||||
if (!menu || [self _windowsMenuItemForWindow:aWindow])
|
||||
return;
|
||||
|
||||
// Separate the window list from the menu's own items.
|
||||
if (!_windowsMenuSeparator && [menu numberOfItems] > 0)
|
||||
{
|
||||
_windowsMenuSeparator = [CPMenuItem separatorItem];
|
||||
[menu addItem:_windowsMenuSeparator];
|
||||
}
|
||||
|
||||
var item = [[CPMenuItem alloc] initWithTitle:[self _windowsMenuTitle:aString filename:isFilename]
|
||||
action:@selector(makeKeyAndOrderFront:)
|
||||
keyEquivalent:@""];
|
||||
|
||||
[item setTarget:aWindow];
|
||||
[menu addItem:item];
|
||||
|
||||
[self updateWindowsItem:aWindow];
|
||||
}
|
||||
|
||||
/*!
|
||||
Changes the title of the Windows menu item for \c aWindow, adding the item if the menu has
|
||||
none for it.
|
||||
@param aWindow the listed window
|
||||
@param aString the item's new title
|
||||
@param isFilename if \c YES, \c aString is a file path, shown as the file name followed by its
|
||||
directory
|
||||
*/
|
||||
- (void)changeWindowsItem:(CPWindow)aWindow title:(CPString)aString filename:(BOOL)isFilename
|
||||
{
|
||||
var item = [self _windowsMenuItemForWindow:aWindow];
|
||||
|
||||
if (item)
|
||||
[item setTitle:[self _windowsMenuTitle:aString filename:isFilename]];
|
||||
else
|
||||
[self addWindowsItem:aWindow title:aString filename:isFilename];
|
||||
}
|
||||
|
||||
/*!
|
||||
Removes the Windows menu item for \c aWindow, and removes the separator above the window
|
||||
list when the last window item is removed.
|
||||
@param aWindow the window to remove from the list
|
||||
*/
|
||||
- (void)removeWindowsItem:(CPWindow)aWindow
|
||||
{
|
||||
var item = [self _windowsMenuItemForWindow:aWindow];
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
var menu = [self windowsMenu];
|
||||
|
||||
[menu removeItem:item];
|
||||
|
||||
if (_windowsMenuSeparator && ![[self _windowsInWindowsMenu] count])
|
||||
{
|
||||
[menu removeItem:_windowsMenuSeparator];
|
||||
_windowsMenuSeparator = nil;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Updates the state of the Windows menu item for \c aWindow: a check mark for the main window,
|
||||
the mixed state for a miniaturized window, and no mark otherwise.
|
||||
@param aWindow the listed window
|
||||
*/
|
||||
- (void)updateWindowsItem:(CPWindow)aWindow
|
||||
{
|
||||
var item = [self _windowsMenuItemForWindow:aWindow];
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
if ([aWindow isMiniaturized])
|
||||
[item setState:CPMixedState];
|
||||
else
|
||||
[item setState:aWindow === [self mainWindow] ? CPOnState : CPOffState];
|
||||
}
|
||||
|
||||
/*!
|
||||
Orders front every window listed in the Windows menu that is not miniaturized, keeping their
|
||||
order relative to each other.
|
||||
@param aSender the object requesting this
|
||||
*/
|
||||
- (void)arrangeInFront:(id)aSender
|
||||
{
|
||||
var listedWindows = [self _windowsInWindowsMenu],
|
||||
orderedWindows = [self orderedWindows],
|
||||
index = [orderedWindows count];
|
||||
|
||||
// orderedWindows lists front to back, so order front from the back.
|
||||
while (index--)
|
||||
{
|
||||
var aWindow = orderedWindows[index];
|
||||
|
||||
if ([listedWindows containsObjectIdenticalTo:aWindow] && ![aWindow isMiniaturized])
|
||||
[aWindow orderFront:aSender];
|
||||
}
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (CPMenuItem)_windowsMenuItemForWindow:(CPWindow)aWindow
|
||||
{
|
||||
var items = [[self windowsMenu] itemArray],
|
||||
count = [items count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
if ([item target] === aWindow && [item action] === @selector(makeKeyAndOrderFront:))
|
||||
return item;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (CPArray)_windowsInWindowsMenu
|
||||
{
|
||||
var items = [[self windowsMenu] itemArray],
|
||||
count = [items count],
|
||||
listedWindows = [];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
if ([item action] === @selector(makeKeyAndOrderFront:) && [[item target] isKindOfClass:[CPWindow class]])
|
||||
listedWindows.push([item target]);
|
||||
}
|
||||
|
||||
return listedWindows;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (CPString)_windowsMenuTitle:(CPString)aString filename:(BOOL)isFilename
|
||||
{
|
||||
if (!isFilename)
|
||||
return aString;
|
||||
|
||||
var directory = [aString stringByDeletingLastPathComponent];
|
||||
|
||||
if (![directory length])
|
||||
return [aString lastPathComponent];
|
||||
|
||||
return [aString lastPathComponent] + @" — " + directory;
|
||||
}
|
||||
|
||||
/*!
|
||||
Opens the shared color panel.
|
||||
@param aSender
|
||||
@@ -1260,7 +1500,45 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0,
|
||||
|
||||
+ (CPString)defaultThemeName
|
||||
{
|
||||
return ([[CPBundle mainBundle] objectForInfoDictionaryKey:"CPDefaultTheme"] || @"Aristo2");
|
||||
return ([[CPBundle mainBundle] objectForInfoDictionaryKey:"CPDefaultTheme"] || @"Aristo3");
|
||||
}
|
||||
|
||||
// See CPApplication_Constants.j for comments
|
||||
- (void)setOSBehavior:(CPApplicationOSBehavior)anOSBehavior
|
||||
{
|
||||
// Verify if provided OS behavior is valid
|
||||
if ([[CPApplicationOSBehaviors allKeysForObject:anOSBehavior] count] == 0)
|
||||
{
|
||||
CPLog.warn("CPApplication setOSBehavior: invalid CPApplicationOSBehavior (received "+anOSBehavior+"). Ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
_OSBehavior = anOSBehavior;
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationOSBehaviorDidChangeNotification object:CPApp userInfo:nil];
|
||||
}
|
||||
|
||||
- (CPApplicationOSBehavior)OSBehavior
|
||||
{
|
||||
return _OSBehavior;
|
||||
}
|
||||
|
||||
- (BOOL)shouldMimicWindows
|
||||
{
|
||||
return (_OSBehavior == CPApplicationFollowOSBehavior) && (CPBrowserIsOperatingSystem(CPWindowsOperatingSystem) || _simulatesWindows);
|
||||
}
|
||||
|
||||
- (void)setSimulatesWindows:(BOOL)shouldSimulateWindows
|
||||
{
|
||||
if (_simulatesWindows === shouldSimulateWindows)
|
||||
return;
|
||||
|
||||
_simulatesWindows = shouldSimulateWindows;
|
||||
}
|
||||
|
||||
- (BOOL)simulatesWindows
|
||||
{
|
||||
return _simulatesWindows;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1304,22 +1582,22 @@ _CPRunModalLoop = function(anEvent)
|
||||
|
||||
function CPApplicationMain(args, namedArgs)
|
||||
{
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
// hook to allow recorder, etc to manipulate things before starting AppKit
|
||||
if (window.parent !== window && typeof window.parent._childAppIsStarting === "function")
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
try
|
||||
// hook to allow recorder, etc to manipulate things before starting AppKit
|
||||
if (window.parent !== window && typeof window.parent._childAppIsStarting === "function")
|
||||
{
|
||||
window.parent._childAppIsStarting(window);
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
// This could happen if we're in an iframe without access to the parent frame.
|
||||
CPLog.warn("Failed to call parent frame's _childAppIsStarting().");
|
||||
try
|
||||
{
|
||||
window.parent._childAppIsStarting(window);
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
// This could happen if we're in an iframe without access to the parent frame.
|
||||
CPLog.warn("Failed to call parent frame's _childAppIsStarting().");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var mainBundle = [CPBundle mainBundle],
|
||||
principalClass = [mainBundle principalClass];
|
||||
@@ -1375,7 +1653,7 @@ var _CPAppBootstrapperActions = nil;
|
||||
var defaultThemeName = [CPApplication defaultThemeName],
|
||||
themeURL = nil;
|
||||
|
||||
if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2")
|
||||
if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2" || defaultThemeName === @"Aristo3")
|
||||
themeURL = [[CPBundle bundleForClass:[CPApplication class]] pathForResource:defaultThemeName + @".blend"];
|
||||
else
|
||||
themeURL = [[CPBundle mainBundle] pathForResource:defaultThemeName + @".blend"];
|
||||
@@ -1391,6 +1669,34 @@ var _CPAppBootstrapperActions = nil;
|
||||
[[CPApplication sharedApplication] setThemeBlend:aThemeBlend];
|
||||
[CPTheme setDefaultTheme:[CPTheme themeNamed:[CPApplication defaultThemeName]]];
|
||||
|
||||
// Search in the Info.plist if the special CPApplicationSimulateWindowsOS flag is set (for testing)
|
||||
[CPApp setSimulatesWindows:!![[CPBundle mainBundle] objectForInfoDictionaryKey:"CPApplicationSimulateWindowsOS"]];
|
||||
|
||||
// Before loading the main CIB, try to find if a CPApplicationOSBehavior is specified in the Info.plist or in the user defaults
|
||||
// (with user defaults precedence). Value stored must be a string representing the name of the OS behavior.
|
||||
var plistOSBehavior = [[CPBundle mainBundle] objectForInfoDictionaryKey:"CPApplicationOSBehavior"],
|
||||
userOSBehavior = [[CPUserDefaults standardUserDefaults] objectForKey:@"CPApplicationOSBehavior"];
|
||||
|
||||
if (userOSBehavior)
|
||||
{
|
||||
var osBehavior = [CPApplicationOSBehaviors objectForKey:userOSBehavior];
|
||||
|
||||
if (osBehavior)
|
||||
[CPApp setOSBehavior:osBehavior];
|
||||
else
|
||||
CPLog.warn("Invalid CPApplicationOSBehavior specified in user defaults (found:"+userOSBehavior+"). Ignored.");
|
||||
}
|
||||
|
||||
else if (plistOSBehavior)
|
||||
{
|
||||
var osBehavior = [CPApplicationOSBehaviors objectForKey:plistOSBehavior];
|
||||
|
||||
if (osBehavior)
|
||||
[CPApp setOSBehavior:osBehavior];
|
||||
else
|
||||
CPLog.warn("Invalid CPApplicationOSBehavior specified in Info.plist (found:"+plistOSBehavior+"). Ignored.");
|
||||
}
|
||||
|
||||
[self performActions];
|
||||
}
|
||||
|
||||
|
||||
@@ -39,3 +39,15 @@ CPTerminateLater = -1; // not currently supported
|
||||
CPRunStoppedResponse = -1000;
|
||||
CPRunAbortedResponse = -1001;
|
||||
CPRunContinuesResponse = -1002;
|
||||
|
||||
// Should the application follow Cappuccino UX-UI (which is OSX like) or OS UX-UI (mainly Windows) ?
|
||||
// See explanation on https://github.com/cappuccino/cappuccino/wiki/CPApplicationSelectedOSBehavior
|
||||
@typedef CPApplicationOSBehavior
|
||||
CPApplicationLegacyOSBehavior = 1;
|
||||
CPApplicationFollowOSBehavior = 2;
|
||||
|
||||
CPApplicationOSBehaviorDidChangeNotification = @"CPApplicationOSBehaviorDidChangeNotification";
|
||||
CPApplicationOSBehaviors = @{
|
||||
@"CPApplicationLegacyOSBehavior": CPApplicationLegacyOSBehavior,
|
||||
@"CPApplicationFollowOSBehavior": CPApplicationFollowOSBehavior
|
||||
};
|
||||
@@ -378,3 +378,47 @@ var DefaultLineWidth = 1.0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CPBezierPath (AnimationAdditions)
|
||||
|
||||
- (CPString)SVGString
|
||||
{
|
||||
var pathString = "";
|
||||
var elements = _path.elements;
|
||||
var count = _path.count;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var element = elements[i];
|
||||
|
||||
// Use the kCGPathElement* constants defined in CGPath.j
|
||||
switch (element.type)
|
||||
{
|
||||
case kCGPathElementMoveToPoint:
|
||||
pathString += "M " + element.x + " " + element.y + " ";
|
||||
break;
|
||||
|
||||
case kCGPathElementAddLineToPoint:
|
||||
pathString += "L " + element.x + " " + element.y + " ";
|
||||
break;
|
||||
|
||||
case kCGPathElementAddQuadCurveToPoint:
|
||||
pathString += "Q " + element.cpx + " " + element.cpy + " " + element.x + " " + element.y + " ";
|
||||
break;
|
||||
|
||||
case kCGPathElementAddCurveToPoint:
|
||||
pathString += "C " + element.cp1x + " " + element.cp1y + " " + element.cp2x + " " + element.cp2y + " " + element.x + " " + element.y + " ";
|
||||
break;
|
||||
|
||||
case kCGPathElementCloseSubpath:
|
||||
pathString += "Z ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pathString.trim();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+10
-10
@@ -264,8 +264,8 @@ CPBelowBottom = 6;
|
||||
[self _manageTitlePositioning];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Style properties which override theme values
|
||||
// MARK: -
|
||||
// MARK: Style properties which override theme values
|
||||
/*!
|
||||
The borderColor, borderWidth, cornerRadius and fillColor properties for the receiver
|
||||
are only supported for boxes with boxType === CPBoxCustom and borderType === CPLineBorder.
|
||||
@@ -282,10 +282,10 @@ CPBelowBottom = 6;
|
||||
*/
|
||||
|
||||
// See discussion above.
|
||||
#pragma mark borderColor
|
||||
// MARK: borderColor
|
||||
- (CPColor)borderColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"border-color"];
|
||||
return [self currentValueForThemeAttribute:@"border-color"];
|
||||
}
|
||||
|
||||
- (void)setBorderColor:(CPColor)color
|
||||
@@ -308,7 +308,7 @@ CPBelowBottom = 6;
|
||||
}
|
||||
|
||||
// See discussion above.
|
||||
#pragma mark borderWidth
|
||||
// MARK: borderWidth
|
||||
- (float)borderWidth
|
||||
{
|
||||
return [self valueForThemeAttribute:@"border-width"];
|
||||
@@ -334,7 +334,7 @@ CPBelowBottom = 6;
|
||||
}
|
||||
|
||||
// See discussion above.
|
||||
#pragma mark cornerRadius
|
||||
// MARK: cornerRadius
|
||||
- (float)cornerRadius
|
||||
{
|
||||
return [self valueForThemeAttribute:@"corner-radius"];
|
||||
@@ -360,7 +360,7 @@ CPBelowBottom = 6;
|
||||
}
|
||||
|
||||
// See discussion above.
|
||||
#pragma mark fillColor
|
||||
// MARK: fillColor
|
||||
- (CPColor)fillColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"background-color"];
|
||||
@@ -761,7 +761,7 @@ CPBelowBottom = 6;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPBox (CSSTheming)
|
||||
|
||||
@@ -785,7 +785,7 @@ CPBelowBottom = 6;
|
||||
[_boxView setFrame:CGRectMake(0,2,bounds.size.width,1)];
|
||||
}
|
||||
|
||||
[_boxView setBackgroundColor:[self valueForThemeAttribute:@"border-color"]];
|
||||
[_boxView setBackgroundColor:[self currentValueForThemeAttribute:@"border-color"]];
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -829,7 +829,7 @@ CPBelowBottom = 6;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
CPBoxBorderTypeKey = @"CPBoxBorderTypeKey",
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ var CPBrowserDelegate_browser_acceptDrop_atRow_column_dropOperation_
|
||||
|
||||
_prototypeView = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
|
||||
[_prototypeView setVerticalAlignment:CPCenterVerticalTextAlignment];
|
||||
[_prototypeView setValue:[CPColor whiteColor] forThemeAttribute:"text-color" inState:CPThemeStateSelectedDataView];
|
||||
[_prototypeView setValue:[CPColor whiteColor] forThemeAttribute:"text-color" inState:CPThemeStateSelectedDataView.and(CPThemeStateTableDataView)];
|
||||
[_prototypeView setLineBreakMode:CPLineBreakByTruncatingTail];
|
||||
|
||||
_horizontalScrollView = [[CPScrollView alloc] initWithFrame:[self bounds]];
|
||||
|
||||
+13
-7
@@ -98,7 +98,8 @@ var CPButtonBezelStyleStateMap = @{
|
||||
CPRegularSquareBezelStyle: CPButtonStateBezelStyleRegularSquare,
|
||||
CPTexturedSquareBezelStyle: CPButtonStateBezelStyleTextured,
|
||||
CPDisclosureBezelStyle: CPButtonStateBezelStyleDisclosure,
|
||||
CPRoundedDisclosureBezelStyle: CPButtonStateBezelStyleRoundedDisclosure
|
||||
CPRoundedDisclosureBezelStyle: CPButtonStateBezelStyleRoundedDisclosure,
|
||||
CPHUDBezelStyle: CPThemeStateHUD
|
||||
};
|
||||
|
||||
/// @cond IGNORE
|
||||
@@ -220,8 +221,8 @@ CPButtonImageOffset = 3.0;
|
||||
[self setButtonType:CPMomentaryPushInButton];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -232,7 +233,7 @@ CPButtonImageOffset = 3.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
// Setting the state
|
||||
/*!
|
||||
@@ -707,12 +708,17 @@ CPButtonImageOffset = 3.0;
|
||||
// Note : We have to split content and image visual states as, for example, radio buttons don't follow push buttons behavior
|
||||
- (CPThemeState)_contentVisualState
|
||||
{
|
||||
var visualState = [self themeState] || CPThemeStateNormal, // Needed during theme compilation
|
||||
var visualState = [self themeState] || CPThemeStateNormal,
|
||||
currentState = [self state],
|
||||
buttonIsOn = (currentState !== CPOffState);
|
||||
|
||||
// If the button is pushed (_isHighlighted), always add the highlighted state
|
||||
if (_isHighlighted || (((_showsStateBy & CPChangeGrayCellMask) || (_showsStateBy & CPChangeBackgroundCellMask)) && buttonIsOn))
|
||||
// Define masks that imply the background changes color (Blue/Gray)
|
||||
// If the background changes, we usually want the text to turn White (Highlighted state).
|
||||
var highlightMask = CPPushInCellMask | CPChangeGrayCellMask | CPChangeBackgroundCellMask;
|
||||
|
||||
// Only add CPThemeStateHighlighted if the button is configured to highlight visually (Background change)
|
||||
if ((_isHighlighted && (_highlightsBy & highlightMask)) ||
|
||||
(((_showsStateBy & CPChangeGrayCellMask) || (_showsStateBy & CPChangeBackgroundCellMask)) && buttonIsOn))
|
||||
visualState = visualState.and(CPThemeStateHighlighted);
|
||||
else
|
||||
visualState = visualState.without(CPThemeStateHighlighted);
|
||||
|
||||
+1719
-148
File diff suppressed because it is too large.
Load diff
+3
-3
@@ -95,8 +95,8 @@ CPCheckBoxImageOffset = 4.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override methods from CPButton
|
||||
// MARK: -
|
||||
// MARK: Override methods from CPButton
|
||||
|
||||
- (CGSize)_minimumFrameSize
|
||||
{
|
||||
@@ -177,7 +177,7 @@ CPCheckBoxImageOffset = 4.0;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPCheckBox (TableDataView)
|
||||
|
||||
|
||||
@@ -202,8 +202,8 @@ var HORIZONTAL_MARGIN = 2;
|
||||
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate
|
||||
// MARK: -
|
||||
// MARK: Delegate
|
||||
|
||||
/*!
|
||||
Set the delegate of the receiver
|
||||
@@ -600,9 +600,12 @@ var HORIZONTAL_MARGIN = 2;
|
||||
if (_maxNumberOfRows > 0)
|
||||
numberOfRows = MIN(numberOfRows, _maxNumberOfRows);
|
||||
|
||||
height = MAX(height, numberOfRows * (_minItemSize.height + _verticalMargin));
|
||||
// calculate the required height: (sum of item heights) + (sum of margins between items).
|
||||
var requiredHeight = (numberOfRows * _minItemSize.height) + (MAX(0, numberOfRows - 1) * _verticalMargin);
|
||||
height = MAX(height, requiredHeight);
|
||||
|
||||
var itemSizeHeight = FLOOR(height / numberOfRows) - _verticalMargin;
|
||||
// calculate individual item height based on the total available height.
|
||||
var itemSizeHeight = (numberOfRows > 0) ? FLOOR((height - (MAX(0, numberOfRows - 1) * _verticalMargin)) / numberOfRows) : 0;
|
||||
|
||||
if (maxItemSizeHeight > 0)
|
||||
itemSizeHeight = MIN(itemSizeHeight, maxItemSizeHeight);
|
||||
@@ -621,7 +624,7 @@ var HORIZONTAL_MARGIN = 2;
|
||||
_horizontalMargin = _uniformSubviewsResizing ? FLOOR((aFrameSize.width - numberOfColumns * anItemSize.width) / (numberOfColumns + 1)) : HORIZONTAL_MARGIN;
|
||||
|
||||
var x = _horizontalMargin,
|
||||
y = -anItemSize.height;
|
||||
y = -anItemSize.height;
|
||||
|
||||
[displayItems enumerateObjectsUsingBlock:function(item, idx, stop)
|
||||
{
|
||||
@@ -636,7 +639,11 @@ var HORIZONTAL_MARGIN = 2;
|
||||
if (idx % numberOfColumns == 0)
|
||||
{
|
||||
x = _horizontalMargin;
|
||||
y += _verticalMargin + anItemSize.height;
|
||||
// For the first row, don't add a margin. For all subsequent rows, add the margin.
|
||||
if (idx === 0)
|
||||
y += anItemSize.height;
|
||||
else
|
||||
y += _verticalMargin + anItemSize.height;
|
||||
}
|
||||
|
||||
[view setFrameOrigin:CGPointMake(x, y)];
|
||||
|
||||
+12
-15
@@ -89,8 +89,8 @@ var cachedBlackColor,
|
||||
}
|
||||
@global document
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theming
|
||||
// MARK: -
|
||||
// MARK: Theming
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -109,8 +109,8 @@ var cachedBlackColor,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Static methods
|
||||
// MARK: -
|
||||
// MARK: Static methods
|
||||
|
||||
/*!
|
||||
Creates a color in the RGB colorspace, with an alpha value.
|
||||
@@ -563,9 +563,10 @@ var cachedBlackColor,
|
||||
{
|
||||
// Fix to avoid a problem when compiling a theme (missing the alpha component as theme compiler doesn't have CSS rgba capability)
|
||||
var hasAlpha = YES;
|
||||
#if PLATFORM(DOM)
|
||||
hasAlpha = CPFeatureIsCompatible(CPCSSRGBAFeature) && _components[3] != 1.0;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
hasAlpha = CPFeatureIsCompatible(CPCSSRGBAFeature) && _components[3] != 1.0;
|
||||
}
|
||||
|
||||
_cssString = (hasAlpha ? "rgba(" : "rgb(") +
|
||||
parseInt(_components[0] * 255.0) + ", " +
|
||||
@@ -884,8 +885,8 @@ url("data:image/png;base64,BASE64ENCODEDDATA") // if there is a pattern image
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CSS Theming
|
||||
// MARK: -
|
||||
// MARK: CSS Theming
|
||||
|
||||
// The code below adds support for CSS theming with 100% compatibility with current theming system.
|
||||
// The idea is to extend CPColor (and CPImage) with CSS components and adapt low level UI components to
|
||||
@@ -1001,19 +1002,16 @@ url("data:image/png;base64,BASE64ENCODEDDATA") // if there is a pattern image
|
||||
|
||||
+ (void)restorePreviousCSSState:(CPArrayRef)aPreviousStateRef forDOMElement:(DOMElement)aDOMElement
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var aPreviousState = @deref(aPreviousStateRef);
|
||||
|
||||
for (var i = 0, count = aPreviousState.length; i < count; i++)
|
||||
aDOMElement.style[aPreviousState[i][0]] = aPreviousState[i][1];
|
||||
|
||||
@deref(aPreviousStateRef) = @[];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (DOMElement)applyCSSColorForView:(CPView)aView onDOMElement:(DOMElement)aDOMElement styleNode:(DOMElement)aStyleNode previousState:(CPArrayRef)aPreviousStateRef
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var aPreviousState = @deref(aPreviousStateRef);
|
||||
|
||||
[_cssDictionary enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop)
|
||||
@@ -1086,12 +1084,11 @@ url("data:image/png;base64,BASE64ENCODEDDATA") // if there is a pattern image
|
||||
@deref(aPreviousStateRef) = aPreviousState;
|
||||
|
||||
return aStyleNode;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/// @cond IGNORE
|
||||
var CPColorComponentsKey = @"CPColorComponentsKey",
|
||||
@@ -1494,7 +1491,7 @@ var patternColorsFromPattern = function(pattern, attributes, imageFactory)
|
||||
else
|
||||
{
|
||||
numParts = 3;
|
||||
isVertical = orientation === PatternIsVertical;
|
||||
isVertical = orientation === CPColorPatternIsVertical;
|
||||
|
||||
if (isVertical)
|
||||
{
|
||||
|
||||
+80
-32
@@ -51,6 +51,7 @@ CPColorPickerViewWidth = 265;
|
||||
CPColorPickerViewHeight = 370;
|
||||
|
||||
CPColorPanelColorDidChangeNotification = @"CPColorPanelColorDidChangeNotification";
|
||||
CPColorDragType = CPColorPboardType;
|
||||
|
||||
var PREVIEW_HEIGHT = 20.0,
|
||||
TOOLBAR_HEIGHT = 32.0,
|
||||
@@ -129,8 +130,6 @@ var SharedColorPanel = nil,
|
||||
|
||||
if (self)
|
||||
{
|
||||
//[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
|
||||
|
||||
[self setTitle:@"Color Panel"];
|
||||
[self setLevel:CPFloatingWindowLevel];
|
||||
|
||||
@@ -149,20 +148,37 @@ var SharedColorPanel = nil,
|
||||
*/
|
||||
- (void)setColor:(CPColor)aColor
|
||||
{
|
||||
if ([_color isEqual:aColor])
|
||||
return;
|
||||
|
||||
_color = aColor;
|
||||
[_previewView setBackgroundColor:_color];
|
||||
|
||||
[CPApp sendAction:@selector(changeColor:) to:nil from:self];
|
||||
// Check if the color change originated from user interaction inside the panel itself.
|
||||
// We only broadcast `changeColor:` if the user picked a color via the panel's UI.
|
||||
// If an external CPColorWell called `setColor:` programmatically, broadcasting it
|
||||
// back down the responder chain would incorrectly change the previous First Responder.
|
||||
var currentEvent = [CPApp currentEvent],
|
||||
isFromPanel = currentEvent && ([currentEvent window] === self);
|
||||
|
||||
if (_target && _action)
|
||||
[CPApp sendAction:_action to:_target from:self];
|
||||
if (isFromPanel)
|
||||
{
|
||||
// Push color via Responder Chain (targets First Responder, i.e., the active CPColorWell)
|
||||
[CPApp sendAction:@selector(changeColor:) to:nil from:self];
|
||||
|
||||
if (_target && _action)
|
||||
[CPApp sendAction:_action to:_target from:self];
|
||||
}
|
||||
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
postNotificationName:CPColorPanelColorDidChangeNotification
|
||||
object:self];
|
||||
|
||||
[_activePicker setColor:_color];
|
||||
[_opacitySlider setFloatValue:[_color alphaComponent]];
|
||||
if (_activePicker)
|
||||
[_activePicker setColor:_color];
|
||||
|
||||
if (_opacitySlider)
|
||||
[_opacitySlider setFloatValue:[_color alphaComponent]];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -174,7 +190,7 @@ var SharedColorPanel = nil,
|
||||
{
|
||||
[self setColor:aColor];
|
||||
|
||||
if (bool)
|
||||
if (bool && _activePicker)
|
||||
[_activePicker setColor:_color];
|
||||
}
|
||||
|
||||
@@ -332,7 +348,7 @@ var SharedColorPanel = nil,
|
||||
buttonForLater = button;
|
||||
}
|
||||
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
// Preview
|
||||
var previewBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10, CGRectGetWidth(bounds) - 86, PREVIEW_HEIGHT)];
|
||||
|
||||
_previewView = [[_CPColorPanelPreview alloc] initWithFrame:CGRectInset([previewBox bounds], 2.0, 2.0)];
|
||||
@@ -350,7 +366,7 @@ var SharedColorPanel = nil,
|
||||
[_previewLabel setTextColor:[CPColor blackColor]];
|
||||
[_previewLabel setAlignment:CPRightTextAlignment];
|
||||
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
// Swatches
|
||||
var swatchBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10 + PREVIEW_HEIGHT + 5, CGRectGetWidth(bounds) - 86, SWATCH_HEIGHT + 2.0)];
|
||||
|
||||
[swatchBox setBackgroundColor:[CPColor colorWithWhite:0.8 alpha:1.0]];
|
||||
@@ -391,8 +407,6 @@ var SharedColorPanel = nil,
|
||||
[contentView addSubview:opacityLabel];
|
||||
[contentView addSubview:_opacitySlider];
|
||||
|
||||
_target = nil;
|
||||
_action = nil;
|
||||
_activePicker = nil;
|
||||
|
||||
[_previewView setBackgroundColor:_color];
|
||||
@@ -412,8 +426,6 @@ var SharedColorPanel = nil,
|
||||
@end
|
||||
|
||||
|
||||
CPColorDragType = "CPColorDragType";
|
||||
|
||||
var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
/* @ignore */
|
||||
@@ -423,12 +435,14 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
CPColor _dragColor;
|
||||
CPColorPanel _colorPanel;
|
||||
CPCookie _swatchCookie;
|
||||
CGPoint _mouseDownPoint;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
|
||||
_mouseDownPoint = CGPointMake(0, 0);
|
||||
[self setBackgroundColor:[CPColor grayColor]];
|
||||
|
||||
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]];
|
||||
@@ -442,7 +456,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
var view = [[CPView alloc] initWithFrame:CGRectMake(13 * i + 1, 1, 12, 12)],
|
||||
fillView = [[CPView alloc] initWithFrame:CGRectInset([view bounds], 1.0, 1.0)];
|
||||
|
||||
@@ -494,7 +507,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
- (CPArray)saveColorList
|
||||
{
|
||||
var result = [];
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
for (var i = 0; i < _swatches.length; i++)
|
||||
result.push([[[_swatches[i] subviews][0] backgroundColor] hexString]);
|
||||
|
||||
@@ -521,39 +533,44 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
- (void)setColor:(CPColor)aColor atIndex:(int)index
|
||||
{
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
[[_swatches[index] subviews][0] setBackgroundColor:aColor];
|
||||
[self saveColorList];
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
_mouseDownPoint = [anEvent locationInWindow];
|
||||
}
|
||||
|
||||
- (void)mouseUp:(CPEvent)anEvent
|
||||
{
|
||||
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil],
|
||||
bounds = [self bounds];
|
||||
|
||||
if (!CGRectContainsPoint(bounds, point) || point.x > [self bounds].size.width - 1 || point.x < 1)
|
||||
return NO;
|
||||
return;
|
||||
|
||||
[_colorPanel setColor:[self colorAtIndex:FLOOR(point.x / 13)] updatePicker:YES];
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(CPEvent)anEvent
|
||||
{
|
||||
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
var windowPoint = [anEvent locationInWindow];
|
||||
|
||||
// Prevent accidental drags from rapid clicking causing small micro-movements
|
||||
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
|
||||
return;
|
||||
|
||||
var point = [self convertPoint:windowPoint fromView:nil];
|
||||
|
||||
if (point.x > [self bounds].size.width - 1 || point.x < 1)
|
||||
return NO;
|
||||
|
||||
[[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
|
||||
return;
|
||||
|
||||
var swatch = _swatches[FLOOR(point.x / 13)];
|
||||
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
_dragColor = [[swatch subviews][0] backgroundColor];
|
||||
|
||||
var bounds = CGRectMakeCopy([swatch bounds]);
|
||||
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
var dragView = [[CPView alloc] initWithFrame:bounds],
|
||||
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
|
||||
|
||||
@@ -562,11 +579,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
[dragView addSubview:dragFillView];
|
||||
|
||||
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
||||
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
|
||||
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:CPColorDragType];
|
||||
|
||||
[self dragView:dragView
|
||||
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
|
||||
offset:CGPointMake(0.0, 0.0)
|
||||
event:anEvent
|
||||
pasteboard:nil
|
||||
pasteboard:pasteboard
|
||||
source:self
|
||||
slideBack:YES];
|
||||
}
|
||||
@@ -577,7 +598,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:aType];
|
||||
}
|
||||
|
||||
- (void)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
|
||||
- (BOOL)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
|
||||
{
|
||||
var location = [self convertPoint:[aSender draggingLocation] fromView:nil],
|
||||
pasteboard = [aSender draggingPasteboard],
|
||||
@@ -587,6 +608,13 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
return NO;
|
||||
|
||||
[self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]] atIndex:FLOOR(location.x / 13)];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
|
||||
{
|
||||
return CPDragOperationCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -595,11 +623,13 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
@implementation _CPColorPanelPreview : CPView
|
||||
{
|
||||
CPColorPanel _colorPanel;
|
||||
CGPoint _mouseDownPoint;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
_mouseDownPoint = CGPointMake(0, 0);
|
||||
|
||||
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]];
|
||||
|
||||
@@ -616,7 +646,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
return _colorPanel;
|
||||
}
|
||||
|
||||
- (void)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
|
||||
- (BOOL)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
|
||||
{
|
||||
var pasteboard = [aSender draggingPasteboard];
|
||||
|
||||
@@ -625,6 +655,8 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]];
|
||||
[_colorPanel setColor:color updatePicker:YES];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isOpaque
|
||||
@@ -632,15 +664,22 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
_mouseDownPoint = [anEvent locationInWindow];
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(CPEvent)anEvent
|
||||
{
|
||||
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
var windowPoint = [anEvent locationInWindow];
|
||||
|
||||
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
|
||||
return;
|
||||
|
||||
[[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPColorDragType] owner:self];
|
||||
var point = [self convertPoint:windowPoint fromView:nil];
|
||||
|
||||
var bounds = CGRectMake(0, 0, 15, 15);
|
||||
|
||||
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
|
||||
var dragView = [[CPView alloc] initWithFrame:bounds],
|
||||
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
|
||||
|
||||
@@ -649,11 +688,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
|
||||
[dragView addSubview:dragFillView];
|
||||
|
||||
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
||||
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
|
||||
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:CPColorDragType];
|
||||
|
||||
[self dragView:dragView
|
||||
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
|
||||
offset:CGPointMake(0.0, 0.0)
|
||||
event:anEvent
|
||||
pasteboard:nil
|
||||
pasteboard:pasteboard
|
||||
source:self
|
||||
slideBack:YES];
|
||||
}
|
||||
@@ -664,6 +707,11 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
|
||||
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:aType];
|
||||
}
|
||||
|
||||
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
|
||||
{
|
||||
return CPDragOperationCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@import "CPColorPicker.j"
|
||||
|
||||
+33
-30
@@ -23,6 +23,7 @@
|
||||
@import <Foundation/CPObject.j>
|
||||
|
||||
@import "CPView.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPSlider
|
||||
@class CPColorPanel
|
||||
@@ -235,24 +236,25 @@
|
||||
{
|
||||
if (self = [super initWithFrame:aFrame])
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel.png"];
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel.png"];
|
||||
|
||||
_wheelImage = new Image();
|
||||
_wheelImage.src = path;
|
||||
_wheelImage.style.position = "absolute";
|
||||
_wheelImage = new Image();
|
||||
_wheelImage.src = path;
|
||||
_wheelImage.style.position = "absolute";
|
||||
|
||||
path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel_black.png"];
|
||||
path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel_black.png"];
|
||||
|
||||
_blackWheelImage = new Image();
|
||||
_blackWheelImage.src = path;
|
||||
_blackWheelImage.style.opacity = "0";
|
||||
_blackWheelImage.style.filter = "alpha(opacity=0)";
|
||||
_blackWheelImage.style.position = "absolute";
|
||||
_blackWheelImage = new Image();
|
||||
_blackWheelImage.src = path;
|
||||
_blackWheelImage.style.opacity = "0";
|
||||
_blackWheelImage.style.filter = "alpha(opacity=0)";
|
||||
_blackWheelImage.style.position = "absolute";
|
||||
|
||||
_DOMElement.appendChild(_wheelImage);
|
||||
_DOMElement.appendChild(_blackWheelImage);
|
||||
#endif
|
||||
_DOMElement.appendChild(_wheelImage);
|
||||
_DOMElement.appendChild(_blackWheelImage);
|
||||
}
|
||||
|
||||
[self setWheelSize:aFrame.size];
|
||||
|
||||
@@ -272,10 +274,10 @@
|
||||
|
||||
- (void)setWheelBrightness:(float)brightness
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
_blackWheelImage.style.opacity = 1.0 - brightness;
|
||||
_blackWheelImage.style.filter = "alpha(opacity=" + (1.0 - brightness) * 100 + ")"
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setFrameSize:(CGSize)aSize
|
||||
@@ -288,21 +290,22 @@
|
||||
{
|
||||
var min = MIN(aSize.width, aSize.height);
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_blackWheelImage.style.width = min;
|
||||
_blackWheelImage.style.height = min;
|
||||
_blackWheelImage.width = min;
|
||||
_blackWheelImage.height = min;
|
||||
_blackWheelImage.style.top = (aSize.height - min) / 2.0 + "px";
|
||||
_blackWheelImage.style.left = (aSize.width - min) / 2.0 + "px";
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_blackWheelImage.style.width = min;
|
||||
_blackWheelImage.style.height = min;
|
||||
_blackWheelImage.width = min;
|
||||
_blackWheelImage.height = min;
|
||||
_blackWheelImage.style.top = (aSize.height - min) / 2.0 + "px";
|
||||
_blackWheelImage.style.left = (aSize.width - min) / 2.0 + "px";
|
||||
|
||||
_wheelImage.style.width = min;
|
||||
_wheelImage.style.height = min;
|
||||
_wheelImage.width = min;
|
||||
_wheelImage.height = min;
|
||||
_wheelImage.style.top = (aSize.height - min) / 2.0 + "px";
|
||||
_wheelImage.style.left = (aSize.width - min) / 2.0 + "px";
|
||||
#endif
|
||||
_wheelImage.style.width = min;
|
||||
_wheelImage.style.height = min;
|
||||
_wheelImage.width = min;
|
||||
_wheelImage.height = min;
|
||||
_wheelImage.style.top = (aSize.height - min) / 2.0 + "px";
|
||||
_wheelImage.style.left = (aSize.width - min) / 2.0 + "px";
|
||||
}
|
||||
|
||||
_radius = min / 2.0;
|
||||
|
||||
|
||||
+200
-136
@@ -25,7 +25,7 @@
|
||||
@import "CPView.j"
|
||||
@import "CPColor.j"
|
||||
@import "CPColorPanel.j"
|
||||
|
||||
@import "CPPasteboard.j"
|
||||
|
||||
var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiveNotification";
|
||||
|
||||
@@ -39,10 +39,10 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
*/
|
||||
@implementation CPColorWell : CPControl
|
||||
{
|
||||
BOOL _active;
|
||||
BOOL _bordered;
|
||||
|
||||
CPColor _color;
|
||||
BOOL _isChangingColorFromPanel; // Guard flag to prevent recursion
|
||||
CGPoint _mouseDownPoint;
|
||||
}
|
||||
|
||||
+ (Class)_binderClassForBinding:(CPString)aBinding
|
||||
@@ -77,52 +77,54 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
[theBinding reverseSetValueFor:@"color"];
|
||||
}
|
||||
|
||||
- (BOOL)isFirstResponder
|
||||
{
|
||||
return [[self window] firstResponder] === self;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
return [self isEnabled];
|
||||
}
|
||||
|
||||
- (void)activate:(BOOL)shouldBeExclusive
|
||||
{
|
||||
[[self window] makeFirstResponder:self];
|
||||
[[CPColorPanel sharedColorPanel] orderFront:self];
|
||||
}
|
||||
|
||||
- (BOOL)isActive
|
||||
{
|
||||
return [self isFirstResponder] && [self isEnabled];
|
||||
}
|
||||
|
||||
/*!
|
||||
Deactivates the color well.
|
||||
*/
|
||||
- (void)deactivate
|
||||
{
|
||||
if ([self isFirstResponder])
|
||||
[[self window] makeFirstResponder:nil];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_active = NO;
|
||||
_color = [CPColor whiteColor];
|
||||
_mouseDownPoint = CGPointMakeZero();
|
||||
[self setBordered:YES];
|
||||
|
||||
[self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_registerNotifications
|
||||
{
|
||||
var defaultCenter = [CPNotificationCenter defaultCenter];
|
||||
|
||||
[defaultCenter
|
||||
addObserver:self
|
||||
selector:@selector(colorWellDidBecomeExclusive:)
|
||||
name:_CPColorWellDidBecomeExclusiveNotification
|
||||
object:nil];
|
||||
|
||||
[defaultCenter
|
||||
addObserver:self
|
||||
selector:@selector(colorPanelWillClose:)
|
||||
name:CPWindowWillCloseNotification
|
||||
object:[CPColorPanel sharedColorPanel]];
|
||||
}
|
||||
|
||||
- (void)_removeNotifications
|
||||
{
|
||||
var defaultCenter = [CPNotificationCenter defaultCenter];
|
||||
|
||||
[defaultCenter
|
||||
removeObserver:self
|
||||
name:_CPColorWellDidBecomeExclusiveNotification
|
||||
object:nil];
|
||||
|
||||
[defaultCenter
|
||||
removeObserver:self
|
||||
name:CPWindowWillCloseNotification
|
||||
object:[CPColorPanel sharedColorPanel]];
|
||||
|
||||
}
|
||||
// MARK: -
|
||||
// MARK: Draw
|
||||
|
||||
/*!
|
||||
Sets whether the color well is bordered.
|
||||
@@ -143,7 +145,8 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
return [self hasThemeState:CPThemeStateBordered];
|
||||
}
|
||||
|
||||
// Managing Color From Color Wells
|
||||
// MARK: -
|
||||
// MARK: Managing Color
|
||||
|
||||
/*!
|
||||
Returns the color well's current color.
|
||||
@@ -158,12 +161,17 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
*/
|
||||
- (void)setColor:(CPColor)aColor
|
||||
{
|
||||
if (_color == aColor)
|
||||
if ([_color isEqual:aColor])
|
||||
return;
|
||||
|
||||
_color = aColor;
|
||||
|
||||
[self setNeedsLayout];
|
||||
|
||||
// Only push back to the panel if we initiated the change (not if the panel pushed it to us)
|
||||
// AND if we are the current focus.
|
||||
if (!_isChangingColorFromPanel && [self isFirstResponder])
|
||||
[[CPColorPanel sharedColorPanel] setColor:_color];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -175,96 +183,175 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
[self setColor:[aSender color]];
|
||||
}
|
||||
|
||||
// Activating and Deactivating Color Wells
|
||||
/*!
|
||||
Standard action method sent by CPColorPanel via the Responder Chain.
|
||||
*/
|
||||
- (void)changeColor:(id)aSender
|
||||
{
|
||||
if ([aSender isKindOfClass:[CPColorPanel class]])
|
||||
{
|
||||
_isChangingColorFromPanel = YES;
|
||||
[self setColor:[aSender color]];
|
||||
_isChangingColorFromPanel = NO;
|
||||
|
||||
// Forward the action to our target (e.g. controller)
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Activating and Deactivating
|
||||
|
||||
/*!
|
||||
Activates the color well, displays the color panel, and makes the panel's current color the same as its own.
|
||||
If exclusive is \c YES, deactivates any other CPColorWells. \c NO, keeps them active.
|
||||
@param shouldBeExclusive whether other color wells should be deactivated.
|
||||
*/
|
||||
- (void)activate:(BOOL)shouldBeExclusive
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
if (shouldBeExclusive)
|
||||
// FIXME: make this queue!
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
postNotificationName:_CPColorWellDidBecomeExclusiveNotification
|
||||
object:self];
|
||||
[self setThemeState:CPThemeStateFirstResponder];
|
||||
|
||||
var panel = [CPColorPanel sharedColorPanel];
|
||||
|
||||
// EXPLICITLY set ourselves as the target.
|
||||
// This ensures that when the user clicks the panel (making Panel key),
|
||||
// the panel still knows to send messages back to us.
|
||||
[panel setTarget:self];
|
||||
[panel setAction:@selector(changeColor:)];
|
||||
|
||||
// Sync panel to our current color
|
||||
[panel setColor:_color];
|
||||
|
||||
|
||||
if ([self isActive])
|
||||
return;
|
||||
|
||||
_active = YES;
|
||||
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(colorPanelDidChangeColor:)
|
||||
name:CPColorPanelColorDidChangeNotification
|
||||
object:[CPColorPanel sharedColorPanel]];
|
||||
}
|
||||
|
||||
/*!
|
||||
Deactivates the color well.
|
||||
*/
|
||||
- (void)deactivate
|
||||
{
|
||||
if (![self isActive])
|
||||
return;
|
||||
|
||||
_active = NO;
|
||||
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
removeObserver:self
|
||||
name:CPColorPanelColorDidChangeNotification
|
||||
object:[CPColorPanel sharedColorPanel]];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns \c YES if the color well is active.
|
||||
*/
|
||||
- (BOOL)isActive
|
||||
{
|
||||
return _active;
|
||||
}
|
||||
|
||||
- (void)colorPanelDidChangeColor:(CPNotification)aNotification
|
||||
{
|
||||
[self takeColorFrom:[aNotification object]];
|
||||
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
}
|
||||
|
||||
- (void)colorWellDidBecomeExclusive:(CPNotification)aNotification
|
||||
{
|
||||
if (self != [aNotification object])
|
||||
[self deactivate];
|
||||
}
|
||||
|
||||
- (void)colorPanelWillClose:(CPNotification)aNotification
|
||||
{
|
||||
[self deactivate];
|
||||
}
|
||||
|
||||
- (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
|
||||
{
|
||||
[self highlight:NO];
|
||||
|
||||
if (!mouseIsUp || !CGRectContainsPoint([self bounds], aPoint) || ![self isEnabled])
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPColorWellDidBecomeExclusiveNotification object:self];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)resignFirstResponder
|
||||
{
|
||||
[self unsetThemeState:CPThemeStateFirstResponder];
|
||||
|
||||
var panel = [CPColorPanel sharedColorPanel];
|
||||
|
||||
// Clean up if we were the target
|
||||
if ([panel target] == self)
|
||||
[panel setTarget:nil];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Event Handling
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
if (![self isEnabled])
|
||||
return;
|
||||
|
||||
_mouseDownPoint = [anEvent locationInWindow];
|
||||
[self activate:YES];
|
||||
|
||||
var colorPanel = [CPColorPanel sharedColorPanel];
|
||||
|
||||
[colorPanel setPlatformWindow:[[self window] platformWindow]];
|
||||
|
||||
[colorPanel setColor:_color];
|
||||
[colorPanel orderFront:self];
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(CPEvent)anEvent
|
||||
{
|
||||
if (![self isEnabled])
|
||||
return;
|
||||
|
||||
var windowPoint = [anEvent locationInWindow];
|
||||
|
||||
// Prevent accidental drags from rapid clicking causing small micro-movements
|
||||
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
|
||||
return;
|
||||
|
||||
var bounds = CGRectMake(0, 0, 15, 15);
|
||||
var dragView = [[CPView alloc] initWithFrame:bounds],
|
||||
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
|
||||
|
||||
[dragView setBackgroundColor:[CPColor blackColor]];
|
||||
[dragFillView setBackgroundColor:_color];
|
||||
|
||||
[dragView addSubview:dragFillView];
|
||||
|
||||
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
||||
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorPboardType] owner:self];
|
||||
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:CPColorPboardType];
|
||||
|
||||
var point = [self convertPoint:windowPoint fromView:nil];
|
||||
|
||||
[self dragView:dragView
|
||||
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
|
||||
offset:CGPointMake(0.0, 0.0)
|
||||
event:anEvent
|
||||
pasteboard:pasteboard
|
||||
source:self
|
||||
slideBack:YES];
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Drag and Drop
|
||||
|
||||
- (void)draggingEntered:(id)sender
|
||||
{
|
||||
var pasteboard = [sender draggingPasteboard];
|
||||
|
||||
if ([[pasteboard types] containsObject:CPColorPboardType])
|
||||
{
|
||||
[self setThemeState:CPThemeStateHighlighted];
|
||||
return CPDragOperationCopy;
|
||||
}
|
||||
|
||||
return CPDragOperationNone;
|
||||
}
|
||||
|
||||
- (void)draggingExited:(id)sender
|
||||
{
|
||||
[self unsetThemeState:CPThemeStateHighlighted];
|
||||
}
|
||||
|
||||
- (BOOL)performDragOperation:(id)sender
|
||||
{
|
||||
var pasteboard = [sender draggingPasteboard];
|
||||
|
||||
if ([[pasteboard types] containsObject:CPColorPboardType])
|
||||
{
|
||||
var data = [pasteboard dataForType:CPColorPboardType],
|
||||
newColor = [CPKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
|
||||
if (newColor && [newColor isKindOfClass:[CPColor class]])
|
||||
{
|
||||
[self setColor:newColor];
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
|
||||
// Activate nicely after drop
|
||||
[self activate:YES];
|
||||
[self unsetThemeState:CPThemeStateHighlighted];
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType
|
||||
{
|
||||
if (aType == CPColorPboardType)
|
||||
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:aType];
|
||||
}
|
||||
|
||||
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
|
||||
{
|
||||
return CPDragOperationCopy;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Layout
|
||||
|
||||
- (CGRect)contentRectForBounds:(CGRect)bounds
|
||||
{
|
||||
var contentInset = [self currentValueForThemeAttribute:@"content-inset"];
|
||||
|
||||
return CGRectInsetByInset(bounds, contentInset);
|
||||
}
|
||||
|
||||
@@ -318,7 +405,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:@"bezel-view"];
|
||||
|
||||
|
||||
[contentView setBackgroundColor:_color];
|
||||
|
||||
var contentBorderView = [self layoutEphemeralSubviewNamed:@"content-border-view"
|
||||
@@ -328,28 +414,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
|
||||
[contentBorderView setBackgroundColor:[self currentValueForThemeAttribute:@"content-border-color"]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Observers method
|
||||
|
||||
- (void)_addObservers
|
||||
{
|
||||
if (_isObserving)
|
||||
return;
|
||||
|
||||
[super _addObservers];
|
||||
[self _registerNotifications];
|
||||
}
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
if (!_isObserving)
|
||||
return;
|
||||
|
||||
[super _removeObservers];
|
||||
[self _removeNotifications];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPColorWellValueBinder : CPBinder
|
||||
@@ -398,9 +462,9 @@ var CPColorWellColorKey = "CPColorWellColorKey",
|
||||
|
||||
if (self)
|
||||
{
|
||||
_active = NO;
|
||||
_color = [aCoder decodeObjectForKey:CPColorWellColorKey];
|
||||
[self setBordered:[aCoder decodeBoolForKey:CPColorWellBorderedKey]];
|
||||
[self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
+22
-33
@@ -91,7 +91,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
{
|
||||
return @{
|
||||
@"popup-button-size": CGSizeMake(21.0, 29.0),
|
||||
@"border-inset": CGInsetMake(3.0, 3.0, 3.0, 3.0),
|
||||
@"border-inset": CGInsetMake(3.0, 3.0, 3.0, 3.0)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self setThemeState:CPComboBoxStateButtonBordered];
|
||||
}
|
||||
|
||||
#pragma mark Setting Display Attributes
|
||||
// MARK: Setting Display Attributes
|
||||
|
||||
- (BOOL)hasVerticalScroller
|
||||
{
|
||||
@@ -214,7 +214,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
_numberOfVisibleItems = MAX(visibleItems, 1);
|
||||
}
|
||||
|
||||
#pragma mark Setting a Delegate
|
||||
// MARK: Setting a Delegate
|
||||
|
||||
- (id <CPComboBoxDelegate>)delegate
|
||||
{
|
||||
@@ -254,7 +254,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[super setDelegate:aDelegate];
|
||||
}
|
||||
|
||||
#pragma mark Setting a Data Source
|
||||
// MARK: Setting a Data Source
|
||||
|
||||
- (id <CPComboBoxDataSource>)dataSource
|
||||
{
|
||||
@@ -305,7 +305,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
#pragma mark Working with an Internal List
|
||||
// MARK: Working with an Internal List
|
||||
|
||||
- (void)addItemsWithObjectValues:(CPArray)objects
|
||||
{
|
||||
@@ -378,7 +378,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
return _items.length;
|
||||
}
|
||||
|
||||
#pragma mark Manipulating the Displayed List
|
||||
// MARK: Manipulating the Displayed List
|
||||
|
||||
/*!
|
||||
Returns the delegate to be used when creating the pop up list.
|
||||
@@ -533,7 +533,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
CPComboBoxFocusRingWidth = inset.bottom;
|
||||
}
|
||||
|
||||
[_listDelegate popUpRelativeToRect:[self _borderFrame] view:self offset:CPComboBoxFocusRingWidth - 1];
|
||||
[_listDelegate popUpRelativeToRect:[self bounds] view:self offset:CPComboBoxFocusRingWidth - 1];
|
||||
[self _selectMatchingItem];
|
||||
}
|
||||
|
||||
@@ -570,6 +570,14 @@ var CPComboBoxTextSubview = @"text",
|
||||
_selectedStringValue = selectedStringValue;
|
||||
|
||||
[self setStringValue:_selectedStringValue];
|
||||
|
||||
[self _updatePlaceholderState];
|
||||
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _setCSSStyleForInputElement];
|
||||
}
|
||||
|
||||
[self _reverseSetBinding];
|
||||
|
||||
return YES;
|
||||
@@ -594,7 +602,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
}
|
||||
|
||||
#pragma mark Manipulating the Selection
|
||||
// MARK: Manipulating the Selection
|
||||
|
||||
- (void)deselectItemAtIndex:(int)index
|
||||
{
|
||||
@@ -646,7 +654,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self selectItemAtIndex:index];
|
||||
}
|
||||
|
||||
#pragma mark Completing the Text Field
|
||||
// MARK: Completing the Text Field
|
||||
|
||||
- (BOOL)completes
|
||||
{
|
||||
@@ -693,7 +701,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
_forceSelection = !!flag;
|
||||
}
|
||||
|
||||
#pragma mark CPTextField Delegate Methods and Overrides
|
||||
// MARK: CPTextField Delegate Methods and Overrides
|
||||
|
||||
/*! @ignore */
|
||||
- (BOOL)sendAction:(SEL)anAction to:(id)anObject
|
||||
@@ -839,7 +847,6 @@ var CPComboBoxTextSubview = @"text",
|
||||
|
||||
if (!shouldResign)
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
// In FireFox this needs to be done in setTimeout, otherwise there is no caret
|
||||
// We have to save the input element now, when we lose focus it will change.
|
||||
var element = [self _inputElement];
|
||||
@@ -854,7 +861,6 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
@@ -893,7 +899,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[_listDelegate setAlignment:alignment];
|
||||
}
|
||||
|
||||
#pragma mark Pop Up Button Layout
|
||||
// MARK: Pop Up Button Layout
|
||||
|
||||
- (CGRect)popupButtonRectForBounds:(CGRect)bounds
|
||||
{
|
||||
@@ -938,7 +944,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
relativeToEphemeralSubviewNamed:@"content-view"];
|
||||
}
|
||||
|
||||
#pragma mark Internal Helpers
|
||||
// MARK: Internal Helpers
|
||||
|
||||
/*! @ignore */
|
||||
- (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag
|
||||
@@ -975,23 +981,6 @@ var CPComboBoxTextSubview = @"text",
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Calculate the frame in base coordinates that will nestle just below the visible border of the text field.
|
||||
@ignore
|
||||
*/
|
||||
- (CGRect)_borderFrame
|
||||
{
|
||||
var inset = [self currentValueForThemeAttribute:@"border-inset"],
|
||||
frame = [self bounds];
|
||||
|
||||
frame.origin.x += inset.left;
|
||||
frame.origin.y += inset.top;
|
||||
frame.size.width -= inset.left + inset.right;
|
||||
frame.size.height -= inset.top + inset.bottom;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)_popUpButtonWasClicked
|
||||
{
|
||||
@@ -1016,8 +1005,8 @@ var CPComboBoxTextSubview = @"text",
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Observers method
|
||||
// MARK: -
|
||||
// MARK: Observers method
|
||||
|
||||
- (void)_addObservers
|
||||
{
|
||||
|
||||
@@ -24,6 +24,29 @@
|
||||
@import "CPPlatform.j"
|
||||
|
||||
|
||||
/*
|
||||
CPDOMAvailable is the authoritative runtime predicate for DOM capability.
|
||||
|
||||
It replaces the former `#if PLATFORM(DOM)` preprocessor conditional, which
|
||||
selected code at build time between the browser (DOM) and CommonJS (Node)
|
||||
targets. It is a CAPABILITY test: YES only when a
|
||||
functioning document object with createElement exists.
|
||||
|
||||
The predicate subsumes all direct local probing for the presence of a DOM.
|
||||
|
||||
YES - browsers (the only deployment target)
|
||||
YES - any environment providing a working DOM (e.g. jsdom), where DOM
|
||||
operations genuinely function
|
||||
NO - CommonJS/Node without a DOM (legacy tooling: theme blending, nib2cib)
|
||||
|
||||
Rules for consumers:
|
||||
- Platform-conditional code MUST test CPDOMAvailable.
|
||||
- Do NOT reference `document` (or test for it) anywhere else in AppKit;
|
||||
this declaration is the single environment peek.
|
||||
- The preprocessor form must not return: there is no preprocessor.
|
||||
*/
|
||||
CPDOMAvailable = (typeof document !== "undefined") && document && (typeof document.createElement === "function");
|
||||
|
||||
// Browser Engines
|
||||
CPUnknownBrowserEngine = 0;
|
||||
CPGeckoBrowserEngine = 1 << 0;
|
||||
@@ -55,8 +78,6 @@ CPJavaScriptClipboardAccessFeature = 12;
|
||||
CPJavaScriptCanvasDrawFeature = 13;
|
||||
CPJavaScriptCanvasTransformFeature = 14;
|
||||
|
||||
CPVMLFeature = 15;
|
||||
|
||||
CPJavaScriptRemedialKeySupport = 16;
|
||||
CPJavaScriptShadowFeature = 20;
|
||||
|
||||
@@ -133,7 +154,6 @@ else if (typeof window !== "undefined" && (window.attachEvent || (!(window.Activ
|
||||
PLATFORM_ENGINE |= CPInternetExplorerBrowserEngine;
|
||||
|
||||
// Features we can only be sure of with IE (no known independent tests)
|
||||
PLATFORM_FEATURES[CPVMLFeature] = YES;
|
||||
PLATFORM_FEATURES[CPJavaScriptRemedialKeySupport] = YES;
|
||||
PLATFORM_FEATURES[CPJavaScriptShadowFeature] = YES;
|
||||
|
||||
@@ -388,7 +408,6 @@ function CPBrowserStyleProperty(aProperty)
|
||||
{
|
||||
var r = nil;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var testElement = document.createElement('div');
|
||||
|
||||
switch (lowerProperty)
|
||||
@@ -455,7 +474,6 @@ function CPBrowserStyleProperty(aProperty)
|
||||
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
PLATFORM_STYLE_JS_PROPERTIES[lowerProperty] = r;
|
||||
}
|
||||
|
||||
+13
-8
@@ -48,9 +48,10 @@
|
||||
CPRegularControlSize = 0;
|
||||
CPSmallControlSize = 1;
|
||||
CPMiniControlSize = 2;
|
||||
CPLargeControlSize = 3; // Since MacOS 11, there's a new control size "Large"
|
||||
|
||||
// To get the theme state corresponding to a control size, use CPControlSizeThemeStates[controlSize]
|
||||
CPControlSizeThemeStates = @[CPThemeStateControlSizeRegular, CPThemeStateControlSizeSmall, CPThemeStateControlSizeMini];
|
||||
CPControlSizeThemeStates = @[CPThemeStateControlSizeRegular, CPThemeStateControlSizeSmall, CPThemeStateControlSizeMini, CPThemeStateControlSizeLarge];
|
||||
|
||||
@typedef CPLineBreakMode
|
||||
CPLineBreakByWordWrapping = 0;
|
||||
@@ -207,8 +208,8 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
/*!
|
||||
Returns the control's control size
|
||||
@@ -248,6 +249,9 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
|
||||
case CPMiniControlSize:
|
||||
return CPThemeStateControlSizeMini;
|
||||
|
||||
case CPLargeControlSize:
|
||||
return CPThemeStateControlSizeLarge;
|
||||
|
||||
case CPRegularControlSize:
|
||||
default:
|
||||
@@ -286,7 +290,7 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*!
|
||||
Sets the receiver's target action.
|
||||
@@ -1023,8 +1027,8 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Base writing direction
|
||||
// MARK: -
|
||||
// MARK: Base writing direction
|
||||
|
||||
/*!
|
||||
Sets the initial writing direction of the receiver
|
||||
@@ -1039,7 +1043,8 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
_baseWritingDirection = writingDirection;
|
||||
[self didChangeValueForKey:@"baseWritingDirection"];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
|
||||
var style;
|
||||
|
||||
@@ -1062,7 +1067,7 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
}
|
||||
|
||||
_DOMElement.style.direction = style;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+23
-17
@@ -22,6 +22,7 @@
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPString.j>
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
|
||||
/*!
|
||||
@@ -95,32 +96,37 @@
|
||||
else
|
||||
domain = "";
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
document.cookie = _cookieName + "=" + value + expires + "; path=/" + domain;
|
||||
#else
|
||||
_cookieValue = value;
|
||||
_expires = expires;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
document.cookie = _cookieName + "=" + value + expires + "; path=/" + domain;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cookieValue = value;
|
||||
_expires = expires;
|
||||
}
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (CPString)_readCookieValue
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var nameEQ = _cookieName + "=",
|
||||
ca = document.cookie.split(';');
|
||||
|
||||
for (var i = 0; i < ca.length; i++)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var c = ca[i];
|
||||
var nameEQ = _cookieName + "=",
|
||||
ca = document.cookie.split(';');
|
||||
|
||||
while (c.charAt(0) == ' ')
|
||||
c = c.substring(1, c.length);
|
||||
for (var i = 0; i < ca.length; i++)
|
||||
{
|
||||
var c = ca[i];
|
||||
|
||||
if (c.indexOf(nameEQ) == 0)
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
while (c.charAt(0) == ' ')
|
||||
c = c.substring(1, c.length);
|
||||
|
||||
if (c.indexOf(nameEQ) == 0)
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -135,9 +135,7 @@ CPCursorPlatformBoth = 3;
|
||||
|
||||
currentCursor = self;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
[[self class] _setCursorCSS:_cssString];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mouseEntered:(CPEvent)event
|
||||
@@ -155,12 +153,10 @@ CPCursorPlatformBoth = 3;
|
||||
|
||||
+ (void)_setCursorCSS:(CPString)aString
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var platformWindows = [[CPPlatformWindow visiblePlatformWindows] allObjects];
|
||||
|
||||
for (var i = 0, count = [platformWindows count]; i < count; i++)
|
||||
platformWindows[i]._DOMBodyElement.style.cursor = aString;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Internal method that is used to return the system cursors. Caches the system cursors for performance.
|
||||
@@ -217,7 +213,6 @@ CPCursorPlatformBoth = 3;
|
||||
|
||||
+ (BOOL)_nativeCursorExists:(CPString)cursorCSSName
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
// FIXME: Trick until FF/Win & Chrome/Win correctly implement context-menu cursor
|
||||
// They will answer that they implement it but they actually don't
|
||||
@@ -240,7 +235,6 @@ CPCursorPlatformBoth = 3;
|
||||
|
||||
return doesExist;
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
@import <Foundation/CPDate.j>
|
||||
@import <Foundation/CPLocale.j>
|
||||
@import <Foundation/CPTimeZone.j>
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPStepper
|
||||
@class CPApp
|
||||
@@ -82,8 +83,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme methods
|
||||
// MARK: -
|
||||
// MARK: Theme methods
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -174,8 +175,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Binding methods
|
||||
// MARK: -
|
||||
// MARK: Binding methods
|
||||
|
||||
+ (Class)_binderClassForBinding:(CPString)theBinding
|
||||
{
|
||||
@@ -200,8 +201,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -258,10 +259,11 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
[_datePickerComponent setDatePickerElements:_datePickerElements];
|
||||
|
||||
// FIXME: Don't know why but next line will cause theme compilation to fail...
|
||||
// Workaround: added "if PLATFORM(DOM)"
|
||||
#if PLATFORM(DOM)
|
||||
[_datePickerComponent setEnabled:[self isEnabled]];
|
||||
#endif
|
||||
// Workaround: guarded with CPDOMAvailable (was: "if PLATFORM(DOM)")
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[_datePickerComponent setEnabled:[self isEnabled]];
|
||||
}
|
||||
|
||||
if (_isTextual)
|
||||
// We need to transmit text color to the text field version (Cocoa doesn't permit adapting the calendar view text color)
|
||||
@@ -271,8 +273,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -285,8 +287,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
/*! Set the delegate of the datePicker
|
||||
@param aDelegate delegate of the datePicker
|
||||
@@ -302,8 +304,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout method
|
||||
// MARK: -
|
||||
// MARK: Layout method
|
||||
|
||||
/*! Layout the subviews
|
||||
*/
|
||||
@@ -313,8 +315,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
[_datePickerComponent setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setter
|
||||
// MARK: -
|
||||
// MARK: Setter
|
||||
|
||||
/*! Return the objectValue of the datePicker. The objectValue should take the timeZoneEffect
|
||||
*/
|
||||
@@ -651,8 +653,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark First responder methods
|
||||
// MARK: -
|
||||
// MARK: First responder methods
|
||||
|
||||
/*! Return YES if style is set to CPTextFieldAndStepperDatePickerStyle or CPTextFieldDatePickerStyle
|
||||
*/
|
||||
@@ -689,8 +691,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark getter
|
||||
// MARK: -
|
||||
// MARK: getter
|
||||
|
||||
/*!
|
||||
Returns \c YES if the textfield is bezeled.
|
||||
@@ -723,8 +725,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
return [[_locale objectForKey:CPLocaleCountryCode] isEqualToString:@"US"];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Key event
|
||||
// MARK: -
|
||||
// MARK: Key event
|
||||
|
||||
/*! Key down event
|
||||
@param anEvent
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init method
|
||||
// MARK: Init method
|
||||
|
||||
/*! Init a _CPDatePickerCalendar
|
||||
@param aFrame
|
||||
@@ -111,8 +111,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Responder methods
|
||||
// MARK: -
|
||||
// MARK: Responder methods
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
@@ -120,8 +120,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter Setter methods
|
||||
// MARK: -
|
||||
// MARK: Getter Setter methods
|
||||
|
||||
/*! Set the date value of the component. It sets the dateValue of the header and the monthView also
|
||||
@param aDateValue
|
||||
@@ -160,8 +160,8 @@
|
||||
[self _init];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Manager the subviews. It hides or not the clock.
|
||||
*/
|
||||
@@ -221,8 +221,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Action methods
|
||||
// MARK: -
|
||||
// MARK: Action methods
|
||||
|
||||
/*! Move to the nextMonth without changing the dateValue of the datePicker
|
||||
*/
|
||||
@@ -251,7 +251,15 @@
|
||||
|
||||
- (void)_displayNextMonth
|
||||
{
|
||||
[self setDateValue:[_monthView nextMonth]];
|
||||
// Copy the date so we don't modify the view's state directly
|
||||
var nextDate = [[_monthView nextMonth] copy];
|
||||
|
||||
// Set to the middle of the month (15th).
|
||||
// This prevents [setDateValue:]'s timezone adjustment from
|
||||
// shifting the date back into the previous month (e.g., Nov 1 -> Oct 31).
|
||||
nextDate.setDate(15);
|
||||
|
||||
[self setDateValue:nextDate];
|
||||
}
|
||||
|
||||
- (void)_displayPreviousMonth
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
@import "CPTextField.j"
|
||||
@import "CPImage.j"
|
||||
@import "CALayer.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class _CPCibCustomResource
|
||||
@class CPDatePicker
|
||||
@@ -71,8 +72,8 @@ _CPDatePickerClockSeconds = 3;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame datePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -194,13 +195,13 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
// Check if we have to display the hand second
|
||||
// FIXME: Don't know why but next line will cause theme compilation to fail...
|
||||
// Workaround: added "if PLATFORM(DOM)"
|
||||
#if PLATFORM(DOM)
|
||||
// Workaround: guarded with CPDOMAvailable (was: "if PLATFORM(DOM)")
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
[_secondHandLayer setHidden:!((_datePickerElements & CPHourMinuteSecondDatePickerElementFlag) == CPHourMinuteSecondDatePickerElementFlag)];
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark Layout methods
|
||||
// MARK: Layout methods
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
@@ -246,7 +247,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
// [_middleHandLayer setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
// MARK: Accessors
|
||||
|
||||
- (void)setEnabled:(BOOL)shouldEnable
|
||||
{
|
||||
@@ -261,7 +262,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark Mouse actions
|
||||
// MARK: Mouse actions
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
@@ -381,13 +382,15 @@ _CPDatePickerClockSeconds = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
}
|
||||
[_datePicker _setDateValue:dateValue timeInterval:[_datePicker timeInterval]];
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
}
|
||||
|
||||
// We have to adapt represented values
|
||||
_representedHours = dateValue.getHours();
|
||||
@@ -425,7 +428,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation HandLayer : CALayer
|
||||
{
|
||||
@@ -434,7 +437,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
float _rotationRadians;
|
||||
}
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithSize:(CGSize)aSize
|
||||
{
|
||||
@@ -453,7 +456,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Setter Getter methods
|
||||
// MARK: Setter Getter methods
|
||||
|
||||
/*!
|
||||
Set the bounds of the layer. The imageLayer will be at the center of this bounds.
|
||||
@@ -516,7 +519,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation HandImageLayer : CALayer
|
||||
{
|
||||
@@ -550,7 +553,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation HoursLayer : CALayer
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
/*! Create a new instance of _CPDatePickerDayView
|
||||
@param aFrame
|
||||
@@ -123,8 +123,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme methods
|
||||
// MARK: -
|
||||
// MARK: Theme methods
|
||||
|
||||
/*! Set a theme
|
||||
*/
|
||||
@@ -143,8 +143,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter methods
|
||||
// MARK: -
|
||||
// MARK: Getter methods
|
||||
|
||||
/*! Select the tile
|
||||
*/
|
||||
@@ -205,8 +205,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Layout the subviews
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
@import "CPTextField.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPDatePicker
|
||||
@class _CPDatePickerElementView
|
||||
@@ -27,12 +28,7 @@
|
||||
CPDatePickerElementTextFieldBecomeFirstResponder = @"CPDatePickerElementTextFieldBecomeFirstResponder";
|
||||
CPDatePickerElementTextFieldAMPMChangedNotification = @"CPDatePickerElementTextFieldAMPMChangedNotification";
|
||||
|
||||
var CPZeroKeyCode = 48,
|
||||
CPNineKeyCode = 57,
|
||||
CPMajAKeyCode = 65,
|
||||
CPMajPKeyCode = 80,
|
||||
CPAKeyCode = 97,
|
||||
CPPKeyCode = 112;
|
||||
// Removed hardcoded KeyCodes (CPZeroKeyCode, etc) as they are unreliable across browsers/layouts.
|
||||
|
||||
CPMonthDateType = 0;
|
||||
CPDayDateType = 1;
|
||||
@@ -190,23 +186,38 @@ CPAMPMDateType = 6;
|
||||
*/
|
||||
- (void)setValueForKeyEvent:(CPEvent)anEvent
|
||||
{
|
||||
var keyCode = [anEvent keyCode];
|
||||
var keyCode = [anEvent keyCode],
|
||||
characters = [anEvent characters];
|
||||
|
||||
if (keyCode != CPDeleteKeyCode && keyCode != CPDeleteForwardKeyCode && keyCode < CPZeroKeyCode || keyCode > CPNineKeyCode)
|
||||
// Check if the event is a deletion
|
||||
var isDelete = (keyCode === CPDeleteKeyCode || keyCode === CPDeleteForwardKeyCode);
|
||||
|
||||
// Check if the event is a numeric input.
|
||||
// By testing the character string against a regex, we support num-pads and
|
||||
// international keyboards correctly, rather than relying on keyCode ranges.
|
||||
var isNumeric = (characters && [characters length] > 0 && /^[0-9]$/.test(characters));
|
||||
|
||||
// If it is neither a delete command nor a digit, we ignore it.
|
||||
if (!isDelete && !isNumeric)
|
||||
return;
|
||||
|
||||
var newValue = [self stringValue].replace(/\s/g, ''),
|
||||
length = [newValue length],
|
||||
eventKeyValue = parseInt([anEvent characters]).toString();
|
||||
length = [newValue length];
|
||||
|
||||
if (keyCode == CPDeleteKeyCode || keyCode == CPDeleteForwardKeyCode)
|
||||
if (isDelete)
|
||||
{
|
||||
[_timerEdition invalidate];
|
||||
_timerEdition = nil;
|
||||
newValue = [newValue substringToIndex:(length - 1)];
|
||||
|
||||
// Ensure we don't substring if length is 0
|
||||
if (length > 0)
|
||||
newValue = [newValue substringToIndex:(length - 1)];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since isNumeric is true, characters is a valid digit string
|
||||
var eventKeyValue = characters;
|
||||
|
||||
if (!_timerEdition)
|
||||
{
|
||||
_timerEdition = [CPTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(_timerKeyEvent:) userInfo:nil repeats:NO];
|
||||
@@ -227,7 +238,12 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
}
|
||||
|
||||
if (parseInt(newValue) > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && parseInt(newValue) > 12))
|
||||
// Safety check for NaN before comparison
|
||||
var numericValue = parseInt(newValue);
|
||||
if (isNaN(numericValue))
|
||||
numericValue = 0;
|
||||
|
||||
if (numericValue > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && numericValue > 12))
|
||||
return;
|
||||
|
||||
_firstEvent = NO;
|
||||
@@ -385,6 +401,18 @@ CPAMPMDateType = 6;
|
||||
return;
|
||||
}
|
||||
|
||||
// if we enter a day that is too high for the current month
|
||||
// we need to increase the month by one
|
||||
// if we do not do this, the user input would be silently reset
|
||||
// very poor user experience
|
||||
|
||||
if (parseInt(anObjectValue, 10) > [dateValue _daysInMonth])
|
||||
{
|
||||
[_datePickerElementView._textFieldMonth setIntValue:(dateValue.getMonth() + 2)];
|
||||
[super setObjectValue:objectValue];
|
||||
return;
|
||||
}
|
||||
|
||||
[super setObjectValue:objectValue];
|
||||
break;
|
||||
|
||||
@@ -444,18 +472,20 @@ CPAMPMDateType = 6;
|
||||
newDateValue.setSeconds(newDateValue.getSeconds() + secondsFromGMT - secondsFromGMTTimeZone);
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
}
|
||||
[_datePicker _setDateValue:newDateValue timeInterval:[_datePicker timeInterval]];
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Mouse event
|
||||
// MARK: -
|
||||
// MARK: Mouse event
|
||||
|
||||
/*! Mouse down event. Launch a notification to notif the new first responder textField
|
||||
*/
|
||||
@@ -469,8 +499,8 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme functions
|
||||
// MARK: -
|
||||
// MARK: Theme functions
|
||||
|
||||
/*! Set the theme CPThemeStateSelected
|
||||
*/
|
||||
@@ -490,8 +520,8 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override
|
||||
// MARK: -
|
||||
// MARK: Override
|
||||
|
||||
/*!
|
||||
We override this method to get all the time the good width
|
||||
@@ -557,7 +587,7 @@ CPAMPMDateType = 6;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPDatePickerElementSeparator : CPTextField
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
@import "CPControl.j"
|
||||
@import "_CPDatePickerElementTextField.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPDatePicker
|
||||
|
||||
@@ -50,7 +51,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init
|
||||
// MARK: Init
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame withDatePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -141,8 +142,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Responder methods
|
||||
// MARK: -
|
||||
// MARK: Responder methods
|
||||
|
||||
/*! @ignore */
|
||||
- (BOOL)acceptsFirstResponder
|
||||
@@ -154,8 +155,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override observers
|
||||
// MARK: -
|
||||
// MARK: Override observers
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
@@ -177,8 +178,8 @@
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_datePickerElementTextFieldAMPMChangedNotification:) name:CPDatePickerElementTextFieldAMPMChangedNotification object:_textFieldPMAM];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Mouse event
|
||||
// MARK: -
|
||||
// MARK: Mouse event
|
||||
|
||||
- (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
|
||||
{
|
||||
@@ -229,8 +230,8 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setter Getter methods
|
||||
// MARK: -
|
||||
// MARK: Setter Getter methods
|
||||
|
||||
/*! Set the value of the textFields
|
||||
@param aDateValue the value
|
||||
@@ -378,8 +379,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Notification methods
|
||||
// MARK: -
|
||||
// MARK: Notification methods
|
||||
|
||||
/*! Called when changing AM or PM
|
||||
@param aNotification
|
||||
@@ -403,18 +404,20 @@
|
||||
dateValue.setHours(dateValue.getHours() - 12);
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
}
|
||||
[_datePicker _setDateValue:dateValue timeInterval:[_datePicker timeInterval]];
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
|
||||
{
|
||||
@@ -886,8 +889,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Responder methods
|
||||
// MARK: -
|
||||
// MARK: Responder methods
|
||||
|
||||
- (void)_updateResponderTextField
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ var CPShortWeekDayNameArrayEn = [@"Mo", @"Tu", @"We", @"Th", @"Fr", @"Sa", @"Su"
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
/*! Init a new instance of _CPDatePickerHeaderView
|
||||
@param aFrame
|
||||
@@ -153,8 +153,8 @@ var CPShortWeekDayNameArrayEn = [@"Mo", @"Tu", @"We", @"Th", @"Fr", @"Sa", @"Su"
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter Setter methods
|
||||
// MARK: -
|
||||
// MARK: Getter Setter methods
|
||||
|
||||
/*! Return the day names depending on the CPLocale of the datePicker
|
||||
@return an array
|
||||
@@ -242,8 +242,8 @@ var CPShortWeekDayNameArrayEn = [@"Mo", @"Tu", @"We", @"Th", @"Fr", @"Sa", @"Su"
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Layout the subviews
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
@import "CPControl.j"
|
||||
@import "_CPDatePickerDayView.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPDatePicker
|
||||
|
||||
@@ -46,7 +47,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
/*! Init a _CPDatePickerMonthView
|
||||
@param aFrame
|
||||
@@ -77,8 +78,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter Setter methods
|
||||
// MARK: -
|
||||
// MARK: Getter Setter methods
|
||||
|
||||
/*! Set the monthDate of the component
|
||||
@param aDate
|
||||
@@ -194,8 +195,8 @@
|
||||
return tileIndex;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Reload data
|
||||
// MARK: -
|
||||
// MARK: Reload data
|
||||
|
||||
/*! Reload the data
|
||||
*/
|
||||
@@ -235,8 +236,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Select methods
|
||||
// MARK: -
|
||||
// MARK: Select methods
|
||||
|
||||
/*! Select one date or several date depending of the giving interval
|
||||
@param aStartDate
|
||||
@@ -297,8 +298,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Tile the view
|
||||
*/
|
||||
@@ -438,8 +439,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Mouse event
|
||||
// MARK: -
|
||||
// MARK: Mouse event
|
||||
|
||||
/*! Mouse down event
|
||||
*/
|
||||
@@ -457,9 +458,10 @@
|
||||
_indexDayTile = -1;
|
||||
_eventDragged = nil;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
}
|
||||
|
||||
// Check if we have to change or not the month of the component
|
||||
if ([dayTile date].getMonth() == _date.getMonth())
|
||||
@@ -514,9 +516,10 @@
|
||||
[_delegate _displayNextMonth];
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
/*! Mouse dragged event
|
||||
@@ -534,9 +537,10 @@
|
||||
_indexDayTile = [self indexOfTileForEvent:anEvent];
|
||||
_eventDragged = anEvent;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = YES;
|
||||
}
|
||||
|
||||
if ([_datePicker datePickerMode] == CPSingleDateMode)
|
||||
{
|
||||
@@ -580,9 +584,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._invokedByUserEvent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseUp:(CPEvent)anEvent
|
||||
@@ -596,8 +601,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Timer
|
||||
// MARK: -
|
||||
// MARK: Timer
|
||||
|
||||
- (void)_timerNextMonthEvent:(CPEvent)anEvent
|
||||
{
|
||||
@@ -618,8 +623,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Date methods
|
||||
// MARK: -
|
||||
// MARK: Date methods
|
||||
|
||||
- (CPDate)_hoursMinutesSecondsFromDatePickerForDate:(CPDate)aDate
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
@class _CPDatePickerElementView
|
||||
|
||||
@import "_CPDatePickerElementView.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@global CPSingleDateMode
|
||||
@global CPRangeDateMode
|
||||
@@ -48,13 +49,6 @@
|
||||
@global CPYearMonthDayDatePickerElementFlag
|
||||
@global CPEraDatePickerElementFlag
|
||||
|
||||
var CPZeroKeyCode = 48,
|
||||
CPNineKeyCode = 57,
|
||||
CPMajAKeyCode = 65,
|
||||
CPMajPKeyCode = 80,
|
||||
CPAKeyCode = 97,
|
||||
CPPKeyCode = 112;
|
||||
|
||||
// This class is used to represente the datePicker with the CPTextFieldAndStepperDatePickerStyle/CPTextFieldDatePickerStyle mode
|
||||
@implementation _CPDatePickerTextField : CPControl
|
||||
{
|
||||
@@ -69,7 +63,7 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init
|
||||
// MARK: Init
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame withDatePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -96,22 +90,23 @@ var CPZeroKeyCode = 48,
|
||||
[self setNeedsLayout];
|
||||
[self setNeedsDisplay:YES];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if ([_datePicker currentValueForThemeAttribute:@"uses-focus-ring"])
|
||||
{
|
||||
// As with overflow:hidden, views are clipping their content, in order
|
||||
// to show a focus ring (which is external to a view), we need to let
|
||||
// content extend outside the view.
|
||||
_datePicker._DOMElement.style.overflow = "visible";
|
||||
_DOMElement.style.overflow = "visible";
|
||||
_datePickerElementView._DOMElement.style.overflow = "visible";
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_datePicker._DOMElement.style.overflow = "visible";
|
||||
_DOMElement.style.overflow = "visible";
|
||||
_datePickerElementView._DOMElement.style.overflow = "visible";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override responder methods
|
||||
// MARK: -
|
||||
// MARK: Override responder methods
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
@@ -131,7 +126,7 @@ var CPZeroKeyCode = 48,
|
||||
// Don't forget to unbind, otherwise several steppers will increase or decrease
|
||||
[_currentTextField unbind:@"objectValue"];
|
||||
[_currentTextField makeDeselectable];
|
||||
_currentTextField = nil
|
||||
_currentTextField = nil;
|
||||
|
||||
// This is usefull when clicking on the stepper when the datePicker is not selected
|
||||
[_stepper setObjectValue:0];
|
||||
@@ -145,8 +140,8 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setter Getter methods
|
||||
// MARK: -
|
||||
// MARK: Setter Getter methods
|
||||
|
||||
/*! Set the value of the control
|
||||
@param aDateValue
|
||||
@@ -195,8 +190,8 @@ var CPZeroKeyCode = 48,
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Notification methods
|
||||
// MARK: -
|
||||
// MARK: Notification methods
|
||||
|
||||
/*! This is called to when the user just changed the selected textField
|
||||
@param aNotification
|
||||
@@ -214,20 +209,37 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark SelectTextField action
|
||||
// MARK: -
|
||||
// MARK: SelectTextField action
|
||||
|
||||
- (void)_selectTextFieldWithFlags:(unsigned)flags
|
||||
{
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// We select the firstTextField when the datePicker becomes firstResponder if _currentTextField is null. It can be null just when using tab
|
||||
if (!_currentTextField)
|
||||
{
|
||||
var targetField = nil;
|
||||
|
||||
if (flags & CPShiftKeyMask)
|
||||
[self _selectTextField:_lastTextField];
|
||||
{
|
||||
// Try last field; if hidden, find previous visible
|
||||
if ([_lastTextField isHidden])
|
||||
targetField = [self _previousVisibleTextFieldFrom:_lastTextField];
|
||||
else
|
||||
targetField = _lastTextField;
|
||||
}
|
||||
else
|
||||
[self _selectTextField:_firstTextField];
|
||||
{
|
||||
// Try first field; if hidden, find next visible
|
||||
if ([_firstTextField isHidden])
|
||||
targetField = [self _nextVisibleTextFieldFrom:_firstTextField];
|
||||
else
|
||||
targetField = _firstTextField;
|
||||
}
|
||||
|
||||
// Only select if we actually found a valid visible field
|
||||
if (targetField)
|
||||
[self _selectTextField:targetField];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,8 +275,8 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Events
|
||||
// MARK: -
|
||||
// MARK: Events
|
||||
|
||||
/*! Called when the user click on the stepper
|
||||
*/
|
||||
@@ -334,23 +346,92 @@ var CPZeroKeyCode = 48,
|
||||
return [super performKeyEquivalent:anEvent];
|
||||
}
|
||||
|
||||
- (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
|
||||
{
|
||||
var runner = [aTextField nextTextField];
|
||||
|
||||
// If we wrapped back to the start immediately, or runner is nil, we are done.
|
||||
if (!runner || runner == _firstTextField)
|
||||
return nil;
|
||||
|
||||
// Traverse hidden fields
|
||||
while (runner && [runner isHidden])
|
||||
{
|
||||
// If we hit the absolute last field and it is hidden, we've reached the end.
|
||||
if (runner == _lastTextField)
|
||||
return nil;
|
||||
|
||||
runner = [runner nextTextField];
|
||||
|
||||
// Safety: if we wrapped back to the start inside the loop
|
||||
if (runner == _firstTextField)
|
||||
return nil;
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
- (_CPDatePickerElementTextField)_previousVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
|
||||
{
|
||||
var runner = [aTextField previousTextField];
|
||||
|
||||
// If we wrapped back to the end immediately, or runner is nil, we are done.
|
||||
if (!runner || runner == _lastTextField)
|
||||
return nil;
|
||||
|
||||
// Traverse hidden fields
|
||||
while (runner && [runner isHidden])
|
||||
{
|
||||
// If we hit the absolute first field and it is hidden, we've reached the start.
|
||||
if (runner == _firstTextField)
|
||||
return nil;
|
||||
|
||||
runner = [runner previousTextField];
|
||||
|
||||
// Safety: if we wrapped back to the end inside the loop
|
||||
if (runner == _lastTextField)
|
||||
return nil;
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
- (void)insertTab:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
if (_currentTextField == _lastTextField)
|
||||
[[self window] selectNextKeyView:self];
|
||||
// Ensure boundaries are up to date
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (nextField)
|
||||
{
|
||||
[self _selectTextField:nextField];
|
||||
}
|
||||
else
|
||||
[self moveRight:sender];
|
||||
}
|
||||
{
|
||||
// We reached the visual end. Manually find the next external view.
|
||||
// We cannot rely on [[self window] selectNextKeyView:self] because it might
|
||||
// loop back into our own internal fields or select 'self' which refuses focus.
|
||||
var nextView = [_currentTextField nextValidKeyView];
|
||||
|
||||
- (void)moveRight:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
// Skip any view that is part of this control (descendant)
|
||||
while (nextView && [nextView isDescendantOf:self])
|
||||
{
|
||||
// If we looped back to the current field, we are trapped in a closed loop with no exit.
|
||||
if (nextView == _currentTextField)
|
||||
{
|
||||
nextView = nil;
|
||||
break;
|
||||
}
|
||||
nextView = [nextView nextValidKeyView];
|
||||
}
|
||||
|
||||
[self _selectTextField:[_currentTextField nextTextField]];
|
||||
if (nextView)
|
||||
[[self window] makeFirstResponder:nextView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)insertBacktab:(id)sender
|
||||
@@ -358,10 +439,47 @@ var CPZeroKeyCode = 48,
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
if (_currentTextField == _firstTextField)
|
||||
[[self window] selectPreviousKeyView:self];
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (prevField)
|
||||
{
|
||||
[self _selectTextField:prevField];
|
||||
}
|
||||
else
|
||||
[self moveLeft:sender];
|
||||
{
|
||||
// We reached the visual start. Manually find the previous external view.
|
||||
var prevView = [_currentTextField previousValidKeyView];
|
||||
|
||||
// Skip any view that is part of this control
|
||||
while (prevView && [prevView isDescendantOf:self])
|
||||
{
|
||||
if (prevView == _currentTextField)
|
||||
{
|
||||
prevView = nil;
|
||||
break;
|
||||
}
|
||||
prevView = [prevView previousValidKeyView];
|
||||
}
|
||||
|
||||
if (prevView)
|
||||
[[self window] makeFirstResponder:prevView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)moveRight:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// Use the helper to skip hidden fields
|
||||
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (nextField)
|
||||
[self _selectTextField:nextField];
|
||||
}
|
||||
|
||||
- (void)moveLeft:(id)sender
|
||||
@@ -369,7 +487,13 @@ var CPZeroKeyCode = 48,
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
[self _selectTextField:[_currentTextField previousTextField]];
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// Use the helper to skip hidden fields to be safe
|
||||
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (prevField)
|
||||
[self _selectTextField:prevField];
|
||||
}
|
||||
|
||||
- (void)moveDown:(id)sender
|
||||
@@ -401,7 +525,7 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
/*! KeyDown event
|
||||
We just care care about the event A/P and every numbers
|
||||
We just care about the event A/P and every numbers
|
||||
*/
|
||||
- (void)keyDown:(CPEvent)anEvent
|
||||
{
|
||||
@@ -410,24 +534,33 @@ var CPZeroKeyCode = 48,
|
||||
|
||||
[self interpretKeyEvents:[anEvent]];
|
||||
|
||||
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPPKeyCode || [anEvent keyCode] == CPMajAKeyCode || [anEvent keyCode] == CPMajPKeyCode))
|
||||
var characters = [anEvent characters];
|
||||
|
||||
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && [characters length] > 0)
|
||||
{
|
||||
if ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPMajAKeyCode)
|
||||
var charUpper = [characters uppercaseString];
|
||||
|
||||
if (charUpper === "A")
|
||||
{
|
||||
[_currentTextField setStringValue:@"AM"];
|
||||
else
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
return;
|
||||
}
|
||||
else if (charUpper === "P")
|
||||
{
|
||||
[_currentTextField setStringValue:@"PM"];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
|
||||
return;
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the event down to the specific field (which handles numeric input validation via regex)
|
||||
[_currentTextField setValueForKeyEvent:anEvent];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Layout the subviews
|
||||
*/
|
||||
@@ -476,8 +609,8 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override observers
|
||||
// MARK: -
|
||||
// MARK: Override observers
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
|
||||
+5
-2
@@ -871,10 +871,13 @@ var CPDocumentUntitledCount = 0;
|
||||
{
|
||||
var theDelegate = context.delegate;
|
||||
|
||||
if (aDocument === self && shouldClose)
|
||||
// Only close the document explicitly if there is NO delegate to handle the action.
|
||||
// If a delegate exists (e.g., the CPWindow), it is responsible for performing the close
|
||||
// upon receiving the callback below. Calling [self close] here would cause a double-close.
|
||||
if (aDocument === self && shouldClose && theDelegate == nil)
|
||||
[self close];
|
||||
|
||||
if (theDelegate != null)
|
||||
if (theDelegate)
|
||||
theDelegate.isa.objj_msgSend3(theDelegate, context.selector, aDocument, shouldClose, context.context);
|
||||
}
|
||||
|
||||
|
||||
+60
-3
@@ -26,13 +26,17 @@
|
||||
@import "CPPasteboard.j"
|
||||
@import "CPView.j"
|
||||
@import "CPWindow_Constants.j"
|
||||
@import "CPViewAnimation.j"
|
||||
|
||||
@class CPWindow // This file is imported by CPWindow.j
|
||||
@class _CPDOMDataTransferPasteboard
|
||||
|
||||
@global CPApp
|
||||
|
||||
#define DRAGGING_WINDOW(anObject) ([anObject isKindOfClass:[CPWindow class]] ? anObject : [anObject window])
|
||||
function DRAGGING_WINDOW(anObject)
|
||||
{
|
||||
return ([anObject isKindOfClass:[CPWindow class]] ? anObject : [anObject window]);
|
||||
}
|
||||
|
||||
var CPDragServerPreviousEvent = nil,
|
||||
CPDragServerPeriodicUpdateInterval = 0.05;
|
||||
@@ -132,6 +136,10 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
|
||||
unsigned _dragOperation;
|
||||
|
||||
CPTimer _draggingUpdateTimer;
|
||||
|
||||
// Animation State
|
||||
CGPoint _pendingEndLocation;
|
||||
CPDragOperation _pendingEndOperation;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -261,10 +269,19 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
|
||||
{
|
||||
var contentView = [scrollView contentView],
|
||||
bounds = [contentView bounds],
|
||||
insetBounds = CGRectInset(bounds, 30, 30),
|
||||
eventLocation = [contentView convertPoint:_draggingLocation fromView:nil],
|
||||
deltaX = 0,
|
||||
deltaY = 0;
|
||||
deltaY = 0,
|
||||
insetSize = 30;
|
||||
|
||||
if ([contentView respondsToSelector:@selector(documentView)] &&
|
||||
[[contentView documentView] respondsToSelector:@selector(rowHeight)])
|
||||
{
|
||||
// Adjust inset bounds based on CPTableView row height
|
||||
insetSize = MAX(insetSize, [[contentView documentView] rowHeight]);
|
||||
}
|
||||
|
||||
var insetBounds = CGRectInset(bounds, insetSize, insetSize);
|
||||
|
||||
if (!CGRectContainsPoint(insetBounds, eventLocation))
|
||||
{
|
||||
@@ -316,6 +333,46 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
|
||||
[_draggingUpdateTimer invalidate];
|
||||
_draggingUpdateTimer = nil;
|
||||
|
||||
// Check if we should slide back (drag failed + slideBack requested)
|
||||
if (![CPPlatform supportsDragAndDrop] && _shouldSlideBack && anOperation === CPDragOperationNone)
|
||||
{
|
||||
// Store state to finalize drag after animation completes
|
||||
_pendingEndLocation = aLocation;
|
||||
_pendingEndOperation = anOperation;
|
||||
|
||||
var currentFrame = [_draggedWindow frame],
|
||||
targetFrame = CGRectMake(_startDragLocation.x, _startDragLocation.y, currentFrame.size.width, currentFrame.size.height);
|
||||
|
||||
// We use CPViewAnimation. Even though _draggedWindow is a CPWindow,
|
||||
// CPViewAnimation supports targets that respond to setFrame: (like NSViewAnimation does for NSWindow).
|
||||
var animation = [[CPViewAnimation alloc] initWithViewAnimations:[
|
||||
[CPDictionary dictionaryWithObjects:[_draggedWindow, currentFrame, targetFrame]
|
||||
forKeys:[CPViewAnimationTargetKey, CPViewAnimationStartFrameKey, CPViewAnimationEndFrameKey]]
|
||||
]];
|
||||
|
||||
[animation setAnimationCurve:CPAnimationEaseOut];
|
||||
[animation setDuration:0.25];
|
||||
[animation setDelegate:self];
|
||||
[animation startAnimation];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[self _performFinalCleanupWithLocation:aLocation operation:anOperation];
|
||||
}
|
||||
|
||||
- (void)animationDidEnd:(CPAnimation)anAnimation
|
||||
{
|
||||
[self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation];
|
||||
}
|
||||
|
||||
- (void)animationDidStop:(CPAnimation)anAnimation
|
||||
{
|
||||
[self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation];
|
||||
}
|
||||
|
||||
- (void)_performFinalCleanupWithLocation:(CGPoint)aLocation operation:(CPDragOperation)anOperation
|
||||
{
|
||||
[_draggedView removeFromSuperview];
|
||||
|
||||
if (![CPPlatform supportsDragAndDrop])
|
||||
|
||||
+31
-9
@@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
BOOL _isARepeat;
|
||||
unsigned _keyCode;
|
||||
DOMEvent _DOMEvent;
|
||||
BOOL _isActionKey;
|
||||
int _data1;
|
||||
int _data2;
|
||||
short _subtype;
|
||||
@@ -81,10 +82,8 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
float _scrollingDeltaY;
|
||||
BOOL _hasPreciseScrollingDeltas;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
BOOL _suppressCappuccinoCut;
|
||||
BOOL _suppressCappuccinoPaste;
|
||||
#endif
|
||||
|
||||
CPTrackingArea _trackingArea;
|
||||
}
|
||||
@@ -110,17 +109,28 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
@param unmodCharacters the string of keys pressed without the presence of any modifiers other than Shift
|
||||
@param repeatKey \c YES if this is caused by the system repeat as opposed to the user pressing the key again
|
||||
@param code a number associated with the keyboard key of this event
|
||||
@param isAnActionKey a BOOL indicating whether this key is an action key (e.g. a function key)
|
||||
@throws CPInternalInconsistencyException if \c anEventType is not a CPKeyDown,
|
||||
CPKeyUp or CPFlagsChanged
|
||||
@return the keyboard event
|
||||
*/
|
||||
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
|
||||
{
|
||||
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
|
||||
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code];
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:isAnActionKey];
|
||||
}
|
||||
|
||||
// for backwards compatibility only
|
||||
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
|
||||
{
|
||||
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
|
||||
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:NO];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -252,7 +262,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
/* @ignore */
|
||||
- (id)_initKeyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
|
||||
{
|
||||
if (self = [self _initWithType:anEventType])
|
||||
{
|
||||
@@ -264,6 +274,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
_charactersIgnoringModifiers = unmodCharacters;
|
||||
_isARepeat = isARepeat;
|
||||
_keyCode = code;
|
||||
_isActionKey = isAnActionKey;
|
||||
_windowNumber = aWindowNumber;
|
||||
}
|
||||
|
||||
@@ -571,6 +582,21 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
return !firstResponderIsText;
|
||||
}
|
||||
|
||||
- (BOOL)_isActionOrCommandEvent
|
||||
{
|
||||
// This method is now platform-agnostic. It checks for abstract properties
|
||||
// of the event, including the _isActionKey flag that was set at creation time.
|
||||
return (
|
||||
// Is it a command shortcut?
|
||||
(_modifierFlags & (CPCommandKeyMask | CPControlKeyMask)) ||
|
||||
|
||||
// Is it a key that doesn't produce a character?
|
||||
([_characters length] === 0) ||
|
||||
|
||||
// Was it identified as an action key by the platform-specific layer?
|
||||
_isActionKey
|
||||
);
|
||||
}
|
||||
/*!
|
||||
Return YES if this event is a part of processing a browser controlled cut or paste event
|
||||
where the browser will go ahead and do the work of cutting or pasting within the input
|
||||
@@ -581,11 +607,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
*/
|
||||
- (BOOL)_platformIsEffectingCutOrPaste
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
return _suppressCappuccinoCut || _suppressCappuccinoPaste;
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
+50
-8
@@ -51,14 +51,40 @@ var _CPFontCache = {},
|
||||
_CPFontSystemFontFaceSpecified = NO;
|
||||
|
||||
|
||||
#define _CPRealFontSize(aSize) (aSize <= 0 ? _CPFontSystemFontSize : aSize)
|
||||
#define _CPFontNormalizedNames(aName) _CPFontNormalizedNameArray(aName).join(", ")
|
||||
#define _CPCachedFont(aName, aSize, isBold, isItalic) _CPFontCache[_CPFontCreateCSSString(_CPFontNormalizedNames(aName), aSize, isBold, isItalic)]
|
||||
#define _CPUserFont(aName, aSize, isBold, isItalic) _CPCachedFont(aName, aSize, isBold, isItalic) || [[CPFont alloc] _initWithName:aName size:aSize bold:isBold italic:isItalic system:NO]
|
||||
function _CPRealFontSize(aSize)
|
||||
{
|
||||
return (aSize <= 0 ? _CPFontSystemFontSize : aSize);
|
||||
}
|
||||
|
||||
#define _CPSystemFontCacheKey(aSize, isBold) (String(aSize) + (isBold ? "b" : ""))
|
||||
#define _CPCachedSystemFont(aSize, isBold) _CPSystemFontCache[_CPSystemFontCacheKey(aSize, isBold)]
|
||||
#define _CPSystemFont(aSize, isBold) (_CPCachedSystemFont(aSize, isBold) || [[CPFont alloc] _initWithName:_CPFontSystemFacePlaceholder size:aSize bold:isBold italic:NO system:YES])
|
||||
function _CPFontNormalizedNames(aName)
|
||||
{
|
||||
return _CPFontNormalizedNameArray(aName).join(", ");
|
||||
}
|
||||
|
||||
function _CPCachedFont(aName, aSize, isBold, isItalic)
|
||||
{
|
||||
return _CPFontCache[_CPFontCreateCSSString(_CPFontNormalizedNames(aName), aSize, isBold, isItalic)];
|
||||
}
|
||||
|
||||
function _CPUserFont(aName, aSize, isBold, isItalic)
|
||||
{
|
||||
return _CPCachedFont(aName, aSize, isBold, isItalic) || [[CPFont alloc] _initWithName:aName size:aSize bold:isBold italic:isItalic system:NO];
|
||||
}
|
||||
|
||||
function _CPSystemFontCacheKey(aSize, isBold)
|
||||
{
|
||||
return (String(aSize) + (isBold ? "b" : ""));
|
||||
}
|
||||
|
||||
function _CPCachedSystemFont(aSize, isBold)
|
||||
{
|
||||
return _CPSystemFontCache[_CPSystemFontCacheKey(aSize, isBold)];
|
||||
}
|
||||
|
||||
function _CPSystemFont(aSize, isBold)
|
||||
{
|
||||
return (_CPCachedSystemFont(aSize, isBold) || [[CPFont alloc] _initWithName:_CPFontSystemFacePlaceholder size:aSize bold:isBold italic:NO system:YES]);
|
||||
}
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@@ -220,7 +246,7 @@ following:
|
||||
if (systemFontStyle)
|
||||
{
|
||||
// Yes, so install it in the DOM Style element
|
||||
document.getElementsByTagName("STYLE")[0].innerHTML += "\n" + [aTheme setCSSResourcesPathInString:systemFontStyle];
|
||||
document.getElementsByTagName("STYLE")[0].innerHTML += "\n" + [aTheme setCSSResourcesPath:systemFontStyle];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +490,22 @@ following:
|
||||
return _CPRealFontSize(_size);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the font size. Cocoa/AppKit compatibility alias for -size.
|
||||
*/
|
||||
- (float)pointSize
|
||||
{
|
||||
return [self size];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the font name. Cocoa/AppKit compatibility alias for -familyName.
|
||||
*/
|
||||
- (CPString)fontName
|
||||
{
|
||||
return [self familyName];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the font as a CSS string
|
||||
*/
|
||||
|
||||
+29
-21
@@ -25,6 +25,7 @@
|
||||
@import "CPControl.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPFontDescriptor.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@global CPApp
|
||||
@class CPFontPanel
|
||||
@@ -132,28 +133,31 @@ CPRemoveTraitFontAction = 7;
|
||||
{
|
||||
_availableFonts = [];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_CPFontDetectSpan = document.createElement("span");
|
||||
_CPFontDetectSpan.fontSize = "24px";
|
||||
_CPFontDetectSpan.appendChild(document.createTextNode("mmmmmmmmmml"));
|
||||
var div = document.createElement("div");
|
||||
div.style.position = "absolute";
|
||||
div.style.top = "-1000px";
|
||||
div.appendChild(_CPFontDetectSpan);
|
||||
document.getElementsByTagName("body")[0].appendChild(div);
|
||||
|
||||
_CPFontDetectReferenceFonts = _CPFontDetectPickTwoDifferentFonts(["monospace", "serif", "sans-serif", "cursive"]);
|
||||
|
||||
for (var i = 0; i < _CPFontDetectAllFonts.length; i++)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var available = _CPFontDetectFontAvailable(_CPFontDetectAllFonts[i]);
|
||||
if (available)
|
||||
_availableFonts.push(_CPFontDetectAllFonts[i]);
|
||||
_CPFontDetectSpan = document.createElement("span");
|
||||
_CPFontDetectSpan.fontSize = "24px";
|
||||
_CPFontDetectSpan.appendChild(document.createTextNode("mmmmmmmmmml"));
|
||||
var div = document.createElement("div");
|
||||
div.style.position = "absolute";
|
||||
div.style.top = "-1000px";
|
||||
div.appendChild(_CPFontDetectSpan);
|
||||
document.getElementsByTagName("body")[0].appendChild(div);
|
||||
|
||||
_CPFontDetectReferenceFonts = _CPFontDetectPickTwoDifferentFonts(["monospace", "serif", "sans-serif", "cursive"]);
|
||||
|
||||
for (var i = 0; i < _CPFontDetectAllFonts.length; i++)
|
||||
{
|
||||
var available = _CPFontDetectFontAvailable(_CPFontDetectAllFonts[i]);
|
||||
if (available)
|
||||
_availableFonts.push(_CPFontDetectAllFonts[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there's no font detection, just assume all fonts are available.
|
||||
_availableFonts = _CPFontDetectAllFonts;
|
||||
}
|
||||
#else
|
||||
// If there's no font detection, just assume all fonts are available.
|
||||
_availableFonts = _CPFontDetectAllFonts;
|
||||
#endif
|
||||
}
|
||||
return _availableFonts;
|
||||
}
|
||||
@@ -208,7 +212,11 @@ CPRemoveTraitFontAction = 7;
|
||||
|
||||
- (@action)addFontTrait:(id)sender
|
||||
{
|
||||
var tag = [sender tag];
|
||||
var tag = sender;
|
||||
|
||||
if ([sender respondsToSelector:@selector(tag)])
|
||||
tag = [sender tag];
|
||||
|
||||
_activeChange = tag == nil ? @{} : @{ @"addTraits": tag };
|
||||
_fontAction = CPAddTraitFontAction;
|
||||
|
||||
|
||||
+51
-12
@@ -31,6 +31,7 @@
|
||||
|
||||
@import "CGGeometry.j"
|
||||
@import "CPCompatibility.j"
|
||||
@import "CPGraphicsContext.j"
|
||||
|
||||
@class CPColor
|
||||
@global document
|
||||
@@ -238,7 +239,8 @@ function CPAppKitImage(aFilename, aSize)
|
||||
*/
|
||||
- (CPData)data
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
var dataURL;
|
||||
|
||||
if ([_filename hasPrefix:@"data:image"])
|
||||
@@ -260,7 +262,6 @@ function CPAppKitImage(aFilename, aSize)
|
||||
|
||||
var base64 = dataURL.replace(/^data:image\/png;base64,/, "");
|
||||
return [CPData dataWithBase64:base64];
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -383,7 +384,8 @@ function CPAppKitImage(aFilename, aSize)
|
||||
|
||||
_loadStatus = CPImageLoadStatusLoading;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
_image = new Image();
|
||||
|
||||
var isSynchronous = YES;
|
||||
@@ -430,7 +432,6 @@ function CPAppKitImage(aFilename, aSize)
|
||||
// onload and friends may fire after this point but BEFORE the end of the run loop,
|
||||
// crazy, I know. So don't set isSynchronous here, rather wait a bit longer.
|
||||
window.setTimeout(function() { isSynchronous = NO; }, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)isSingleImage
|
||||
@@ -516,8 +517,8 @@ function CPAppKitImage(aFilename, aSize)
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CSS Theming
|
||||
// MARK: -
|
||||
// MARK: CSS Theming
|
||||
|
||||
// The code below adds support for CSS theming with 100% compatibility with current theming system.
|
||||
// The idea is to extend CPImage (and CPColor) with CSS components and adapt low level UI components to
|
||||
@@ -640,7 +641,8 @@ function CPAppKitImage(aFilename, aSize)
|
||||
|
||||
- (DOMElement)applyCSSImageForView:(CPView)aView onDOMElement:(DOMElement)aDOMElement styleNode:(DOMElement)aStyleNode previousState:(CPArrayRef)aPreviousStateRef
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
// First, restore previous CSS styling before applying the new one
|
||||
|
||||
var aPreviousState = @deref(aPreviousStateRef);
|
||||
@@ -723,7 +725,6 @@ function CPAppKitImage(aFilename, aSize)
|
||||
@deref(aPreviousStateRef) = aPreviousState;
|
||||
|
||||
return aStyleNode;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)_shouldBeResized
|
||||
@@ -737,7 +738,7 @@ var CPImageCSSDictionaryKey = @"CPImageCSSDictionaryKey",
|
||||
CPImageCSSBeforeDictionaryKey = @"CPImageCSSBeforeDictionaryKey",
|
||||
CPImageCSSAfterDictionaryKey = @"CPImageCSSAfterDictionaryKey";
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPImage (CPCoding)
|
||||
|
||||
@@ -774,7 +775,45 @@ var CPImageCSSDictionaryKey = @"CPImageCSSDictionaryKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
// MARK: Drawing
|
||||
|
||||
@implementation CPImage (Drawing)
|
||||
|
||||
- (void)drawAtPoint:(CGPoint)point fromRect:(CPRect)fromRect operation:(CGBlendMode)op fraction:(float)delta
|
||||
{
|
||||
if (_loadStatus !== CPImageLoadStatusCompleted)
|
||||
return;
|
||||
|
||||
var context = [CPGraphicsContext currentContext].graphicsPort;
|
||||
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CGContextSetBlendMode(context, op);
|
||||
CGContextSetAlpha(context, delta);
|
||||
|
||||
context.drawImage(
|
||||
_image,
|
||||
fromRect.origin.x,
|
||||
fromRect.origin.y,
|
||||
fromRect.size.width,
|
||||
fromRect.size.height,
|
||||
point.x,
|
||||
point.y,
|
||||
fromRect.size.width,
|
||||
fromRect.size.height
|
||||
);
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMaterialIconImage : CPImage
|
||||
{
|
||||
@@ -934,7 +973,7 @@ var CPImageCSSDictionaryKey = @"CPImageCSSDictionaryKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPThreePartImage : CPObject
|
||||
{
|
||||
@@ -1077,7 +1116,7 @@ var CPNinePartImageImageSlicesKey = @"CPNinePartImageImageSlicesKey";
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPImage (Duplication)
|
||||
|
||||
|
||||
+56
-41
@@ -25,6 +25,7 @@
|
||||
@import "CPControl.j"
|
||||
@import "CPImage.j"
|
||||
@import "CPShadowView.j"
|
||||
@import "CPDOMDisplayServer.j"
|
||||
|
||||
@global CPImagesPboardType
|
||||
@global appkit_tag_dom_elements
|
||||
@@ -89,9 +90,10 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
|
||||
if (self)
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
[self _createDOMImageElement];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _createDOMImageElement];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -99,7 +101,9 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
|
||||
- (void)_createDOMImageElement
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
var image = [self objectValue],
|
||||
isCSSBasedImage = [image isCSSBased],
|
||||
isIMGImageElement = _DOMImageElement && (_DOMImageElement.nodeName == "IMG");
|
||||
@@ -140,7 +144,6 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
AppKitTagDOMElement(self, _DOMImageElement);
|
||||
|
||||
CPDOMDisplayServerAppendChild(_DOMElement, _DOMImageElement);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -173,17 +176,18 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
|
||||
var newImage = [self objectValue];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
[self _createDOMImageElement];
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _createDOMImageElement];
|
||||
|
||||
if ([newImage isCSSBased])
|
||||
_cssStyleNode = [newImage applyCSSImageForView:self
|
||||
onDOMElement:_DOMImageElement
|
||||
styleNode:_cssStyleNode
|
||||
previousState:@ref(_cssStylePreviousState)];
|
||||
else
|
||||
_DOMImageElement.src = newImage ? [newImage filename] : [CPImageViewEmptyPlaceholderImage filename];
|
||||
#endif
|
||||
if ([newImage isCSSBased])
|
||||
_cssStyleNode = [newImage applyCSSImageForView:self
|
||||
onDOMElement:_DOMImageElement
|
||||
styleNode:_cssStyleNode
|
||||
previousState:@ref(_cssStylePreviousState)];
|
||||
else
|
||||
_DOMImageElement.src = newImage ? [newImage filename] : [CPImageViewEmptyPlaceholderImage filename];
|
||||
}
|
||||
|
||||
var size = [newImage size];
|
||||
|
||||
@@ -191,10 +195,11 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
{
|
||||
[defaultCenter addObserver:self selector:@selector(imageDidLoad:) name:CPImageDidLoadNotification object:newImage];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_DOMImageElement.width = 0;
|
||||
_DOMImageElement.height = 0;
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_DOMImageElement.width = 0;
|
||||
_DOMImageElement.height = 0;
|
||||
}
|
||||
|
||||
[_shadowView setHidden:YES];
|
||||
}
|
||||
@@ -237,6 +242,8 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
|
||||
if (_hasShadow)
|
||||
{
|
||||
[self setClipsToBounds:NO];
|
||||
|
||||
_shadowView = [[CPShadowView alloc] initWithFrame:[self bounds]];
|
||||
|
||||
[self addSubview:_shadowView];
|
||||
@@ -287,12 +294,10 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
{
|
||||
[super setImageScaling:anImageScaling];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if ([self currentValueForThemeAttribute:@"image-scaling"] === CPImageScaleAxesIndependently)
|
||||
if (CPDOMAvailable && [self currentValueForThemeAttribute:@"image-scaling"] === CPImageScaleAxesIndependently)
|
||||
{
|
||||
CPDOMDisplayServerSetStyleLeftTop(_DOMImageElement, NULL, 0.0, 0.0);
|
||||
}
|
||||
#endif
|
||||
|
||||
[self setNeedsLayout];
|
||||
[self setNeedsDisplay:YES];
|
||||
@@ -310,16 +315,18 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
{
|
||||
if (![self image])
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
_DOMImageElement.style.visibility = "hidden";
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_DOMImageElement.style.visibility = "hidden";
|
||||
}
|
||||
[_shadowView setHidden:YES];
|
||||
}
|
||||
else
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
_DOMImageElement.style.visibility = "visible";
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_DOMImageElement.style.visibility = "visible";
|
||||
}
|
||||
[_shadowView setHidden:NO];
|
||||
}
|
||||
}
|
||||
@@ -354,10 +361,11 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
|
||||
if (imageScaling === CPImageScaleAxesIndependently)
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_DOMImageElement.width = ROUND(width);
|
||||
_DOMImageElement.height = ROUND(height);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -395,10 +403,11 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
break;
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
_DOMImageElement.width = ROUND(width);
|
||||
_DOMImageElement.height = ROUND(height);
|
||||
#endif
|
||||
}
|
||||
|
||||
var x,
|
||||
y;
|
||||
@@ -441,13 +450,13 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
break;
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
CPDOMDisplayServerSetStyleLeftTop(_DOMImageElement, NULL, x, y);
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
CPDOMDisplayServerSetStyleLeftTop(_DOMImageElement, NULL, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if ([image isCSSBased] && [image _shouldBeResized])
|
||||
if (CPDOMAvailable && [image isCSSBased] && [image _shouldBeResized])
|
||||
{
|
||||
[image _setDisplaySize:CGSizeMake(ROUND(width), ROUND(height))];
|
||||
|
||||
@@ -456,7 +465,12 @@ var CPImageViewEmptyPlaceholderImage = nil;
|
||||
styleNode:_cssStyleNode
|
||||
previousState:@ref(_cssStylePreviousState)];
|
||||
}
|
||||
#endif
|
||||
|
||||
if (CPDOMAvailable && [image isCSSBased])
|
||||
{
|
||||
_DOMImageElement.style.width = _DOMImageElement.width + 'px';
|
||||
_DOMImageElement.style.height = _DOMImageElement.height + 'px';
|
||||
}
|
||||
|
||||
_imageRect = CGRectMake(x, y, width, height);
|
||||
|
||||
@@ -578,9 +592,10 @@ var CPImageViewImageKey = @"CPImageViewImageKey",
|
||||
|
||||
if (self)
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
[self _createDOMImageElement];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _createDOMImageElement];
|
||||
}
|
||||
|
||||
[self setHasShadow:[aCoder decodeBoolForKey:CPImageViewHasShadowKey]];
|
||||
[self setImageAlignment:[aCoder decodeIntForKey:CPImageViewImageAlignmentKey]];
|
||||
|
||||
@@ -26,10 +26,17 @@
|
||||
@import "CPEvent_Constants.j"
|
||||
@import "CPText.j"
|
||||
|
||||
// TODO: These are the macOS standard key bindings and are applied on every
|
||||
// platform. "@" is always Command, and the Emacs-style Control bindings
|
||||
// (^a, ^e, ^k, ^v, ^y, ...) and arrow/Home/End behaviour conflict with
|
||||
// Windows and Linux conventions. Provide per-platform tables keyed off
|
||||
// CPBrowserIsOperatingSystem, mapping the primary modifier through
|
||||
// CPPlatformActionKeyMask.
|
||||
CPStandardKeyBindings = {
|
||||
@"@.": @"cancelOperation:",
|
||||
|
||||
@"@a": @"selectAll:",
|
||||
@"@~$v": @"pasteAsPlainText:",
|
||||
@"^a": @"moveToBeginningOfParagraph:",
|
||||
@"^$a": @"moveToBeginningOfParagraphAndModifySelection:",
|
||||
@"^b": @"moveBackward:",
|
||||
@@ -81,6 +88,7 @@ CPStandardKeyBindings[@"~" + CPDeleteFunctionKey] = @"deleteWordForward:";
|
||||
CPStandardKeyBindings[CPTabCharacter] = @"insertTab:";
|
||||
CPStandardKeyBindings[@"~" + CPTabCharacter] = @"insertTabIgnoringFieldEditor:";
|
||||
CPStandardKeyBindings[@"^" + CPTabCharacter] = @"selectNextKeyView:";
|
||||
CPStandardKeyBindings[@"$" + CPTabCharacter] = @"insertBacktab:";
|
||||
CPStandardKeyBindings[CPBackTabCharacter] = @"insertBacktab:";
|
||||
CPStandardKeyBindings[@"^" + CPBackTabCharacter] = @"selectPreviousKeyView:";
|
||||
|
||||
|
||||
@@ -208,7 +208,11 @@ var CPBindingOperationAnd = 0,
|
||||
options = [_info objectForKey:CPOptionsKey],
|
||||
newValue = [destination valueForKeyPath:keyPath];
|
||||
|
||||
if (CPIsControllerMarker(newValue))
|
||||
// give nil values the chance to be transformed to true via CPNegateBoolean (issue #1986)
|
||||
if ((newValue == nil || CPIsControllerMarker(newValue)) && [options objectForKey:CPValueTransformerNameBindingOption] === CPNegateBooleanTransformerName)
|
||||
[self setValue:[self transformValue:NO withOptions:options] forBinding:theBinding];
|
||||
|
||||
else if (CPIsControllerMarker(newValue))
|
||||
{
|
||||
[self raiseIfNotApplicable:newValue forKeyPath:keyPath options:options];
|
||||
|
||||
|
||||
+26
-22
@@ -88,43 +88,61 @@ CPRatingLevelIndicatorStyle = 3;
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
// 1. Calculate the Theme State
|
||||
// We explicitly check the window style mask to see if we are in a HUD.
|
||||
// This allows us to pass CPThemeStateHUD to the theme system, even if the control
|
||||
// itself isn't explicitly set to HUD, inheriting the style from the window.
|
||||
var themeState = [self themeState];
|
||||
|
||||
if ([[self window] styleMask] & CPHUDBackgroundWindowMask)
|
||||
themeState = themeState.and(CPThemeStateHUD);
|
||||
|
||||
// 2. Layout the Bezel
|
||||
var bezelView = [self layoutEphemeralSubviewNamed:"bezel"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
// TODO Make themable.
|
||||
[bezelView setBackgroundColor:[self valueForThemeAttribute:@"bezel-color"]];
|
||||
|
||||
[bezelView setBackgroundColor:[self valueForThemeAttribute:@"bezel-color" inState:themeState]];
|
||||
|
||||
var segmentCount = _maxValue - _minValue;
|
||||
|
||||
if (segmentCount <= 0)
|
||||
return;
|
||||
|
||||
var filledColor = [self valueForThemeAttribute:@"color-normal"],
|
||||
// 3. Determine the Color based on Value and Thresholds
|
||||
// We pass 'themeState' here. If it contains CPThemeStateHUD, the ThemeDescriptor
|
||||
// will return the monochrome color for normal/warning/critical.
|
||||
// If not, it returns Green/Yellow/Red.
|
||||
var filledColor = [self valueForThemeAttribute:@"color-normal" inState:themeState],
|
||||
value = [self doubleValue];
|
||||
|
||||
if (_warningValue < _criticalValue)
|
||||
{
|
||||
// Standard ascending scale (e.g. Volume)
|
||||
if (value >= _criticalValue)
|
||||
filledColor = [self valueForThemeAttribute:@"color-critical"];
|
||||
filledColor = [self valueForThemeAttribute:@"color-critical" inState:themeState];
|
||||
else if (value >= _warningValue)
|
||||
filledColor = [self valueForThemeAttribute:@"color-warning"];
|
||||
filledColor = [self valueForThemeAttribute:@"color-warning" inState:themeState];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Descending scale (e.g. Battery Life)
|
||||
if (value <= _criticalValue)
|
||||
filledColor = [self valueForThemeAttribute:@"color-critical"];
|
||||
filledColor = [self valueForThemeAttribute:@"color-critical" inState:themeState];
|
||||
else if (value <= _warningValue)
|
||||
filledColor = [self valueForThemeAttribute:@"color-warning"];
|
||||
filledColor = [self valueForThemeAttribute:@"color-warning" inState:themeState];
|
||||
}
|
||||
|
||||
var emptyColor = [self valueForThemeAttribute:@"color-empty" inState:themeState];
|
||||
|
||||
// 4. Paint Segments
|
||||
for (var i = 0; i < segmentCount; i++)
|
||||
{
|
||||
var segmentView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:bezelView];
|
||||
|
||||
[segmentView setBackgroundColor:(_minValue + i) < value ? filledColor : [self valueForThemeAttribute:@"color-empty"]];
|
||||
[segmentView setBackgroundColor:(_minValue + i) < value ? filledColor : emptyColor];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,20 +323,6 @@ CPRatingLevelIndicatorStyle = 3;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
/*
|
||||
- (CPTickMarkPosition)tickMarkPosition;
|
||||
- (void)setTickMarkPosition:(CPTickMarkPosition)position;
|
||||
|
||||
- (int)numberOfTickMarks;
|
||||
- (void)setNumberOfTickMarks:(int)count;
|
||||
|
||||
- (int)numberOfMajorTickMarks;
|
||||
- (void)setNumberOfMajorTickMarks:(int)count;
|
||||
|
||||
- (double)tickMarkValueAtIndex:(int)index;
|
||||
- (CGRect)rectOfTickMarkAtIndex:(int)index;
|
||||
*/
|
||||
|
||||
@end
|
||||
|
||||
var CPLevelIndicatorStyleKey = "CPLevelIndicatorStyleKey",
|
||||
|
||||
+117
-6
@@ -27,6 +27,8 @@
|
||||
|
||||
@import "CPKeyValueBinding.j"
|
||||
@import "CPMenuItem.j"
|
||||
@import "CALayer.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@global CPApp
|
||||
|
||||
@@ -50,7 +52,8 @@ CPMenuDidEndTrackingNotification = @"CPMenuDidEndTrackingNotification";
|
||||
var _CPMenuBarVisible = NO,
|
||||
_CPMenuBarTitle = @"",
|
||||
_CPMenuBarAttributes = nil,
|
||||
_CPMenuBarSharedWindow = nil;
|
||||
_CPMenuBarSharedWindow = nil,
|
||||
_CPMenuBarImage = nil;
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@@ -135,9 +138,10 @@ var _CPMenuBarVisible = NO,
|
||||
[_CPMenuBarSharedWindow orderOut:self];
|
||||
|
||||
// FIXME: There must be a better way to do this.
|
||||
#if PLATFORM(DOM)
|
||||
[[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)setMenuBarTitle:(CPString)aTitle
|
||||
@@ -268,10 +272,13 @@ var _CPMenuBarVisible = NO,
|
||||
if (self)
|
||||
{
|
||||
_title = aTitle;
|
||||
_items = [];
|
||||
// Use CPMutableArray instead of raw JS array for consistency with removeAllItems
|
||||
_items = [CPMutableArray array];
|
||||
|
||||
_autoenablesItems = YES;
|
||||
_showsStateColumn = YES;
|
||||
|
||||
_themeState = CPThemeStateNormal;
|
||||
|
||||
[self setMinimumWidth:0];
|
||||
}
|
||||
@@ -284,6 +291,37 @@ var _CPMenuBarVisible = NO,
|
||||
return [self initWithTitle:@""];
|
||||
}
|
||||
|
||||
// Managing Theme States (HUD Support)
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
if ([self hasThemeState:aState])
|
||||
return;
|
||||
|
||||
_themeState = _themeState.and(aState);
|
||||
|
||||
// Propagate to the view if the menu is currently visible
|
||||
if (_menuWindow)
|
||||
[[_menuWindow _menuView] setThemeState:_themeState];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
if (![self hasThemeState:aState])
|
||||
return;
|
||||
|
||||
_themeState = _themeState.without(aState);
|
||||
|
||||
// Propagate to the view if the menu is currently visible
|
||||
if (_menuWindow)
|
||||
[[_menuWindow _menuView] setThemeState:_themeState];
|
||||
}
|
||||
|
||||
- (CPThemeState)themeState
|
||||
{
|
||||
return _themeState;
|
||||
}
|
||||
|
||||
// Setting Up Menu Commands
|
||||
/*!
|
||||
Inserts a menu item at the specified index.
|
||||
@@ -373,6 +411,10 @@ var _CPMenuBarVisible = NO,
|
||||
[self willChangeValueForKey:@"items"];
|
||||
_items = [CPMutableArray array];
|
||||
[self didChangeValueForKey:@"items"];
|
||||
|
||||
// Ensure the main menu updates if cleared
|
||||
if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
|
||||
[_CPMenuBarSharedWindow setMenu:self];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -389,6 +431,9 @@ var _CPMenuBarVisible = NO,
|
||||
if ([aMenuItem menu] !== self || !_items)
|
||||
return;
|
||||
|
||||
if (_menuWindow)
|
||||
[[_menuWindow _menuView] tile];
|
||||
|
||||
[aMenuItem setValue:[aMenuItem valueForKey:@"changeCount"] + 1 forKey:@"changeCount"];
|
||||
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
@@ -789,6 +834,10 @@ var _CPMenuBarVisible = NO,
|
||||
// Create the window for our menu.
|
||||
var menuWindow = [_CPMenuWindow menuWindowWithMenu:self font:[self font]];
|
||||
|
||||
// This pushes the state (e.g., CPThemeStateHUD) to the actual view that renders the menu
|
||||
if (_themeState)
|
||||
[[menuWindow _menuView] setThemeState:_themeState];
|
||||
|
||||
[menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
|
||||
|
||||
if (anItem)
|
||||
@@ -879,6 +928,10 @@ var _CPMenuBarVisible = NO,
|
||||
var theWindow = [aView window],
|
||||
menuWindow = [_CPMenuWindow menuWindowWithMenu:aMenu font:aFont];
|
||||
|
||||
// --- APPLY THEME STATE FROM MENU OBJECT TO VIEW ---
|
||||
if ([aMenu respondsToSelector:@selector(themeState)])
|
||||
[[menuWindow _menuView] setThemeState:[aMenu themeState]];
|
||||
|
||||
[menuWindow setBackgroundStyle:_CPMenuWindowPopUpBackgroundStyle];
|
||||
|
||||
var constraintRect = [CPMenu _constraintRectForView:aView],
|
||||
@@ -1016,6 +1069,10 @@ var _CPMenuBarVisible = NO,
|
||||
// an additional mouse move after all this, but not in other browsers.
|
||||
// This will be fixed correctly with the coming run loop changes.
|
||||
[_CPDisplayServer run];
|
||||
|
||||
// If layers are updated within the menu callback, the re-draw & re-layout doesn't occur until a mouse move.
|
||||
// We force the update here.
|
||||
[CALayer runLoopUpdateLayers];
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
@@ -1057,7 +1114,23 @@ var _CPMenuBarVisible = NO,
|
||||
if ([anEvent _triggersKeyEquivalent:[item keyEquivalent] withModifierMask:[item keyEquivalentModifierMask]])
|
||||
{
|
||||
if ([item isEnabled])
|
||||
{
|
||||
// Flash the top-level item if this is the Main Menu
|
||||
if (self === [CPApp mainMenu])
|
||||
[self _flashItemAtIndex:index];
|
||||
|
||||
anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview
|
||||
[self performActionForItemAtIndex:index];
|
||||
|
||||
// we are done with this event in cappuccino space. do not let the browser do something weird additionally (e.g. command-o).
|
||||
// but we must not stop copy/paste events as these can only be handled by the browser at this time even if they are in our menu
|
||||
// (until we move to CPTextView as the fieleditor)
|
||||
|
||||
if (CPDOMAvailable && characters != "c" && characters != "x" && characters != "v")
|
||||
{
|
||||
_CPDOMEventStop(anEvent._DOMEvent);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//beep?
|
||||
@@ -1067,7 +1140,13 @@ var _CPMenuBarVisible = NO,
|
||||
}
|
||||
|
||||
if ([[item submenu] performKeyEquivalent:anEvent])
|
||||
{
|
||||
// Flash the top-level item if a submenu handled the event
|
||||
if (self === [CPApp mainMenu])
|
||||
[self _flashItemAtIndex:index];
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
@@ -1147,6 +1226,25 @@ var _CPMenuBarVisible = NO,
|
||||
return nil;
|
||||
}
|
||||
|
||||
//
|
||||
/*
|
||||
@ignore
|
||||
*/
|
||||
- (void)_flashItemAtIndex:(int)anIndex
|
||||
{
|
||||
// If we are using a native bridge (like a desktop wrapper), let the OS handle the visual feedback.
|
||||
if ([CPPlatform supportsNativeMainMenu])
|
||||
return;
|
||||
|
||||
[self _highlightItemAtIndex:anIndex];
|
||||
[self performSelector:@selector(_stopFlashingItem) withObject:nil afterDelay:0.2];
|
||||
}
|
||||
|
||||
- (void)_stopFlashingItem
|
||||
{
|
||||
[self _highlightItemAtIndex:CPNotFound];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1218,6 +1316,11 @@ var _CPMenuBarVisible = NO,
|
||||
postNotificationName:CPMenuDidAddItemNotification
|
||||
object:self
|
||||
userInfo:@{ @"CPMenuItemIndex": anIndex }];
|
||||
|
||||
// FIX #1222: If this is the main menu, force the shared menu bar window to refresh its layout.
|
||||
// This ensures new items are positioned correctly (e.g. not pushed to the far right by previous layout states).
|
||||
if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
|
||||
[_CPMenuBarSharedWindow setMenu:self];
|
||||
}
|
||||
|
||||
- (void)removeObjectFromItemsAtIndex:(CPUInteger)anIndex
|
||||
@@ -1233,6 +1336,10 @@ var _CPMenuBarVisible = NO,
|
||||
postNotificationName:CPMenuDidRemoveItemNotification
|
||||
object:self
|
||||
userInfo:@{ @"CPMenuItemIndex": anIndex }];
|
||||
|
||||
// FIX #1222: Ensure the shared menu bar updates layout when items are removed.
|
||||
if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
|
||||
[_CPMenuBarSharedWindow setMenu:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1265,6 +1372,9 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
|
||||
|
||||
_autoenablesItems = ![aCoder containsValueForKey:CPMenuAutoEnablesItemsKey] || [aCoder decodeBoolForKey:CPMenuAutoEnablesItemsKey];
|
||||
|
||||
// Ensure theme state is initialized to avoid undefined issues.
|
||||
_themeState = CPThemeStateNormal;
|
||||
|
||||
[self setMinimumWidth:0];
|
||||
}
|
||||
|
||||
@@ -1293,7 +1403,7 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPMenu (CSSTheming)
|
||||
|
||||
@@ -1327,3 +1437,4 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
|
||||
@import "_CPMenuBarWindow.j"
|
||||
@import "_CPMenuWindow.j"
|
||||
|
||||
@global _CPDOMEventStop
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
@import "CPPanel.j"
|
||||
@import "_CPMenuManager.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class _CPMenuView
|
||||
@class CPMenu
|
||||
@@ -100,16 +101,17 @@
|
||||
|
||||
- (void)setTitle:(CPString)aTitle
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
var bundleName = [[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPBundleName"];
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var bundleName = [CPApp _applicationName];
|
||||
|
||||
if (![bundleName length])
|
||||
document.title = aTitle;
|
||||
else if ([aTitle length])
|
||||
document.title = aTitle + @" - " + bundleName;
|
||||
else
|
||||
document.title = bundleName;
|
||||
#endif
|
||||
if (![bundleName length])
|
||||
document.title = aTitle;
|
||||
else if ([aTitle length])
|
||||
document.title = aTitle + @" - " + bundleName;
|
||||
else
|
||||
document.title = bundleName;
|
||||
}
|
||||
|
||||
[_titleField setStringValue:aTitle];
|
||||
[_titleField sizeToFit];
|
||||
@@ -370,12 +372,41 @@
|
||||
|
||||
- (void)tile
|
||||
{
|
||||
var bounds = [[self contentView] bounds],
|
||||
height = CGRectGetHeight(bounds),
|
||||
x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView];
|
||||
|
||||
// 1. Layout the Icon (if present)
|
||||
if (_iconImageView && ![_iconImageView isHidden])
|
||||
{
|
||||
var iconFrame = [_iconImageView frame];
|
||||
|
||||
iconFrame.origin.x = x;
|
||||
// Vertically center
|
||||
iconFrame.origin.y = (height - CGRectGetHeight(iconFrame)) / 2.0;
|
||||
|
||||
[_iconImageView setFrame:iconFrame];
|
||||
|
||||
x = CGRectGetMaxX(iconFrame) + 6.0; // Spacing between icon and title
|
||||
}
|
||||
|
||||
// 2. Layout the Title (if present)
|
||||
if (_titleField && [_titleField stringValue] && [[_titleField stringValue] length] > 0)
|
||||
{
|
||||
var titleFrame = [_titleField frame];
|
||||
|
||||
titleFrame.origin.x = x;
|
||||
titleFrame.origin.y = (height - CGRectGetHeight(titleFrame)) / 2.0;
|
||||
|
||||
[_titleField setFrame:titleFrame];
|
||||
|
||||
x = CGRectGetMaxX(titleFrame) + 12.0; // Spacing between title and menu items
|
||||
}
|
||||
|
||||
// 3. Layout the Menu Items
|
||||
var items = [_menu itemArray],
|
||||
index = 0,
|
||||
count = items.length,
|
||||
|
||||
x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView],
|
||||
y = 0.0,
|
||||
count = items ? items.length : 0,
|
||||
isLeftAligned = YES;
|
||||
|
||||
for (; index < count; ++index)
|
||||
@@ -390,11 +421,18 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([item isHidden])
|
||||
// Fix for #1742: If a main menu item does not have a submenu, it should not appear in the menu bar.
|
||||
if ([item isHidden] || ![item submenu])
|
||||
{
|
||||
[[item _menuItemView] setHidden:YES];
|
||||
continue;
|
||||
}
|
||||
|
||||
var menuItemView = [item _menuItemView],
|
||||
frame = [menuItemView frame];
|
||||
var menuItemView = [item _menuItemView];
|
||||
|
||||
[menuItemView setHidden:NO];
|
||||
|
||||
var frame = [menuItemView frame];
|
||||
|
||||
if (isLeftAligned)
|
||||
{
|
||||
@@ -409,21 +447,6 @@
|
||||
x = CGRectGetMinX([menuItemView frame]);
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = [[self contentView] bounds],
|
||||
titleFrame = [_titleField frame];
|
||||
|
||||
if ([_iconImageView isHidden])
|
||||
[_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(titleFrame)) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)];
|
||||
else
|
||||
{
|
||||
var iconFrame = [_iconImageView frame],
|
||||
iconWidth = CGRectGetWidth(iconFrame),
|
||||
totalWidth = iconWidth + CGRectGetWidth(titleFrame);
|
||||
|
||||
[_iconImageView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(iconFrame)) / 2.0)];
|
||||
[_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0 + iconWidth, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)aRect display:(BOOL)shouldDisplay animate:(BOOL)shouldAnimate
|
||||
@@ -464,13 +487,18 @@
|
||||
{
|
||||
var item = items[index];
|
||||
|
||||
if ([item isHidden] || [item isSeparatorItem])
|
||||
if ([item isHidden] || [item isSeparatorItem] || ![item submenu])
|
||||
continue;
|
||||
|
||||
if (CGRectContainsPoint([self rectForItemAtIndex:index], aPoint))
|
||||
return index;
|
||||
}
|
||||
|
||||
// If the mouse is within the menu bar bounds but not over an item
|
||||
// (e.g. dragging far left or right), force the menu to unhighlight.
|
||||
if (CGRectContainsPoint([[self contentView] bounds], aPoint))
|
||||
[_menu _highlightItemAtIndex:CPNotFound];
|
||||
|
||||
return CPNotFound;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
// Do this so that coordinates will be accurate.
|
||||
[menuWindow setFrameOrigin:CGPointMakeZero()];
|
||||
// we need to reset the HUD state as this may be a recycled instance with HUD rund on non-HUD
|
||||
[[menuWindow _windowView] unsetThemeState:CPThemeStateHUD];
|
||||
[[menuWindow _menuView] unsetThemeState:CPThemeStateHUD];
|
||||
}
|
||||
else
|
||||
menuWindow = [[_CPMenuWindow alloc] init];
|
||||
@@ -108,23 +111,33 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
[contentView addSubview:_menuClipView];
|
||||
|
||||
_moreAboveView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[_moreAboveView setImage:[_menuView valueForThemeAttribute:@"menu-window-more-above-image"]];
|
||||
[_moreAboveView setFrameSize:[[_menuView valueForThemeAttribute:@"menu-window-more-above-image"] size]];
|
||||
|
||||
[contentView addSubview:_moreAboveView];
|
||||
|
||||
_moreBelowView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[_moreBelowView setImage:[_menuView valueForThemeAttribute:@"menu-window-more-below-image"]];
|
||||
[_moreBelowView setFrameSize:[[_menuView valueForThemeAttribute:@"menu-window-more-below-image"] size]];
|
||||
|
||||
[contentView addSubview:_moreBelowView];
|
||||
|
||||
// Initial setup using default attributes
|
||||
[self updateScrollArrows];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateScrollArrows
|
||||
{
|
||||
if (!_menuView)
|
||||
return;
|
||||
|
||||
var aboveImage = [_menuView currentValueForThemeAttribute:@"menu-window-more-above-image"],
|
||||
belowImage = [_menuView currentValueForThemeAttribute:@"menu-window-more-below-image"];
|
||||
|
||||
[_moreAboveView setImage:aboveImage];
|
||||
[_moreAboveView setFrameSize:[aboveImage size]];
|
||||
|
||||
[_moreBelowView setImage:belowImage];
|
||||
[_moreBelowView setFrameSize:[belowImage size]];
|
||||
}
|
||||
|
||||
+ (float)_standardLeftMargin
|
||||
{
|
||||
return [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-window-margin-inset" forClass:_CPMenuView].left;
|
||||
@@ -162,7 +175,12 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
- (void)setBackgroundStyle:(_CPMenuWindowBackgroundStyle)aBackgroundStyle
|
||||
{
|
||||
[self setBackgroundColor:[[self class] backgroundColorForBackgroundStyle:aBackgroundStyle]];
|
||||
var color = [_menuView currentValueForThemeAttribute:@"menu-window-pop-up-background-style-color"];
|
||||
|
||||
if (!color)
|
||||
color = [[self class] backgroundColorForBackgroundStyle:aBackgroundStyle];
|
||||
|
||||
[self setBackgroundColor:color];
|
||||
}
|
||||
|
||||
- (void)setMenu:(CPMenu)aMenu
|
||||
@@ -234,6 +252,32 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
// FIXME: This gets called far too often.
|
||||
_unconstrainedFrame = CGRectMakeCopy(aFrame);
|
||||
|
||||
// If we are a submenu and we are being displayed off the right of the screen,
|
||||
// we should try and display on the left of our supermenu.
|
||||
var supermenu = [[self menu] supermenu];
|
||||
|
||||
if (supermenu)
|
||||
{
|
||||
if (CGRectGetMaxX(_unconstrainedFrame) > CGRectGetMaxX(_constraintRect))
|
||||
{
|
||||
var supermenuWindow = supermenu._menuWindow;
|
||||
if (supermenuWindow)
|
||||
{
|
||||
var supermenuFrame = [supermenuWindow frame];
|
||||
_unconstrainedFrame.origin.x = CGRectGetMinX(supermenuFrame) - CGRectGetWidth(_unconstrainedFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// Shift true submenus vertically to fit within the screen if they extend past the bottom
|
||||
if (supermenu !== [CPApp mainMenu] && CGRectGetMaxY(_unconstrainedFrame) > CGRectGetMaxY(_constraintRect))
|
||||
_unconstrainedFrame.origin.y -= CGRectGetMaxY(_unconstrainedFrame) - CGRectGetMaxY(_constraintRect);
|
||||
}
|
||||
|
||||
// Ensure no menu starts above the visible screen area, preventing it from incorrectly
|
||||
// starting scrolled with a top arrow.
|
||||
if (CGRectGetMinY(_unconstrainedFrame) < CGRectGetMinY(_constraintRect))
|
||||
_unconstrainedFrame.origin.y = CGRectGetMinY(_constraintRect);
|
||||
|
||||
var constrainedFrame = CGRectIntersection(_unconstrainedFrame, _constraintRect),
|
||||
marginInset = [_menuView valueForThemeAttribute:@"menu-window-margin-inset"],
|
||||
scrollIndicatorHeight = [_menuView valueForThemeAttribute:@"menu-window-scroll-indicator-height"];
|
||||
@@ -255,7 +299,8 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
[super setFrame:constrainedFrame display:shouldDisplay animate:shouldAnimate];
|
||||
|
||||
// This needs to happen before changing the frame.
|
||||
var menuViewOrigin = CGPointMake(CGRectGetMinX(aFrame) + marginInset.left, CGRectGetMinY(aFrame) + marginInset.top),
|
||||
// Base the view origin on our modified _unconstrainedFrame instead of aFrame
|
||||
var menuViewOrigin = CGPointMake(CGRectGetMinX(_unconstrainedFrame) + marginInset.left, CGRectGetMinY(_unconstrainedFrame) + marginInset.top),
|
||||
moreAbove = menuViewOrigin.y < CGRectGetMinY(constrainedFrame) + marginInset.top,
|
||||
moreBelow = menuViewOrigin.y + CGRectGetHeight([_menuView frame]) > CGRectGetMaxY(constrainedFrame) - marginInset.bottom,
|
||||
|
||||
@@ -420,7 +465,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuWindow (CSSTheming)
|
||||
|
||||
@@ -433,7 +478,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*
|
||||
@ignore
|
||||
@@ -489,6 +534,21 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
[_menuItemViews makeObjectsPerformSelector:@selector(setThemeState:) withObject:aState];
|
||||
|
||||
if ([[self window] respondsToSelector:@selector(updateScrollArrows)])
|
||||
[[self window] updateScrollArrows];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super unsetThemeState:aState];
|
||||
[_menuItemViews makeObjectsPerformSelector:@selector(unsetThemeState:) withObject:aState];
|
||||
}
|
||||
|
||||
- (unsigned)numberOfUnhiddenItems
|
||||
{
|
||||
return _visibleMenuItemInfos.length;
|
||||
@@ -604,7 +664,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuView (CSSTheming)
|
||||
|
||||
|
||||
@@ -826,7 +826,7 @@ CPControlKeyMask
|
||||
return [[self menu] highlightedItem] == self;
|
||||
}
|
||||
|
||||
#pragma mark CPObject Overrides
|
||||
// MARK: CPObject Overrides
|
||||
|
||||
/*!
|
||||
Returns a copy of the item. The copy does not belong If the item has a submenu, it is NOT copied.
|
||||
@@ -867,7 +867,7 @@ CPControlKeyMask
|
||||
return [self copy];
|
||||
}
|
||||
|
||||
#pragma mark Internal
|
||||
// MARK: Internal
|
||||
|
||||
/*
|
||||
@ignore
|
||||
@@ -895,9 +895,20 @@ CPControlKeyMask
|
||||
return [super description] + @" target: " + [self target] + @" action: " + CPStringFromSelector([self action]);
|
||||
}
|
||||
|
||||
- (id)_decodeValueForKey:(CPString)aKey defaultValue:(id)aDefaultValue coder:(CPCoder)aCoder
|
||||
{
|
||||
return [aCoder containsValueForKey:aKey] ? [aCoder decodeObjectForKey:aKey] : aDefaultValue;
|
||||
}
|
||||
|
||||
- (void)_encodeValue:(id)aValue forKey:(CPString)aKey ifNotEqualTo:(id)aDefaultValue coder:(CPCoder)aCoder
|
||||
{
|
||||
if (aValue !== aDefaultValue)
|
||||
[aCoder encodeObject:aValue forKey:aKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPMenuItem (CSSTheming)
|
||||
|
||||
@@ -909,7 +920,7 @@ CPControlKeyMask
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
|
||||
@@ -940,9 +951,6 @@ var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
CPMenuItemRepresentedObjectKey = @"CPMenuItemRepresentedObjectKey",
|
||||
CPMenuItemViewKey = @"CPMenuItemViewKey";
|
||||
|
||||
#define DEFAULT_VALUE(aKey, aDefaultValue) [aCoder containsValueForKey:(aKey)] ? [aCoder decodeObjectForKey:(aKey)] : (aDefaultValue)
|
||||
#define ENCODE_IFNOT(aKey, aValue, aDefaultValue) if ((aValue) !== (aDefaultValue)) [aCoder encodeObject:(aValue) forKey:(aKey)];
|
||||
|
||||
@implementation CPMenuItem (CPCoding)
|
||||
/*!
|
||||
Initializes the menu item from a coder.
|
||||
@@ -965,7 +973,7 @@ var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
_target = [aCoder decodeObjectForKey:CPMenuItemTargetKey];
|
||||
_action = [aCoder decodeObjectForKey:CPMenuItemActionKey];
|
||||
|
||||
_isEnabled = DEFAULT_VALUE(CPMenuItemIsEnabledKey, YES);
|
||||
_isEnabled = [self _decodeValueForKey:CPMenuItemIsEnabledKey defaultValue:YES coder:aCoder];
|
||||
_isHidden = [aCoder decodeBoolForKey:CPMenuItemIsHiddenKey];
|
||||
_tag = [aCoder decodeIntForKey:CPMenuItemTagKey];
|
||||
_state = [aCoder decodeIntForKey:CPMenuItemStateKey];
|
||||
@@ -1012,20 +1020,20 @@ var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
[aCoder encodeObject:_target forKey:CPMenuItemTargetKey];
|
||||
[aCoder encodeObject:_action forKey:CPMenuItemActionKey];
|
||||
|
||||
ENCODE_IFNOT(CPMenuItemIsEnabledKey, _isEnabled, YES);
|
||||
ENCODE_IFNOT(CPMenuItemIsHiddenKey, _isHidden, NO);
|
||||
[self _encodeValue:_isEnabled forKey:CPMenuItemIsEnabledKey ifNotEqualTo:YES coder:aCoder]
|
||||
[self _encodeValue:_isHidden forKey:CPMenuItemIsHiddenKey ifNotEqualTo:NO coder:aCoder]
|
||||
|
||||
ENCODE_IFNOT(CPMenuItemTagKey, _tag, 0);
|
||||
ENCODE_IFNOT(CPMenuItemStateKey, _state, CPOffState);
|
||||
[self _encodeValue:_tag forKey:CPMenuItemTagKey ifNotEqualTo:0 coder:aCoder]
|
||||
[self _encodeValue:_state forKey:CPMenuItemStateKey ifNotEqualTo:CPOffState coder:aCoder]
|
||||
|
||||
ENCODE_IFNOT(CPMenuItemImageKey, _image, nil);
|
||||
ENCODE_IFNOT(CPMenuItemAlternateImageKey, _alternateImage, nil);
|
||||
ENCODE_IFNOT(CPMenuItemOnStateImageKey, _onStateImage, nil);
|
||||
ENCODE_IFNOT(CPMenuItemOffStateImageKey, _offStateImage, nil);
|
||||
ENCODE_IFNOT(CPMenuItemMixedStateImageKey, _mixedStateImage, nil);
|
||||
[self _encodeValue:_image forKey:CPMenuItemImageKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_alternateImage forKey:CPMenuItemAlternateImageKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_onStateImage forKey:CPMenuItemOnStateImageKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_offStateImage forKey:CPMenuItemOffStateImageKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_mixedStateImage forKey:CPMenuItemMixedStateImageKey ifNotEqualTo:nil coder:aCoder]
|
||||
|
||||
ENCODE_IFNOT(CPMenuItemSubmenuKey, _submenu, nil);
|
||||
ENCODE_IFNOT(CPMenuItemMenuKey, _menu, nil);
|
||||
[self _encodeValue:_submenu forKey:CPMenuItemSubmenuKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_menu forKey:CPMenuItemMenuKey ifNotEqualTo:nil coder:aCoder]
|
||||
|
||||
if (_keyEquivalent && _keyEquivalent.length)
|
||||
[aCoder encodeObject:_keyEquivalent forKey:CPMenuItemKeyEquivalentKey];
|
||||
@@ -1036,8 +1044,9 @@ var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
if (_indentationLevel > 0)
|
||||
[aCoder encodeInt:_indentationLevel forKey:CPMenuItemIndentationLevelKey];
|
||||
|
||||
ENCODE_IFNOT(CPMenuItemRepresentedObjectKey, _representedObject, nil);
|
||||
ENCODE_IFNOT(CPMenuItemViewKey, _view, nil);
|
||||
[self _encodeValue:_representedObject forKey:CPMenuItemRepresentedObjectKey ifNotEqualTo:nil coder:aCoder]
|
||||
[self _encodeValue:_view forKey:CPMenuItemViewKey ifNotEqualTo:nil coder:aCoder]
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -145,6 +145,26 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
var oldState = [self themeState];
|
||||
[super setThemeState:aState];
|
||||
// If the state changed (e.g. adding HUD), we must re-run update
|
||||
// to fetch the new text color defined for that state.
|
||||
if (oldState !== [self themeState])
|
||||
[self update];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
var oldState = [self themeState];
|
||||
[super unsetThemeState:aState];
|
||||
|
||||
if (oldState !== [self themeState])
|
||||
[self update];
|
||||
}
|
||||
|
||||
|
||||
- (CPColor)textColor
|
||||
{
|
||||
if (![_menuItem isEnabled])
|
||||
@@ -153,7 +173,7 @@
|
||||
if (_highlighted)
|
||||
return [CPColor whiteColor];
|
||||
|
||||
return [self valueForThemeAttribute:@"menu-item-text-color"];
|
||||
return [self currentValueForThemeAttribute:@"menu-item-text-color"];
|
||||
}
|
||||
|
||||
- (CPColor)textShadowColor
|
||||
@@ -164,7 +184,7 @@
|
||||
if (_highlighted)
|
||||
return nil;
|
||||
|
||||
return [self valueForThemeAttribute:@"menu-item-text-shadow-color"];
|
||||
return [self currentValueForThemeAttribute:@"menu-item-text-shadow-color"];
|
||||
}
|
||||
|
||||
- (void)setFont:(CPFont)aFont
|
||||
@@ -178,6 +198,11 @@
|
||||
return _font || [_menuItem font] || [CPFont systemFontOfSize:CPFontCurrentSystemSize];
|
||||
}
|
||||
|
||||
// override needed to cancel out the standard HUD propagation
|
||||
- (void)viewDidMoveToWindow
|
||||
{
|
||||
}
|
||||
|
||||
// FIXME: update is called 2 times at each display. Find why and fix.
|
||||
- (void)update
|
||||
{
|
||||
@@ -188,6 +213,8 @@
|
||||
|
||||
// When possible, use specific vertical margin/offset value based on font size (which could have been set by control size)
|
||||
correspondingControlSize = [myFont controlSizeCorrespondingToFontSize],
|
||||
controlSizeState = CPControlSizeThemeStates[correspondingControlSize],
|
||||
queryState = [self themeState] ? [self themeState].and(controlSizeState) : controlSizeState,
|
||||
verticalMargin = [self valueForThemeAttribute:@"vertical-margin" inState:CPControlSizeThemeStates[correspondingControlSize]],
|
||||
verticalOffset = [self valueForThemeAttribute:@"vertical-offset" inState:CPControlSizeThemeStates[correspondingControlSize]];
|
||||
|
||||
@@ -199,15 +226,15 @@
|
||||
switch ([_menuItem state])
|
||||
{
|
||||
case CPOnState:
|
||||
[_stateView setImage:[_menuItem onStateImage] || [self valueForThemeAttribute:@"menu-item-default-on-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[self valueForThemeAttribute:@"menu-item-default-on-state-image" inState:queryState] || [_menuItem onStateImage]];
|
||||
break;
|
||||
|
||||
case CPOffState:
|
||||
[_stateView setImage:[_menuItem offStateImage] || [self valueForThemeAttribute:@"menu-item-default-off-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[self valueForThemeAttribute:@"menu-item-default-off-state-image" inState:queryState] || [_menuItem offStateImage]];
|
||||
break;
|
||||
|
||||
case CPMixedState:
|
||||
[_stateView setImage:[_menuItem mixedStateImage] || [self valueForThemeAttribute:@"menu-item-default-mixed-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[self valueForThemeAttribute:@"menu-item-default-mixed-state-image" inState:queryState] || [_menuItem mixedStateImage]];
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -326,7 +353,10 @@
|
||||
|
||||
_highlighted = shouldHighlight;
|
||||
|
||||
var correspondingControlSize = [[self font] controlSizeCorrespondingToFontSize];
|
||||
var correspondingControlSize = [[self font] controlSizeCorrespondingToFontSize],
|
||||
// Construct the query state including the view's current theme state (e.g. HUD)
|
||||
controlSizeState = CPControlSizeThemeStates[correspondingControlSize],
|
||||
queryState = [self themeState] ? [self themeState].and(controlSizeState) : controlSizeState;
|
||||
|
||||
[_imageAndTextView setTextColor:[self textColor]];
|
||||
[_keyEquivalentView setTextColor:[self textColor]];
|
||||
@@ -339,7 +369,7 @@
|
||||
[_imageAndTextView setImage:[_menuItem alternateImage] || [_menuItem image]];
|
||||
|
||||
if (_hasSubmenuIndicatorImage)
|
||||
[_submenuIndicatorView setImage:[self valueForThemeAttribute:@"submenu-indicator-highlighted-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_submenuIndicatorView setImage:[self valueForThemeAttribute:@"submenu-indicator-highlighted-image" inState:queryState]];
|
||||
else
|
||||
[_submenuIndicatorView setColor:[self textColor]];
|
||||
}
|
||||
@@ -349,7 +379,7 @@
|
||||
[_imageAndTextView setImage:[_menuItem image]];
|
||||
|
||||
if (_hasSubmenuIndicatorImage)
|
||||
[_submenuIndicatorView setImage:[self valueForThemeAttribute:@"submenu-indicator-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_submenuIndicatorView setImage:[self valueForThemeAttribute:@"submenu-indicator-image" inState:queryState]];
|
||||
else
|
||||
[_submenuIndicatorView setColor:[self valueForThemeAttribute:@"submenu-indicator-color"]];
|
||||
}
|
||||
@@ -361,15 +391,15 @@
|
||||
switch ([_menuItem state])
|
||||
{
|
||||
case CPOnState:
|
||||
[_stateView setImage:[_menuItem onStateImage] || [self valueForThemeAttribute:@"menu-item-default-on-state-highlighted-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem onStateImage] || [self valueForThemeAttribute:@"menu-item-default-on-state-highlighted-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
case CPOffState:
|
||||
[_stateView setImage:[_menuItem offStateImage] || [self valueForThemeAttribute:@"menu-item-default-off-state-highlighted-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem offStateImage] || [self valueForThemeAttribute:@"menu-item-default-off-state-highlighted-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
case CPMixedState:
|
||||
[_stateView setImage:[_menuItem mixedImage] || [self valueForThemeAttribute:@"menu-item-default-mixed-state-highlighted-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem mixedImage] || [self valueForThemeAttribute:@"menu-item-default-mixed-state-highlighted-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -381,15 +411,15 @@
|
||||
switch ([_menuItem state])
|
||||
{
|
||||
case CPOnState:
|
||||
[_stateView setImage:[_menuItem onStateImage] || [self valueForThemeAttribute:@"menu-item-default-on-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem onStateImage] || [self valueForThemeAttribute:@"menu-item-default-on-state-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
case CPOffState:
|
||||
[_stateView setImage:[_menuItem offStateImage] || [self valueForThemeAttribute:@"menu-item-default-off-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem offStateImage] || [self valueForThemeAttribute:@"menu-item-default-off-state-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
case CPMixedState:
|
||||
[_stateView setImage:[_menuItem mixedImage] || [self valueForThemeAttribute:@"menu-item-default-mixed-state-image" inState:CPControlSizeThemeStates[correspondingControlSize]]];
|
||||
[_stateView setImage:[_menuItem mixedImage] || [self valueForThemeAttribute:@"menu-item-default-mixed-state-image" inState:queryState]];
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -406,11 +436,11 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemStandardView (CSSTheming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -420,7 +450,7 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemSubmenuIndicatorView : CPView
|
||||
{
|
||||
|
||||
@@ -83,6 +83,28 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
// override needed to cancel out the standard HUD propagation
|
||||
- (void)viewDidMoveToWindow
|
||||
{
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
|
||||
// Propagate state to the actual content view (StandardView, Separator, etc.)
|
||||
if ([_view respondsToSelector:@selector(setThemeState:)])
|
||||
[_view setThemeState:aState];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super unsetThemeState:aState];
|
||||
|
||||
if ([_view respondsToSelector:@selector(unsetThemeState:)])
|
||||
[_view unsetThemeState:aState];
|
||||
}
|
||||
|
||||
- (CGSize)minSize
|
||||
{
|
||||
return _minSize;
|
||||
@@ -252,11 +274,11 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemView (CSSTheming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -275,7 +297,7 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemArrowView : CPView
|
||||
{
|
||||
|
||||
@@ -808,7 +808,8 @@ var CPObjectControllerContentKey = @"CPObjectControllerCo
|
||||
var value = [self _controllerMarkerForValues:values];
|
||||
[_cachedValues setObject:value forKey:theKeyPath];
|
||||
|
||||
return value;
|
||||
// Apple's implementation returns nil instead of CPNullMarker
|
||||
return value === CPNullMarker ? nil : value;
|
||||
}
|
||||
else
|
||||
return values;
|
||||
@@ -948,4 +949,4 @@ var CPManagedProxyEntityNameKey = @"CPManagedProxyEntity
|
||||
[aCoder encodeObject:[self fetchPredicate] forKey:CPManagedProxyFetchPredicateKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
+1052
-148
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -121,8 +121,8 @@ CPDocModalWindowMask = 1 << 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
|
||||
@@ -45,6 +45,7 @@ CPURLPboardType = @"CPURLPboardType";
|
||||
CPImagesPboardType = @"CPImagesPboardType";
|
||||
CPVideosPboardType = @"CPVideosPboardType";
|
||||
CPRTFPboardType = @"CPRTFPboardType";
|
||||
CPRulerPboardType = @"CPRulerPboardType";
|
||||
_CPSmartPboardType = @"_CPSmartPboardType";
|
||||
_CPASPboardType = @"_CPASPboardType";
|
||||
|
||||
@@ -323,8 +324,6 @@ var CPPasteboards = nil;
|
||||
|
||||
@end
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
var DOMDataTransferPasteboard = nil;
|
||||
|
||||
@implementation _CPDOMDataTransferPasteboard : CPPasteboard
|
||||
@@ -388,4 +387,3 @@ var DOMDataTransferPasteboard = nil;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -79,6 +79,8 @@ CPPopUpButtonStatePullsDown = CPThemeState("pulls-down");
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self setBezelStyle:CPRoundedBezelStyle];
|
||||
|
||||
[self selectItemAtIndex:CPNotFound];
|
||||
|
||||
_preferredEdge = CPMaxYEdge;
|
||||
@@ -689,6 +691,12 @@ CPPopUpButtonStatePullsDown = CPThemeState("pulls-down");
|
||||
// FIXME: setFont: should set the font on the menu.
|
||||
[menu setFont:[self font]];
|
||||
|
||||
// Propagate the HUD theme state to the menu
|
||||
if ([self hasThemeState:CPThemeStateHUD])
|
||||
[menu setThemeState:CPThemeStateHUD];
|
||||
else
|
||||
[menu unsetThemeState:CPThemeStateHUD];
|
||||
|
||||
if ([self pullsDown])
|
||||
{
|
||||
var positionedItem = nil,
|
||||
|
||||
+10
-10
@@ -84,8 +84,8 @@ var CPPopoverDelegate_popover_willShow_ = 1 << 0,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*!
|
||||
Initialize the CPPopover witn default values
|
||||
@@ -105,8 +105,8 @@ var CPPopoverDelegate_popover_willShow_ = 1 << 0,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
/*!
|
||||
Returns the current rect of the popover
|
||||
@@ -220,8 +220,8 @@ Set the behavior of the CPPopover. It can be:
|
||||
_implementedDelegateMethods |= CPPopoverDelegate_popover_didClose_;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Positioning
|
||||
// MARK: -
|
||||
// MARK: Positioning
|
||||
|
||||
/*!
|
||||
Show the popover
|
||||
@@ -298,8 +298,8 @@ Set the behavior of the CPPopover. It can be:
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Action
|
||||
// MARK: -
|
||||
// MARK: Action
|
||||
|
||||
/*!
|
||||
Close the popover
|
||||
@@ -318,8 +318,8 @@ Set the behavior of the CPPopover. It can be:
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegates
|
||||
// MARK: -
|
||||
// MARK: Delegates
|
||||
|
||||
/*! @ignore */
|
||||
- (BOOL)_popoverWindowShouldClose
|
||||
|
||||
+121
-33
@@ -43,7 +43,6 @@ CPProgressIndicatorSpinningStyle = 1;
|
||||
*/
|
||||
CPProgressIndicatorHUDBarStyle = 2;
|
||||
|
||||
var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@@ -71,6 +70,43 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
BOOL _isDisplayedWhenStopped;
|
||||
}
|
||||
|
||||
// Inject CSS Keyframes for spinning animation (Standard + WebKit)
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self !== [CPProgressIndicator class])
|
||||
return;
|
||||
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
if (document.getElementById("cp-progress-indicator-style"))
|
||||
return;
|
||||
|
||||
var style = document.createElement("style");
|
||||
style.id = "cp-progress-indicator-style";
|
||||
style.type = "text/css";
|
||||
|
||||
// We define two animations:
|
||||
// 1. cp-progress-indicator-spin: Rotates 360 degrees (for spinners)
|
||||
// 2. cp-progress-indicator-bar-slide: Moves background-position (for striped bars)
|
||||
var css =
|
||||
"@keyframes cp-progress-indicator-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } " +
|
||||
"@-webkit-keyframes cp-progress-indicator-spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } " +
|
||||
|
||||
"@keyframes cp-progress-indicator-bar-slide { 0% { background-position: 0 0; } 100% { background-position: 30px 0; } } " +
|
||||
"@-webkit-keyframes cp-progress-indicator-bar-slide { 0% { background-position: 0 0; } 100% { background-position: 30px 0; } }";
|
||||
|
||||
if (style.styleSheet)
|
||||
style.styleSheet.cssText = css;
|
||||
else
|
||||
style.appendChild(document.createTextNode(css));
|
||||
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
if (head)
|
||||
head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
return @"progress-indicator";
|
||||
@@ -83,12 +119,19 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
@"bar-color": [CPNull null],
|
||||
@"default-height": 20,
|
||||
@"bezel-color": [CPNull null],
|
||||
@"spinning-mini-gif": [CPNull null],
|
||||
@"spinning-small-gif": [CPNull null],
|
||||
@"spinning-regular-gif": [CPNull null],
|
||||
|
||||
// CSS Spinner Attributes
|
||||
@"spinner-color": [CPColor grayColor],
|
||||
@"spinner-track-color": [CPColor colorWithWhite:0.9 alpha:1.0],
|
||||
@"spinner-line-width": 3.0,
|
||||
|
||||
@"circular-border-color": [CPNull null],
|
||||
@"circular-border-size": 1,
|
||||
@"circular-color": [CPNull null]
|
||||
@"circular-color": [CPNull null],
|
||||
|
||||
@"spinning-mini-gif": [CPNull null],
|
||||
@"spinning-small-gif": [CPNull null],
|
||||
@"spinning-regular-gif": [CPNull null]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,6 +181,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
_isAnimating = YES;
|
||||
|
||||
[self _hideOrDisplay];
|
||||
[self setNeedsLayout]; // Trigger layout to update CSS animation state
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -149,6 +193,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
_isAnimating = NO;
|
||||
|
||||
[self _hideOrDisplay];
|
||||
[self setNeedsLayout]; // Trigger layout to remove CSS animation state
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -233,7 +278,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
_controlSize = aControlSize;
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -285,7 +330,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
_indeterminate = indeterminate;
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -307,9 +352,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
_style = aStyle;
|
||||
|
||||
[self setTheme:(_style === CPProgressIndicatorHUDBarStyle) ? [CPTheme defaultHudTheme] : [CPTheme defaultTheme]];
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -318,7 +361,14 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
- (void)sizeToFit
|
||||
{
|
||||
if (_style == CPProgressIndicatorSpinningStyle)
|
||||
[self setFrameSize:[[CPProgressIndicatorSpinningStyleColors[_controlSize] patternImage] size]];
|
||||
{
|
||||
var size = 32.0;
|
||||
if (_controlSize === CPMiniControlSize) size = 16.0;
|
||||
else if (_controlSize === CPSmallControlSize) size = 24.0;
|
||||
else if (_controlSize === CPRegularControlSize) size = 32.0;
|
||||
|
||||
[self setFrameSize:CGSizeMake(size, size)];
|
||||
}
|
||||
else
|
||||
[self setFrameSize:CGSizeMake(CGRectGetWidth([self frame]), [self valueForThemeAttribute:@"default-height"])];
|
||||
}
|
||||
@@ -381,6 +431,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aViewName
|
||||
{
|
||||
// Handle the standard Bar View
|
||||
if (aViewName === @"bar-view" && _style !== CPProgressIndicatorSpinningStyle)
|
||||
{
|
||||
var width = CGRectGetWidth([self bounds]),
|
||||
@@ -394,41 +445,78 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
return CGRectMake(0, 0, barWidth, [self valueForThemeAttribute:@"default-height"]);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)updateBackgroundColor
|
||||
{
|
||||
if ([CPProgressIndicatorSpinningStyleColors count] === 0)
|
||||
|
||||
// Handle the Spinning View (CSS Spinner)
|
||||
if (aViewName === @"spinner-view" && _style == CPProgressIndicatorSpinningStyle && _indeterminate)
|
||||
{
|
||||
CPProgressIndicatorSpinningStyleColors[CPMiniControlSize] = [self valueForThemeAttribute:@"spinning-mini-gif"];
|
||||
CPProgressIndicatorSpinningStyleColors[CPSmallControlSize] = [self valueForThemeAttribute:@"spinning-small-gif"];
|
||||
CPProgressIndicatorSpinningStyleColors[CPRegularControlSize] = [self valueForThemeAttribute:@"spinning-regular-gif"];
|
||||
return [self bounds];
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
// Return nil for views that shouldn't appear in the current style
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
if (YES)//_isBezeled)
|
||||
{
|
||||
// === SPINNING STYLE ===
|
||||
if (_style == CPProgressIndicatorSpinningStyle)
|
||||
{
|
||||
if (!_indeterminate)
|
||||
return;
|
||||
// If indeterminate, use CSS spinner
|
||||
if (_indeterminate)
|
||||
{
|
||||
var spinnerView = [self layoutEphemeralSubviewNamed:"spinner-view"
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
// Ensure other views are hidden
|
||||
[self layoutEphemeralSubviewNamed:"bar-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
// This will cause the bar view to go away due to having a nil rect when _style == CPProgressIndicatorSpinningStyle.
|
||||
[self layoutEphemeralSubviewNamed:"bar-view"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
// Configure CSS on the subview, NOT self._DOMElement, to avoid transform conflicts
|
||||
var domEl = spinnerView._DOMElement,
|
||||
spinnerColor = [self currentValueForThemeAttribute:@"spinner-color"],
|
||||
trackColor = [self currentValueForThemeAttribute:@"spinner-track-color"],
|
||||
lineWidth = [self currentValueForThemeAttribute:@"spinner-line-width"],
|
||||
widthStr = lineWidth + "px";
|
||||
|
||||
[self setBackgroundColor:CPProgressIndicatorSpinningStyleColors[_controlSize]];
|
||||
domEl.style.boxSizing = "border-box";
|
||||
domEl.style.borderRadius = "50%";
|
||||
domEl.style.borderStyle = "solid";
|
||||
domEl.style.borderWidth = widthStr;
|
||||
domEl.style.borderColor = [trackColor cssString];
|
||||
domEl.style.borderTopColor = [spinnerColor cssString];
|
||||
|
||||
// Animation logic
|
||||
if (_isAnimating)
|
||||
{
|
||||
var anim = "cp-progress-indicator-spin 1s linear infinite";
|
||||
domEl.style.animation = anim;
|
||||
domEl.style.WebkitAnimation = anim;
|
||||
}
|
||||
else
|
||||
{
|
||||
domEl.style.animation = "none";
|
||||
domEl.style.WebkitAnimation = "none";
|
||||
}
|
||||
|
||||
// Ensure main view is transparent
|
||||
[self setBackgroundColor:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Determinate Spinner (Pie Chart drawn in drawRect)
|
||||
// Hide CSS spinner
|
||||
[self layoutEphemeralSubviewNamed:"spinner-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil];
|
||||
[self setBackgroundColor:nil];
|
||||
}
|
||||
}
|
||||
// === BAR STYLE ===
|
||||
else
|
||||
{
|
||||
// Hide spinner
|
||||
[self layoutEphemeralSubviewNamed:"spinner-view" positioned:CPWindowBelow relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[self setBackgroundColor:[self currentValueForThemeAttribute:@"bezel-color"]];
|
||||
|
||||
var barView = [self layoutEphemeralSubviewNamed:"bar-view"
|
||||
@@ -447,6 +535,8 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
// Handle determinate state for Spinning style (Pie chart progress) via CoreGraphics.
|
||||
// If indeterminate, the CSS animation (spinner-view) takes over and we draw nothing.
|
||||
if (_style == CPProgressIndicatorSpinningStyle && !_indeterminate)
|
||||
{
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort],
|
||||
@@ -512,7 +602,7 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
_isDisplayedWhenStoppedSet = [aCoder decodeObjectForKey:@"_isDisplayedWhenStoppedSet"];
|
||||
_isDisplayedWhenStopped = [aCoder decodeObjectForKey:@"_isDisplayedWhenStopped"];
|
||||
|
||||
[self updateBackgroundColor];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -520,8 +610,6 @@ var CPProgressIndicatorSpinningStyleColors = [];
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
// Don't encode the background colour. It can be recreated based on the flags
|
||||
// and if encoded causes hardcoded image paths in the cib while just wasting space.
|
||||
var backgroundColor = [self backgroundColor];
|
||||
[self setBackgroundColor:nil];
|
||||
[super encodeWithCoder:aCoder];
|
||||
|
||||
+4
-4
@@ -187,7 +187,7 @@ CPRadioImageOffset = 4.0;
|
||||
[self _setRadioGroup];
|
||||
}
|
||||
|
||||
#pragma mark Private methods
|
||||
// MARK: Private methods
|
||||
|
||||
- (void)_setRadioGroup
|
||||
{
|
||||
@@ -260,8 +260,8 @@ var CPRadioRadioGroupKey = @"CPRadioRadioGroupKey";
|
||||
[aCoder encodeObject:_radioGroup forKey:CPRadioRadioGroupKey];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override methods from CPButton
|
||||
// MARK: -
|
||||
// MARK: Override methods from CPButton
|
||||
|
||||
- (CPThemeState)_contentVisualState
|
||||
{
|
||||
@@ -412,7 +412,7 @@ var CPRadioRadioGroupKey = @"CPRadioRadioGroupKey";
|
||||
[_radios makeObjectsPerformSelector:@selector(setHidden:) withObject:hidden];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
// MARK: Private
|
||||
|
||||
- (void)_addRadio:(CPRadio)aRadio
|
||||
{
|
||||
|
||||
@@ -290,6 +290,16 @@ CPDeleteForwardKeyCode = 46;
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
@ignore
|
||||
Returns YES if the receiver handles the Tab key itself, as an editable text view does by
|
||||
inserting a tab character. Otherwise the window uses Tab to move to the next key view.
|
||||
*/
|
||||
- (BOOL)_wantsTabKey
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)insertBackTab:(id)sender
|
||||
{
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
SEL _predicateAction @accessors(property=action);
|
||||
}
|
||||
|
||||
#pragma mark public methods
|
||||
// MARK: public methods
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPPredicateEditor
|
||||
@@ -251,7 +251,7 @@
|
||||
return tree;
|
||||
}
|
||||
|
||||
#pragma mark Set the Predicate
|
||||
// MARK: Set the Predicate
|
||||
|
||||
- (void)setObjectValue:(id)objectValue
|
||||
{
|
||||
@@ -269,6 +269,8 @@
|
||||
_currentAnimation = nil;
|
||||
_sendAction = NO;
|
||||
|
||||
var rows = [];
|
||||
|
||||
if (predicate != nil)
|
||||
{
|
||||
if ((_nestingMode == CPRuleEditorNestingModeSimple || _nestingMode == CPRuleEditorNestingModeCompound)
|
||||
@@ -276,10 +278,13 @@
|
||||
predicate = [[CPCompoundPredicate alloc] initWithType:[self _compoundPredicateTypeForRootRows] subpredicates:[CPArray arrayWithObject:predicate]];
|
||||
|
||||
var row = [self _rowObjectFromPredicate:predicate];
|
||||
|
||||
if (row != nil)
|
||||
[_boundArrayOwner setValue:[CPArray arrayWithObject:row] forKey:_boundArrayKeyPath];
|
||||
[rows addObject:row];
|
||||
}
|
||||
|
||||
[_boundArrayOwner setValue:rows forKey:_boundArrayKeyPath];
|
||||
|
||||
[self setAnimation:animation];
|
||||
}
|
||||
|
||||
@@ -369,7 +374,7 @@
|
||||
return row;
|
||||
}
|
||||
|
||||
#pragma mark Get the predicate
|
||||
// MARK: Get the predicate
|
||||
|
||||
- (void)_updatePredicate
|
||||
{
|
||||
@@ -447,7 +452,7 @@
|
||||
return CPAndPredicateType;
|
||||
}
|
||||
|
||||
#pragma mark Control delegate
|
||||
// MARK: Control delegate
|
||||
|
||||
- (void)_sendRuleAction
|
||||
{
|
||||
@@ -482,7 +487,7 @@
|
||||
}
|
||||
*/
|
||||
|
||||
#pragma mark RuleEditor delegate methods
|
||||
// MARK: RuleEditor delegate methods
|
||||
|
||||
- (int)_queryNumberOfChildrenOfItem:(id)rowItem withRowType:(CPRuleEditorRowType)type
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
@import "_CPRuleEditorViewSliceRow.j"
|
||||
@import "_CPRuleEditorLocalizer.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPCompoundPredicate
|
||||
@class CPComparisonPredicate
|
||||
@@ -80,6 +81,35 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
@n An ordered to-many relation containing the display values for the row.
|
||||
@n@n @c @@"criteria"
|
||||
@n An ordered to-many relation containing the criteria for the row.
|
||||
@n@n
|
||||
Localization & Positional Reordering
|
||||
@n
|
||||
CPRuleEditor supports complete localization of menu items and grammatical positional reordering (sentence structure layout adjustment) via strings resource files (.strings) or custom programmatic CPDictionary tables.
|
||||
@n@n
|
||||
Since sentence structures vary significantly across languages, the editor can dynamically reposition views (such as popups, static labels, and text fields) from left to right to form grammatically correct sentences.
|
||||
@n@n
|
||||
Formatting Keys (English representation):
|
||||
@n @c %[%]@@
|
||||
@n Represents a popup button displaying its selected value (e.g. @c %[firstName]@@).
|
||||
@n @c %@@
|
||||
@n Represents an editable text input field.
|
||||
@n Static text represents a literal label placed directly inside the formatting key.
|
||||
@n@n
|
||||
Example English format key:
|
||||
@n @c "%[firstName]@ %[is equal to]@ %@"
|
||||
@n@n
|
||||
Translation Patterns (Target language):
|
||||
@n Positional specifiers such as @c %1$@@, @c %2$@@, @c %3$@@ dictate the visual order of views from left to right.
|
||||
@n Bracketed values inside positional specifiers (e.g. @c %1$[Nombre]@@) define the localized title for popup selection items.
|
||||
@n Literal text outside the specifiers (such as @c "y" or @c "und") is automatically instantiated as static text labels positioned between controls.
|
||||
@n@n
|
||||
Example translations:
|
||||
@n@n
|
||||
Spanish (Reorders to: [1: Name] y [3: Value] [2: are equal]):
|
||||
@n @c "%[firstName]@ %[is equal to]@ %@" = "%1$[Nombre]@ y %3$@ %2$[son iguales]@";
|
||||
@n@n
|
||||
German (Reorders to: [1: First Name] und [3: Value] [2: are equal]):
|
||||
@n @c "%[firstName]@ %[is equal to]@ %@" = "%1$[Vorname]@ und %3$@ %2$[sind gleich]@";
|
||||
*/
|
||||
|
||||
@implementation CPRuleEditor : CPControl
|
||||
@@ -127,7 +157,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
BOOL _isKeyDown;
|
||||
BOOL _nestingModeDidChange;
|
||||
|
||||
_CPRuleEditorLocalizer _standardLocalizer @accessors(property=standardLocalizer);
|
||||
_CPRuleEditorLocalizer _standardLocalizer;
|
||||
CPDictionary _itemsAndValuesToAddForRowType;
|
||||
}
|
||||
|
||||
@@ -207,8 +237,34 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
|
||||
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPRuleEditorItemPBoardType,nil]];
|
||||
[_boundArrayOwner addObserver:self forKeyPath:_boundArrayKeyPath options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:boundArrayContext];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(_ruleEditorLocalizerDidLoad:)
|
||||
name:@"_CPRuleEditorLocalizerDidLoadNotification"
|
||||
object:nil];
|
||||
}
|
||||
|
||||
|
||||
- (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification
|
||||
{
|
||||
if ([aNotification object] === [self standardLocalizer])
|
||||
{
|
||||
// Defer execution to the next run loop cycle so that any active slice
|
||||
// insertions have fully completed and are present in the `_slices` array.
|
||||
[[CPRunLoop mainRunLoop] performBlock:function() {
|
||||
var count = [_slices count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var slice = [_slices objectAtIndex:i];
|
||||
[slice _reconfigureSubviews];
|
||||
[slice _updateButtonVisibilities]; // Force updates on row button tooltips
|
||||
}
|
||||
|
||||
[self _updatePredicate];
|
||||
[self _sendRuleAction];
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
}
|
||||
}
|
||||
/*! @endcond */
|
||||
|
||||
/*!
|
||||
@@ -384,7 +440,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
*/
|
||||
- (CPDictionary)formattingDictionary
|
||||
{
|
||||
return [_standardLocalizer dictionary];
|
||||
return [[self standardLocalizer] dictionary];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -396,6 +452,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
*/
|
||||
- (void)setFormattingDictionary:(CPDictionary)dictionary
|
||||
{
|
||||
if (_standardLocalizer == nil)
|
||||
_standardLocalizer = [_CPRuleEditorLocalizer new];
|
||||
|
||||
[_standardLocalizer setDictionary:dictionary];
|
||||
_stringsFilename = nil;
|
||||
}
|
||||
@@ -440,6 +499,19 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
}
|
||||
}
|
||||
|
||||
- (_CPRuleEditorLocalizer)standardLocalizer
|
||||
{
|
||||
if (_standardLocalizer == nil)
|
||||
_standardLocalizer = [_CPRuleEditorLocalizer new];
|
||||
|
||||
return _standardLocalizer;
|
||||
}
|
||||
|
||||
- (void)setStandardLocalizer:(_CPRuleEditorLocalizer)aLocalizer
|
||||
{
|
||||
_standardLocalizer = aLocalizer;
|
||||
}
|
||||
|
||||
/*!
|
||||
@name Providing Data
|
||||
*/
|
||||
@@ -538,7 +610,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
if ([self rowTypeForRow:current_index] === CPRuleEditorRowTypeCompound)
|
||||
{
|
||||
var candidate = [[self _rowCacheForIndex:current_index] rowObject],
|
||||
subObjects = [[self _subrowObjectsOfObject:candidate] _representedObject];
|
||||
subObjects = [self _subrowObjectsOfObject:candidate]; // Standard direct array query
|
||||
|
||||
if ([subObjects indexOfObjectIdenticalTo:targetObject] !== CPNotFound)
|
||||
return current_index;
|
||||
@@ -605,7 +677,7 @@ TODO: implement
|
||||
for (var i = rowIndex + 1; i < count; i++)
|
||||
{
|
||||
var candidate = [[self _rowCacheForIndex:i] rowObject],
|
||||
indexInSubrows = [[subobjects _representedObject] indexOfObjectIdenticalTo:candidate];
|
||||
indexInSubrows = [subobjects indexOfObjectIdenticalTo:candidate]; // Standard direct array query
|
||||
|
||||
if (indexInSubrows !== CPNotFound)
|
||||
{
|
||||
@@ -774,7 +846,7 @@ TODO: implement
|
||||
while (current_index !== CPNotFound)
|
||||
{
|
||||
var rowObject = [[self _rowCacheForIndex:current_index] rowObject],
|
||||
relativeChildIndex = [[subrows _representedObject] indexOfObjectIdenticalTo:rowObject];
|
||||
relativeChildIndex = [subrows indexOfObjectIdenticalTo:rowObject]; // Standard direct array query
|
||||
|
||||
if (relativeChildIndex !== CPNotFound)
|
||||
[childsIndexes addIndex:relativeChildIndex];
|
||||
@@ -831,7 +903,6 @@ TODO: implement
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
var item = [items objectAtIndex:i],
|
||||
//var displayValue = [self _queryValueForItem:item inRow:aRow]; Ask the delegate or get cached value ?.
|
||||
displayValue = [[self displayValuesForRow:aRow] objectAtIndex:i],
|
||||
predpart = [self _sendDelegateRuleEditorPredicatePartsForCriterion:item withDisplayValue:displayValue inRow:aRow];
|
||||
|
||||
@@ -849,6 +920,7 @@ TODO: implement
|
||||
return nil;
|
||||
|
||||
var current_index = [subrowsIndexes firstIndex];
|
||||
|
||||
while (current_index !== CPNotFound)
|
||||
{
|
||||
var subpredicate = [self predicateForRow:current_index];
|
||||
@@ -1233,7 +1305,7 @@ TODO: implement
|
||||
return shouldHide;
|
||||
}
|
||||
|
||||
#pragma mark Rows management
|
||||
// MARK: Rows management
|
||||
|
||||
- (id)_rowCacheForIndex:(int)index
|
||||
{
|
||||
@@ -1278,23 +1350,28 @@ TODO: implement
|
||||
|
||||
while (current_index !== CPNotFound)
|
||||
{
|
||||
var parentIndex = [self parentRowForRow:current_index],
|
||||
subrowsIndexes = [self subrowIndexesForRow:parentIndex];
|
||||
|
||||
if ([subrowsIndexes count] === 1)
|
||||
var parentIndex = [self parentRowForRow:current_index];
|
||||
|
||||
// If the row has a valid parent in the editor (i.e. not a root row)
|
||||
if (parentIndex !== -1)
|
||||
{
|
||||
if (parentIndex !== -1)
|
||||
return [CPIndexSet indexSetWithIndex:0];
|
||||
var subrowsIndexes = [self subrowIndexesForRow:parentIndex];
|
||||
|
||||
var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]];
|
||||
[childlessParents addIndexes:childlessGranPa];
|
||||
// If deleting this row leaves the parent with no remaining child rows
|
||||
if ([subrowsIndexes count] === 1)
|
||||
{
|
||||
[childlessParents addIndex:parentIndex];
|
||||
|
||||
// Recursively check if deleting this parent row leaves the grandparent childless
|
||||
var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]];
|
||||
[childlessParents addIndexes:childlessGranPa];
|
||||
}
|
||||
}
|
||||
|
||||
current_index = [indexes indexGreaterThanIndex:current_index];
|
||||
}
|
||||
|
||||
return childlessParents;
|
||||
// (id)-[RuleEditor _includeSubslicesForSlicesAtIndexes:]
|
||||
}
|
||||
|
||||
- (CPIndexSet)_includeSubslicesForSlicesAtIndexes:(CPIndexSet)indexes
|
||||
@@ -1356,8 +1433,25 @@ TODO: implement
|
||||
|
||||
if ([self rowTypeForRow:row] === type && itemIndex < [aCriteria count])
|
||||
{
|
||||
var crit = [aCriteria objectAtIndex:itemIndex];
|
||||
[current_criterions addObject:crit];
|
||||
// Verify that this row's parent path matches the path currently being built
|
||||
var pathMatches = true;
|
||||
for (var p = 0; p < itemIndex; p++)
|
||||
{
|
||||
var criterionA = [aCriteria objectAtIndex:p],
|
||||
criterionB = [items objectAtIndex:p];
|
||||
|
||||
if (criterionA !== criterionB && (typeof criterionA.isEqual !== "function" || ![criterionA isEqual:criterionB]))
|
||||
{
|
||||
pathMatches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathMatches)
|
||||
{
|
||||
var crit = [aCriteria objectAtIndex:itemIndex];
|
||||
[current_criterions addObject:crit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1473,7 +1567,7 @@ TODO: implement
|
||||
return row;
|
||||
}
|
||||
|
||||
#pragma mark Key value observing
|
||||
// MARK: Key value observing
|
||||
|
||||
- (void)_startObservingRowObjectsRecursively:(CPArray)rowObjects
|
||||
{
|
||||
@@ -1774,7 +1868,12 @@ TODO: implement
|
||||
|
||||
- (_CPRuleEditorViewSliceRow)_createNewSliceWithFrame:(CGRect)frame ruleEditorView:(CPRuleEditor)editor
|
||||
{
|
||||
return [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor];
|
||||
var slice = [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor];
|
||||
|
||||
// Ensure the slice resizes with the editor
|
||||
[slice setAutoresizingMask:CPViewWidthSizable];
|
||||
|
||||
return slice;
|
||||
}
|
||||
|
||||
- (void)_reconfigureSubviewsAnimate:(BOOL)animate
|
||||
@@ -1859,57 +1958,63 @@ TODO: implement
|
||||
|
||||
- (CPArray)_backgroundColors
|
||||
{
|
||||
return [self valueForThemeAttribute:@"alternating-row-colors"];
|
||||
return [self currentValueForThemeAttribute:@"alternating-row-colors"];
|
||||
}
|
||||
|
||||
- (CPColor)_selectedRowColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"selected-color"];
|
||||
return [self currentValueForThemeAttribute:@"selected-color"];
|
||||
}
|
||||
|
||||
- (CPColor)_sliceTopBorderColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"slice-top-border-color"];
|
||||
return [self currentValueForThemeAttribute:@"slice-top-border-color"];
|
||||
}
|
||||
|
||||
- (CPColor)_sliceBottomBorderColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"slice-bottom-border-color"];
|
||||
return [self currentValueForThemeAttribute:@"slice-bottom-border-color"];
|
||||
}
|
||||
|
||||
- (CPColor)_sliceLastBottomBorderColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"slice-last-bottom-border-color"];
|
||||
return [self currentValueForThemeAttribute:@"slice-last-bottom-border-color"];
|
||||
}
|
||||
|
||||
- (CPFont)font
|
||||
{
|
||||
return [self valueForThemeAttribute:@"font"];
|
||||
return [self currentValueForThemeAttribute:@"font"];
|
||||
}
|
||||
|
||||
- (CPColor)_fontColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"font-color"];
|
||||
return [self currentValueForThemeAttribute:@"font-color"];
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
[_slices makeObjectsPerformSelector:@selector(setThemeState:) withObject:aState];
|
||||
}
|
||||
|
||||
- (CPImage)_imageAdd
|
||||
{
|
||||
return [self valueForThemeAttribute:@"add-image" inState:CPThemeStateNormal];
|
||||
return [self valueForThemeAttribute:@"add-image" inState:CPThemeStateNormal.and([self themeState])];
|
||||
}
|
||||
|
||||
- (CPImage)_imageAddHighlighted
|
||||
{
|
||||
return [self valueForThemeAttribute:@"add-image" inState:CPThemeStateHighlighted];
|
||||
return [self valueForThemeAttribute:@"add-image" inState:CPThemeStateHighlighted.and([self themeState])];
|
||||
}
|
||||
|
||||
- (CPImage)_imageRemove
|
||||
{
|
||||
return [self valueForThemeAttribute:@"remove-image" inState:CPThemeStateNormal];
|
||||
return [self valueForThemeAttribute:@"remove-image" inState:CPThemeStateNormal.and([self themeState])];
|
||||
}
|
||||
|
||||
- (CPImage)_imageRemoveHighlighted
|
||||
{
|
||||
return [self valueForThemeAttribute:@"remove-image" inState:CPThemeStateHighlighted];
|
||||
return [self valueForThemeAttribute:@"remove-image" inState:CPThemeStateHighlighted.and([self themeState])];
|
||||
}
|
||||
|
||||
- (CPVerticalTextAlignment)_verticalAlignment
|
||||
@@ -1919,17 +2024,17 @@ TODO: implement
|
||||
|
||||
- (CPString)_toolTipForAddCompoundRowButton
|
||||
{
|
||||
return [_standardLocalizer localizedStringForString:@"Add compound row"];
|
||||
return [[self standardLocalizer] localizedStringForString:@"Add compound row"];
|
||||
}
|
||||
|
||||
- (CPString)_toolTipForAddSimpleRowButton
|
||||
{
|
||||
return [_standardLocalizer localizedStringForString:@"Add row"];
|
||||
return [[self standardLocalizer] localizedStringForString:@"Add row"];
|
||||
}
|
||||
|
||||
- (CPString)_toolTipForDeleteRowButton
|
||||
{
|
||||
return [_standardLocalizer localizedStringForString:@"Delete row"];
|
||||
return [[self standardLocalizer] localizedStringForString:@"Delete row"];
|
||||
}
|
||||
|
||||
- (void)_updateSliceIndentations
|
||||
@@ -2121,7 +2226,13 @@ TODO: implement
|
||||
return;
|
||||
|
||||
var point = [self convertPoint:[event locationInWindow] fromView:nil],
|
||||
view = [_slices objectAtIndex:FLOOR(MAX(0, point.y) / _sliceHeight)];
|
||||
index = FLOOR(MAX(0, point.y) / _sliceHeight);
|
||||
|
||||
// Check bounds before accessing the array to prevent CPRangeException
|
||||
if (index >= [_slices count])
|
||||
return;
|
||||
|
||||
var view = [_slices objectAtIndex:FLOOR(MAX(0, point.y) / _sliceHeight)];
|
||||
|
||||
if ([self _dragShouldBeginFromMouseDown:view])
|
||||
[self _performDragForSlice:view withEvent:event];
|
||||
@@ -2150,10 +2261,11 @@ TODO: implement
|
||||
firstSlice = [_slices objectAtIndex:firstIndex],
|
||||
dragview = [[CPView alloc] initWithFrame:[firstSlice frame]];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var html = firstSlice._DOMElement.innerHTML;
|
||||
dragview._DOMElement.innerHTML = [html copy];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var html = firstSlice._DOMElement.innerHTML;
|
||||
dragview._DOMElement.innerHTML = [html copy];
|
||||
}
|
||||
[dragview setBackgroundColor:[firstSlice backgroundColor]];
|
||||
[dragview setAlphaValue:0.7];
|
||||
|
||||
@@ -2244,33 +2356,41 @@ TODO: implement
|
||||
|
||||
- (BOOL)performDragOperation:(id /*< CPDraggingInfo >*/)info
|
||||
{
|
||||
var aboveInsertIndexCount = 0,
|
||||
object,
|
||||
removeIndex;
|
||||
var object;
|
||||
|
||||
var rowObjects = [_rowCache valueForKey:@"rowObject"],
|
||||
index = [_draggingRows lastIndex];
|
||||
|
||||
var parentRowIndex = [self parentRowForRow:index], // first index of draggingrows
|
||||
var firstDraggingIndex = [_draggingRows firstIndex],
|
||||
parentRowIndex = [self parentRowForRow:firstDraggingIndex],
|
||||
parentRowObject = (parentRowIndex === -1) ? _boundArrayOwner : [[self _rowCacheForIndex:parentRowIndex] rowObject],
|
||||
insertIndex = _subviewIndexOfDropLine;
|
||||
|
||||
while (index !== CPNotFound)
|
||||
{
|
||||
if (index >= insertIndex)
|
||||
// Identify if this row's parent is also inside the dragging set.
|
||||
// If it is, we skip it because it will move automatically with its parent.
|
||||
var parentOfCurrent = [self parentRowForRow:index];
|
||||
if (parentOfCurrent !== -1 && [_draggingRows containsIndex:parentOfCurrent])
|
||||
{
|
||||
removeIndex = index + aboveInsertIndexCount;
|
||||
aboveInsertIndexCount += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
removeIndex = index;
|
||||
insertIndex -= 1;
|
||||
index = [_draggingRows indexLessThanIndex:index];
|
||||
continue;
|
||||
}
|
||||
|
||||
object = [rowObjects objectAtIndex:removeIndex];
|
||||
[self removeRowAtIndex:removeIndex];
|
||||
[[self _subrowObjectsOfObject:parentRowObject] insertObject:object atIndex:insertIndex - parentRowIndex - 1];
|
||||
object = [rowObjects objectAtIndex:index];
|
||||
|
||||
// Find the current live index of the object inside the editor
|
||||
var cache = [self _searchCacheForRowObject:object];
|
||||
var liveIndex = [_rowCache indexOfObjectIdenticalTo:cache];
|
||||
|
||||
if (liveIndex !== CPNotFound)
|
||||
{
|
||||
if (liveIndex < insertIndex)
|
||||
insertIndex -= 1;
|
||||
|
||||
[self removeRowAtIndex:liveIndex];
|
||||
[[self _subrowObjectsOfObject:parentRowObject] insertObject:object atIndex:insertIndex - parentRowIndex - 1];
|
||||
}
|
||||
|
||||
index = [_draggingRows indexLessThanIndex:index];
|
||||
}
|
||||
@@ -2280,6 +2400,7 @@ TODO: implement
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (CPIndexSet)_draggingTypes
|
||||
{
|
||||
return [CPIndexSet indexSetWithIndex:CPDragOperationMove];
|
||||
|
||||
@@ -77,6 +77,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+)
|
||||
}
|
||||
|
||||
_dictionary = [CPDictionary dictionaryWithDictionary:dict];
|
||||
|
||||
// Post notification to let the rule editor know the translation dictionary is ready
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:self];
|
||||
}
|
||||
|
||||
- (CPString)localizedStringForString:(CPString)aString
|
||||
@@ -94,4 +97,231 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+)
|
||||
return aString;
|
||||
}
|
||||
|
||||
// MARK: - Formatting & Reordering Helpers
|
||||
|
||||
- (CPString)_englishRepresentationForView:(id)aView
|
||||
{
|
||||
if ([aView isKindOfClass:[CPPopUpButton class]])
|
||||
{
|
||||
var selectedItem = [aView selectedItem];
|
||||
if (selectedItem)
|
||||
{
|
||||
var originalTitle = selectedItem._originalTitle;
|
||||
|
||||
// Fallback: If not cached directly, inspect representedObject payload dictionary
|
||||
if (!originalTitle)
|
||||
{
|
||||
var rep = [selectedItem representedObject];
|
||||
if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)])
|
||||
{
|
||||
originalTitle = [rep objectForKey:@"value"];
|
||||
}
|
||||
else if (rep && typeof rep === "string")
|
||||
{
|
||||
originalTitle = rep;
|
||||
}
|
||||
}
|
||||
if (!originalTitle)
|
||||
{
|
||||
originalTitle = [selectedItem title];
|
||||
}
|
||||
return "%[" + originalTitle + "]@";
|
||||
}
|
||||
return "%[]@";
|
||||
}
|
||||
else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable])
|
||||
{
|
||||
return aView._originalText || [aView stringValue];
|
||||
}
|
||||
else
|
||||
{
|
||||
return "%@";
|
||||
}
|
||||
}
|
||||
|
||||
- (CPString)formattingKeyForViews:(CPArray)views
|
||||
{
|
||||
var keyParts = [];
|
||||
var count = [views count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var view = [views objectAtIndex:i];
|
||||
[keyParts addObject:[self _englishRepresentationForView:view]];
|
||||
}
|
||||
return [keyParts componentsJoinedByString:@" "];
|
||||
}
|
||||
|
||||
- (void)localizeMenuItemsForViews:(CPArray)views
|
||||
{
|
||||
var count = [views count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var view = [views objectAtIndex:i];
|
||||
if ([view isKindOfClass:[CPPopUpButton class]])
|
||||
{
|
||||
var menuItems = [view itemArray];
|
||||
var menuItemsCount = [menuItems count];
|
||||
var selectedItem = [view selectedItem];
|
||||
|
||||
for (var j = 0; j < menuItemsCount; j++)
|
||||
{
|
||||
var item = [menuItems objectAtIndex:j];
|
||||
|
||||
if (!item._originalTitle)
|
||||
{
|
||||
var rep = [item representedObject];
|
||||
if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)])
|
||||
{
|
||||
item._originalTitle = [rep objectForKey:@"value"];
|
||||
}
|
||||
else
|
||||
{
|
||||
item._originalTitle = [item title];
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily select item to generate formatting key context
|
||||
[view selectItem:item];
|
||||
|
||||
var tempKey = [self formattingKeyForViews:views];
|
||||
var tempPattern = [self localizedStringForString:tempKey];
|
||||
|
||||
if (tempPattern !== tempKey)
|
||||
{
|
||||
var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g;
|
||||
var match;
|
||||
while ((match = regex.exec(tempPattern)) !== null)
|
||||
{
|
||||
var position = parseInt(match[1], 10) - 1;
|
||||
var translatedValue = match[2];
|
||||
|
||||
if (position === i && translatedValue)
|
||||
{
|
||||
[item setTitle:translatedValue];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
[item setTitle:item._originalTitle];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedItem)
|
||||
{
|
||||
[view selectItem:selectedItem];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CPArray)localizeAndReorderViews:(CPArray)views
|
||||
{
|
||||
var key = [self formattingKeyForViews:views];
|
||||
var localizedPattern = [self localizedStringForString:key];
|
||||
|
||||
if (localizedPattern === key)
|
||||
{
|
||||
var count = [views count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var originalView = [views objectAtIndex:i];
|
||||
if ([originalView isKindOfClass:[CPPopUpButton class]])
|
||||
{
|
||||
var selectedItem = [originalView selectedItem];
|
||||
if (selectedItem && selectedItem._originalTitle)
|
||||
{
|
||||
[selectedItem setTitle:selectedItem._originalTitle];
|
||||
}
|
||||
}
|
||||
else if ([originalView respondsToSelector:@selector(setStringValue:)] && originalView._originalText)
|
||||
{
|
||||
[originalView setStringValue:originalView._originalText];
|
||||
|
||||
if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable])
|
||||
{
|
||||
var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]],
|
||||
size = [originalView._originalText sizeWithFont:font];
|
||||
[originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
var newViews = [CPMutableArray array];
|
||||
var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g;
|
||||
var lastIndex = 0;
|
||||
var match;
|
||||
|
||||
while ((match = regex.exec(localizedPattern)) !== null)
|
||||
{
|
||||
var literalText = localizedPattern.substring(lastIndex, match.index);
|
||||
|
||||
// Only add a label if there are actual non-whitespace characters (like 'y')
|
||||
if (literalText.length > 0 && /\S/.test(literalText))
|
||||
{
|
||||
var label = [CPTextField labelWithTitle:literalText];
|
||||
[newViews addObject:label];
|
||||
}
|
||||
|
||||
var position = parseInt(match[1], 10) - 1;
|
||||
var translatedValue = match[2];
|
||||
|
||||
if (position >= 0 && position < [views count])
|
||||
{
|
||||
var originalView = [views objectAtIndex:position];
|
||||
|
||||
if (translatedValue !== undefined && translatedValue !== null)
|
||||
{
|
||||
if ([originalView isKindOfClass:[CPPopUpButton class]])
|
||||
{
|
||||
var selectedItem = [originalView selectedItem];
|
||||
if (selectedItem)
|
||||
{
|
||||
if (!selectedItem._originalTitle)
|
||||
{
|
||||
selectedItem._originalTitle = [selectedItem title];
|
||||
}
|
||||
[selectedItem setTitle:translatedValue];
|
||||
}
|
||||
}
|
||||
else if ([originalView respondsToSelector:@selector(setStringValue:)])
|
||||
{
|
||||
[originalView setStringValue:translatedValue];
|
||||
|
||||
// Recalculate frame size if it is a static CPTextField to avoid visual clipping
|
||||
if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable])
|
||||
{
|
||||
var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]],
|
||||
size = [translatedValue sizeWithFont:font];
|
||||
[originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[newViews addObject:originalView];
|
||||
}
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < localizedPattern.length)
|
||||
{
|
||||
var literalText = localizedPattern.substring(lastIndex);
|
||||
|
||||
// Only add a label if there are actual non-whitespace characters
|
||||
if (literalText.length > 0 && /\S/.test(literalText))
|
||||
{
|
||||
var label = [CPTextField labelWithTitle:literalText];
|
||||
[newViews addObject:label];
|
||||
}
|
||||
}
|
||||
|
||||
return newViews;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -49,6 +49,13 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setRowIndex:(int)anIndex
|
||||
{
|
||||
_rowIndex = anIndex;
|
||||
[self _updateBackgroundColor];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (void)_setSelected:(BOOL)select
|
||||
{
|
||||
if (select == _selected)
|
||||
@@ -57,6 +64,47 @@
|
||||
var selector = select ? @selector(setThemeState:) : @selector(unsetThemeState:);
|
||||
[[self subviews] makeObjectsPerformSelector:selector withObject:CPThemeStateSelectedDataView];
|
||||
_selected = select;
|
||||
|
||||
[self _updateBackgroundColor];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (void)_updateBackgroundColor
|
||||
{
|
||||
var color = nil;
|
||||
|
||||
if ([self _isSelected])
|
||||
{
|
||||
color = [_ruleEditor _selectedRowColor];
|
||||
}
|
||||
else
|
||||
{
|
||||
var colors = [_ruleEditor _backgroundColors],
|
||||
count = [colors count];
|
||||
|
||||
if (count > 0)
|
||||
color = [colors objectAtIndex:(_rowIndex % count)];
|
||||
}
|
||||
|
||||
[self setBackgroundColor:color];
|
||||
}
|
||||
|
||||
- (void)viewDidMoveToWindow
|
||||
{
|
||||
[super viewDidMoveToWindow];
|
||||
[self _updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
[self _updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super unsetThemeState:aState];
|
||||
[self _updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
@@ -66,35 +114,31 @@
|
||||
maxX = CGRectGetWidth(bounds),
|
||||
maxY = CGRectGetHeight(bounds);
|
||||
|
||||
// Draw background
|
||||
if ([self _isSelected])
|
||||
_backgroundColor = [_ruleEditor _selectedRowColor];
|
||||
else
|
||||
{
|
||||
var colors = [_ruleEditor _backgroundColors],
|
||||
count = [colors count];
|
||||
_backgroundColor = [colors objectAtIndex:(_rowIndex % count)];
|
||||
}
|
||||
|
||||
CGContextSetFillColor(context, _backgroundColor);
|
||||
CGContextFillRect(context, rect);
|
||||
// Note: Background is now handled by setBackgroundColor in _updateBackgroundColor
|
||||
// to support CSS-based colors and transparency correctly.
|
||||
|
||||
// Draw Top Border
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, 0, 0);
|
||||
CGContextAddLineToPoint(context, maxX, 0);
|
||||
CGContextSetStrokeColor(context, [_ruleEditor _sliceTopBorderColor]);
|
||||
CGContextStrokePath(context);
|
||||
var topColor = [_ruleEditor _sliceTopBorderColor];
|
||||
if (topColor)
|
||||
{
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, 0, 0);
|
||||
CGContextAddLineToPoint(context, maxX, 0);
|
||||
CGContextSetStrokeColor(context, topColor);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
// Draw Bottom Border
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, 0, maxY);
|
||||
CGContextAddLineToPoint(context, maxX, maxY);
|
||||
|
||||
var bottomColor = (_rowIndex == [_ruleEditor _lastRow]) ? [_ruleEditor _sliceLastBottomBorderColor] : [_ruleEditor _sliceBottomBorderColor];
|
||||
|
||||
CGContextSetStrokeColor(context, bottomColor);
|
||||
CGContextStrokePath(context);
|
||||
if (bottomColor)
|
||||
{
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, 0, maxY);
|
||||
CGContextAddLineToPoint(context, maxX, maxY);
|
||||
CGContextSetStrokeColor(context, bottomColor);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)theEvent
|
||||
|
||||
@@ -67,6 +67,23 @@
|
||||
[self setAutoresizingMask:CPViewWidthSizable];
|
||||
}
|
||||
|
||||
- (void)_setSelected:(BOOL)isSelected
|
||||
{
|
||||
[super _setSelected:isSelected];
|
||||
[self _updateButtonImages];
|
||||
}
|
||||
|
||||
- (void)_updateButtonImages
|
||||
{
|
||||
var rowState = [self themeState];
|
||||
|
||||
if ([self _isSelected])
|
||||
rowState = rowState.and(CPThemeStateSelected);
|
||||
|
||||
[_addButton setImage:[_ruleEditor valueForThemeAttribute:@"add-image" inState:rowState]];
|
||||
[_subtractButton setImage:[_ruleEditor valueForThemeAttribute:@"remove-image" inState:rowState]];
|
||||
}
|
||||
|
||||
- (CPButton)_createRowButton
|
||||
{
|
||||
var button = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
|
||||
@@ -89,6 +106,8 @@
|
||||
var button = [self _createRowButton];
|
||||
|
||||
[button setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]];
|
||||
|
||||
// Initial setup, _updateButtonImages will be called later to set correct state-based images
|
||||
[button setValue:[_ruleEditor _imageAdd] forThemeAttribute:@"image" inState:CPThemeStateNormal];
|
||||
[button setValue:[_ruleEditor _imageAddHighlighted] forThemeAttribute:@"image" inState:CPThemeStateHighlighted];
|
||||
|
||||
@@ -103,6 +122,8 @@
|
||||
var button = [self _createRowButton];
|
||||
|
||||
[button setToolTip:[_ruleEditor _toolTipForDeleteRowButton]];
|
||||
|
||||
// Initial setup
|
||||
[button setValue:[_ruleEditor _imageRemove] forThemeAttribute:@"image" inState:CPThemeStateNormal];
|
||||
[button setValue:[_ruleEditor _imageRemoveHighlighted] forThemeAttribute:@"image" inState:CPThemeStateHighlighted];
|
||||
|
||||
@@ -114,8 +135,32 @@
|
||||
|
||||
- (CPMenuItem)_createMenuItemWithTitle:(CPString )title
|
||||
{
|
||||
title = [[_ruleEditor standardLocalizer] localizedStringForString:title];
|
||||
return [[CPMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""];
|
||||
var originalTitle = title;
|
||||
var localizedTitle = [[_ruleEditor standardLocalizer] localizedStringForString:title];
|
||||
var item = [[CPMenuItem alloc] initWithTitle:localizedTitle action:nil keyEquivalent:@""];
|
||||
|
||||
// Cache the raw English title for pattern-matching
|
||||
item._originalTitle = originalTitle;
|
||||
return item;
|
||||
}
|
||||
|
||||
- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text
|
||||
{
|
||||
var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
|
||||
ruleEditorFont = [_ruleEditor font],
|
||||
font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2],
|
||||
localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text],
|
||||
size = [localizedText sizeWithFont:font];
|
||||
|
||||
[textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])];
|
||||
[textField setValue:font forThemeAttribute:@"font"];
|
||||
[textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"];
|
||||
[textField setStringValue:localizedText];
|
||||
|
||||
// Cache the raw English text for pattern-matching
|
||||
textField._originalText = text;
|
||||
|
||||
return textField;
|
||||
}
|
||||
|
||||
- (CPPopUpButton)_createPopUpButtonWithItems:(CPArray)itemsArray selectedItemIndex:(int)index
|
||||
@@ -142,22 +187,6 @@
|
||||
return [CPMenuItem separatorItem];
|
||||
}
|
||||
|
||||
- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text
|
||||
{
|
||||
var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
|
||||
ruleEditorFont = [_ruleEditor font],
|
||||
font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2],
|
||||
localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text],
|
||||
size = [localizedText sizeWithFont:font];
|
||||
|
||||
[textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])];
|
||||
[textField setValue:font forThemeAttribute:@"font"];
|
||||
[textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"];
|
||||
[textField setStringValue:localizedText];
|
||||
|
||||
return textField;
|
||||
}
|
||||
|
||||
- (void)_addOption:(id)sender
|
||||
{
|
||||
if (_rowIndex == [_ruleEditor numberOfRows] - 1)
|
||||
@@ -334,6 +363,28 @@
|
||||
|
||||
[_correspondingRuleItems setArray:ruleItems];
|
||||
|
||||
// Localize drop-down options in context and reorder/insert intermediate labels natively
|
||||
var localizer = [_ruleEditor standardLocalizer];
|
||||
if (localizer)
|
||||
{
|
||||
[localizer localizeMenuItemsForViews:_ruleOptionViews];
|
||||
_ruleOptionViews = [localizer localizeAndReorderViews:_ruleOptionViews];
|
||||
}
|
||||
|
||||
// Rebuild frame configurations to match the new localized layout order
|
||||
[_ruleOptionFrames removeAllObjects];
|
||||
[_ruleOptionInitialViewFrames removeAllObjects];
|
||||
|
||||
var newCount = [_ruleOptionViews count];
|
||||
for (var i = 0; i < newCount; i++)
|
||||
{
|
||||
var view = [_ruleOptionViews objectAtIndex:i],
|
||||
frame = [view frame];
|
||||
|
||||
[_ruleOptionFrames addObject:frame];
|
||||
[_ruleOptionInitialViewFrames addObject:frame];
|
||||
}
|
||||
|
||||
if (!_editable)
|
||||
[self _updateEnabledStateForSubviews];
|
||||
|
||||
@@ -410,6 +461,9 @@
|
||||
{
|
||||
[_addButton setHidden:[_ruleEditor _shouldHideAddButtonForSlice:self]];
|
||||
[_subtractButton setHidden:[_ruleEditor _shouldHideSubtractButtonForSlice:self]];
|
||||
|
||||
[_addButton setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]];
|
||||
[_subtractButton setToolTip:[_ruleEditor _toolTipForDeleteRowButton]];
|
||||
}
|
||||
|
||||
- (void)_configurePlusButtonByRowType:(CPRuleEditorRowType)type
|
||||
@@ -492,9 +546,23 @@
|
||||
|
||||
- (void)viewDidMoveToWindow
|
||||
{
|
||||
[super viewDidMoveToWindow];
|
||||
[self _updateButtonImages];
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
[self _updateButtonImages];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super unsetThemeState:aState];
|
||||
[self _updateButtonImages];
|
||||
}
|
||||
|
||||
- (void)_addObservers
|
||||
{
|
||||
if (_isObserving)
|
||||
|
||||
+3
-4
@@ -42,11 +42,10 @@
|
||||
*/
|
||||
- (CGRect)visibleFrame
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
return CGRectMake(window.screen.availLeft, window.screen.availTop, window.screen.availWidth, window.screen.availHeight);
|
||||
#else
|
||||
if (CPDOMAvailable)
|
||||
return CGRectMake(window.screen.availLeft, window.screen.availTop, window.screen.availWidth, window.screen.availHeight);
|
||||
|
||||
return CGRectMakeZero();
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
+499
-50
@@ -29,10 +29,12 @@
|
||||
@import "CPClipView.j"
|
||||
@import "CPScroller.j"
|
||||
@import "CPView.j"
|
||||
@import "CPRulerView.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class CPTableView
|
||||
@class CPRulerView
|
||||
|
||||
#define SHOULD_SHOW_CORNER_VIEW() (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden])
|
||||
|
||||
|
||||
@protocol CPScrollViewDelegate <CPObject>
|
||||
@@ -47,7 +49,8 @@
|
||||
/*! @ignore */
|
||||
var _isBrowserUsingOverlayScrollers = function()
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return NO;
|
||||
|
||||
/*
|
||||
Even if the system supports overlay (Lion) scrollers,
|
||||
the browser (e.g. FireFox *cough*) may not.
|
||||
@@ -83,9 +86,6 @@ var _isBrowserUsingOverlayScrollers = function()
|
||||
document.body.removeChild(outer);
|
||||
|
||||
return usingOverlayScrollers;
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
};
|
||||
|
||||
var TIMER_INTERVAL = 0.2,
|
||||
@@ -94,9 +94,28 @@ var TIMER_INTERVAL = 0.2,
|
||||
|
||||
CPScrollViewFadeOutTime = 1.3;
|
||||
|
||||
var CPScrollViewWillStartLiveScrollNotification = @"CPScrollViewWillStartLiveScrollNotification",
|
||||
CPScrollViewDidLiveScrollNotification = @"CPScrollViewDidLiveScrollNotification",
|
||||
CPScrollViewDidEndLiveScrollNotification = @"CPScrollViewDidEndLiveScrollNotification";
|
||||
|
||||
var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
CPScrollerStyleGlobalChangeNotification = @"CPScrollerStyleGlobalChangeNotification";
|
||||
|
||||
var CPScrollViewBorderSuffixes = @[@"no-border", @"line-border", @"bezel-border", @"groove-border"];
|
||||
|
||||
// _CPScrollViews will hold all created CPScrollView's in order to propagate changes
|
||||
// of scroller global style
|
||||
var _CPScrollViews;
|
||||
|
||||
/*
|
||||
The default smallest and largest magnifications. These are not arbitrary: they are the
|
||||
defaults that Apple documents for NSScrollView's minMagnification and maxMagnification, so a
|
||||
scroll view behaves as in Cocoa unless its limits are set. They apply to a scroll view made in
|
||||
code, and to one decoded from an archive that has no valid limits.
|
||||
*/
|
||||
var CPScrollViewDefaultMinMagnification = 0.25,
|
||||
CPScrollViewDefaultMaxMagnification = 4.0;
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPScrollView
|
||||
@@ -136,11 +155,24 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
|
||||
int _scrollerStyle;
|
||||
int _scrollerKnobStyle;
|
||||
|
||||
// Ruler Support
|
||||
BOOL _hasVerticalRuler;
|
||||
BOOL _hasHorizontalRuler;
|
||||
BOOL _rulersVisible;
|
||||
|
||||
CPRulerView _verticalRuler;
|
||||
CPRulerView _horizontalRuler;
|
||||
|
||||
// Magnification Support
|
||||
BOOL _allowsMagnification;
|
||||
float _minMagnification;
|
||||
float _maxMagnification;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
@@ -153,6 +185,8 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
CPScrollerStyleGlobal = _isBrowserUsingOverlayScrollers() ? CPScrollerStyleOverlay : CPScrollerStyleLegacy
|
||||
else
|
||||
CPScrollerStyleGlobal = globalValue;
|
||||
|
||||
_CPScrollViews = @[];
|
||||
}
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
@@ -164,7 +198,15 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
{
|
||||
return @{
|
||||
@"bottom-corner-color": [CPColor whiteColor],
|
||||
@"border-color": [CPColor blackColor]
|
||||
@"border-color": [CPColor blackColor],
|
||||
@"content-inset-no-border": CGInsetMake(0, 0, 0, 0),
|
||||
@"content-inset-line-border": CGInsetMake(1, 1, 1, 1),
|
||||
@"content-inset-bezel-border": CGInsetMake(1, 1, 1, 1),
|
||||
@"content-inset-groove-border": CGInsetMake(2, 2, 2, 2),
|
||||
@"background-color-no-border": [CPNull null],
|
||||
@"background-color-line-border": [CPNull null],
|
||||
@"background-color-bezel-border": [CPNull null],
|
||||
@"background-color-groove-border": [CPNull null]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,7 +235,7 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
bounds.size.height -= [horizontalScrollerClass scrollerWidthInStyle:scrollerStyle];
|
||||
|
||||
if (verticalScrollerClass)
|
||||
bounds.size.width -= [verticalScrollerClass scrollerWidthForControlSize:scrollerStyle];
|
||||
bounds.size.width -= [verticalScrollerClass scrollerWidthInStyle:scrollerStyle];
|
||||
|
||||
return bounds.size;
|
||||
}
|
||||
@@ -222,30 +264,46 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
heightInset = contentSize.height - bounds.size.height,
|
||||
frameSize = CGSizeMake(contentSize.width + widthInset, contentSize.height + heightInset);
|
||||
|
||||
if (hFlag)
|
||||
if (horizontalScrollerClass)
|
||||
frameSize.height += [horizontalScrollerClass scrollerWidthInStyle:scrollerStyle];
|
||||
|
||||
if (vFlag)
|
||||
frameSize.width += [verticalScrollerClass scrollerWidthForControlSize:scrollerStyle];
|
||||
if (verticalScrollerClass)
|
||||
frameSize.width += [verticalScrollerClass scrollerWidthInStyle:scrollerStyle];
|
||||
|
||||
return frameSize;
|
||||
}
|
||||
|
||||
+ (CGRect)_insetBounds:(CGRect)bounds borderType:(CPBorderType)borderType
|
||||
{
|
||||
// First, we have to check if we are compiling a theme or running an application because if working on a theme,
|
||||
// we can't use theme attributes to determine the inset ! This would be a kind of circular reference...
|
||||
|
||||
var compilingATheme = [[[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPApplicationDelegateClass"] isEqualToString:@"BKShowcaseController"];
|
||||
|
||||
if (compilingATheme)
|
||||
return bounds;
|
||||
|
||||
var contentInset = [[CPTheme defaultTheme] valueForAttributeWithName:@"content-inset-"+CPScrollViewBorderSuffixes[borderType] forClass:CPScrollView];
|
||||
|
||||
// As this is a class method, we don't have object attributes, so if the theme doesn't declare content insets, we don't automatically get default value.
|
||||
// Get it by hand.
|
||||
if (!contentInset)
|
||||
contentInset = [[self themeAttributes] objectForKey:@"content-inset-"+CPScrollViewBorderSuffixes[borderType]];
|
||||
|
||||
switch (borderType)
|
||||
{
|
||||
case CPNoBorder:
|
||||
case CPLineBorder:
|
||||
case CPBezelBorder:
|
||||
return CGRectInset(bounds, 1.0, 1.0);
|
||||
return CGRectInsetByInset(bounds, contentInset);
|
||||
|
||||
case CPGrooveBorder:
|
||||
bounds = CGRectInset(bounds, 2.0, 2.0);
|
||||
// FIXME: Do something better with this
|
||||
bounds = CGRectInsetByInset(bounds, contentInset);
|
||||
++bounds.origin.y;
|
||||
--bounds.size.height;
|
||||
return bounds;
|
||||
|
||||
case CPNoBorder:
|
||||
default:
|
||||
return bounds;
|
||||
}
|
||||
@@ -267,12 +325,15 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
+ (void)setGlobalScrollerStyle:(CPScrollerStyle)aStyle
|
||||
{
|
||||
CPScrollerStyleGlobal = aStyle;
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollerStyleGlobalChangeNotification object:nil];
|
||||
|
||||
// We propagate the new scroller global style to all existing CPScrollView's
|
||||
for (var i = 0, count = [_CPScrollViews count]; i < count; i++)
|
||||
[_CPScrollViews[i] setScrollerStyle:CPScrollerStyleGlobal];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -302,17 +363,27 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
_scrollerKnobStyle = CPScrollerKnobStyleDefault;
|
||||
[self setScrollerStyle:CPScrollerStyleGlobal];
|
||||
|
||||
_hasVerticalRuler = NO;
|
||||
_hasHorizontalRuler = NO;
|
||||
_rulersVisible = NO;
|
||||
|
||||
_allowsMagnification = NO;
|
||||
_minMagnification = CPScrollViewDefaultMinMagnification;
|
||||
_maxMagnification = CPScrollViewDefaultMaxMagnification;
|
||||
|
||||
_delegate = nil;
|
||||
_scrollTimer = nil;
|
||||
_implementedDelegateMethods = 0;
|
||||
|
||||
[_CPScrollViews addObject:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
/*!
|
||||
The delegate of the scroll view
|
||||
@@ -790,8 +861,223 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Privates
|
||||
// MARK: -
|
||||
// MARK: Magnification
|
||||
|
||||
/*!
|
||||
Returns \c YES if the user may change the magnification. The default is \c NO. As in Cocoa,
|
||||
-setMagnification: and -setMagnification:centeredAtPoint: change it in code either way.
|
||||
*/
|
||||
- (BOOL)allowsMagnification
|
||||
{
|
||||
return _allowsMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the user may change the magnification.
|
||||
*/
|
||||
- (void)setAllowsMagnification:(BOOL)shouldAllowMagnification
|
||||
{
|
||||
_allowsMagnification = !!shouldAllowMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the smallest magnification. The default is 0.25, as in Cocoa.
|
||||
*/
|
||||
- (float)minMagnification
|
||||
{
|
||||
return _minMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the smallest magnification.
|
||||
*/
|
||||
- (void)setMinMagnification:(float)aMagnification
|
||||
{
|
||||
_minMagnification = aMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the largest magnification. The default is 4.0, as in Cocoa.
|
||||
*/
|
||||
- (float)maxMagnification
|
||||
{
|
||||
return _maxMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the largest magnification.
|
||||
*/
|
||||
- (void)setMaxMagnification:(float)aMagnification
|
||||
{
|
||||
_maxMagnification = aMagnification;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the magnification of the document view: 1.0 at actual size, 2.0 at twice the size.
|
||||
*/
|
||||
- (float)magnification
|
||||
{
|
||||
var boundsWidth = CGRectGetWidth([_contentView bounds]);
|
||||
|
||||
return boundsWidth ? CGRectGetWidth([_contentView frame]) / boundsWidth : 1.0;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the magnification, keeping the centre of the visible part of the document in place.
|
||||
@param aMagnification the magnification, limited to the minimum and maximum magnification
|
||||
*/
|
||||
- (void)setMagnification:(float)aMagnification
|
||||
{
|
||||
var visibleRect = [_contentView bounds];
|
||||
|
||||
[self setMagnification:aMagnification centeredAtPoint:CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect))];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the magnification, and scrolls so that aPoint is at the centre of the visible area. As
|
||||
in Cocoa, the clip view's bounds are scaled: at a magnification of 2.0 its bounds are half its
|
||||
frame, so the document view is drawn at twice its size. A document view that resizes its width
|
||||
with the clip view, as a text view that wraps to the window does, takes the new bounds width.
|
||||
@param aMagnification the magnification, limited to the minimum and maximum magnification
|
||||
@param aPoint the point in the clip view's bounds, which are the document view's coordinates,
|
||||
to show at the centre
|
||||
*/
|
||||
- (void)setMagnification:(float)aMagnification centeredAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
var magnification = MIN(MAX(aMagnification, _minMagnification), _maxMagnification),
|
||||
frameSize = [_contentView frameSize];
|
||||
|
||||
/*
|
||||
If the limits are not numbers, the clamp above gives NaN. The clip view's bounds would
|
||||
then become NaN, which the browser ignores, and -magnification would still report 1.0,
|
||||
so the zoom would silently do nothing. This happened with scroll views loaded from cibs
|
||||
whose limits were never set; see -initWithCoder: and -encodeWithCoder:. A magnification
|
||||
that is not a positive number is therefore ignored.
|
||||
*/
|
||||
if (!(magnification > 0.0) || !frameSize.width || !frameSize.height)
|
||||
return;
|
||||
|
||||
var boundsSize = CGSizeMake(frameSize.width / magnification, frameSize.height / magnification),
|
||||
documentView = [self documentView];
|
||||
|
||||
[_contentView setBoundsSize:boundsSize];
|
||||
|
||||
// The clip view applies its scale to its element when it is marked for display.
|
||||
[_contentView setNeedsDisplay:YES];
|
||||
|
||||
if ([documentView autoresizingMask] & CPViewWidthSizable)
|
||||
[documentView setFrameSize:CGSizeMake(boundsSize.width, [documentView frameSize].height)];
|
||||
|
||||
[_contentView scrollToPoint:CGPointMake(aPoint.x - boundsSize.width / 2.0, aPoint.y - boundsSize.height / 2.0)];
|
||||
[self reflectScrolledClipView:_contentView];
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Rulers
|
||||
|
||||
- (BOOL)hasHorizontalRuler
|
||||
{
|
||||
return _hasHorizontalRuler;
|
||||
}
|
||||
|
||||
- (void)setHasHorizontalRuler:(BOOL)shouldHaveHorizontalRuler
|
||||
{
|
||||
if (_hasHorizontalRuler === shouldHaveHorizontalRuler)
|
||||
return;
|
||||
|
||||
_hasHorizontalRuler = shouldHaveHorizontalRuler;
|
||||
|
||||
if (_hasHorizontalRuler && !_horizontalRuler)
|
||||
{
|
||||
_horizontalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationHorizontal];
|
||||
}
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
- (BOOL)hasVerticalRuler
|
||||
{
|
||||
return _hasVerticalRuler;
|
||||
}
|
||||
|
||||
- (void)setHasVerticalRuler:(BOOL)shouldHaveVerticalRuler
|
||||
{
|
||||
if (_hasVerticalRuler === shouldHaveVerticalRuler)
|
||||
return;
|
||||
|
||||
_hasVerticalRuler = shouldHaveVerticalRuler;
|
||||
|
||||
if (_hasVerticalRuler && !_verticalRuler)
|
||||
{
|
||||
_verticalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationVertical];
|
||||
}
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
- (BOOL)rulersVisible
|
||||
{
|
||||
return _rulersVisible;
|
||||
}
|
||||
|
||||
- (void)setRulersVisible:(BOOL)areRulersVisible
|
||||
{
|
||||
if (_rulersVisible === areRulersVisible)
|
||||
return;
|
||||
|
||||
_rulersVisible = areRulersVisible;
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
- (CPRulerView)horizontalRulerView
|
||||
{
|
||||
return _horizontalRuler;
|
||||
}
|
||||
|
||||
- (void)setHorizontalRulerView:(CPRulerView)aRulerView
|
||||
{
|
||||
if (_horizontalRuler === aRulerView)
|
||||
return;
|
||||
|
||||
[_horizontalRuler removeFromSuperview];
|
||||
_horizontalRuler = aRulerView;
|
||||
|
||||
if (_horizontalRuler)
|
||||
{
|
||||
[_horizontalRuler setScrollView:self];
|
||||
[self addSubview:_horizontalRuler];
|
||||
}
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
- (CPRulerView)verticalRulerView
|
||||
{
|
||||
return _verticalRuler;
|
||||
}
|
||||
|
||||
- (void)setVerticalRulerView:(CPRulerView)aRulerView
|
||||
{
|
||||
if (_verticalRuler === aRulerView)
|
||||
return;
|
||||
|
||||
[_verticalRuler removeFromSuperview];
|
||||
_verticalRuler = aRulerView;
|
||||
|
||||
if (_verticalRuler)
|
||||
{
|
||||
[_verticalRuler setScrollView:self];
|
||||
[self addSubview:_verticalRuler];
|
||||
}
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Privates
|
||||
|
||||
/* @ignore */
|
||||
- (void)_updateScrollerStyle
|
||||
@@ -885,7 +1171,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
if (_cornerView)
|
||||
{
|
||||
[_cornerView setHidden:!SHOULD_SHOW_CORNER_VIEW()];
|
||||
[_cornerView setHidden:![self _shouldShowCornerView]];
|
||||
[self addSubview:_cornerView];
|
||||
}
|
||||
}
|
||||
@@ -928,7 +1214,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
frame.size.height = CGRectGetHeight([headerView frame]);
|
||||
|
||||
if (SHOULD_SHOW_CORNER_VIEW())
|
||||
if ([self _shouldShowCornerView])
|
||||
frame.size.width -= CGRectGetWidth([self _cornerViewFrame]);
|
||||
|
||||
return frame;
|
||||
@@ -987,6 +1273,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[self _sendDelegateMessages];
|
||||
|
||||
[_contentView scrollToPoint:contentBounds.origin];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollViewDidLiveScrollNotification object:self];
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
@@ -1025,6 +1313,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
[_contentView scrollToPoint:contentBounds.origin];
|
||||
[_headerClipView scrollToPoint:CGPointMake(contentBounds.origin.x, 0.0)];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollViewDidLiveScrollNotification object:self];
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
@@ -1035,6 +1325,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
if (!_scrollTimer)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollViewWillStartLiveScrollNotification object:self];
|
||||
[self _scrollViewWillScroll];
|
||||
_scrollTimer = [CPTimer scheduledTimerWithTimeInterval:TIMER_INTERVAL target:self selector:@selector(_scrollViewDidScroll) userInfo:nil repeats:YES];
|
||||
}
|
||||
@@ -1073,6 +1364,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[_contentView scrollToPoint:constrainedOrigin];
|
||||
[_headerClipView scrollToPoint:CGPointMake(constrainedOrigin.x, 0.0)];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollViewDidLiveScrollNotification object:self];
|
||||
|
||||
if (extraX || extraY)
|
||||
[enclosingScrollView _respondToScrollWheelEventWithDeltaX:extraX deltaY:extraY];
|
||||
}
|
||||
@@ -1092,6 +1385,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
if (_implementedDelegateMethods & CPScrollViewDelegate_scrollViewDidScroll_)
|
||||
[_delegate scrollViewDidScroll:self];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPScrollViewDidEndLiveScrollNotification object:self];
|
||||
}
|
||||
|
||||
/*! @ignore*/
|
||||
@@ -1102,18 +1397,15 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Utilities
|
||||
// MARK: -
|
||||
// MARK: Utilities
|
||||
|
||||
/*!
|
||||
Lays out the scroll view's components.
|
||||
*/
|
||||
- (void)tile
|
||||
{
|
||||
// yuck.
|
||||
// RESIZE: tile->setHidden AND refl
|
||||
// Outside Change: refl->tile->setHidden AND refl
|
||||
// scroll: refl.
|
||||
[self reflectScrolledClipView:_contentView];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -1157,6 +1449,41 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
contentFrame.origin.y += headerClipViewHeight;
|
||||
contentFrame.size.height -= headerClipViewHeight;
|
||||
|
||||
// Adjust content view based on horizontal / vertical ruler presence
|
||||
var showHorizontalRuler = _rulersVisible && _hasHorizontalRuler && _horizontalRuler,
|
||||
showVerticalRuler = _rulersVisible && _hasVerticalRuler && _verticalRuler;
|
||||
|
||||
var horizRulerThickness = showHorizontalRuler ? [_horizontalRuler requiredThickness] : 0.0,
|
||||
vertRulerThickness = showVerticalRuler ? [_verticalRuler requiredThickness] : 0.0;
|
||||
|
||||
if (showHorizontalRuler)
|
||||
{
|
||||
if ([_horizontalRuler superview] !== self)
|
||||
[self addSubview:_horizontalRuler];
|
||||
[_horizontalRuler setHidden:NO];
|
||||
}
|
||||
else if (_horizontalRuler)
|
||||
{
|
||||
[_horizontalRuler setHidden:YES];
|
||||
}
|
||||
|
||||
if (showVerticalRuler)
|
||||
{
|
||||
if ([_verticalRuler superview] !== self)
|
||||
[self addSubview:_verticalRuler];
|
||||
[_verticalRuler setHidden:NO];
|
||||
}
|
||||
else if (_verticalRuler)
|
||||
{
|
||||
[_verticalRuler setHidden:YES];
|
||||
}
|
||||
|
||||
contentFrame.origin.y += horizRulerThickness;
|
||||
contentFrame.size.height -= horizRulerThickness;
|
||||
|
||||
contentFrame.origin.x += vertRulerThickness;
|
||||
contentFrame.size.width -= vertRulerThickness;
|
||||
|
||||
var difference = CGSizeMake(CGRectGetWidth(documentFrame) - CGRectGetWidth(contentFrame), CGRectGetHeight(documentFrame) - CGRectGetHeight(contentFrame)),
|
||||
verticalScrollerWidth = [_verticalScroller scrollerWidth],
|
||||
horizontalScrollerHeight = [_horizontalScroller scrollerWidth],
|
||||
@@ -1249,7 +1576,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[_contentView setFrame:contentFrame];
|
||||
[_headerClipView setFrame:[self _headerClipViewFrame]];
|
||||
[[_headerClipView documentView] setNeedsDisplay:YES];
|
||||
if (SHOULD_SHOW_CORNER_VIEW())
|
||||
|
||||
if ([self _shouldShowCornerView])
|
||||
{
|
||||
[_cornerView setFrame:[self _cornerViewFrame]];
|
||||
[_cornerView setHidden:NO];
|
||||
@@ -1259,10 +1587,37 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
if (_scrollerStyle === CPScrollerStyleLegacy)
|
||||
{
|
||||
[[self bottomCornerView] setFrame:[self _bottomCornerViewFrame]];
|
||||
var bottomCornerFrame = [self _bottomCornerViewFrame];
|
||||
[[self bottomCornerView] setFrame:bottomCornerFrame];
|
||||
[[self bottomCornerView] setHidden:CGRectIsEmpty(bottomCornerFrame)];
|
||||
[[self bottomCornerView] setBackgroundColor:[self currentValueForThemeAttribute:@"bottom-corner-color"]];
|
||||
}
|
||||
|
||||
// Position and redraw rulers to track viewport updates
|
||||
if (showHorizontalRuler)
|
||||
{
|
||||
[_horizontalRuler setFrame:CGRectMake(
|
||||
CGRectGetMinX(contentFrame),
|
||||
CGRectGetMinY(contentFrame) - horizRulerThickness,
|
||||
CGRectGetWidth(contentFrame),
|
||||
horizRulerThickness
|
||||
)];
|
||||
|
||||
[_horizontalRuler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
if (showVerticalRuler)
|
||||
{
|
||||
[_verticalRuler setFrame:CGRectMake(
|
||||
CGRectGetMinX(contentFrame) - vertRulerThickness,
|
||||
CGRectGetMinY(contentFrame),
|
||||
vertRulerThickness,
|
||||
CGRectGetHeight(contentFrame)
|
||||
)];
|
||||
|
||||
[_verticalRuler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
--_recursionCount;
|
||||
}
|
||||
|
||||
@@ -1303,9 +1658,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
return [_contentView documentVisibleRect];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
@@ -1324,9 +1678,6 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
if (_isObserving)
|
||||
return;
|
||||
|
||||
//Make sure to have the last global style for the scroller
|
||||
[self _didReceiveDefaultStyleChange:nil];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(_didReceiveDefaultStyleChange:)
|
||||
name:CPScrollerStyleGlobalChangeNotification
|
||||
@@ -1335,13 +1686,11 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[super _addObservers];
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
[super drawRect:aRect];
|
||||
|
||||
if (_borderType == CPNoBorder)
|
||||
if ([self isCSSBased] || (_borderType == CPNoBorder))
|
||||
return;
|
||||
|
||||
var strokeRect = [self bounds],
|
||||
@@ -1425,8 +1774,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
y = maxY - 1.5;
|
||||
|
||||
CGContextMoveToPoint(context, maxX - 1.0, y);
|
||||
CGContextAddLineToPoint(context, minX + 2.0, y);
|
||||
CGContextMoveToPoint(maxX - 1.0, y);
|
||||
CGContextAddLineToPoint(minX + 2.0, y);
|
||||
|
||||
x = minX + 0.5;
|
||||
|
||||
@@ -1542,9 +1891,44 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[_headerClipView scrollToPoint:CGPointMake(contentBounds.origin.x, 0)];
|
||||
}
|
||||
|
||||
- (BOOL)_shouldShowCornerView
|
||||
{
|
||||
return (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden]);
|
||||
}
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPScrollView (CSSTheming)
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
if (![self isCSSBased])
|
||||
return;
|
||||
|
||||
if (_borderType !== CPNoBorder)
|
||||
[self setBackgroundColor:[self currentValueForThemeAttribute:@"background-color-"+CPScrollViewBorderSuffixes[_borderType]]];
|
||||
|
||||
if (_scrollerStyle === CPScrollerStyleLegacy)
|
||||
[[self bottomCornerView] setBackgroundColor:[self currentValueForThemeAttribute:@"bottom-corner-color"]];
|
||||
}
|
||||
|
||||
- (BOOL)isCSSBased
|
||||
{
|
||||
return [[self theme] isCSSBased];
|
||||
}
|
||||
|
||||
- (void)refreshDisplay
|
||||
{
|
||||
if ([self isCSSBased])
|
||||
[self setNeedsLayout:YES];
|
||||
else
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// pragma mark -
|
||||
|
||||
@implementation CPScrollView (FirstResponder)
|
||||
|
||||
@@ -1588,7 +1972,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
CPScrollViewHeaderClipViewKey = @"CPScrollViewHeaderClipViewKey",
|
||||
@@ -1605,7 +1989,17 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
CPScrollViewBottomCornerViewKey = @"CPScrollViewBottomCornerViewKey",
|
||||
CPScrollViewBorderTypeKey = @"CPScrollViewBorderTypeKey",
|
||||
CPScrollViewScrollerStyleKey = @"CPScrollViewScrollerStyleKey",
|
||||
CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey";
|
||||
CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey",
|
||||
|
||||
// Ruler Coding Keys
|
||||
CPScrollViewHasVRulerKey = @"CPScrollViewHasVRuler",
|
||||
CPScrollViewHasHRulerKey = @"CPScrollViewHasHRuler",
|
||||
CPScrollViewRulersVisibleKey = @"CPScrollViewRulersVisible",
|
||||
CPScrollViewVRulerKey = @"CPScrollViewVRuler",
|
||||
CPScrollViewHRulerKey = @"CPScrollViewHRuler",
|
||||
CPScrollViewAllowsMagnificationKey = @"CPScrollViewAllowsMagnification",
|
||||
CPScrollViewMinMagnificationKey = @"CPScrollViewMinMagnification",
|
||||
CPScrollViewMaxMagnificationKey = @"CPScrollViewMaxMagnification";
|
||||
|
||||
@implementation CPScrollView (CPCoding)
|
||||
|
||||
@@ -1640,6 +2034,38 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
_cornerView = [aCoder decodeObjectForKey:CPScrollViewCornerViewKey];
|
||||
_bottomCornerView = [aCoder decodeObjectForKey:CPScrollViewBottomCornerViewKey];
|
||||
|
||||
// Magnification decoding
|
||||
_allowsMagnification = [aCoder decodeBoolForKey:CPScrollViewAllowsMagnificationKey];
|
||||
_minMagnification = [aCoder decodeFloatForKey:CPScrollViewMinMagnificationKey];
|
||||
_maxMagnification = [aCoder decodeFloatForKey:CPScrollViewMaxMagnificationKey];
|
||||
|
||||
/*
|
||||
Cibs compiled by nib2cib can hold limits that are not numbers. nib2cib builds a
|
||||
scroll view from a xib through NSScrollView's -NS_initWithCoder:, which does not run
|
||||
-initWithFrame:, so the limits are never set; it then archives the scroll view with
|
||||
-encodeWithCoder: below, which wrote the unset limits into the cib. nib2cib belongs
|
||||
to the legacy toolchain, which is frozen, so the problem is handled here: a limit
|
||||
that is missing or not a positive number gets the Cocoa default. Without this, a
|
||||
scroll view loaded from such a cib could not change its magnification.
|
||||
*/
|
||||
if (!(_minMagnification > 0.0))
|
||||
_minMagnification = CPScrollViewDefaultMinMagnification;
|
||||
|
||||
if (!(_maxMagnification > 0.0))
|
||||
_maxMagnification = CPScrollViewDefaultMaxMagnification;
|
||||
|
||||
// Ruler decoding
|
||||
_hasVerticalRuler = [aCoder decodeBoolForKey:CPScrollViewHasVRulerKey];
|
||||
_hasHorizontalRuler = [aCoder decodeBoolForKey:CPScrollViewHasHRulerKey];
|
||||
_rulersVisible = [aCoder decodeBoolForKey:CPScrollViewRulersVisibleKey];
|
||||
|
||||
_verticalRuler = [aCoder decodeObjectForKey:CPScrollViewVRulerKey];
|
||||
_horizontalRuler = [aCoder decodeObjectForKey:CPScrollViewHRulerKey];
|
||||
|
||||
// A ruler does not archive its scroll view, which owns it.
|
||||
[_verticalRuler setScrollView:self];
|
||||
[_horizontalRuler setScrollView:self];
|
||||
|
||||
_delegate = nil;
|
||||
_scrollTimer = nil;
|
||||
_implementedDelegateMethods = 0;
|
||||
@@ -1647,10 +2073,7 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
_scrollerStyle = [aCoder decodeObjectForKey:CPScrollViewScrollerStyleKey] || CPScrollerStyleGlobal;
|
||||
_scrollerKnobStyle = [aCoder decodeObjectForKey:CPScrollViewScrollerKnobStyleKey] || CPScrollerKnobStyleDefault;
|
||||
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(_didReceiveDefaultStyleChange:)
|
||||
name:CPScrollerStyleGlobalChangeNotification
|
||||
object:nil];
|
||||
[_CPScrollViews addObject:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -1693,6 +2116,32 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
|
||||
[aCoder encodeInt:_scrollerStyle forKey:CPScrollViewScrollerStyleKey];
|
||||
[aCoder encodeInt:_scrollerKnobStyle forKey:CPScrollViewScrollerKnobStyleKey];
|
||||
|
||||
// Magnification encoding
|
||||
[aCoder encodeBool:_allowsMagnification forKey:CPScrollViewAllowsMagnificationKey];
|
||||
|
||||
/*
|
||||
nib2cib archives scroll views with this method, after building them from a xib through
|
||||
NSScrollView's -NS_initWithCoder:, which does not run -initWithFrame:. Their limits are
|
||||
then unset, and writing them put values that are not numbers into the cib. nib2cib
|
||||
belongs to the legacy toolchain, which is frozen, so the problem is handled here: only
|
||||
limits that are positive numbers are written, and -initWithCoder: supplies the defaults
|
||||
for the rest.
|
||||
*/
|
||||
if (_minMagnification > 0.0)
|
||||
[aCoder encodeFloat:_minMagnification forKey:CPScrollViewMinMagnificationKey];
|
||||
|
||||
if (_maxMagnification > 0.0)
|
||||
[aCoder encodeFloat:_maxMagnification forKey:CPScrollViewMaxMagnificationKey];
|
||||
|
||||
// Ruler encoding
|
||||
[aCoder encodeBool:_hasVerticalRuler forKey:CPScrollViewHasVRulerKey];
|
||||
[aCoder encodeBool:_hasHorizontalRuler forKey:CPScrollViewHasHRulerKey];
|
||||
[aCoder encodeBool:_rulersVisible forKey:CPScrollViewRulersVisibleKey];
|
||||
|
||||
[aCoder encodeObject:_verticalRuler forKey:CPScrollViewVRulerKey];
|
||||
[aCoder encodeObject:_horizontalRuler forKey:CPScrollViewHRulerKey];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
+37
-20
@@ -23,12 +23,11 @@
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "../Foundation/Foundation.h"
|
||||
|
||||
@import "CPAnimation.j"
|
||||
@import "CPControl.j"
|
||||
@import "CPWindow_Constants.j"
|
||||
@import "CPViewAnimation.j"
|
||||
@import "CPWindow_Constants.j"
|
||||
|
||||
@global CPApp
|
||||
|
||||
@@ -102,8 +101,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -167,8 +166,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -203,8 +202,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
/*!
|
||||
Returns the scroller's style
|
||||
@@ -259,7 +258,7 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
*/
|
||||
- (void)setKnobProportion:(float)aProportion
|
||||
{
|
||||
if (!_IS_NUMERIC(aProportion))
|
||||
if (!CPIsNumeric(aProportion))
|
||||
[CPException raise:CPInvalidArgumentException reason:"aProportion must be numeric, was: "+aProportion];
|
||||
|
||||
_knobProportion = MIN(1.0, MAX(0.0001, aProportion));
|
||||
@@ -269,8 +268,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Privates
|
||||
// MARK: -
|
||||
// MARK: Privates
|
||||
|
||||
/*! @ignore */
|
||||
- (void)_adjustScrollerSize
|
||||
@@ -295,8 +294,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Utilities
|
||||
// MARK: -
|
||||
// MARK: Utilities
|
||||
|
||||
- (CGRect)rectForPart:(CPScrollerPart)aPart
|
||||
{
|
||||
@@ -321,7 +320,25 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
if (![self hasThemeState:CPThemeStateSelected] && ![self hasThemeState:CPThemeStateScrollViewLegacy])
|
||||
return CPScrollerNoPart;
|
||||
|
||||
if (CGRectContainsPoint([self rectForPart:CPScrollerKnob], aPoint))
|
||||
// Fetch the visual rect for the knob
|
||||
var bounds = [self bounds],
|
||||
knobRect = [self rectForPart:CPScrollerKnob],
|
||||
hitKnobRect = CGRectMake(CGRectGetMinX(knobRect), CGRectGetMinY(knobRect), CGRectGetWidth(knobRect), CGRectGetHeight(knobRect));
|
||||
|
||||
// Expand the hit test rect to span the entire track on the minor axis
|
||||
// so dragging works smoothly even if the user clicks slightly off-center.
|
||||
if ([self isVertical])
|
||||
{
|
||||
hitKnobRect.origin.x = 0;
|
||||
hitKnobRect.size.width = CGRectGetWidth(bounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
hitKnobRect.origin.y = 0;
|
||||
hitKnobRect.size.height = CGRectGetHeight(bounds);
|
||||
}
|
||||
|
||||
if (CGRectContainsPoint(hitKnobRect, aPoint))
|
||||
return CPScrollerKnob;
|
||||
|
||||
if (CGRectContainsPoint([self rectForPart:CPScrollerDecrementPage], aPoint))
|
||||
@@ -468,8 +485,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Drawing
|
||||
// MARK: -
|
||||
// MARK: Drawing
|
||||
|
||||
/*!
|
||||
Draws the specified arrow and sets the highlight.
|
||||
@@ -703,8 +720,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
- (id)currentValueForThemeAttribute:(CPString)anAttributeName
|
||||
{
|
||||
@@ -780,8 +797,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
return [self currentValueForThemeAttribute:@"scroller-width"];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegates
|
||||
// MARK: -
|
||||
// MARK: Delegates
|
||||
|
||||
- (void)animationDidEnd:(CPAnimation)animation
|
||||
{
|
||||
|
||||
+14
-6
@@ -104,6 +104,8 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
_sendsWholeSearchString = NO;
|
||||
_sendsSearchStringImmediately = NO;
|
||||
_recentsAutosaveName = nil;
|
||||
|
||||
[self setPlaceholderString:@"Search"];
|
||||
|
||||
[self _init];
|
||||
}
|
||||
@@ -122,8 +124,14 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
[self setContinuous:YES];
|
||||
|
||||
var bounds = [self bounds],
|
||||
cancelButton = [[CPButton alloc] initWithFrame:[self cancelButtonRectForBounds:bounds]],
|
||||
cancelButton = nil,
|
||||
searchButton = nil;
|
||||
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
cancelButton = [[CPButton alloc] initWithFrame:[self cancelButtonRectForBounds:bounds]];
|
||||
searchButton = [[CPButton alloc] initWithFrame:[self searchButtonRectForBounds:bounds]];
|
||||
}
|
||||
|
||||
[self setCancelButton:cancelButton];
|
||||
[self resetCancelButton];
|
||||
@@ -136,8 +144,8 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override observers
|
||||
// MARK: -
|
||||
// MARK: Override observers
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
@@ -788,7 +796,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSearchField (ThemingAdditions)
|
||||
|
||||
@@ -980,7 +988,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSearchField (CPTrackingArea)
|
||||
{
|
||||
@@ -1035,7 +1043,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPRecentsAutosaveNameKey = @"CPRecentsAutosaveNameKey",
|
||||
CPSendsWholeSearchStringKey = @"CPSendsWholeSearchStringKey",
|
||||
|
||||
+284
-153
@@ -71,7 +71,7 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
@"right-segment-bezel-color": [CPNull null],
|
||||
@"center-segment-bezel-color": [CPNull null],
|
||||
@"divider-bezel-color": [CPNull null],
|
||||
@"divider-thickness": 1.0,
|
||||
@"divider-thickness": 1.0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
// Working with Individual Segments
|
||||
/*!
|
||||
Sets the width of the specified segment.
|
||||
@param aWidth the new width for the segment
|
||||
@param aWidth the new width for the segment (0 for automatic width)
|
||||
@param aSegment the segment to set the width for
|
||||
@throws CPRangeException if \c aSegment is out of bounds
|
||||
*/
|
||||
@@ -420,7 +420,14 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
|
||||
[segment setSelected:isSelected];
|
||||
|
||||
_themeStates[aSegment] = isSelected ? CPThemeStateSelected : CPThemeStateNormal;
|
||||
if (_themeStates[aSegment] == undefined)
|
||||
_themeStates[aSegment] = CPThemeStateNormal;
|
||||
|
||||
// Update state using .and() / .without() to preserve existing states (like Disabled)
|
||||
if (isSelected)
|
||||
_themeStates[aSegment] = _themeStates[aSegment].and(CPThemeStateSelected);
|
||||
else
|
||||
_themeStates[aSegment] = _themeStates[aSegment].without(CPThemeStateSelected);
|
||||
|
||||
// We need to do some cleanup if we only allow one selection.
|
||||
if (isSelected)
|
||||
@@ -432,7 +439,10 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
if (_trackingMode == CPSegmentSwitchTrackingSelectOne && oldSelectedSegment != aSegment && oldSelectedSegment != -1 && oldSelectedSegment < _segments.length)
|
||||
{
|
||||
[_segments[oldSelectedSegment] setSelected:NO];
|
||||
_themeStates[oldSelectedSegment] = CPThemeStateNormal;
|
||||
|
||||
// Update old segment using .without() to preserve other states
|
||||
// Previously: _themeStates[oldSelectedSegment] = CPThemeStateNormal;
|
||||
_themeStates[oldSelectedSegment] = _themeStates[oldSelectedSegment].without(CPThemeStateSelected);
|
||||
|
||||
[self drawSegmentBezel:oldSelectedSegment highlight:NO];
|
||||
}
|
||||
@@ -530,12 +540,24 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
|
||||
- (float)_leftOffsetForSegment:(unsigned)segment
|
||||
{
|
||||
if (segment == 0)
|
||||
return [self currentValueForThemeAttribute:@"bezel-inset"].left;
|
||||
if ([[self actualTheme] isCSSBased])
|
||||
{
|
||||
// CSS styling
|
||||
if (segment == 0)
|
||||
return 0;
|
||||
|
||||
var thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
|
||||
return [self _leftOffsetForSegment:segment - 1] + CGRectGetWidth([self frameForSegment:segment - 1]) - 2; // FIXME: -2 for collapse of borders
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy styling
|
||||
if (segment == 0)
|
||||
return [self currentValueForThemeAttribute:@"bezel-inset"].left;
|
||||
|
||||
return [self _leftOffsetForSegment:segment - 1] + CGRectGetWidth([self frameForSegment:segment - 1]) + thickness;
|
||||
var thickness = [self currentValueForThemeAttribute:@"divider-thickness"];
|
||||
|
||||
return [self _leftOffsetForSegment:segment - 1] + CGRectGetWidth([self frameForSegment:segment - 1]) + thickness;
|
||||
}
|
||||
}
|
||||
|
||||
- (unsigned)_indexOfLastSegment
|
||||
@@ -561,26 +583,36 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
}
|
||||
else if (aName === "right-segment-bezel")
|
||||
{
|
||||
return CGRectMake(CGRectGetWidth([self bounds]) - contentInset.right,
|
||||
bezelInset.top,
|
||||
contentInset.right,
|
||||
height);
|
||||
// we have to FLOOR the coordinates to prevent a 1px glitch in Safari
|
||||
return CGRectMake(FLOOR(CGRectGetWidth([self bounds]) - contentInset.right),
|
||||
FLOOR(bezelInset.top),
|
||||
FLOOR(contentInset.right),
|
||||
FLOOR(height));
|
||||
}
|
||||
else if (aName.indexOf("segment-bezel") === 0)
|
||||
{
|
||||
var segment = parseInt(aName.substring("segment-bezel-".length), 10),
|
||||
if ([[self actualTheme] isCSSBased])
|
||||
{
|
||||
var segment = parseInt(aName.substring("segment-bezel-".length), 10);
|
||||
|
||||
return [self bezelFrameForSegment:segment];
|
||||
}
|
||||
else
|
||||
{
|
||||
var segment = parseInt(aName.substring("segment-bezel-".length), 10),
|
||||
frame = CGRectCreateCopy([self frameForSegment:segment]);
|
||||
|
||||
if (segment === 0)
|
||||
{
|
||||
frame.origin.x += contentInset.left;
|
||||
frame.size.width -= contentInset.left;
|
||||
if (segment === 0)
|
||||
{
|
||||
frame.origin.x += contentInset.left;
|
||||
frame.size.width -= contentInset.left;
|
||||
}
|
||||
|
||||
if (segment === [self segmentCount] - 1)
|
||||
frame.size.width = CGRectGetWidth([self bounds]) - contentInset.right - frame.origin.x;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
if (segment === [self segmentCount] - 1)
|
||||
frame.size.width = CGRectGetWidth([self bounds]) - contentInset.right - frame.origin.x;
|
||||
|
||||
return frame;
|
||||
}
|
||||
else if (aName.indexOf("divider-bezel") === 0)
|
||||
{
|
||||
@@ -619,48 +651,86 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
if ([self segmentCount] <= 0)
|
||||
return;
|
||||
|
||||
var themeState = _themeStates[0],
|
||||
isDisabled = [self hasThemeState:CPThemeStateDisabled],
|
||||
isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
|
||||
isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini];
|
||||
// Check for HUD state globally
|
||||
var isHUD = [self hasThemeState:CPThemeStateHUD];
|
||||
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
themeState = themeState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
|
||||
inState:themeState],
|
||||
|
||||
leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[leftBezelView setBackgroundColor:leftCapColor];
|
||||
|
||||
var themeState = _themeStates[_themeStates.length - 1];
|
||||
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
themeState = themeState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
|
||||
inState:themeState],
|
||||
|
||||
rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[rightBezelView setBackgroundColor:rightCapColor];
|
||||
|
||||
for (var i = 0, count = _themeStates.length; i < count; i++)
|
||||
if ([[self actualTheme] isCSSBased])
|
||||
{
|
||||
var themeState = _themeStates[i];
|
||||
// CSS Styling
|
||||
|
||||
var isDisabled = [self hasThemeState:CPThemeStateDisabled],
|
||||
isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
|
||||
isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini],
|
||||
isKeyWindow = [self hasThemeState:CPThemeStateKeyWindow];
|
||||
|
||||
for (var i = 0, count = _themeStates.length; i < count; i++)
|
||||
{
|
||||
var themeState = _themeStates[i];
|
||||
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
themeState = themeState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
// Apply HUD state to lookup
|
||||
if (isHUD)
|
||||
themeState = themeState.and(CPThemeStateHUD);
|
||||
|
||||
themeState = isKeyWindow ? themeState.and(CPThemeStateKeyWindow) : themeState;
|
||||
|
||||
var bezelColor,
|
||||
segment = _segments[i],
|
||||
bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil],
|
||||
contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:"segment-bezel-" + i];
|
||||
|
||||
if (i == 0)
|
||||
bezelColor = [self valueForThemeAttribute:@"left-segment-bezel-color" inState:themeState];
|
||||
else if (i < count - 1)
|
||||
bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color" inState:themeState];
|
||||
else
|
||||
bezelColor = [self valueForThemeAttribute:@"right-segment-bezel-color" inState:themeState];
|
||||
|
||||
[bezelView setBackgroundColor:bezelColor];
|
||||
|
||||
// Trick : Put selected segments over unselected ones to automaticaly manage borders
|
||||
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
contentView._DOMElement.style.zIndex = ([segment selected] ? 1 : 0);
|
||||
bezelView._DOMElement.style.zIndex = ([segment selected] ? 1 : 0);
|
||||
}
|
||||
|
||||
[contentView setText:[segment label]];
|
||||
[contentView setImage:[segment image]];
|
||||
|
||||
[contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
|
||||
[contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:([segment enabled] ? themeState : themeState.and(CPThemeStateDisabled))]];
|
||||
[contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
|
||||
[contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
|
||||
[contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
|
||||
[contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
|
||||
[contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
|
||||
[contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
|
||||
|
||||
if ([segment image] && [segment label])
|
||||
[contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
|
||||
else if ([segment image])
|
||||
[contentView setImagePosition:CPImageOnly];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy (Canvas) Styling
|
||||
var themeState = _themeStates[0],
|
||||
isDisabled = [self hasThemeState:CPThemeStateDisabled],
|
||||
isControlSizeSmall = [self hasThemeState:CPThemeStateControlSizeSmall],
|
||||
isControlSizeMini = [self hasThemeState:CPThemeStateControlSizeMini];
|
||||
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
@@ -669,59 +739,114 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
else if (isControlSizeMini)
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
|
||||
inState:themeState],
|
||||
// Apply HUD state to Left Cap
|
||||
if (isHUD)
|
||||
themeState = themeState.and(CPThemeStateHUD);
|
||||
|
||||
bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
var leftCapColor = [self valueForThemeAttribute:@"left-segment-bezel-color"
|
||||
inState:themeState],
|
||||
|
||||
[bezelView setBackgroundColor:bezelColor];
|
||||
leftBezelView = [self layoutEphemeralSubviewNamed:@"left-segment-bezel"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
// layout image/title views
|
||||
var segment = _segments[i],
|
||||
contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
|
||||
[leftBezelView setBackgroundColor:leftCapColor];
|
||||
|
||||
[contentView setText:[segment label]];
|
||||
[contentView setImage:[segment image]];
|
||||
var themeState = _themeStates[_themeStates.length - 1];
|
||||
|
||||
[contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
|
||||
[contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
|
||||
[contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
|
||||
[contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
|
||||
[contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
|
||||
[contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
|
||||
[contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
|
||||
[contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
|
||||
|
||||
if ([segment image] && [segment label])
|
||||
[contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
|
||||
else if ([segment image])
|
||||
[contentView setImagePosition:CPImageOnly];
|
||||
|
||||
if (i == count - 1)
|
||||
continue;
|
||||
|
||||
|
||||
var borderState = _themeStates[i].and(_themeStates[i + 1]);
|
||||
|
||||
borderState = isDisabled ? borderState.and(CPThemeStateDisabled) : borderState;
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
borderState = borderState.and(CPThemeStateControlSizeSmall);
|
||||
themeState = themeState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
borderState = borderState.and(CPThemeStateControlSizeMini);
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
|
||||
inState:borderState],
|
||||
// Apply HUD state to Right Cap
|
||||
if (isHUD)
|
||||
themeState = themeState.and(CPThemeStateHUD);
|
||||
|
||||
var rightCapColor = [self valueForThemeAttribute:@"right-segment-bezel-color"
|
||||
inState:themeState],
|
||||
|
||||
rightBezelView = [self layoutEphemeralSubviewNamed:@"right-segment-bezel"
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[rightBezelView setBackgroundColor:rightCapColor];
|
||||
|
||||
for (var i = 0, count = _themeStates.length; i < count; i++)
|
||||
{
|
||||
var themeState = _themeStates[i];
|
||||
|
||||
themeState = isDisabled ? themeState.and(CPThemeStateDisabled) : themeState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
themeState = themeState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
themeState = themeState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
// Apply HUD state to Center Segments
|
||||
if (isHUD)
|
||||
themeState = themeState.and(CPThemeStateHUD);
|
||||
|
||||
var bezelColor = [self valueForThemeAttribute:@"center-segment-bezel-color"
|
||||
inState:themeState],
|
||||
|
||||
bezelView = [self layoutEphemeralSubviewNamed:"segment-bezel-" + i
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[bezelView setBackgroundColor:bezelColor];
|
||||
|
||||
// layout image/title views
|
||||
var segment = _segments[i],
|
||||
contentView = [self layoutEphemeralSubviewNamed:@"segment-content-" + i
|
||||
positioned:CPWindowAbove
|
||||
relativeToEphemeralSubviewNamed:@"segment-bezel-" + i];
|
||||
|
||||
[contentView setText:[segment label]];
|
||||
[contentView setImage:[segment image]];
|
||||
|
||||
[contentView setFont:[self valueForThemeAttribute:@"font" inState:themeState]];
|
||||
[contentView setTextColor:[self valueForThemeAttribute:@"text-color" inState:themeState]];
|
||||
[contentView setAlignment:[self valueForThemeAttribute:@"alignment" inState:themeState]];
|
||||
[contentView setVerticalAlignment:[self valueForThemeAttribute:@"vertical-alignment" inState:themeState]];
|
||||
[contentView setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode" inState:themeState]];
|
||||
[contentView setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color" inState:themeState]];
|
||||
[contentView setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset" inState:themeState]];
|
||||
[contentView setImageScaling:[self valueForThemeAttribute:@"image-scaling" inState:themeState]];
|
||||
|
||||
if ([segment image] && [segment label])
|
||||
[contentView setImagePosition:[self valueForThemeAttribute:@"image-position" inState:themeState]];
|
||||
else if ([segment image])
|
||||
[contentView setImagePosition:CPImageOnly];
|
||||
|
||||
if (i == count - 1)
|
||||
continue;
|
||||
|
||||
|
||||
var borderState = _themeStates[i].and(_themeStates[i + 1]);
|
||||
|
||||
borderState = isDisabled ? borderState.and(CPThemeStateDisabled) : borderState;
|
||||
|
||||
if (isControlSizeSmall)
|
||||
borderState = borderState.and(CPThemeStateControlSizeSmall);
|
||||
else if (isControlSizeMini)
|
||||
borderState = borderState.and(CPThemeStateControlSizeMini);
|
||||
|
||||
// Apply HUD state to Dividers
|
||||
if (isHUD)
|
||||
borderState = borderState.and(CPThemeStateHUD);
|
||||
|
||||
var borderColor = [self valueForThemeAttribute:@"divider-bezel-color"
|
||||
inState:borderState],
|
||||
|
||||
borderView = [self layoutEphemeralSubviewNamed:"divider-bezel-" + i
|
||||
positioned:CPWindowBelow
|
||||
relativeToEphemeralSubviewNamed:nil];
|
||||
|
||||
[borderView setBackgroundColor:borderColor];
|
||||
[borderView setBackgroundColor:borderColor];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,8 +927,10 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
label = [segment label],
|
||||
image = [segment image];
|
||||
|
||||
contentInsetWidth += ([[self actualTheme] isCSSBased] && ((aSegment == 0) || (aSegment == [self _indexOfLastSegment])) ? [self valueForThemeAttribute:@"divider-thickness" inState:themeState] : 0);
|
||||
|
||||
// add 1 pixel to account for possible fractional pixels at right edge
|
||||
width = (label ? [label sizeWithFont:[self font]].width + 1 : 4.0) + (image ? [image size].width : 0) + contentInsetWidth;
|
||||
width = CEIL((label ? [label sizeWithFont:[self font]].width + 1: 4.0) + (image ? [image size].width : 0) + contentInsetWidth);
|
||||
}
|
||||
|
||||
return CGRectMake(left, top, width, height);
|
||||
@@ -811,6 +938,14 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
|
||||
- (CGRect)contentFrameForSegment:(unsigned)aSegment
|
||||
{
|
||||
if ([[self actualTheme] isCSSBased])
|
||||
{
|
||||
var bezelFrame = [self bezelFrameForSegment:aSegment],
|
||||
contentInset = [self currentValueForThemeAttribute:@"content-inset"];
|
||||
|
||||
return CGRectInsetByInset(bezelFrame, contentInset);
|
||||
}
|
||||
|
||||
var height = [self currentValueForThemeAttribute:@"min-size"].height,
|
||||
contentInset = [self currentValueForThemeAttribute:@"content-inset"],
|
||||
width = CGRectGetWidth([self frameForSegment:aSegment]),
|
||||
@@ -873,68 +1008,64 @@ CPSegmentSwitchTrackingMomentary = 2;
|
||||
var type = [anEvent type],
|
||||
location = [self convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
|
||||
switch (type)
|
||||
if (type == CPLeftMouseUp)
|
||||
{
|
||||
case CPLeftMouseUp:
|
||||
|
||||
if (_trackingSegment === CPNotFound)
|
||||
return;
|
||||
|
||||
if (_trackingSegment === [self testSegment:location])
|
||||
{
|
||||
if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
|
||||
{
|
||||
[self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
|
||||
|
||||
// With ANY, _selectedSegment means last pressed.
|
||||
_selectedSegment = _trackingSegment;
|
||||
}
|
||||
else
|
||||
[self setSelected:YES forSegment:_trackingSegment];
|
||||
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
|
||||
if (_trackingMode == CPSegmentSwitchTrackingMomentary)
|
||||
{
|
||||
[self setSelected:NO forSegment:_trackingSegment];
|
||||
|
||||
_selectedSegment = CPNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
[self drawSegmentBezel:_trackingSegment highlight:NO];
|
||||
|
||||
_trackingSegment = CPNotFound;
|
||||
|
||||
if (_trackingSegment == -1)
|
||||
return;
|
||||
|
||||
case CPLeftMouseDown:
|
||||
|
||||
var trackingSegment = [self testSegment:location];
|
||||
if (trackingSegment > CPNotFound && [self isEnabledForSegment:trackingSegment])
|
||||
if (_trackingSegment === [self testSegment:location])
|
||||
{
|
||||
if (_trackingMode == CPSegmentSwitchTrackingSelectAny)
|
||||
{
|
||||
_trackingHighlighted = YES;
|
||||
_trackingSegment = trackingSegment;
|
||||
[self drawSegmentBezel:_trackingSegment highlight:YES];
|
||||
[self setSelected:![self isSelectedForSegment:_trackingSegment] forSegment:_trackingSegment];
|
||||
|
||||
// With ANY, _selectedSegment means last pressed.
|
||||
_selectedSegment = _trackingSegment;
|
||||
}
|
||||
else
|
||||
[self setSelected:YES forSegment:_trackingSegment];
|
||||
|
||||
break;
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
|
||||
case CPLeftMouseDragged:
|
||||
|
||||
if (_trackingSegment === CPNotFound)
|
||||
return;
|
||||
|
||||
var highlighted = [self testSegment:location] === _trackingSegment;
|
||||
|
||||
if (highlighted != _trackingHighlighted)
|
||||
if (_trackingMode == CPSegmentSwitchTrackingMomentary)
|
||||
{
|
||||
_trackingHighlighted = highlighted;
|
||||
[self setSelected:NO forSegment:_trackingSegment];
|
||||
|
||||
[self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
|
||||
_selectedSegment = CPNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
[self drawSegmentBezel:_trackingSegment highlight:NO];
|
||||
|
||||
_trackingSegment = -1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == CPLeftMouseDown)
|
||||
{
|
||||
var trackingSegment = [self testSegment:location];
|
||||
if (trackingSegment > -1 && [self isEnabledForSegment:trackingSegment])
|
||||
{
|
||||
_trackingHighlighted = YES;
|
||||
_trackingSegment = trackingSegment;
|
||||
[self drawSegmentBezel:_trackingSegment highlight:YES];
|
||||
}
|
||||
}
|
||||
|
||||
else if (type == CPLeftMouseDragged)
|
||||
{
|
||||
if (_trackingSegment == -1)
|
||||
return;
|
||||
|
||||
var highlighted = [self testSegment:location] === _trackingSegment;
|
||||
|
||||
if (highlighted != _trackingHighlighted)
|
||||
{
|
||||
_trackingHighlighted = highlighted;
|
||||
|
||||
[self drawSegmentBezel:_trackingSegment highlight:_trackingHighlighted];
|
||||
}
|
||||
}
|
||||
|
||||
[CPApp setTarget:self selector:@selector(trackSegment:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES];
|
||||
|
||||
@@ -102,6 +102,7 @@ CPThemeStateShadowViewHeavy = CPThemeState("shadowview-style-heavy");
|
||||
[self setWeight:CPLightShadow];
|
||||
|
||||
[self setHitTests:NO];
|
||||
[self setClipsToBounds:NO];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -169,7 +170,6 @@ CPThemeStateShadowViewHeavy = CPThemeState("shadowview-style-heavy");
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
[self setBackgroundColor:[self currentValueForThemeAttribute:@"bezel-color"]];
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -312,6 +312,12 @@ var AFFINITY = 5;
|
||||
if (!trackRect || CGRectIsEmpty(trackRect))
|
||||
trackRect = bounds;
|
||||
|
||||
if (_allowsTickMarkValuesOnly)
|
||||
{
|
||||
[self closestTickMarkValueToValue:[self doubleValue]]; // we only need the side effect …
|
||||
_currentTickMarkSegment = _closestTickMarkIndex;
|
||||
}
|
||||
|
||||
if (_isCircular)
|
||||
{
|
||||
var angle = 3 * PI_2 - (1.0 - [self doubleValue] - _minValue) / (_maxValue - _minValue) * PI2,
|
||||
@@ -580,7 +586,7 @@ var AFFINITY = 5;
|
||||
return [self setFloatValue:1.0];
|
||||
}
|
||||
|
||||
#pragma mark - New methods as in High Sierra (10.13)
|
||||
// MARK: - New methods as in High Sierra (10.13)
|
||||
|
||||
/*!
|
||||
Creates and returns a continuous horizontal slider whose values range from 0.0 to 1.0.
|
||||
@@ -806,7 +812,7 @@ var AFFINITY = 5;
|
||||
return [self valueForThemeAttribute:@"left-track-color" inStates:normalState];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
// MARK: - Private methods
|
||||
|
||||
- (void)_refreshCachesAndStates
|
||||
{
|
||||
@@ -996,15 +1002,13 @@ var AFFINITY = 5;
|
||||
}
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
// Don't do this in nib2cib !..
|
||||
if (_allowsTickMarkValuesOnly)
|
||||
if (CPDOMAvailable && _allowsTickMarkValuesOnly)
|
||||
{
|
||||
[self setObjectValue:[self closestTickMarkValueToValue:[self doubleValue]]];
|
||||
_currentTickMarkSegment = _closestTickMarkIndex;
|
||||
[self sendAction:[self action] to:[self target]];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*! @ignore */
|
||||
@@ -1044,7 +1048,7 @@ var AFFINITY = 5;
|
||||
return value;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
// MARK: - Overrides
|
||||
|
||||
- (void)setFrameSize:(CGSize)aSize
|
||||
{
|
||||
@@ -1054,7 +1058,7 @@ var AFFINITY = 5;
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+11
-11
@@ -66,8 +66,8 @@ CPSoundPlayBackStatePause = 2;
|
||||
unsigned _implementedDelegateMethods;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)init
|
||||
{
|
||||
@@ -147,8 +147,8 @@ CPSoundPlayBackStatePause = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
/*!
|
||||
Sets the sound's delegate.
|
||||
@@ -166,8 +166,8 @@ CPSoundPlayBackStatePause = 2;
|
||||
_implementedDelegateMethods |= CPSoundDelegate_sound_didFinishPlaying_;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Events listener
|
||||
// MARK: -
|
||||
// MARK: Events listener
|
||||
|
||||
/*! @ignore
|
||||
*/
|
||||
@@ -199,8 +199,8 @@ CPSoundPlayBackStatePause = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Media controls
|
||||
// MARK: -
|
||||
// MARK: Media controls
|
||||
|
||||
/*!
|
||||
Play the sound.
|
||||
@@ -324,8 +324,8 @@ CPSoundPlayBackStatePause = 2;
|
||||
_audioTag.volume = aVolume;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Accessors
|
||||
// MARK: -
|
||||
// MARK: Accessors
|
||||
|
||||
/*!
|
||||
Returns the duration in seconds of the sound.
|
||||
@@ -364,4 +364,4 @@ CPSoundPlayBackStatePause = 2;
|
||||
[_delegate sound:self didFinishPlaying:finishedPlaying];
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
+65
-50
@@ -23,13 +23,11 @@
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "../Foundation/Foundation.h"
|
||||
|
||||
@import "CPButtonBar.j"
|
||||
@import "CPImage.j"
|
||||
@import "CPView.j"
|
||||
@import "CPCursor.j"
|
||||
@import "CPImage.j"
|
||||
@import "CPTrackingArea.j"
|
||||
@import "CPView.j"
|
||||
|
||||
@class CPUserDefaults
|
||||
@global CPApp
|
||||
@@ -64,28 +62,6 @@ var CPSplitViewDelegate_splitView_canCollapseSubview_
|
||||
CPSplitViewDelegate_splitViewDidResizeSubviews_ = 1 << 9,
|
||||
CPSplitViewDelegate_splitViewWillResizeSubviews_ = 1 << 10;
|
||||
|
||||
#define SPLIT_VIEW_MAYBE_POST_WILL_RESIZE() \
|
||||
if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0) \
|
||||
{ \
|
||||
[self _postNotificationWillResize]; \
|
||||
_suppressResizeNotificationsMask |= DidPostWillResizeNotification; \
|
||||
}
|
||||
|
||||
#define SPLIT_VIEW_MAYBE_POST_DID_RESIZE() \
|
||||
if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0) \
|
||||
_suppressResizeNotificationsMask |= DidSuppressResizeNotification; \
|
||||
else \
|
||||
[self _postNotificationDidResize];
|
||||
|
||||
#define SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION() \
|
||||
((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0)
|
||||
|
||||
#define SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(shouldSuppress) \
|
||||
if (shouldSuppress) \
|
||||
_suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications; \
|
||||
else \
|
||||
_suppressResizeNotificationsMask = 0;
|
||||
|
||||
CPSplitViewDidResizeSubviewsNotification = @"CPSplitViewDidResizeSubviewsNotification";
|
||||
CPSplitViewWillResizeSubviewsNotification = @"CPSplitViewWillResizeSubviewsNotification";
|
||||
|
||||
@@ -216,7 +192,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
[self _setVertical:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
// MARK: - Properties
|
||||
|
||||
- (CPSplitViewDividerStyle)dividerStyle
|
||||
{
|
||||
@@ -479,7 +455,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
return _delegate;
|
||||
}
|
||||
|
||||
#pragma mark - Subviews management
|
||||
// MARK: - Subviews management
|
||||
|
||||
// FIXME: il faut également tenir compte des button bars quand on ajouter / insert une vue.
|
||||
// Par exemple, si une button bar est placée sur la dernière vue, pas de resize à droite mais
|
||||
@@ -652,7 +628,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
_subviewsManagementDisabled = NO;
|
||||
}
|
||||
|
||||
#pragma mark - Layout subviews
|
||||
// MARK: - Layout subviews
|
||||
|
||||
- (CGRect)rectOfDividerAtIndex:(int)aDivider
|
||||
{
|
||||
@@ -735,7 +711,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
*/
|
||||
- (void)adjustSubviews
|
||||
{
|
||||
SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
|
||||
[self _maybePostWillResizeNotification];
|
||||
[self _postNotificationWillResize];
|
||||
|
||||
// 2 possibilities : we have, or not, enough flexible space to accomodate fixed size subviews
|
||||
@@ -817,13 +793,16 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
[self _distribute:remainingSpace amoung:fixedCount onFlexible:NO fromIndex:0 toIndex:nbSubviews-1];
|
||||
}
|
||||
|
||||
SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
|
||||
[self _maybePostDidResizeNotification];
|
||||
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
// Retrieve the current color based on the current theme state (Standard vs HUD)
|
||||
var currentDividerColor = [self dividerColor];
|
||||
|
||||
for (var i = 0, count = _arrangedSubviews.length, position = 0, origin; i < count; i++)
|
||||
{
|
||||
origin = CGPointMakeZero();
|
||||
@@ -838,6 +817,9 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
origin[_originComponent] = position;
|
||||
position += [_dividerSubviews[i] frame].size[_sizeComponent];
|
||||
|
||||
// Apply the color to the specific divider view
|
||||
[_dividerSubviews[i] setBackgroundColor:currentDividerColor];
|
||||
|
||||
[_dividerSubviews[i] setFrameOrigin:origin];
|
||||
}
|
||||
}
|
||||
@@ -845,7 +827,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
[self updateTrackingAreas];
|
||||
}
|
||||
|
||||
#pragma mark - Private layout utilities
|
||||
// MARK: - Private layout utilities
|
||||
|
||||
- (void)_distribute:(CPInteger)remainingSpace amoung:(CPInteger)count onFlexible:(BOOL)onFlexible fromIndex:(CPInteger)fromIndex toIndex:(CPInteger)toIndex
|
||||
{
|
||||
@@ -900,7 +882,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
[_ratios addObject:(_initialSizes[i] / fixedSpace)];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*!
|
||||
Returns YES if the supplied subview is collapsed, otherwise NO.
|
||||
@@ -1171,7 +1153,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
// Silently ignore bad positions which could result from odd delegate responses. We don't want these
|
||||
// bad results to go into the system and cause havoc with frame sizes as the split view tries to resize
|
||||
// its subviews.
|
||||
if (_IS_NUMERIC(proposedPosition))
|
||||
if (CPIsNumeric(proposedPosition))
|
||||
position = proposedPosition;
|
||||
|
||||
var proposedMax = [self maxPossiblePositionOfDividerAtIndex:dividerIndex],
|
||||
@@ -1181,10 +1163,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
proposedActualMin = [self _sendDelegateSplitViewConstrainMinCoordinate:proposedMin ofSubviewAt:dividerIndex],
|
||||
proposedActualMax = [self _sendDelegateSplitViewConstrainMaxCoordinate:proposedMax ofSubviewAt:dividerIndex];
|
||||
|
||||
if (_IS_NUMERIC(proposedActualMin))
|
||||
if (CPIsNumeric(proposedActualMin))
|
||||
actualMin = proposedActualMin;
|
||||
|
||||
if (_IS_NUMERIC(proposedActualMax))
|
||||
if (CPIsNumeric(proposedActualMax))
|
||||
actualMax = proposedActualMax;
|
||||
|
||||
var viewA = _arrangedSubviews[dividerIndex],
|
||||
@@ -1213,7 +1195,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
// autosave.
|
||||
_shouldRestoreFromAutosaveUnlessFrameSize = nil;
|
||||
|
||||
SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(YES);
|
||||
[self _setResizeNotificationsSuppressed:YES];
|
||||
|
||||
var realPosition = [self _realPositionForPosition:position ofDividerAtIndex:dividerIndex],
|
||||
viewA = _arrangedSubviews[dividerIndex],
|
||||
@@ -1230,9 +1212,9 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
if (preSize !== frameA.size[_sizeComponent])
|
||||
{
|
||||
SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
|
||||
[self _maybePostWillResizeNotification];
|
||||
[_arrangedSubviews[dividerIndex] setFrame:frameA];
|
||||
SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
|
||||
[self _maybePostDidResizeNotification];
|
||||
}
|
||||
|
||||
preSize = frameB.size[_sizeComponent];
|
||||
@@ -1247,9 +1229,9 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
if (preSize !== frameB.size[_sizeComponent] || preOrigin !== frameB.origin[_originComponent])
|
||||
{
|
||||
SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
|
||||
[self _maybePostWillResizeNotification];
|
||||
[_arrangedSubviews[dividerIndex + 1] setFrame:frameB];
|
||||
SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
|
||||
[self _maybePostDidResizeNotification];
|
||||
}
|
||||
|
||||
if (preCollapsePosition)
|
||||
@@ -1262,10 +1244,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
[self _updateRatios];
|
||||
|
||||
if (SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION())
|
||||
if ([self _didSuppressResizeNotifications])
|
||||
[self _postNotificationDidResize];
|
||||
|
||||
SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(NO);
|
||||
[self _setResizeNotificationsSuppressed:NO];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -1287,8 +1269,11 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
- (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(CPUInteger)dividerIndex
|
||||
{
|
||||
// For compatibility with previous behavior
|
||||
[aButtonBar setAutomaticResizeControl:YES];
|
||||
[self attachButtonBar:aButtonBar];
|
||||
if ([aButtonBar respondsToSelector:@selector(setAutomaticResizeControl:)])
|
||||
{
|
||||
[aButtonBar setAutomaticResizeControl:YES];
|
||||
[self attachButtonBar:aButtonBar];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)attachButtonBar:(CPButtonBar)aButtonBar
|
||||
@@ -1485,9 +1470,39 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
return @"CPSplitView Subview Precollapse Positions " + theAutosaveName;
|
||||
}
|
||||
|
||||
- (void)_maybePostWillResizeNotification
|
||||
{
|
||||
if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0)
|
||||
{
|
||||
[self _postNotificationWillResize];
|
||||
_suppressResizeNotificationsMask |= DidPostWillResizeNotification;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_maybePostDidResizeNotification
|
||||
{
|
||||
if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0)
|
||||
_suppressResizeNotificationsMask |= DidSuppressResizeNotification;
|
||||
else
|
||||
[self _postNotificationDidResize];
|
||||
}
|
||||
|
||||
- (BOOL)_didSuppressResizeNotifications
|
||||
{
|
||||
return ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0);
|
||||
}
|
||||
|
||||
- (void)_setResizeNotificationsSuppressed:(BOOL)shouldSuppress
|
||||
{
|
||||
if (shouldSuppress)
|
||||
_suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications;
|
||||
else
|
||||
_suppressResizeNotificationsMask = 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (CPTrackingArea)
|
||||
{
|
||||
@@ -1567,7 +1582,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (CPSplitViewDelegate)
|
||||
|
||||
@@ -1745,7 +1760,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey",
|
||||
CPSplitViewIsVerticalKey = @"CPSplitViewIsVerticalKey",
|
||||
@@ -1870,7 +1885,7 @@ var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (Deprecated)
|
||||
|
||||
@@ -1898,5 +1913,5 @@ var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey",
|
||||
[self setDividerStyle:(shouldBePaneSplitter ? CPSplitViewDividerStylePaneSplitter : CPSplitViewDividerStyleThin)];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* CPSplitViewController.j
|
||||
*
|
||||
* Created by Daniel Boehringer on September 2, 2025.
|
||||
* Copyright (c) 2025 Daniel Boehringer. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "CPViewController.j"
|
||||
@import "CPSplitView.j"
|
||||
|
||||
var CPSplitViewControllerAutomaticDimension = -1.0;
|
||||
|
||||
/**
|
||||
* A container view controller that manages two or more child view
|
||||
* controllers in a split view interface.
|
||||
*
|
||||
* This class provides a controller-level abstraction for CPSplitView,
|
||||
* managing the addition, removal, and arrangement of view controllers
|
||||
* through CPSplitViewItem instances.
|
||||
*
|
||||
* This implementation was synthesized with the assistance of an LLM,
|
||||
* directed by the author.
|
||||
*/
|
||||
@implementation CPSplitViewController : CPViewController <CPSplitViewDelegate>
|
||||
{
|
||||
/** @private The underlying split view that arranges the child views. */
|
||||
CPSplitView _splitView;
|
||||
|
||||
/** @private An array of CPSplitViewItem objects managed by this controller. */
|
||||
CPMutableArray _splitViewItems;
|
||||
|
||||
/** @private The minimum thickness for sidebars to be displayed inline. */
|
||||
CPNumber _minimumThicknessForInlineSidebars;
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_splitViewItems = [CPMutableArray array];
|
||||
_minimumThicknessForInlineSidebars = 20.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
if (!_splitView)
|
||||
{
|
||||
_splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0,0,400,400)];
|
||||
[_splitView setDelegate:self];
|
||||
}
|
||||
[self setView:_splitView];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
for (var i = 0; i < [_splitViewItems count]; i++)
|
||||
{
|
||||
var viewController = [[_splitViewItems objectAtIndex:i] viewController];
|
||||
[[self splitView] addArrangedSubview:[viewController view]];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessors
|
||||
|
||||
/**
|
||||
* Returns the CPSplitView instance managed by the controller.
|
||||
*
|
||||
* @returns {CPSplitView} The split view.
|
||||
*/
|
||||
- (CPSplitView)splitView
|
||||
{
|
||||
return _splitView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom split view for the controller.
|
||||
*
|
||||
* @param {CPSplitView} splitView The custom split view to use.
|
||||
*/
|
||||
- (void)setSplitView:(CPSplitView)splitView
|
||||
{
|
||||
if (_splitView !== splitView)
|
||||
{
|
||||
_splitView = splitView;
|
||||
[_splitView setDelegate:self];
|
||||
if ([self isViewLoaded])
|
||||
[self setView:_splitView];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of split view items.
|
||||
*
|
||||
* @returns {CPArray} The array of CPSplitViewItem objects.
|
||||
*/
|
||||
- (CPArray)splitViewItems
|
||||
{
|
||||
return _splitViewItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the array of split view items, replacing any existing items.
|
||||
*
|
||||
* @param {CPArray} splitViewItems An array of CPSplitViewItem objects.
|
||||
*/
|
||||
- (void)setSplitViewItems:(CPArray)splitViewItems
|
||||
{
|
||||
// Remove all existing items
|
||||
while ([_splitViewItems count] > 0)
|
||||
{
|
||||
[self removeSplitViewItem:[_splitViewItems lastObject]];
|
||||
}
|
||||
|
||||
// Add new items
|
||||
for (var i = 0; i < [splitViewItems count]; i++)
|
||||
{
|
||||
[self addSplitViewItem:[splitViewItems objectAtIndex:i]];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Managing Split View Items
|
||||
|
||||
/**
|
||||
* Adds a split view item to the end of the split view.
|
||||
*
|
||||
* @param {CPSplitViewItem} splitViewItem The split view item to add.
|
||||
*/
|
||||
- (void)addSplitViewItem:(CPSplitViewItem)splitViewItem
|
||||
{
|
||||
[self insertSplitViewItem:splitViewItem atIndex:[_splitViewItems count]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a split view item at a specific index.
|
||||
*
|
||||
* @param {CPSplitViewItem} splitViewItem The split view item to insert.
|
||||
* @param {CPInteger} index The zero-based index at which to insert the item.
|
||||
*/
|
||||
- (void)insertSplitViewItem:(CPSplitViewItem)splitViewItem atIndex:(CPInteger)index
|
||||
{
|
||||
[splitViewItem _setSplitViewController:self];
|
||||
[_splitViewItems insertObject:splitViewItem atIndex:index];
|
||||
[self addChildViewController:[splitViewItem viewController]];
|
||||
|
||||
if ([self isViewLoaded])
|
||||
[[self splitView] insertArrangedSubview:[[splitViewItem viewController] view] atIndex:index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified split view item.
|
||||
*
|
||||
* @param {CPSplitViewItem} splitViewItem The split view item to remove.
|
||||
*/
|
||||
- (void)removeSplitViewItem:(CPSplitViewItem)splitViewItem
|
||||
{
|
||||
var viewController = [splitViewItem viewController];
|
||||
if ([self isViewLoaded])
|
||||
[[viewController view] removeFromSuperview];
|
||||
|
||||
[viewController removeFromParentViewController];
|
||||
[splitViewItem _setSplitViewController:nil];
|
||||
[_splitViewItems removeObject:splitViewItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the split view item associated with a given view controller.
|
||||
*
|
||||
* @param {CPViewController} viewController The view controller to find.
|
||||
* @returns {CPSplitViewItem | null} The corresponding split view item, or nil if not found.
|
||||
*/
|
||||
- (CPSplitViewItem)splitViewItemForViewController:(CPViewController)viewController
|
||||
{
|
||||
for (var i = 0; i < [_splitViewItems count]; i++)
|
||||
{
|
||||
var item = [_splitViewItems objectAtIndex:i];
|
||||
if ([item viewController] === viewController)
|
||||
return item;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// MARK: - Managing Sidebars and Inspectors
|
||||
|
||||
/**
|
||||
* Toggles the collapsed state of the first split view item, typically a sidebar.
|
||||
*
|
||||
* @param {id} sender The object that initiated the action.
|
||||
*/
|
||||
- (void)toggleSidebar:(id)sender
|
||||
{
|
||||
if ([_splitViewItems count] > 0)
|
||||
{
|
||||
var sidebarItem = [_splitViewItems objectAtIndex:0];
|
||||
[sidebarItem setCollapsed:![sidebarItem isCollapsed]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the collapsed state of the last split view item, typically an inspector.
|
||||
*
|
||||
* @param {id} sender The object that initiated the action.
|
||||
*/
|
||||
- (void)toggleInspector:(id)sender
|
||||
{
|
||||
if ([_splitViewItems count] > 1)
|
||||
{
|
||||
var inspectorItem = [_splitViewItems lastObject];
|
||||
[inspectorItem setCollapsed:![inspectorItem isCollapsed]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum thickness for sidebars to be displayed inline.
|
||||
*
|
||||
* @returns {CPNumber} The minimum thickness.
|
||||
*/
|
||||
- (CPNumber)minimumThicknessForInlineSidebars
|
||||
{
|
||||
return _minimumThicknessForInlineSidebars;
|
||||
}
|
||||
|
||||
// MARK: - CPSplitViewDelegate Methods
|
||||
|
||||
// Note: A more complete implementation would forward these delegate methods
|
||||
// to a separate delegate property on the CPSplitViewController itself.
|
||||
// For now, these are stubbed to demonstrate where they would be handled.
|
||||
|
||||
- (BOOL)splitView:(CPSplitView)splitView canCollapseSubview:(CPView)subview
|
||||
{
|
||||
// Default behavior: allow all subviews to be collapsed.
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)splitView:(CPSplitView)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(CPInteger)dividerIndex
|
||||
{
|
||||
return proposedMinimumPosition;
|
||||
}
|
||||
|
||||
- (CGFloat)splitView:(CPSplitView)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(CPInteger)dividerIndex
|
||||
{
|
||||
return proposedMaximumPosition;
|
||||
}
|
||||
|
||||
- (CGRect)splitView:(CPSplitView)splitView effectiveRect:(CGRect)proposedEffectiveRect forDrawnRect:(CGRect)drawnRect ofDividerAtIndex:(CPInteger)dividerIndex
|
||||
{
|
||||
// Default behavior: return the proposed rectangle.
|
||||
// This can be overridden to provide a larger or custom hit area for the divider.
|
||||
return proposedEffectiveRect;
|
||||
}
|
||||
|
||||
- (void)splitViewDidResizeSubviews:(CPNotification)notification
|
||||
{
|
||||
// Can be used to respond to user-initiated resizing.
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* An object that manages a view controller within a CPSplitViewController.
|
||||
*
|
||||
* A CPSplitViewItem acts as a wrapper around a CPViewController,
|
||||
* maintaining properties like its collapsed state within the parent
|
||||
* split view controller.
|
||||
*/
|
||||
@implementation CPSplitViewItem : CPObject
|
||||
{
|
||||
/** @private The view controller managed by this item. */
|
||||
CPViewController _viewController;
|
||||
|
||||
/** @private A boolean indicating whether the item is collapsed. */
|
||||
BOOL _isCollapsed;
|
||||
|
||||
/** @private A weak reference to the owning split view controller. */
|
||||
CPSplitViewController _splitViewController;
|
||||
}
|
||||
|
||||
// MARK: - Class Methods
|
||||
|
||||
/**
|
||||
* Creates and returns a new split view item with the specified view controller.
|
||||
*
|
||||
* @param {CPViewController} viewController The view controller for the new item.
|
||||
* @returns {instancetype} A new CPSplitViewItem instance.
|
||||
*/
|
||||
+ (instancetype)splitViewItemWithViewController:(CPViewController)viewController
|
||||
{
|
||||
return [[self alloc] initWithViewController:viewController];
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
* Initializes a new split view item with the specified view controller.
|
||||
*
|
||||
* @param {CPViewController} viewController The view controller for the new item.
|
||||
* @returns {id} The initialized CPSplitViewItem instance.
|
||||
*/
|
||||
- (id)initWithViewController:(CPViewController)viewController
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_viewController = viewController;
|
||||
_isCollapsed = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: - Accessors
|
||||
|
||||
/**
|
||||
* Returns the view controller associated with the item.
|
||||
*
|
||||
* @returns {CPViewController} The associated view controller.
|
||||
*/
|
||||
- (CPViewController)viewController
|
||||
{
|
||||
return _viewController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether the item is collapsed.
|
||||
*
|
||||
* @returns {BOOL} YES if the item is collapsed, otherwise NO.
|
||||
*/
|
||||
- (BOOL)isCollapsed
|
||||
{
|
||||
return _isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the collapsed state of the item.
|
||||
* When collapsed, the view controller's view is hidden.
|
||||
*
|
||||
* @param {BOOL} shouldCollapse YES to collapse the item, NO to expand it.
|
||||
*/
|
||||
- (void)setCollapsed:(BOOL)shouldCollapse
|
||||
{
|
||||
if (_isCollapsed === shouldCollapse)
|
||||
return;
|
||||
|
||||
_isCollapsed = shouldCollapse;
|
||||
[[_viewController view] setHidden:shouldCollapse];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the split view controller that owns this item.
|
||||
*
|
||||
* @returns {CPSplitViewController | null} The parent split view controller.
|
||||
*/
|
||||
- (CPSplitViewController)splitViewController
|
||||
{
|
||||
return _splitViewController;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Sets the owning split view controller. This method is for internal use by
|
||||
* CPSplitViewController.
|
||||
*
|
||||
* @param {CPSplitViewController} splitViewController The parent controller.
|
||||
*/
|
||||
- (void)_setSplitViewController:(CPSplitViewController)splitViewController
|
||||
{
|
||||
_splitViewController = splitViewController;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
* CPStackView.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by Daniel Boehringer.
|
||||
* Copyright 2025, Cappuccino Project.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
/*
|
||||
* PLACEHOLDER IMPLEMENTATION — READ BEFORE USE OR MODIFICATION.
|
||||
*
|
||||
* This class lays out views by direct, procedural arithmetic. It has no
|
||||
* constraint solver. It cannot compress, expand, or negotiate space among
|
||||
* views the way NSStackView does; it only places views at their existing
|
||||
* frame size, in order, separated by fixed spacing.
|
||||
*
|
||||
* Known, accepted limitations:
|
||||
* - `distribution` is stored but has no effect on layout. Fill,
|
||||
* FillEqually, FillProportionally, and EqualSpacing are unimplemented.
|
||||
* - Center-gravity views are clamped against the Leading edge only; if
|
||||
* Leading + Center + Trailing content overflows the container, Center
|
||||
* views can overlap Trailing views instead of compressing.
|
||||
* - `visibilityPriority:forView:` only supports the two extreme values
|
||||
* (MustHold / NotVisible). Intermediate priorities are accepted but
|
||||
* have no defined effect.
|
||||
* - No guarantee is made of correctness beyond what a single manual
|
||||
* test (Tests/Manual/CPStackViewTest) exercises: orientation, the
|
||||
* three gravity areas, alignment switching, spacing, and hidden-view
|
||||
* detachment. Insertion, removal, custom spacing, and the CPCoding
|
||||
* archive path are implemented but not verified by that test.
|
||||
*
|
||||
* This exists to give AppKit a working CPStackView symbol now, not to be
|
||||
* a durable design. It is expected to be replaced by a constraint-solver
|
||||
* based implementation (Kiwi.js) when time permits. Do not build on its
|
||||
* internal layout algorithm as if it were a stable foundation.
|
||||
*/
|
||||
|
||||
@import "CPView.j"
|
||||
@import <Foundation/CPMapTable.j>
|
||||
|
||||
// MARK: -
|
||||
// MARK: Minimal local type definitions
|
||||
//
|
||||
// These types support this file only. They are not shared with the rest
|
||||
// of AppKit. A future constraint-solver based Auto Layout engine will
|
||||
// replace them. Numeric values match the equivalent Cocoa constants
|
||||
// (NSUserInterfaceLayoutOrientation, NSLayoutAttribute) so that a later,
|
||||
// solver-based CPLayoutAttribute can reuse these numbers without a
|
||||
// renumbering pass.
|
||||
|
||||
@typedef CPUserInterfaceLayoutOrientation
|
||||
CPUserInterfaceLayoutOrientationHorizontal = 0;
|
||||
CPUserInterfaceLayoutOrientationVertical = 1;
|
||||
|
||||
@typedef CPLayoutAttribute
|
||||
CPLayoutAttributeLeft = 1;
|
||||
CPLayoutAttributeRight = 2;
|
||||
CPLayoutAttributeTop = 3;
|
||||
CPLayoutAttributeBottom = 4;
|
||||
CPLayoutAttributeLeading = 5;
|
||||
CPLayoutAttributeTrailing = 6;
|
||||
CPLayoutAttributeWidth = 7;
|
||||
CPLayoutAttributeHeight = 8;
|
||||
CPLayoutAttributeCenterX = 9;
|
||||
CPLayoutAttributeCenterY = 10;
|
||||
|
||||
@typedef CPEdgeInsets
|
||||
|
||||
/*!
|
||||
Creates a CPEdgeInsets. Argument order matches Cocoa's NSEdgeInsetsMake
|
||||
(top, left, bottom, right). Storage reuses the existing CGInset struct,
|
||||
whose field order is (top, right, bottom, left).
|
||||
*/
|
||||
function CPEdgeInsetsMake(top, left, bottom, right)
|
||||
{
|
||||
return CGInsetMake(top, right, bottom, left);
|
||||
}
|
||||
|
||||
function CPEdgeInsetsEqualToEdgeInsets(lhsInsets, rhsInsets)
|
||||
{
|
||||
return CGInsetEqualToInset(lhsInsets, rhsInsets);
|
||||
}
|
||||
|
||||
// Gravity Areas
|
||||
@typedef CPStackViewGravity
|
||||
CPStackViewGravityTop = 1;
|
||||
CPStackViewGravityLeading = 1;
|
||||
CPStackViewGravityCenter = 2;
|
||||
CPStackViewGravityBottom = 3;
|
||||
CPStackViewGravityTrailing = 3;
|
||||
|
||||
// Distribution (Deprecated in modern macOS, but kept for compatibility/logic)
|
||||
@typedef CPStackViewDistribution
|
||||
CPStackViewDistributionGravityAreas = 0;
|
||||
CPStackViewDistributionFill = 1;
|
||||
CPStackViewDistributionFillEqually = 2;
|
||||
CPStackViewDistributionFillProportionally = 3;
|
||||
CPStackViewDistributionEqualSpacing = 4;
|
||||
CPStackViewDistributionEqualCentering = 5;
|
||||
|
||||
// Visibility Priority
|
||||
@typedef CPStackViewVisibilityPriority
|
||||
CPStackViewVisibilityPriorityMustHold = 1000.0;
|
||||
CPStackViewVisibilityPriorityNotVisible = 0.0;
|
||||
|
||||
var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPStackView
|
||||
|
||||
CPStackView arranges an array of views horizontally or vertically and updates
|
||||
their placement and sizing when the window size changes.
|
||||
|
||||
Unlike a simple list, CPStackView supports "Gravity Areas" (Leading, Center, Trailing),
|
||||
allowing you to pin groups of views to specific sections of the layout.
|
||||
*/
|
||||
@implementation CPStackView : CPView
|
||||
{
|
||||
CPUserInterfaceLayoutOrientation _orientation;
|
||||
CPLayoutAttribute _alignment;
|
||||
CPStackViewDistribution _distribution;
|
||||
float _spacing;
|
||||
CPEdgeInsets _edgeInsets;
|
||||
|
||||
BOOL _detachesHiddenViews;
|
||||
|
||||
// View Storage by Gravity
|
||||
CPMutableArray _viewsLeading;
|
||||
CPMutableArray _viewsCenter;
|
||||
CPMutableArray _viewsTrailing;
|
||||
|
||||
// Internal cache of all arranged subviews to maintain order for hittesting/iterating
|
||||
CPMutableArray _arrangedSubviews;
|
||||
|
||||
// Custom Spacing storage
|
||||
CPMapTable _customSpacings;
|
||||
|
||||
// Visibility Priorities
|
||||
CPMapTable _visibilityPriorities;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
+ (CPStackView)stackViewWithViews:(CPArray)views
|
||||
{
|
||||
var stackView = [[CPStackView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
for (var i = 0, count = [views count]; i < count; i++)
|
||||
[stackView addView:views[i] inGravity:CPStackViewGravityLeading];
|
||||
|
||||
return stackView;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
if (self = [super initWithFrame:aFrame])
|
||||
{
|
||||
_orientation = CPUserInterfaceLayoutOrientationHorizontal;
|
||||
_alignment = CPLayoutAttributeCenterY; // Default alignment
|
||||
_distribution = CPStackViewDistributionGravityAreas;
|
||||
_spacing = 8.0; // Default Cocoa spacing
|
||||
_edgeInsets = CPEdgeInsetsMake(0, 0, 0, 0);
|
||||
_detachesHiddenViews = YES;
|
||||
|
||||
_viewsLeading = [[CPMutableArray alloc] init];
|
||||
_viewsCenter = [[CPMutableArray alloc] init];
|
||||
_viewsTrailing = [[CPMutableArray alloc] init];
|
||||
_arrangedSubviews = [[CPMutableArray alloc] init];
|
||||
|
||||
_customSpacings = [[CPMapTable alloc] init];
|
||||
_visibilityPriorities = [[CPMapTable alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Configuration
|
||||
|
||||
/*!
|
||||
The horizontal or vertical layout direction of the stack view.
|
||||
*/
|
||||
- (CPUserInterfaceLayoutOrientation)orientation
|
||||
{
|
||||
return _orientation;
|
||||
}
|
||||
|
||||
- (void)setOrientation:(CPUserInterfaceLayoutOrientation)anOrientation
|
||||
{
|
||||
if (_orientation === anOrientation)
|
||||
return;
|
||||
|
||||
_orientation = anOrientation;
|
||||
|
||||
// Reset default alignment based on new orientation if needed,
|
||||
// though usually developer sets alignment explicitly.
|
||||
// If switching to Vertical, CenterY makes less sense, usually CenterX.
|
||||
if (_orientation === CPUserInterfaceLayoutOrientationVertical)
|
||||
{
|
||||
if (_alignment === CPLayoutAttributeCenterY)
|
||||
_alignment = CPLayoutAttributeCenterX;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_alignment === CPLayoutAttributeCenterX)
|
||||
_alignment = CPLayoutAttributeCenterY;
|
||||
}
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The view alignment within the stack view.
|
||||
Common values:
|
||||
Horizontal: CPLayoutAttributeTop, CPLayoutAttributeBottom, CPLayoutAttributeCenterY, CPLayoutAttributeHeight (fill)
|
||||
Vertical: CPLayoutAttributeLeading, CPLayoutAttributeTrailing, CPLayoutAttributeCenterX, CPLayoutAttributeWidth (fill)
|
||||
*/
|
||||
- (CPLayoutAttribute)alignment
|
||||
{
|
||||
return _alignment;
|
||||
}
|
||||
|
||||
- (void)setAlignment:(CPLayoutAttribute)anAlignment
|
||||
{
|
||||
if (_alignment === anAlignment)
|
||||
return;
|
||||
|
||||
_alignment = anAlignment;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The distribution mode for the stack view.
|
||||
@note Not yet applied to layout. All views are laid out at their
|
||||
existing frame size regardless of this value, pending the
|
||||
constraint-solver based layout engine. The value is stored and
|
||||
returned so client code can read back what was set.
|
||||
*/
|
||||
- (CPStackViewDistribution)distribution
|
||||
{
|
||||
return _distribution;
|
||||
}
|
||||
|
||||
- (void)setDistribution:(CPStackViewDistribution)aDistribution
|
||||
{
|
||||
if (_distribution === aDistribution)
|
||||
return;
|
||||
|
||||
_distribution = aDistribution;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The minimum spacing, in points, between adjacent views in the stack view.
|
||||
*/
|
||||
- (float)spacing
|
||||
{
|
||||
return _spacing;
|
||||
}
|
||||
|
||||
- (void)setSpacing:(float)aSpacing
|
||||
{
|
||||
if (_spacing === aSpacing)
|
||||
return;
|
||||
|
||||
_spacing = aSpacing;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The geometric padding, in points, inside the stack view, surrounding its views.
|
||||
*/
|
||||
- (CPEdgeInsets)edgeInsets
|
||||
{
|
||||
return _edgeInsets;
|
||||
}
|
||||
|
||||
- (void)setEdgeInsets:(CPEdgeInsets)insets
|
||||
{
|
||||
if (CPEdgeInsetsEqualToEdgeInsets(_edgeInsets, insets))
|
||||
return;
|
||||
|
||||
_edgeInsets = insets;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
A Boolean value that indicates whether the stack view removes hidden views from its view hierarchy.
|
||||
*/
|
||||
- (BOOL)detachesHiddenViews
|
||||
{
|
||||
return _detachesHiddenViews;
|
||||
}
|
||||
|
||||
- (void)setDetachesHiddenViews:(BOOL)shouldDetach
|
||||
{
|
||||
if (_detachesHiddenViews === shouldDetach)
|
||||
return;
|
||||
|
||||
_detachesHiddenViews = shouldDetach;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Managing Views in Gravity Areas
|
||||
|
||||
- (CPArray)_containerForGravity:(CPStackViewGravity)gravity
|
||||
{
|
||||
if (gravity === CPStackViewGravityCenter)
|
||||
return _viewsCenter;
|
||||
else if (gravity === CPStackViewGravityTrailing) // or Bottom
|
||||
return _viewsTrailing;
|
||||
|
||||
return _viewsLeading; // Leading or Top
|
||||
}
|
||||
|
||||
/*!
|
||||
Rebuilds _arrangedSubviews from the three gravity containers, in
|
||||
Leading, Center, Trailing order. Call after any change to a gravity
|
||||
container so _arrangedSubviews stays a correct, single source of truth
|
||||
for ordering, rather than an incrementally and separately maintained
|
||||
(and error-prone) copy.
|
||||
*/
|
||||
- (void)_rebuildArrangedSubviews
|
||||
{
|
||||
_arrangedSubviews = [[CPMutableArray alloc] init];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsLeading];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsCenter];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsTrailing];
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds a view to the end of the stack view gravity area.
|
||||
*/
|
||||
- (void)addView:(CPView)aView inGravity:(CPStackViewGravity)gravity
|
||||
{
|
||||
var container = [self _containerForGravity:gravity];
|
||||
|
||||
// Check if view is already in a container
|
||||
if ([_arrangedSubviews containsObject:aView])
|
||||
[self removeView:aView];
|
||||
|
||||
[container addObject:aView];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
// Add as actual subview
|
||||
if ([aView superview] !== self)
|
||||
[self addSubview:aView];
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds a view to a stack view gravity area at a specified index position.
|
||||
*/
|
||||
- (void)insertView:(CPView)aView atIndex:(CPInteger)index inGravity:(CPStackViewGravity)gravity
|
||||
{
|
||||
var container = [self _containerForGravity:gravity];
|
||||
|
||||
if ([_arrangedSubviews containsObject:aView])
|
||||
[self removeView:aView];
|
||||
|
||||
if (index >= [container count])
|
||||
[container addObject:aView];
|
||||
else
|
||||
[container insertObject:aView atIndex:index];
|
||||
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
if ([aView superview] !== self)
|
||||
[self addSubview:aView];
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Specifies an array of views for a specified gravity area in the stack view, replacing any previous views in that area.
|
||||
*/
|
||||
- (void)setViews:(CPArray)views inGravity:(CPStackViewGravity)gravity
|
||||
{
|
||||
var container = [self _containerForGravity:gravity];
|
||||
|
||||
// Remove old views from superview
|
||||
for (var i = 0; i < [container count]; i++)
|
||||
[container[i] removeFromSuperview];
|
||||
|
||||
[container removeAllObjects];
|
||||
|
||||
for (var i = 0; i < [views count]; i++)
|
||||
{
|
||||
var newView = views[i];
|
||||
[container addObject:newView];
|
||||
[self addSubview:newView];
|
||||
}
|
||||
|
||||
[self _rebuildArrangedSubviews];
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Removes a specified view from the stack view.
|
||||
*/
|
||||
- (void)removeView:(CPView)aView
|
||||
{
|
||||
if (![_arrangedSubviews containsObject:aView])
|
||||
return;
|
||||
|
||||
[_viewsLeading removeObject:aView];
|
||||
[_viewsCenter removeObject:aView];
|
||||
[_viewsTrailing removeObject:aView];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
[aView removeFromSuperview];
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the array of views in the specified gravity area in the stack view.
|
||||
*/
|
||||
- (CPArray)viewsInGravity:(CPStackViewGravity)gravity
|
||||
{
|
||||
return [[self _containerForGravity:gravity] copy];
|
||||
}
|
||||
|
||||
/*!
|
||||
The array of views arranged by the stack view.
|
||||
*/
|
||||
- (CPArray)arrangedSubviews
|
||||
{
|
||||
return [_arrangedSubviews copy];
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds the specified view to the end of the arranged subviews list.
|
||||
(Defaults to Leading gravity if not specified).
|
||||
*/
|
||||
- (void)addArrangedSubview:(CPView)view
|
||||
{
|
||||
[self addView:view inGravity:CPStackViewGravityLeading];
|
||||
}
|
||||
|
||||
/*!
|
||||
Removes the provided view from the stack’s array of arranged subviews.
|
||||
*/
|
||||
- (void)removeArrangedSubview:(CPView)view
|
||||
{
|
||||
[self removeView:view];
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Custom Spacing
|
||||
|
||||
- (float)customSpacingAfterView:(CPView)aView
|
||||
{
|
||||
var val = [_customSpacings objectForKey:aView];
|
||||
if (val)
|
||||
return [val floatValue];
|
||||
|
||||
return CPStackViewSpacingUseDefault;
|
||||
}
|
||||
|
||||
- (void)setCustomSpacing:(float)spacing afterView:(CPView)aView
|
||||
{
|
||||
if (spacing === CPStackViewSpacingUseDefault)
|
||||
[_customSpacings removeObjectForKey:aView];
|
||||
else
|
||||
[_customSpacings setObject:spacing forKey:aView];
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
- (float)_spacingAfterView:(CPView)aView
|
||||
{
|
||||
var custom = [self customSpacingAfterView:aView];
|
||||
if (custom !== CPStackViewSpacingUseDefault)
|
||||
return custom;
|
||||
return _spacing;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Visibility Priority
|
||||
|
||||
- (void)setVisibilityPriority:(float)priority forView:(CPView)aView
|
||||
{
|
||||
[_visibilityPriorities setObject:priority forKey:aView];
|
||||
|
||||
if (priority === CPStackViewVisibilityPriorityNotVisible)
|
||||
{
|
||||
[aView setHidden:YES];
|
||||
}
|
||||
else if (priority === CPStackViewVisibilityPriorityMustHold)
|
||||
{
|
||||
[aView setHidden:NO];
|
||||
}
|
||||
// Note: Intermediate priorities require complex constraint logic
|
||||
// or a multi-pass layout system to determine fitting, which is
|
||||
// simplified here to basic Hidden/Visible states.
|
||||
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
- (float)visibilityPriorityForView:(CPView)aView
|
||||
{
|
||||
var val = [_visibilityPriorities objectForKey:aView];
|
||||
if (val)
|
||||
return [val floatValue];
|
||||
return CPStackViewVisibilityPriorityMustHold;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Layout
|
||||
|
||||
- (void)resizeSubviewsWithOldSize:(CGSize)oldSize
|
||||
{
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
if (_orientation === CPUserInterfaceLayoutOrientationVertical)
|
||||
[self _layoutVertical];
|
||||
else
|
||||
[self _layoutHorizontal];
|
||||
}
|
||||
|
||||
- (void)_layoutHorizontal
|
||||
{
|
||||
var bounds = [self bounds],
|
||||
availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right,
|
||||
availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom,
|
||||
currentX = _edgeInsets.left;
|
||||
|
||||
// 1. Layout Leading Views
|
||||
currentX = [self _layoutViews:_viewsLeading startOffset:currentX availableOrthogonalSize:availHeight direction:1];
|
||||
|
||||
// 2. Layout Trailing Views
|
||||
// We layout backwards from the right
|
||||
var startRight = CGRectGetWidth(bounds) - _edgeInsets.right;
|
||||
[self _layoutViews:_viewsTrailing startOffset:startRight availableOrthogonalSize:availHeight direction:-1];
|
||||
|
||||
// 3. Layout Center Views
|
||||
if ([_viewsCenter count] > 0)
|
||||
{
|
||||
// Calculate total width of center stack
|
||||
var centerStackWidth = 0.0;
|
||||
for (var i = 0; i < [_viewsCenter count]; i++)
|
||||
{
|
||||
var view = _viewsCenter[i];
|
||||
if (_detachesHiddenViews && [view isHidden]) continue;
|
||||
|
||||
centerStackWidth += CGRectGetWidth([view frame]);
|
||||
if (i < [_viewsCenter count] - 1)
|
||||
centerStackWidth += [self _spacingAfterView:view];
|
||||
}
|
||||
|
||||
var centerStart = (CGRectGetWidth(bounds) / 2.0) - (centerStackWidth / 2.0);
|
||||
|
||||
// Clamp to prevent overlap with Leading (simplified collision logic)
|
||||
// ideally stack view compresses views, but here we just shift/clip
|
||||
if (centerStart < currentX)
|
||||
centerStart = currentX;
|
||||
|
||||
[self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availHeight direction:1];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_layoutVertical
|
||||
{
|
||||
var bounds = [self bounds],
|
||||
availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right,
|
||||
availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom,
|
||||
currentY = _edgeInsets.top;
|
||||
|
||||
// 1. Layout Top (Leading) Views
|
||||
currentY = [self _layoutViews:_viewsLeading startOffset:currentY availableOrthogonalSize:availWidth direction:1];
|
||||
|
||||
// 2. Layout Bottom (Trailing) Views
|
||||
var startBottom = CGRectGetHeight(bounds) - _edgeInsets.bottom;
|
||||
[self _layoutViews:_viewsTrailing startOffset:startBottom availableOrthogonalSize:availWidth direction:-1];
|
||||
|
||||
// 3. Layout Center Views
|
||||
if ([_viewsCenter count] > 0)
|
||||
{
|
||||
var centerStackHeight = 0.0;
|
||||
for (var i = 0; i < [_viewsCenter count]; i++)
|
||||
{
|
||||
var view = _viewsCenter[i];
|
||||
if (_detachesHiddenViews && [view isHidden]) continue;
|
||||
|
||||
centerStackHeight += CGRectGetHeight([view frame]);
|
||||
if (i < [_viewsCenter count] - 1)
|
||||
centerStackHeight += [self _spacingAfterView:view];
|
||||
}
|
||||
|
||||
var centerStart = (CGRectGetHeight(bounds) / 2.0) - (centerStackHeight / 2.0);
|
||||
|
||||
if (centerStart < currentY)
|
||||
centerStart = currentY;
|
||||
|
||||
[self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availWidth direction:1];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to layout a specific array of views in one direction
|
||||
// Returns the ending offset
|
||||
- (float)_layoutViews:(CPArray)views startOffset:(float)offset availableOrthogonalSize:(float)orthoSize direction:(int)dir
|
||||
{
|
||||
var cursor = offset;
|
||||
var isVert = (_orientation === CPUserInterfaceLayoutOrientationVertical);
|
||||
|
||||
// If direction is -1 (Trailing/Bottom), we iterate backwards
|
||||
// However, the standard behavior for trailing gravity is that the *last* view added is at the *end*.
|
||||
// Leading: [A] [B] ->
|
||||
// Trailing: -> [C] [D] (where D is rightmost)
|
||||
// To support Trailing logic: We start at Right Edge, move left by Width(D), place D, move left by Spacing...
|
||||
|
||||
var count = [views count];
|
||||
if (count === 0) return cursor;
|
||||
|
||||
// If direction is negative (Trailing), we process list in reverse order to stack them from edge inwards
|
||||
var i = (dir === 1) ? 0 : count - 1;
|
||||
var limit = (dir === 1) ? count : -1;
|
||||
var step = (dir === 1) ? 1 : -1;
|
||||
|
||||
// Spacing is applied as a gap *before* placing an element (except the
|
||||
// first placed one), rather than trailing off the end after the last
|
||||
// element. This keeps the returned cursor at the true content edge,
|
||||
// with no phantom spacing past the final view.
|
||||
var hasPlacedAny = false;
|
||||
var pendingSpacing = 0;
|
||||
|
||||
for (; i !== limit; i += step)
|
||||
{
|
||||
var view = views[i];
|
||||
|
||||
if (_detachesHiddenViews && [view isHidden])
|
||||
continue;
|
||||
|
||||
if (hasPlacedAny)
|
||||
cursor += (dir === 1) ? pendingSpacing : -pendingSpacing;
|
||||
|
||||
var viewFrame = [view frame];
|
||||
var viewSizePrimary = isVert ? CGRectGetHeight(viewFrame) : CGRectGetWidth(viewFrame);
|
||||
|
||||
// Handle Alignment (Orthogonal Axis)
|
||||
var orthoPos = 0.0;
|
||||
var viewOrthoSize = isVert ? CGRectGetWidth(viewFrame) : CGRectGetHeight(viewFrame);
|
||||
|
||||
// Apply Stretch/Fill Alignment
|
||||
if (isVert)
|
||||
{
|
||||
// Vertical Stack, dealing with Width
|
||||
if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeading || _alignment === CPLayoutAttributeTrailing)
|
||||
{
|
||||
// Note: CPLayoutAttributeLeading/Trailing in this context implies filling width usually,
|
||||
// or aligning to edges. Let's assume Width/Fill for Leading/Trailing/Left/Right
|
||||
// in this simplified implementation, or strictly left/right.
|
||||
|
||||
if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeft || _alignment === CPLayoutAttributeLeading)
|
||||
{
|
||||
// Fill width if explicit, or just align left
|
||||
if (_alignment === CPLayoutAttributeWidth) viewOrthoSize = orthoSize;
|
||||
orthoPos = _edgeInsets.left;
|
||||
}
|
||||
else if (_alignment === CPLayoutAttributeRight || _alignment === CPLayoutAttributeTrailing)
|
||||
{
|
||||
orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize);
|
||||
}
|
||||
else // CenterX
|
||||
{
|
||||
orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0;
|
||||
}
|
||||
}
|
||||
else // Default CenterX
|
||||
{
|
||||
orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Horizontal Stack, dealing with Height
|
||||
if (_alignment === CPLayoutAttributeHeight || _alignment === CPLayoutAttributeTop || _alignment === CPLayoutAttributeBottom)
|
||||
{
|
||||
if (_alignment === CPLayoutAttributeHeight)
|
||||
{
|
||||
viewOrthoSize = orthoSize;
|
||||
orthoPos = _edgeInsets.top;
|
||||
}
|
||||
else if (_alignment === CPLayoutAttributeTop)
|
||||
{
|
||||
orthoPos = _edgeInsets.top;
|
||||
}
|
||||
else if (_alignment === CPLayoutAttributeBottom)
|
||||
{
|
||||
orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize);
|
||||
}
|
||||
else // CenterY
|
||||
{
|
||||
orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0;
|
||||
}
|
||||
}
|
||||
else // Default CenterY
|
||||
{
|
||||
orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Position
|
||||
var originX = 0.0, originY = 0.0;
|
||||
var sizeW = 0.0, sizeH = 0.0;
|
||||
|
||||
if (isVert)
|
||||
{
|
||||
// Vertical
|
||||
sizeH = viewSizePrimary;
|
||||
sizeW = viewOrthoSize;
|
||||
originX = orthoPos;
|
||||
|
||||
if (dir === 1) {
|
||||
originY = cursor;
|
||||
cursor += sizeH;
|
||||
} else {
|
||||
cursor -= sizeH;
|
||||
originY = cursor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Horizontal
|
||||
sizeW = viewSizePrimary;
|
||||
sizeH = viewOrthoSize;
|
||||
originY = orthoPos;
|
||||
|
||||
if (dir === 1) {
|
||||
originX = cursor;
|
||||
cursor += sizeW;
|
||||
} else {
|
||||
cursor -= sizeW;
|
||||
originX = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
[view setFrame:CGRectMake(originX, originY, sizeW, sizeH)];
|
||||
|
||||
pendingSpacing = [self _spacingAfterView:view];
|
||||
hasPlacedAny = true;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: CPCoding
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super initWithCoder:aCoder];
|
||||
if (self)
|
||||
{
|
||||
_orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"];
|
||||
_alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"];
|
||||
_distribution = [aCoder decodeIntForKey:@"CPStackViewDistribution"];
|
||||
_spacing = [aCoder decodeFloatForKey:@"CPStackViewSpacing"];
|
||||
_edgeInsets = [aCoder decodeObjectForKey:@"CPStackViewEdgeInsets"]; // Assuming CPEdgeInsets supports obj coding or manual decode
|
||||
if (!_edgeInsets) _edgeInsets = CPEdgeInsetsMake(0,0,0,0);
|
||||
|
||||
_detachesHiddenViews = [aCoder decodeBoolForKey:@"CPStackViewDetachesHiddenViews"];
|
||||
|
||||
_viewsLeading = [aCoder decodeObjectForKey:@"CPStackViewViewsLeading"] || [];
|
||||
_viewsCenter = [aCoder decodeObjectForKey:@"CPStackViewViewsCenter"] || [];
|
||||
_viewsTrailing = [aCoder decodeObjectForKey:@"CPStackViewViewsTrailing"] || [];
|
||||
|
||||
// Rebuild arranged subviews cache
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
_customSpacings = [aCoder decodeObjectForKey:@"CPStackViewCustomSpacings"] || [[CPMapTable alloc] init];
|
||||
_visibilityPriorities = [[CPMapTable alloc] init]; // usually not persisted
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[super encodeWithCoder:aCoder];
|
||||
[aCoder encodeInt:_orientation forKey:@"CPStackViewOrientation"];
|
||||
[aCoder encodeInt:_alignment forKey:@"CPStackViewAlignment"];
|
||||
[aCoder encodeInt:_distribution forKey:@"CPStackViewDistribution"];
|
||||
[aCoder encodeFloat:_spacing forKey:@"CPStackViewSpacing"];
|
||||
[aCoder encodeObject:_edgeInsets forKey:@"CPStackViewEdgeInsets"];
|
||||
[aCoder encodeBool:_detachesHiddenViews forKey:@"CPStackViewDetachesHiddenViews"];
|
||||
|
||||
[aCoder encodeObject:_viewsLeading forKey:@"CPStackViewViewsLeading"];
|
||||
[aCoder encodeObject:_viewsCenter forKey:@"CPStackViewViewsCenter"];
|
||||
[aCoder encodeObject:_viewsTrailing forKey:@"CPStackViewViewsTrailing"];
|
||||
|
||||
[aCoder encodeObject:_customSpacings forKey:@"CPStackViewCustomSpacings"];
|
||||
}
|
||||
|
||||
@end
|
||||
+75
-15
@@ -42,8 +42,8 @@
|
||||
CPButton _buttonUp;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*!
|
||||
Initializes a CPStepper with given values.
|
||||
@@ -147,8 +147,8 @@
|
||||
[self _sizeToFit];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Superclass overrides
|
||||
// MARK: -
|
||||
// MARK: Superclass overrides
|
||||
|
||||
/*!
|
||||
Set if the CPStepper is enabled or not.
|
||||
@@ -175,12 +175,53 @@
|
||||
[super setFrame:frame];
|
||||
}
|
||||
|
||||
- (void)setThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super setThemeState:aState];
|
||||
|
||||
// Force a layout update because the internal buttons (_buttonUp and _buttonDown)
|
||||
// rely on layoutSubviews to receive the new theme attributes (like HUD colors).
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)unsetThemeState:(CPThemeState)aState
|
||||
{
|
||||
[super unsetThemeState:aState];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
/*! @ignore */
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
var controlSizeThemeState = [self _controlSizeThemeState],
|
||||
aFrame = [self frame],
|
||||
upSize = [self valueForThemeAttribute:@"up-button-size" inState:controlSizeThemeState],
|
||||
isHUD = [self hasThemeState:CPThemeStateHUD],
|
||||
|
||||
// 1. Prepare Lookup States (To fetch the correct image/color from the Stepper's theme)
|
||||
normalLookupStates = [controlSizeThemeState, CPThemeStateBordered],
|
||||
disabledLookupStates = [controlSizeThemeState, CPThemeStateBordered, CPThemeStateDisabled],
|
||||
highlightedLookupStates = [controlSizeThemeState, CPThemeStateBordered, CPThemeStateHighlighted],
|
||||
|
||||
// 2. Prepare Target States (To tell the child buttons when to use this image)
|
||||
normalTargetStates = [CPThemeStateBordered, CPButtonStateBezelStyleRoundRect],
|
||||
disabledTargetStates = [CPThemeStateBordered, CPThemeStateDisabled, CPButtonStateBezelStyleRoundRect],
|
||||
highlightedTargetStates = [CPThemeStateBordered, CPThemeStateHighlighted, CPButtonStateBezelStyleRoundRect];
|
||||
|
||||
// If we are in HUD mode, add CPThemeStateHUD to both lookup and target arrays
|
||||
if (isHUD)
|
||||
{
|
||||
// Lookup: Ask theme for "HUD" version of the stepper arrows
|
||||
normalLookupStates.push(CPThemeStateHUD);
|
||||
disabledLookupStates.push(CPThemeStateHUD);
|
||||
highlightedLookupStates.push(CPThemeStateHUD);
|
||||
|
||||
// Target: Tell the child buttons "Use this when you are in HUD state"
|
||||
normalTargetStates.push(CPThemeStateHUD);
|
||||
disabledTargetStates.push(CPThemeStateHUD);
|
||||
highlightedTargetStates.push(CPThemeStateHUD);
|
||||
}
|
||||
|
||||
var upSize = [self valueForThemeAttribute:@"up-button-size" inState:controlSizeThemeState],
|
||||
downSize = [self valueForThemeAttribute:@"down-button-size" inState:controlSizeThemeState],
|
||||
upFrame = CGRectMake(0, 0, upSize.width, upSize.height),
|
||||
downFrame = CGRectMake(0, upSize.height, downSize.width, downSize.height);
|
||||
@@ -188,12 +229,31 @@
|
||||
[_buttonUp setFrame:upFrame];
|
||||
[_buttonDown setFrame:downFrame];
|
||||
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPButtonStateBezelStyleRoundRect]];
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateDisabled]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateDisabled, CPButtonStateBezelStyleRoundRect]];
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateHighlighted]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateHighlighted, CPButtonStateBezelStyleRoundRect]];
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPButtonStateBezelStyleRoundRect]];
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateDisabled]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateDisabled, CPButtonStateBezelStyleRoundRect]];
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:[controlSizeThemeState, CPThemeStateBordered, CPThemeStateHighlighted]] forThemeAttribute:@"bezel-color" inStates:[CPThemeStateBordered, CPThemeStateHighlighted, CPButtonStateBezelStyleRoundRect]];
|
||||
// Apply Up Button Attributes
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:normalLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:normalTargetStates];
|
||||
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:disabledLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:disabledTargetStates];
|
||||
|
||||
[_buttonUp setValue:[self valueForThemeAttribute:@"bezel-color-up-button" inStates:highlightedLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:highlightedTargetStates];
|
||||
|
||||
// Apply Down Button Attributes
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:normalLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:normalTargetStates];
|
||||
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:disabledLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:disabledTargetStates];
|
||||
|
||||
[_buttonDown setValue:[self valueForThemeAttribute:@"bezel-color-down-button" inStates:highlightedLookupStates]
|
||||
forThemeAttribute:@"bezel-color"
|
||||
inStates:highlightedTargetStates];
|
||||
}
|
||||
|
||||
- (void)_sizeToFit
|
||||
@@ -230,8 +290,8 @@
|
||||
[super setDoubleValue:aValue];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Actions
|
||||
// MARK: -
|
||||
// MARK: Actions
|
||||
|
||||
/*! @ignore */
|
||||
- (IBAction)_buttonDidClick:(id)aSender
|
||||
@@ -266,8 +326,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theming
|
||||
// MARK: -
|
||||
// MARK: Theming
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
|
||||
+34
-36
@@ -62,61 +62,59 @@ CPCanvasStringSizingIsFunctional = NO;
|
||||
if ([self class] != [CPString class])
|
||||
return;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
if (CPFeatureIsCompatible(CPHTMLCanvasFeature))
|
||||
{
|
||||
if (!CPStringSizeMeasuringContext)
|
||||
CPStringSizeMeasuringContext = CGBitmapGraphicsContextCreate();
|
||||
|
||||
// This is to make sure that Canvas based string sizing is functional before we use it.
|
||||
// Currently, Chrome has issues with certain strings, FF had issues in the past.
|
||||
// Unfortunately, this test does fit in CPCompatibility.j where things are not sufficiently initialized.
|
||||
|
||||
var testingFont = [CPFont systemFontOfSize:12];
|
||||
var testingText = 'A A A A A A A A';
|
||||
CPStringSizeMeasuringContext.font = [testingFont cssString];
|
||||
CPCanvasStringSizingIsFunctional = ROUND(CPStringSizeMeasuringContext.measureText(testingText).width) == ROUND([CPPlatformString sizeOfString:testingText withFont:testingFont forWidth:NULL].width);
|
||||
// Unfortunately, as of today canvas sizing is not funtional any more. Neither in Chrome nor in FF
|
||||
|
||||
CPCanvasStringSizingIsFunctional = NO;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (CGSize)sizeWithFont:(CPFont)aFont inWidth:(float)aWidth
|
||||
{
|
||||
var size;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var sizeCacheForFont = CPStringSizeWithFontInWidthCache[self];
|
||||
|
||||
if (sizeCacheForFont === undefined)
|
||||
sizeCacheForFont = CPStringSizeWithFontInWidthCache[self] = [];
|
||||
|
||||
var cssString = [aFont cssString],
|
||||
cacheKey = cssString + '_' + (aWidth ? aWidth : '0');
|
||||
|
||||
size = sizeCacheForFont[cacheKey];
|
||||
|
||||
if (size !== undefined && sizeCacheForFont.hasOwnProperty(cacheKey))
|
||||
return CGSizeMakeCopy(size);
|
||||
|
||||
if (!CPCanvasStringSizingIsFunctional || aWidth)
|
||||
size = [CPPlatformString sizeOfString:self withFont:aFont forWidth:aWidth];
|
||||
else
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
if (CPPlatformHasBug(CPTextSizingAlwaysNeedsSetFontBug) || CPStringSizeMeasuringContext.font !== cssString)
|
||||
CPStringSizeMeasuringContext.font = cssString;
|
||||
var sizeCacheForFont = CPStringSizeWithFontInWidthCache[self];
|
||||
|
||||
var fontHeight = CPStringSizeWithFontHeightCache[cssString];
|
||||
if (sizeCacheForFont === undefined)
|
||||
sizeCacheForFont = CPStringSizeWithFontInWidthCache[self] = [];
|
||||
|
||||
if (fontHeight === undefined)
|
||||
fontHeight = CPStringSizeWithFontHeightCache[cssString] = [aFont defaultLineHeightForFont];
|
||||
var cssString = [aFont cssString],
|
||||
cacheKey = cssString + '_' + (aWidth ? aWidth : '0');
|
||||
|
||||
size = CGSizeMake(CPStringSizeMeasuringContext.measureText(self).width, fontHeight);
|
||||
size = sizeCacheForFont[cacheKey];
|
||||
|
||||
if (size !== undefined && sizeCacheForFont.hasOwnProperty(cacheKey))
|
||||
return CGSizeMakeCopy(size);
|
||||
|
||||
if (!CPCanvasStringSizingIsFunctional || aWidth)
|
||||
size = [CPPlatformString sizeOfString:self withFont:aFont forWidth:aWidth];
|
||||
else
|
||||
{
|
||||
if (CPPlatformHasBug(CPTextSizingAlwaysNeedsSetFontBug) || CPStringSizeMeasuringContext.font !== cssString)
|
||||
CPStringSizeMeasuringContext.font = cssString;
|
||||
|
||||
var fontHeight = CPStringSizeWithFontHeightCache[cssString];
|
||||
|
||||
if (fontHeight === undefined)
|
||||
fontHeight = CPStringSizeWithFontHeightCache[cssString] = [aFont defaultLineHeightForFont];
|
||||
|
||||
size = CGSizeMake(CPStringSizeMeasuringContext.measureText(self).width, fontHeight);
|
||||
}
|
||||
|
||||
sizeCacheForFont[cacheKey] = size;
|
||||
}
|
||||
else
|
||||
size = CGSizeMake(0, 0);
|
||||
|
||||
sizeCacheForFont[cacheKey] = size;
|
||||
#else
|
||||
size = CGSizeMake(0, 0);
|
||||
#endif
|
||||
return CGSizeMakeCopy(size);
|
||||
}
|
||||
|
||||
|
||||
+8
-10
@@ -117,7 +117,7 @@ var CPTabViewDidSelectTabViewItemSelector = 1 << 1,
|
||||
var height = [_tabs valueForThemeAttribute:@"min-size"].height;
|
||||
[_tabs setFrameSize:CGSizeMake(0, height)];
|
||||
|
||||
_box = [[_CPTabViewBox alloc] initWithFrame:[self bounds]];
|
||||
_box = [[_CPTabViewBox alloc] initWithFrame:[self bounds]];
|
||||
[_box setTabView:self];
|
||||
[_box setContentInset:[self currentValueForThemeAttribute:@"box-content-inset"]];
|
||||
[_box setContentViewMargins:CGSizeMakeZero()];
|
||||
@@ -696,8 +696,8 @@ var CPTabViewDidSelectTabViewItemSelector = 1 << 1,
|
||||
[self _displayItemView:_placeholderView];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override
|
||||
// MARK: -
|
||||
// MARK: Override
|
||||
|
||||
/*!
|
||||
Enabled controls accept first mouse by default.
|
||||
@@ -886,11 +886,11 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPTabView (CSSTheming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -906,7 +906,7 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPTabViewBox : CPBox
|
||||
{
|
||||
@@ -915,8 +915,8 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override
|
||||
// MARK: -
|
||||
// MARK: Override
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -982,8 +982,6 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
|
||||
@end
|
||||
|
||||
# pragma mark -
|
||||
|
||||
// This subclass of CPSegmentedControl implements specific behaviour needed by CPTabView, which
|
||||
// differs in some ways from normal CPSegmentedControl habits :
|
||||
//
|
||||
|
||||
+231
-23
@@ -25,6 +25,10 @@
|
||||
@import "CPCursor.j"
|
||||
@import "_CPImageAndTextView.j"
|
||||
@import "CPTrackingArea.j"
|
||||
@import "CPAnimationContext.j"
|
||||
@import "CPViewAnimator.j"
|
||||
@import "CPScrollView.j"
|
||||
@import <Foundation/CPGeometry.j>
|
||||
|
||||
@class CPTableView
|
||||
|
||||
@@ -50,10 +54,16 @@
|
||||
@"text-color": [CPNull null],
|
||||
@"font": [CPNull null],
|
||||
@"text-shadow-color": [CPNull null],
|
||||
@"text-shadow-offset": CGSizeMakeZero()
|
||||
@"text-shadow-offset": CGSizeMakeZero(),
|
||||
@"dont-draw-separator": NO
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstMouse:(CPEvent)anEvent
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
@@ -66,18 +76,22 @@
|
||||
|
||||
- (void)_init
|
||||
{
|
||||
[self setBackgroundColor:[self currentValueForThemeAttribute:@"background-color"]];
|
||||
|
||||
var inset = [self valueForThemeAttribute:@"text-inset"];
|
||||
|
||||
_textField = [[_CPImageAndTextView alloc] initWithFrame:
|
||||
CGRectMake(5.0, 0.0, CGRectGetWidth([self bounds]) - 10.0, CGRectGetHeight([self bounds]))];
|
||||
CGRectMake(inset.left, inset.top, CGRectGetWidth([self bounds]) - (inset.left + inset.right), CGRectGetHeight([self bounds]) - (inset.top + inset.bottom))];
|
||||
|
||||
[_textField setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
|
||||
[_textField setLineBreakMode:CPLineBreakByTruncatingTail];
|
||||
[_textField setTextColor:[CPColor colorWithRed:51.0 / 255.0 green:51.0 / 255.0 blue:51.0 / 255.0 alpha:1.0]];
|
||||
[_textField setFont:[CPFont boldSystemFontOfSize:12.0]];
|
||||
[_textField setAlignment:CPLeftTextAlignment];
|
||||
[_textField setLineBreakMode:[self valueForThemeAttribute:@"line-break-mode"]];
|
||||
[_textField setTextColor:[self valueForThemeAttribute:@"text-color"]];
|
||||
[_textField setFont:[self valueForThemeAttribute:@"font"]];
|
||||
[_textField setAlignment:[self valueForThemeAttribute:@"text-alignment"]];
|
||||
[_textField setVerticalAlignment:CPCenterVerticalTextAlignment];
|
||||
[_textField setTextShadowColor:[CPColor whiteColor]];
|
||||
[_textField setTextShadowOffset:CGSizeMake(0,1)];
|
||||
[_textField setTextShadowColor:[self valueForThemeAttribute:@"text-shadow-color"]];
|
||||
[_textField setTextShadowOffset:[self valueForThemeAttribute:@"text-shadow-offset"]];
|
||||
|
||||
[self addSubview:_textField];
|
||||
}
|
||||
@@ -188,6 +202,9 @@
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
if ([self valueForThemeAttribute:@"dont-draw-separator"])
|
||||
return;
|
||||
|
||||
var bounds = [self bounds];
|
||||
|
||||
if (!CGRectIntersectsRect(aRect, bounds))
|
||||
@@ -223,7 +240,8 @@ var _CPTableColumnHeaderViewStringValueKey = @"_CPTableColumnHeaderViewStringVal
|
||||
[self _init];
|
||||
[self _setIndicatorImage:[aCoder decodeObjectForKey:_CPTableColumnHeaderViewImageKey]];
|
||||
[self setStringValue:[aCoder decodeObjectForKey:_CPTableColumnHeaderViewStringValueKey]];
|
||||
[self setFont:[aCoder decodeObjectForKey:_CPTableColumnHeaderViewFontKey]];
|
||||
// FIXME: pourquoi dans actif, font=null ?
|
||||
// [self setFont:[aCoder decodeObjectForKey:_CPTableColumnHeaderViewFontKey]];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -257,11 +275,13 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
|
||||
BOOL _isResizing;
|
||||
BOOL _isDragging;
|
||||
BOOL _isAnimating;
|
||||
BOOL _canDragColumn;
|
||||
|
||||
CPView _columnDragView;
|
||||
CPView _columnDragHeaderView;
|
||||
CPView _columnDragClipView;
|
||||
CPScrollView _columnDragScrollView;
|
||||
|
||||
float _columnOldWidth;
|
||||
|
||||
@@ -278,7 +298,9 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
return @{
|
||||
@"background-color": [CPNull null],
|
||||
@"divider-color": [CPColor grayColor],
|
||||
@"divider-thickness": 1.0
|
||||
@"divider-thickness": 1.0,
|
||||
@"swap-animation": [CPNull null],
|
||||
@"return-animation": [CPNull null]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,7 +362,24 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
|
||||
- (CPInteger)columnAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
return [_tableView columnAtPoint:aPoint];
|
||||
var tableView = [self tableView],
|
||||
tableColumns = [tableView tableColumns],
|
||||
count = [tableColumns count],
|
||||
bounds = [self bounds],
|
||||
// Create a point that keeps the X position but forces Y to be safely
|
||||
// in the middle of the header view.
|
||||
constrainedPoint = CGPointMake(aPoint.x, CGRectGetMidY(bounds));
|
||||
|
||||
// Iterate through columns to find which one contains the constrained X coordinate
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
// headerRectOfColumn: is a utility method defined in CPTableHeaderView
|
||||
// that handles the coordinate conversion from the table view relative to the header.
|
||||
if (CGRectContainsPoint([self headerRectOfColumn:i], constrainedPoint))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (CGRect)headerRectOfColumn:(CPInteger)aColumnIndex
|
||||
@@ -365,6 +404,8 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[self setBackgroundColor:[self currentValueForThemeAttribute:@"background-color"]];
|
||||
|
||||
var tableColumns = [_tableView tableColumns],
|
||||
count = [tableColumns count];
|
||||
|
||||
@@ -442,6 +483,14 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
}
|
||||
else if (_isDragging)
|
||||
{
|
||||
// First, we have to avoid a running condition where user stops dragging while a swap animation is running
|
||||
if (_isAnimating)
|
||||
{
|
||||
[CPTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(_retry_mouseUp:) userInfo:theEvent repeats:NO];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[self _stopDraggingTableColumn:_activeColumn];
|
||||
}
|
||||
else if (_activeColumn != -1)
|
||||
@@ -458,6 +507,11 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
_activeColumn = -1;
|
||||
}
|
||||
|
||||
- (void)_retry_mouseUp:(CPTimer)aTimer
|
||||
{
|
||||
[self mouseUp:[aTimer userInfo]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTableHeaderView (CPTrackingArea)
|
||||
@@ -562,7 +616,21 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
pressure:[theEvent pressure]];
|
||||
|
||||
[self autoscroll:constrainedEvent];
|
||||
|
||||
var contentView = [_tableView superview],
|
||||
boundsOriginBefore = [contentView boundsOrigin];
|
||||
|
||||
[_tableView autoscroll:constrainedEvent];
|
||||
|
||||
var boundsOriginAfter = [contentView boundsOrigin],
|
||||
deltaX = boundsOriginAfter.x - boundsOriginBefore.x;
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
var dragContentView = [_columnDragScrollView contentView],
|
||||
dragContentBoundsOrigin = [dragContentView boundsOrigin];
|
||||
[dragContentView setBoundsOrigin:CGPointMake(dragContentBoundsOrigin.x + deltaX, dragContentBoundsOrigin.y)];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)_headerRectOfLastVisibleColumn
|
||||
@@ -584,16 +652,91 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
- (CGPoint)_constrainDragPoint:(CGPoint)aPoint
|
||||
{
|
||||
// This effectively clamps the value between the minimum and maximum
|
||||
var visibleRect = [_tableView visibleRect],
|
||||
lastColumnRect = [self _headerRectOfLastVisibleColumn],
|
||||
activeColumnRect = [self headerRectOfColumn:_activeColumn],
|
||||
maxX = CGRectGetMaxX(lastColumnRect) - CGRectGetWidth(activeColumnRect) - CGRectGetMinX(visibleRect),
|
||||
point = CGPointMake(MAX(MIN(aPoint.x, maxX), -CGRectGetMinX(visibleRect)), aPoint.y);
|
||||
var tableFrame = [_tableView frame],
|
||||
dragFrame = [_columnDragView frame],
|
||||
maxX = tableFrame.size.width - dragFrame.size.width,
|
||||
point = CGPointMake(MAX(MIN(aPoint.x, maxX),0), aPoint.y);
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
- (void)_moveColumn:(CPInteger)aFromIndex toColumn:(CPInteger)aToIndex
|
||||
{
|
||||
if (_isAnimating)
|
||||
return;
|
||||
|
||||
var swapAnimation = [self currentValueForThemeAttribute:@"swap-animation"];
|
||||
|
||||
if (swapAnimation)
|
||||
{
|
||||
_isAnimating = YES;
|
||||
|
||||
// There's a theme defined animation function, just use it
|
||||
objj_eval("("+swapAnimation+")")(self, aFromIndex, aToIndex, _columnDragClipView, _columnDragView);
|
||||
|
||||
// var animatedColumn = [[_tableView tableColumns] objectAtIndex:aToIndex],
|
||||
// animatedHeader = [animatedColumn headerView],
|
||||
// animatedHeaderOrigin = [animatedHeader frameOrigin],
|
||||
//
|
||||
// destinationX,
|
||||
// draggedHeader = [[[_tableView tableColumns] objectAtIndex:aFromIndex] headerView],
|
||||
//
|
||||
// scrollView = [self enclosingScrollView],
|
||||
// animatedView = [_tableView _animationViewForColumn:aToIndex],
|
||||
// animatedOrigin = [animatedView frameOrigin];
|
||||
//
|
||||
// [_columnDragClipView addSubview:animatedView positioned:CPWindowBelow relativeTo:_columnDragView];
|
||||
//
|
||||
// [[animatedHeader subviews] makeObjectsPerformSelector:@selector(setHidden:) withObject:YES];
|
||||
// [animatedHeader setThemeState:CPThemeStateVertical];
|
||||
//
|
||||
// if (aFromIndex < aToIndex)
|
||||
// destinationX = CGRectGetMinX([_tableView rectOfColumn:aFromIndex]);
|
||||
// else
|
||||
// destinationX = animatedOrigin.x + CGRectGetWidth([_tableView rectOfColumn:aFromIndex]);
|
||||
//
|
||||
// [CPAnimationContext beginGrouping];
|
||||
//
|
||||
// var context = [CPAnimationContext currentContext];
|
||||
//
|
||||
// [context setDuration:0.15];
|
||||
// [context setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
|
||||
// [context setCompletionHandler:function() {
|
||||
// [animatedView removeFromSuperview];
|
||||
//
|
||||
// [self _finalize_moveColumn:aFromIndex toColumn:aToIndex];
|
||||
//
|
||||
// [animatedHeader unsetThemeState:CPThemeStateVertical];
|
||||
// [[animatedHeader subviews] makeObjectsPerformSelector:@selector(setHidden:) withObject:NO];
|
||||
//
|
||||
// if ([animatedView isSelected])
|
||||
// {
|
||||
// [animatedHeader setThemeState:CPThemeStateSelected];
|
||||
//
|
||||
// // We have to reselect the animated column
|
||||
// [[_tableView selectedColumnIndexes] addIndex:aFromIndex];
|
||||
// }
|
||||
//
|
||||
// // Reload animated column
|
||||
// var columnVisRect = CGRectIntersection([_tableView rectOfColumn:aFromIndex], [_tableView visibleRect]),
|
||||
// rowsIndexes = [CPIndexSet indexSetWithIndexesInRange:[_tableView rowsInRect:columnVisRect]],
|
||||
// columnsIndexes = [CPIndexSet indexSetWithIndex:aFromIndex];
|
||||
//
|
||||
// [_tableView _loadDataViewsInRows:rowsIndexes columns:columnsIndexes];
|
||||
// [_tableView _layoutViewsForRowIndexes:rowsIndexes columnIndexes:columnsIndexes];
|
||||
//
|
||||
// [_tableView._tableDrawView displayRect:columnVisRect];
|
||||
// }];
|
||||
//
|
||||
// [[animatedView animator] setFrameOrigin:CGPointMake(destinationX, animatedOrigin.y)];
|
||||
//
|
||||
// [CPAnimationContext endGrouping];
|
||||
}
|
||||
else
|
||||
[self _finalize_moveColumn:aFromIndex toColumn:aToIndex];
|
||||
}
|
||||
|
||||
- (void)_finalize_moveColumn:(CPInteger)aFromIndex toColumn:(CPInteger)aToIndex
|
||||
{
|
||||
[_tableView moveColumn:aFromIndex toColumn:aToIndex];
|
||||
_activeColumn = aToIndex;
|
||||
@@ -602,6 +745,8 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
[_tableView _setDraggedColumn:_activeColumn];
|
||||
|
||||
[self setNeedsDisplay:YES];
|
||||
|
||||
_isAnimating = NO;
|
||||
}
|
||||
|
||||
- (BOOL)isDragging
|
||||
@@ -618,17 +763,30 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
// Create a new clip view for the drag view that clips to the header + visible content
|
||||
var headerHeight = CGRectGetHeight([self frame]),
|
||||
scrollView = [self enclosingScrollView],
|
||||
contentFrame = [[scrollView contentView] frame];
|
||||
contentFrame = [[scrollView contentView] frame],
|
||||
contentBounds = [[scrollView contentView] bounds];
|
||||
|
||||
contentFrame.origin.y -= headerHeight;
|
||||
contentFrame.size.height += headerHeight;
|
||||
|
||||
_columnDragClipView = [[CPView alloc] initWithFrame:contentFrame];
|
||||
_columnDragScrollView = [[CPScrollView alloc] initWithFrame:contentFrame];
|
||||
|
||||
[_columnDragScrollView setHasHorizontalScroller:NO];
|
||||
[_columnDragScrollView setHasVerticalScroller:NO];
|
||||
[_columnDragScrollView setBorderType:CPNoBorder];
|
||||
|
||||
var tableFrame = [_tableView frame],
|
||||
clipFrame = CGRectMake(0, 0, tableFrame.size.width, contentFrame.size.height);
|
||||
|
||||
_columnDragClipView = [[CPView alloc] initWithFrame:clipFrame];
|
||||
|
||||
[_columnDragClipView addSubview:_columnDragView];
|
||||
|
||||
[_columnDragScrollView setDocumentView:_columnDragClipView];
|
||||
[[_columnDragScrollView contentView] setBoundsOrigin:CGPointMake(contentBounds.origin.x, 0)];
|
||||
|
||||
// Insert the clip view above the table header (and content)
|
||||
[scrollView addSubview:_columnDragClipView positioned:CPWindowAbove relativeTo:self];
|
||||
[scrollView addSubview:_columnDragScrollView positioned:CPWindowAbove relativeTo:self];
|
||||
|
||||
// Hide the underlying column header subviews, we just want to draw the chrome
|
||||
var headerView = [[[_tableView tableColumns] objectAtIndex:aColumnIndex] headerView];
|
||||
@@ -638,6 +796,9 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
// The underlying column header shows normal state
|
||||
[headerView unsetThemeStates:[CPThemeStateHighlighted, CPThemeStateSelected]];
|
||||
|
||||
// FIXME: Just a little hack to get a special background (using an unused theme state)
|
||||
[headerView setThemeState:CPThemeStateVertical];
|
||||
|
||||
// Keep track of the location within the column header where the original mousedown occurred
|
||||
_columnDragHeaderView = [_columnDragView viewWithTag:CPTableHeaderViewDragColumnHeaderTag];
|
||||
|
||||
@@ -692,10 +853,43 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
}
|
||||
|
||||
- (void)_stopDraggingTableColumn:(CPInteger)aColumnIndex
|
||||
{
|
||||
var returnAnimation = [self currentValueForThemeAttribute:@"return-animation"];
|
||||
|
||||
if (returnAnimation)
|
||||
{
|
||||
_isAnimating = YES;
|
||||
|
||||
// There's a theme defined animation function, just use it
|
||||
objj_eval("("+returnAnimation+")")(self, aColumnIndex, _columnDragView);
|
||||
|
||||
// var animatedColumn = [[_tableView tableColumns] objectAtIndex:aColumnIndex],
|
||||
// animatedHeader = [animatedColumn headerView],
|
||||
// animatedHeaderOrigin = [animatedHeader frameOrigin];
|
||||
//
|
||||
// [CPAnimationContext beginGrouping];
|
||||
//
|
||||
// var context = [CPAnimationContext currentContext];
|
||||
//
|
||||
// [context setDuration:0.15];
|
||||
// [context setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
|
||||
// [context setCompletionHandler:function() {
|
||||
//
|
||||
// [self _finalize_stopDraggingTableColumn:aColumnIndex];
|
||||
// }];
|
||||
//
|
||||
// [[_columnDragView animator] setFrameOrigin:CGPointMake(animatedHeaderOrigin.x, 0)];
|
||||
//
|
||||
// [CPAnimationContext endGrouping];
|
||||
}
|
||||
else
|
||||
[self _finalize_stopDraggingTableColumn:aColumnIndex];
|
||||
}
|
||||
|
||||
- (void)_finalize_stopDraggingTableColumn:(CPInteger)aColumnIndex
|
||||
{
|
||||
_isDragging = NO;
|
||||
|
||||
[_columnDragClipView removeFromSuperview];
|
||||
[_tableView _setDraggedColumn:-1];
|
||||
|
||||
var tableColumn = [[_tableView tableColumns] objectAtIndex:aColumnIndex],
|
||||
@@ -703,16 +897,30 @@ var CPTableHeaderViewResizeZone = 3.0,
|
||||
|
||||
[[headerView subviews] makeObjectsPerformSelector:@selector(setHidden:) withObject:NO];
|
||||
|
||||
// Restore headerView background
|
||||
[headerView unsetThemeState:CPThemeStateVertical];
|
||||
|
||||
if (_tableView._draggedColumnIsSelected)
|
||||
[headerView setThemeState:CPThemeStateSelected];
|
||||
|
||||
[_tableView _reloadDataViews];
|
||||
[[_tableView headerView] setNeedsLayout];
|
||||
// Reload animated column
|
||||
var columnVisRect = CGRectIntersection([_tableView rectOfColumn:aColumnIndex], [_tableView visibleRect]),
|
||||
rowsIndexes = [CPIndexSet indexSetWithIndexesInRange:[_tableView rowsInRect:columnVisRect]],
|
||||
columnsIndexes = [CPIndexSet indexSetWithIndex:aColumnIndex];
|
||||
|
||||
[[CPCursor arrowCursor] set];
|
||||
[_tableView _loadDataViewsInRows:rowsIndexes columns:columnsIndexes];
|
||||
[_tableView _layoutViewsForRowIndexes:rowsIndexes columnIndexes:columnsIndexes];
|
||||
[_tableView _updateDataViewsFocusState];
|
||||
[_tableView._tableDrawView displayRect:columnVisRect];
|
||||
|
||||
[[CPCursor arrowCursor] set]; // FIXME: retirer ?
|
||||
[self updateTrackingAreas];
|
||||
|
||||
[_columnDragScrollView removeFromSuperview];
|
||||
|
||||
[_tableView _sendDelegateDidDragTableColumn:tableColumn];
|
||||
|
||||
_isAnimating = NO;
|
||||
}
|
||||
|
||||
- (BOOL)_shouldResizeTableColumn:(CPInteger)aColumnIndex at:(CGPoint)aPoint
|
||||
|
||||
+640
-524
File diff suppressed because it is too large.
Load diff
+338
-205
@@ -68,7 +68,11 @@ var CPTextFieldDOMCurrentElement = nil,
|
||||
CPTextFieldCachedDragFunction = nil,
|
||||
CPTextFieldBlurHandler = nil,
|
||||
CPTextFieldInputFunction = nil,
|
||||
CPTexFieldCurrentCSSSelectableField = nil;
|
||||
CPTexFieldCurrentCSSSelectableField = nil,
|
||||
CPTextFieldLastValidationFailureEvent = nil,
|
||||
CPTextFieldLastValidationFailureString = nil,
|
||||
CPTextFieldLastValidationFailureField = nil,
|
||||
CPTextFieldLastValidationFailureResult = NO;
|
||||
|
||||
var CPSecureTextFieldCharacter = "\u2022";
|
||||
|
||||
@@ -104,6 +108,26 @@ function CPTextFieldHandleBlur(anEvent, ownerRef)
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
function _CPTextFieldDetachInputElement(anElement)
|
||||
{
|
||||
if (!anElement || !anElement.parentNode)
|
||||
return;
|
||||
|
||||
var wasResigning = CPTextFieldInputResigning;
|
||||
CPTextFieldInputResigning = YES;
|
||||
|
||||
try
|
||||
{
|
||||
anElement.parentNode.removeChild(anElement);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Bereits durch einen Blur-Handler entfernt – nichts zu tun.
|
||||
}
|
||||
|
||||
CPTextFieldInputResigning = wasResigning;
|
||||
CPTextFieldInputDidBlur = NO;
|
||||
}
|
||||
|
||||
@implementation CPString (CPTextFieldAdditions)
|
||||
|
||||
@@ -241,8 +265,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -253,11 +277,12 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
- (DOMElement)_inputElement
|
||||
{
|
||||
if (!CPDOMAvailable) return nil;
|
||||
|
||||
if (!CPTextFieldDOMTextAreaElement)
|
||||
{
|
||||
CPTextFieldDOMTextAreaElement = document.createElement("textarea");
|
||||
@@ -332,7 +357,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
characters:nil
|
||||
charactersIgnoringModifiers:nil
|
||||
isARepeat:NO
|
||||
keyCode:nil];
|
||||
keyCode:nil
|
||||
isActionKey:NO];
|
||||
|
||||
[CPTextFieldInputOwner keyUp:cappEvent];
|
||||
|
||||
@@ -359,7 +385,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
|
||||
return CPTextFieldDOMCurrentElement;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -376,7 +401,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Controlling Editability and Selectability
|
||||
// MARK: Controlling Editability and Selectability
|
||||
|
||||
/*!
|
||||
Sets whether or not the receiver text field can be edited. If NO, any
|
||||
@@ -650,88 +675,122 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
_stringValue = [self stringValue];
|
||||
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
[self _setCSSStyleForInputElement];
|
||||
|
||||
var element = [self _inputElement];
|
||||
element.value = _stringValue;
|
||||
_DOMElement.appendChild(element);
|
||||
|
||||
CPTextFieldInputIsActive = YES;
|
||||
|
||||
if (document.attachEvent)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
|
||||
CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
|
||||
[self _setCSSStyleForInputElement];
|
||||
|
||||
[[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
|
||||
[[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
|
||||
}
|
||||
var element = [self _inputElement];
|
||||
element.value = _stringValue;
|
||||
|
||||
CPTextFieldInputOwner = self;
|
||||
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
if (CPTextFieldInputOwner !== self)
|
||||
return;
|
||||
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
|
||||
element.focus();
|
||||
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
|
||||
// Select the text if the textfield became first responder through keyboard interaction
|
||||
if (!_willBecomeFirstResponderByClick)
|
||||
if (element.parentNode !== _DOMElement)
|
||||
{
|
||||
[self _selectText:self immediately:YES];
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = [self convertPointFromBase:[[CPApp currentEvent] locationInWindow]].x,
|
||||
contentInset = [self currentValueForThemeAttribute:@"content-inset"],
|
||||
text = [self stringValue],
|
||||
font = [self font];
|
||||
var previousOwner = CPTextFieldInputOwner;
|
||||
|
||||
switch ([self alignment]) {
|
||||
case CPCenterTextAlignment:
|
||||
// Otherwise Chrome will fire the blur during appendChild (NotFoundError).
|
||||
_CPTextFieldDetachInputElement(element);
|
||||
|
||||
var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
|
||||
textWidth = [text sizeWithFont:font].width;
|
||||
try
|
||||
{
|
||||
_DOMElement.appendChild(element);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Race with the Blur-Handler: retry once and yield silently.
|
||||
_CPTextFieldDetachInputElement(element);
|
||||
|
||||
x -= (contentWidth - textWidth) / 2 + contentInset.left;
|
||||
|
||||
break;
|
||||
|
||||
case CPRightTextAlignment:
|
||||
|
||||
var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
|
||||
textWidth = [text sizeWithFont:font].width;
|
||||
|
||||
x -= (contentWidth - textWidth) + contentInset.left;
|
||||
|
||||
break;
|
||||
|
||||
default: // CPLeftTextAlignment, CPJustifiedTextAlignment, CPNaturalTextAlignment
|
||||
|
||||
x -= contentInset.left;
|
||||
|
||||
break;
|
||||
try
|
||||
{
|
||||
_DOMElement.appendChild(element);
|
||||
}
|
||||
catch (e2)
|
||||
{
|
||||
[self unsetThemeState:CPThemeStateEditing];
|
||||
[self _updatePlaceholderState];
|
||||
[self setNeedsLayout];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
var position = [CPPlatformString charPositionOfString:text withFont:font forPoint:CGPointMake(x, 0)];
|
||||
|
||||
[self setSelectedRange:CPMakeRange(position, 0)];
|
||||
if (previousOwner && previousOwner !== self)
|
||||
{
|
||||
[previousOwner unsetThemeState:CPThemeStateEditing];
|
||||
[previousOwner setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
_willBecomeFirstResponderByClick = NO;
|
||||
CPTextFieldInputIsActive = YES;
|
||||
|
||||
[self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
if (document.attachEvent)
|
||||
{
|
||||
CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
|
||||
CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
|
||||
|
||||
#endif
|
||||
[[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
|
||||
[[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
|
||||
}
|
||||
|
||||
CPTextFieldInputOwner = self;
|
||||
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
if (CPTextFieldInputOwner !== self)
|
||||
return;
|
||||
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
|
||||
element.focus();
|
||||
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
|
||||
// Select the text if the textfield became first responder through keyboard interaction
|
||||
if (!_willBecomeFirstResponderByClick)
|
||||
{
|
||||
[self _selectText:self immediately:YES];
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = [self convertPointFromBase:[[CPApp currentEvent] locationInWindow]].x,
|
||||
contentInset = [self currentValueForThemeAttribute:@"content-inset"],
|
||||
text = [self stringValue],
|
||||
font = [self font];
|
||||
|
||||
switch ([self alignment]) {
|
||||
case CPCenterTextAlignment:
|
||||
|
||||
var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
|
||||
textWidth = [text sizeWithFont:font].width;
|
||||
|
||||
x -= (contentWidth - textWidth) / 2 + contentInset.left;
|
||||
|
||||
break;
|
||||
|
||||
case CPRightTextAlignment:
|
||||
|
||||
var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
|
||||
textWidth = [text sizeWithFont:font].width;
|
||||
|
||||
x -= (contentWidth - textWidth) + contentInset.left;
|
||||
|
||||
break;
|
||||
|
||||
default: // CPLeftTextAlignment, CPJustifiedTextAlignment, CPNaturalTextAlignment
|
||||
|
||||
x -= contentInset.left;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var position = [CPPlatformString charPositionOfString:text withFont:font forPoint:CGPointMake(x, 0)];
|
||||
|
||||
[self setSelectedRange:CPMakeRange(position, 0)];
|
||||
}
|
||||
|
||||
_willBecomeFirstResponderByClick = NO;
|
||||
|
||||
[self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -742,13 +801,14 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
*/
|
||||
- (void)_setEnableCSSSelection:(BOOL)shouldEnable
|
||||
{
|
||||
#if PLATFORM (DOM)
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
if (CPTexFieldCurrentCSSSelectableField)
|
||||
CPTexFieldCurrentCSSSelectableField._DOMElement.style[CPBrowserStyleProperty(@"user-select")] = @"none";
|
||||
|
||||
CPTexFieldCurrentCSSSelectableField = self;
|
||||
_DOMElement.style[CPBrowserStyleProperty(@"user-select")] = shouldEnable ? @"text" : @"none";
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -757,7 +817,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
*/
|
||||
- (void)_setCSSStyleForInputElement
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
var element = [self _inputElement],
|
||||
font = [self font],
|
||||
@@ -766,33 +827,27 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"],
|
||||
left = CGRectGetMinX(contentRect);
|
||||
|
||||
// If the browser has a built in left padding, compensate for it. We need the input text to be exactly on top of the original text.
|
||||
if (CPFeatureIsCompatible(CPInput1PxLeftPadding))
|
||||
left -= 1;
|
||||
|
||||
switch (verticalAlign)
|
||||
{
|
||||
case CPTopVerticalTextAlignment:
|
||||
var topPoint = CGRectGetMinY(contentRect) + "px";
|
||||
var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px";
|
||||
break;
|
||||
|
||||
case CPCenterVerticalTextAlignment:
|
||||
var topPoint = (CGRectGetMidY(contentRect) - (lineHeight / 2)) + "px";
|
||||
var topPoint = CEIL((CGRectGetMidY(contentRect) - (lineHeight / 2))) + "px";
|
||||
break;
|
||||
|
||||
case CPBottomVerticalTextAlignment:
|
||||
var topPoint = (CGRectGetMaxY(contentRect) - lineHeight) + "px";
|
||||
var topPoint = CEIL((CGRectGetMaxY(contentRect) - lineHeight)) + "px";
|
||||
break;
|
||||
|
||||
default:
|
||||
var topPoint = CGRectGetMinY(contentRect) + "px";
|
||||
var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px";
|
||||
break;
|
||||
}
|
||||
|
||||
if ([self hasThemeState:CPTextFieldStatePlaceholder])
|
||||
element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder] cssString];
|
||||
else
|
||||
element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPThemeStateEditing] cssString];
|
||||
// Use currentValueForThemeAttribute to respect all current states (HUD, Placeholder, Editing, etc.)
|
||||
element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
|
||||
|
||||
switch ([self alignment])
|
||||
{
|
||||
@@ -826,41 +881,40 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
|
||||
if (isTextArea)
|
||||
element.style.whiteSpace = _wraps ? "pre" : "nowrap";
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
/*! @ignore */
|
||||
- (BOOL)resignFirstResponder
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
// We might have been the first responder without actually editing.
|
||||
if (_isEditing && CPTextFieldInputOwner === self)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var element = [self _inputElement],
|
||||
newValue = element.value,
|
||||
error = @"";
|
||||
|
||||
if (newValue !== _stringValue)
|
||||
// We might have been the first responder without actually editing.
|
||||
if (_isEditing && CPTextFieldInputOwner === self)
|
||||
{
|
||||
[self _setStringValue:newValue];
|
||||
}
|
||||
var element = [self _inputElement],
|
||||
newValue = element.value,
|
||||
error = @"";
|
||||
|
||||
// If there is a formatter, always give it a chance to reject the resignation,
|
||||
// even if the value has not changed.
|
||||
if ([self _valueIsValid:newValue] === NO)
|
||||
{
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
if (newValue !== _stringValue)
|
||||
{
|
||||
[self _setStringValue:newValue];
|
||||
}
|
||||
|
||||
element.focus();
|
||||
// If there is a formatter, always give it a chance to reject the resignation,
|
||||
// even if the value has not changed.
|
||||
if ([self _valueIsValid:newValue] === NO)
|
||||
{
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
element.focus();
|
||||
|
||||
return NO;
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// When we are no longer the first responder we don't worry about the key status of our window anymore.
|
||||
[self _setObserveWindowKeyNotifications:NO];
|
||||
@@ -895,7 +949,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
[self _updatePlaceholderState];
|
||||
[self setNeedsLayout];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
var element = [self _inputElement];
|
||||
|
||||
@@ -908,7 +962,16 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
CPTextFieldBlurHandler();
|
||||
|
||||
if (element.parentNode == _DOMElement)
|
||||
element.parentNode.removeChild(element);
|
||||
{
|
||||
try
|
||||
{
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Bereits durch den Blur-Handler entfernt.
|
||||
}
|
||||
}
|
||||
|
||||
// Previosly, we unflagged CPTextFieldInputDidBlur and CPTextFieldInputResigning before
|
||||
// the call to removeChild. This resulted in DOM exceptions in Chrome under certain conditions.
|
||||
@@ -929,8 +992,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
CPTextFieldCachedSelectStartFunction = nil;
|
||||
CPTextFieldCachedDragFunction = nil;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)_setObserveWindowKeyNotifications:(BOOL)shouldObserve
|
||||
@@ -987,23 +1048,41 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
|
||||
- (BOOL)_valueIsValid:(CPString)aValue
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
var error = @"";
|
||||
|
||||
if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var acceptInvalidValue = NO;
|
||||
var error = @"";
|
||||
|
||||
if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
|
||||
acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
|
||||
if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
|
||||
{
|
||||
var acceptInvalidValue = NO;
|
||||
|
||||
if (acceptInvalidValue === NO)
|
||||
return NO;
|
||||
if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
|
||||
{
|
||||
var currentEvent = [CPApp currentEvent];
|
||||
|
||||
if (currentEvent &&
|
||||
CPTextFieldLastValidationFailureField === self &&
|
||||
CPTextFieldLastValidationFailureString === aValue &&
|
||||
CPTextFieldLastValidationFailureEvent === currentEvent)
|
||||
{
|
||||
acceptInvalidValue = CPTextFieldLastValidationFailureResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
|
||||
|
||||
CPTextFieldLastValidationFailureField = self;
|
||||
CPTextFieldLastValidationFailureString = aValue;
|
||||
CPTextFieldLastValidationFailureEvent = currentEvent;
|
||||
CPTextFieldLastValidationFailureResult = acceptInvalidValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptInvalidValue === NO)
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -1111,17 +1190,18 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
if (!([self isEnabled] && [self isEditable]))
|
||||
return;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var newValue = [self _inputElement].value;
|
||||
|
||||
if (newValue !== _stringValue)
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _setStringValue:newValue];
|
||||
[self _didEdit];
|
||||
}
|
||||
var newValue = [self _inputElement].value;
|
||||
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
|
||||
#endif
|
||||
if (newValue !== _stringValue)
|
||||
{
|
||||
[self _setStringValue:newValue];
|
||||
[self _didEdit];
|
||||
}
|
||||
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyDown:(CPEvent)anEvent
|
||||
@@ -1130,6 +1210,12 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
if (![self isEnabled] || !([self isEditable] || [self isSelectable]))
|
||||
return;
|
||||
|
||||
if ([self isEditable] && !_isEditing)
|
||||
{
|
||||
_isEditing = YES;
|
||||
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
}
|
||||
|
||||
// CPTextField uses an HTML input element to take the input so we need to
|
||||
// propagate the dom event so the element is updated. This has to be done
|
||||
// before interpretKeyEvents: though so individual commands have a chance
|
||||
@@ -1212,7 +1298,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
if (!([self isEnabled] && [self isEditable]))
|
||||
return;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
var oldValue = _stringValue,
|
||||
range = [self selectedRange],
|
||||
@@ -1230,8 +1317,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
{
|
||||
[self _didEdit];
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)textDidBlur:(CPNotification)note
|
||||
@@ -1271,6 +1356,17 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
[super textDidChange:note];
|
||||
}
|
||||
|
||||
- (void)validateEditing
|
||||
{
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
var element = [self _inputElement];
|
||||
|
||||
if (element)
|
||||
[self _setStringValue:element.value isNewValue:YES errorDescription:nil];
|
||||
}
|
||||
|
||||
- (void)textDidBeginEditing:(CPNotification)note
|
||||
{
|
||||
//this looks to prevent false propagation of notifications for other objects
|
||||
@@ -1384,13 +1480,9 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
else
|
||||
_stringValue = [self stringValue];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
if ((CPTextFieldInputOwner === self || [[self window] firstResponder] === self) && [[self window] isKeyWindow])
|
||||
if (CPDOMAvailable && (CPTextFieldInputOwner === self || [[self window] firstResponder] === self) && [[self window] isKeyWindow])
|
||||
[self _inputElement].value = _stringValue;
|
||||
|
||||
#endif
|
||||
|
||||
[self _updatePlaceholderState];
|
||||
}
|
||||
|
||||
@@ -1472,7 +1564,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
![self isBezeled] &&
|
||||
(lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
|
||||
{
|
||||
textSize = [text sizeWithFont:font inWidth:textSize.width] + 1;
|
||||
textSize = [text sizeWithFont:font inWidth:textSize.width];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1520,31 +1612,34 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
{
|
||||
var wind = [self window];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if ([self isEditable])
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var element = [self _inputElement];
|
||||
|
||||
if ([wind firstResponder] === self)
|
||||
if ([self isEditable])
|
||||
{
|
||||
if (immediately)
|
||||
element.select();
|
||||
else
|
||||
[[CPRunLoop mainRunLoop] performBlock:function(){ element.select(); } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
var element = [self _inputElement];
|
||||
|
||||
if ([wind firstResponder] === self)
|
||||
{
|
||||
if (immediately)
|
||||
element.select();
|
||||
else
|
||||
[[CPRunLoop mainRunLoop] performBlock:function(){ element.select(); } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
}
|
||||
else if (wind != nil && [wind makeFirstResponder:self])
|
||||
[self _selectText:sender immediately:immediately];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self setSelectedRange:CPMakeRange(0, _stringValue.length)];
|
||||
}
|
||||
else if (wind != nil && [wind makeFirstResponder:self])
|
||||
[self _selectText:sender immediately:immediately];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self setSelectedRange:CPMakeRange(0, _stringValue.length)];
|
||||
// Even if we can't actually select the text we need to preserve the first
|
||||
// responder side effect.
|
||||
if (wind != nil && [wind firstResponder] !== self)
|
||||
[wind makeFirstResponder:self];
|
||||
}
|
||||
#else
|
||||
// Even if we can't actually select the text we need to preserve the first
|
||||
// responder side effect.
|
||||
if (wind != nil && [wind firstResponder] !== self)
|
||||
[wind makeFirstResponder:self];
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1567,9 +1662,10 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
{
|
||||
// selectedRange won't work if we're displaying our text using a <div>. Instead we have to ask the browser
|
||||
// what's selected and hope it's right in a Cappuccino context as well.
|
||||
#if PLATFORM(DOM)
|
||||
stringToCopy = [[[self window] platformWindow] _selectedText];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
stringToCopy = [[[self window] platformWindow] _selectedText];
|
||||
}
|
||||
}
|
||||
|
||||
var pasteboard = [CPPasteboard generalPasteboard];
|
||||
@@ -1649,35 +1745,34 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
if ([[self window] firstResponder] !== self)
|
||||
return CPMakeRange(0, 0);
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
// we wrap this in try catch because firefox will throw an exception in certain instances
|
||||
try
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
var inputElement = [self _inputElement],
|
||||
selectionStart = inputElement.selectionStart,
|
||||
selectionEnd = inputElement.selectionEnd;
|
||||
|
||||
if ([selectionStart isKindOfClass:CPNumber])
|
||||
return CPMakeRange(selectionStart, selectionEnd - selectionStart);
|
||||
|
||||
// browsers which don't support selectionStart/selectionEnd (aka IE).
|
||||
var theDocument = inputElement.ownerDocument || inputElement.document,
|
||||
selectionRange = theDocument.selection.createRange(),
|
||||
range = inputElement.createTextRange();
|
||||
|
||||
if (range.inRange(selectionRange))
|
||||
// we wrap this in try catch because firefox will throw an exception in certain instances
|
||||
try
|
||||
{
|
||||
range.setEndPoint('EndToStart', selectionRange);
|
||||
return CPMakeRange(range.text.length, selectionRange.text.length);
|
||||
var inputElement = [self _inputElement],
|
||||
selectionStart = inputElement.selectionStart,
|
||||
selectionEnd = inputElement.selectionEnd;
|
||||
|
||||
if ([selectionStart isKindOfClass:CPNumber])
|
||||
return CPMakeRange(selectionStart, selectionEnd - selectionStart);
|
||||
|
||||
// browsers which don't support selectionStart/selectionEnd (aka IE).
|
||||
var theDocument = inputElement.ownerDocument || inputElement.document,
|
||||
selectionRange = theDocument.selection.createRange(),
|
||||
range = inputElement.createTextRange();
|
||||
|
||||
if (range.inRange(selectionRange))
|
||||
{
|
||||
range.setEndPoint('EndToStart', selectionRange);
|
||||
return CPMakeRange(range.text.length, selectionRange.text.length);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// fall through to the return
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// fall through to the return
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return CPMakeRange(0, 0);
|
||||
}
|
||||
@@ -1687,7 +1782,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
if (![[self window] firstResponder] === self)
|
||||
return;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable)
|
||||
return;
|
||||
|
||||
if (![self isEditable])
|
||||
{
|
||||
@@ -1731,7 +1827,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)selectAll:(id)sender
|
||||
@@ -1805,16 +1900,17 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
[self _setObjectValue:newValue useFormatter:NO];
|
||||
[self setSelectedRange:CPMakeRange(range.location, 0)];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
// Since we just performed the deletion manually, we don't need the browser to do anything else.
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
// Since we just performed the deletion manually, we don't need the browser to do anything else.
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
|
||||
}
|
||||
}
|
||||
|
||||
[self _didEdit];
|
||||
}
|
||||
|
||||
#pragma mark Setting the Delegate
|
||||
// MARK: Setting the Delegate
|
||||
|
||||
- (void)setDelegate:(id <CPTextFieldDelegate>)aDelegate
|
||||
{
|
||||
@@ -1974,6 +2070,43 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
[self _setCSSStyleForInputElement];
|
||||
}
|
||||
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
Sets the font of the receiver.
|
||||
|
||||
@param aFont - A CPFont object.
|
||||
*/
|
||||
- (void)setFont:(CPFont)aFont
|
||||
{
|
||||
if ([self currentValueForThemeAttribute:@"font"] === aFont)
|
||||
return;
|
||||
|
||||
// Apply the font to the default/normal state
|
||||
[self setValue:aFont forThemeAttribute:@"font"];
|
||||
|
||||
// Apply to standard editing and border states
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateEditing];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBezeled];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBordered];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPTextFieldStateRounded];
|
||||
|
||||
// Use CPThemeState() function to create composite states instead of array literals
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPTextFieldStateRounded, CPThemeStateEditing)];
|
||||
|
||||
// Apply across all standard control size states
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeRegular];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeSmall];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeMini];
|
||||
|
||||
// Apply to table data view states (ensuring Interface Builder-style lists respect the font)
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateTableDataView];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView)];
|
||||
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView, CPThemeStateFirstResponder, CPThemeStateKeyWindow)];
|
||||
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
|
||||
{
|
||||
var count = objects.length,
|
||||
@@ -1990,7 +2123,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
Sets the text color of the receiver.
|
||||
@@ -2059,7 +2192,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
// MARK: Private
|
||||
|
||||
- (BOOL)_isWithinUsablePlatformRect
|
||||
{
|
||||
@@ -2358,7 +2491,7 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPTextField (TableDataView)
|
||||
|
||||
@@ -2375,7 +2508,7 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPTextField (Deprecated)
|
||||
|
||||
|
||||
+263
-70
@@ -1,34 +1,29 @@
|
||||
/*
|
||||
* CPFontPanel.j
|
||||
* AppKit
|
||||
*
|
||||
* TODOs:
|
||||
* 1. make browser-width for size smaller and fix columns
|
||||
* 2. add all the missing features from the MacOS X counterpart (sampleview)
|
||||
*
|
||||
*
|
||||
* Created by Daniel Boehringer on 2/JAN/2014.
|
||||
* All modifications copyright Daniel Boehringer 2013.
|
||||
* Extensive code formatting and review by Andrew Hankinson
|
||||
* Based on original work by
|
||||
* Created by Emmanuel Maillard on 06/03/2010.
|
||||
* Copyright Emmanuel Maillard 2010.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
CPFontPanel.j
|
||||
AppKit
|
||||
|
||||
Created by Daniel Boehringer on 2/JAN/2014.
|
||||
All modifications copyright Daniel Boehringer 2013.
|
||||
Extensive code formatting and review by Andrew Hankinson
|
||||
|
||||
Based on original work by
|
||||
Created by Emmanuel Maillard on 06/03/2010.
|
||||
Copyright Emmanuel Maillard 2010.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "CPPanel.j"
|
||||
@import "CPColorWell.j"
|
||||
@@ -37,36 +32,39 @@
|
||||
@import "CPText.j"
|
||||
@import "CPFontManager.j"
|
||||
|
||||
|
||||
@class CPTextStorage
|
||||
@class CPLayoutManager
|
||||
@class CPTextContainer
|
||||
@class CPFontManager
|
||||
@class _CPFontPanelPreviewView
|
||||
|
||||
/*
|
||||
Collection indexes
|
||||
*/
|
||||
var kTypefaceIndex_Normal = 0,
|
||||
kTypefaceIndex_Italic = 1,
|
||||
kTypefaceIndex_Bold = 2,
|
||||
var kTypefaceIndex_Normal = 0,
|
||||
kTypefaceIndex_Italic = 1,
|
||||
kTypefaceIndex_Bold = 2,
|
||||
kTypefaceIndex_BoldItalic = 3,
|
||||
kToolbarHeight = 32,
|
||||
kBorderSpacing = 6,
|
||||
kInnerSpacing = 2,
|
||||
kNothingChanged = 0,
|
||||
kFontNameChanged = 1,
|
||||
kTypefaceChanged = 2,
|
||||
kSizeChanged = 3,
|
||||
kTextColorChanged = 4,
|
||||
kBackgroundColorChanged = 5,
|
||||
kUnderlineChanged = 6,
|
||||
kWeightChanged = 7,
|
||||
|
||||
kToolbarHeight = 32,
|
||||
kPreviewHeight = 70,
|
||||
kBorderSpacing = 6,
|
||||
kInnerSpacing = 2,
|
||||
|
||||
kNothingChanged = 0,
|
||||
kFontNameChanged = 1,
|
||||
kTypefaceChanged = 2,
|
||||
kSizeChanged = 3,
|
||||
kTextColorChanged = 4,
|
||||
kBackgroundColorChanged = 5,
|
||||
kUnderlineChanged = 6,
|
||||
kWeightChanged = 7,
|
||||
|
||||
_sharedFontPanel;
|
||||
|
||||
// FIXME<!> Locale support
|
||||
var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"];
|
||||
|
||||
var _availableTraits = [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"64", @"72", @"96", @"144", @"288"];
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@@ -77,6 +75,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
id _fontBrowser;
|
||||
id _traitBrowser;
|
||||
id _sizeBrowser;
|
||||
|
||||
// Preview
|
||||
_CPFontPanelPreviewView _previewView;
|
||||
|
||||
CPArray _availableFonts;
|
||||
id _textColorWell;
|
||||
CPColor _textColor;
|
||||
@@ -85,9 +87,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
int _fontChanges;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
/*!
|
||||
Check if the shared Font panel exists.
|
||||
@@ -108,14 +109,18 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
return _sharedFontPanel;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
/*! @ignore */
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 394) styleMask:(CPTitledWindowMask | CPClosableWindowMask /*| CPResizableWindowMask*/ )])
|
||||
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 420) styleMask:(CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask)])
|
||||
{
|
||||
[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
|
||||
[self setTitle:@"Font Panel"];
|
||||
@@ -152,10 +157,49 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
[aBrowser setDoubleAction:@selector(dblClicked:)];
|
||||
[aBrowser setAllowsEmptySelection:NO];
|
||||
[aBrowser setAllowsMultipleSelection:NO];
|
||||
|
||||
// Config Scrollers
|
||||
//[aBrowser setHasHorizontalScroller:NO];
|
||||
//[aBrowser setHasVerticalScroller:YES];
|
||||
//[aBrowser setAutohidesScrollers:YES];
|
||||
//[aBrowser setMaxVisibleColumns:1];
|
||||
|
||||
[aBrowser setDelegate:self];
|
||||
[[self contentView] addSubview:aBrowser];
|
||||
}
|
||||
|
||||
- (void)_layoutBrowsers
|
||||
{
|
||||
var contentView = [self contentView],
|
||||
contentBounds = [contentView bounds],
|
||||
previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing,
|
||||
browserY = previewY + kPreviewHeight + 10,
|
||||
browserHeight = CGRectGetHeight(contentBounds) - browserY - 10,
|
||||
availableWidth = CGRectGetWidth(contentBounds) - 20; // 10px padding L/R
|
||||
|
||||
// Layout Calculations
|
||||
// Increase sizeWidth slightly to 60 to allow space for the vertical scrollbar without clipping text
|
||||
var sizeWidth = 90,
|
||||
spacing = 5,
|
||||
remainingWidth = availableWidth - sizeWidth - (spacing * 2),
|
||||
// Split remaining roughly 60% font name, 40% trait
|
||||
fontWidth = FLOOR(remainingWidth * 0.60),
|
||||
traitWidth = remainingWidth - fontWidth;
|
||||
|
||||
// Apply frames and column constraints
|
||||
[_fontBrowser setFrame:CGRectMake(10, browserY, fontWidth, browserHeight)];
|
||||
[_fontBrowser setDefaultColumnWidth:fontWidth];
|
||||
[_fontBrowser setLastColumn:0];
|
||||
|
||||
[_traitBrowser setFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)];
|
||||
[_traitBrowser setDefaultColumnWidth:traitWidth];
|
||||
[_traitBrowser setLastColumn:0];
|
||||
|
||||
[_sizeBrowser setFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)];
|
||||
[_sizeBrowser setDefaultColumnWidth:sizeWidth];
|
||||
[_sizeBrowser setLastColumn:0];
|
||||
}
|
||||
|
||||
- (void)_setupContents
|
||||
{
|
||||
if (_setupDone)
|
||||
@@ -163,33 +207,53 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
_setupDone = YES;
|
||||
|
||||
// We set ourselves as delegate to handle resizing layout manually
|
||||
[self setDelegate:self];
|
||||
|
||||
[self _setupToolbarView];
|
||||
|
||||
var contentView = [self contentView],
|
||||
label = [CPTextField labelWithTitle:@"Font name"],
|
||||
contentBounds = [contentView bounds],
|
||||
upperView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(contentBounds), CGRectGetHeight(contentBounds) - (kBorderSpacing + kToolbarHeight + kInnerSpacing))];
|
||||
contentBounds = [contentView bounds];
|
||||
|
||||
[contentView addSubview:_toolbarView];
|
||||
|
||||
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, 35, 150, 350)];
|
||||
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(155, 35, 150, 350)];
|
||||
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(300, 35, 140, 350)];
|
||||
// Preview View
|
||||
var previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing;
|
||||
_previewView = [[_CPFontPanelPreviewView alloc] initWithFrame:CGRectMake(10, previewY, CGRectGetWidth(contentBounds) - 20, kPreviewHeight)];
|
||||
[_previewView setAutoresizingMask:CPViewWidthSizable];
|
||||
[contentView addSubview:_previewView];
|
||||
|
||||
// Initialize Browsers with zero rect, _layoutBrowsers will size them
|
||||
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
// Disable autoresizing masks because we are laying out manually in windowDidResize
|
||||
[_fontBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
[_traitBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
[_sizeBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
|
||||
[self _setupBrowser:_fontBrowser];
|
||||
[self _setupBrowser:_traitBrowser];
|
||||
[self _setupBrowser:_sizeBrowser];
|
||||
|
||||
// Perform initial layout
|
||||
[self _layoutBrowsers];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(textViewDidChangeSelection:)
|
||||
name:CPTextViewDidChangeSelectionNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(CPNotification)aNotification
|
||||
{
|
||||
[self _layoutBrowsers];
|
||||
}
|
||||
|
||||
- (void)textViewDidChangeSelection:(CPNotification)notification
|
||||
{
|
||||
[self _refreshWithTextView:[notification object]];
|
||||
|
||||
[self _refreshWithTextView:[notification object]];
|
||||
}
|
||||
|
||||
- (void)_refreshWithTextView:(CPTextView)textView
|
||||
@@ -197,6 +261,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if (![self isVisible])
|
||||
return;
|
||||
|
||||
if (![textView respondsToSelector:@selector(_attributesForFontPanel)])
|
||||
return;
|
||||
|
||||
var attribs = [textView _attributesForFontPanel],
|
||||
font = [attribs objectForKey:CPFontAttributeName] || [[textView textStorage] font] || [CPFont systemFontOfSize:12.0],
|
||||
color = [attribs objectForKey:CPForegroundColorAttributeName];
|
||||
@@ -217,6 +284,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
[self setCurrentTrait:trait];
|
||||
[self setCurrentSize:[font size] + ""]; //cast to string
|
||||
|
||||
// Update Preview
|
||||
[_previewView setPreviewFont:font];
|
||||
|
||||
if (!color)
|
||||
return;
|
||||
|
||||
@@ -253,7 +323,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
{
|
||||
case kFontNameChanged:
|
||||
newFont = [CPFont fontWithDescriptor:[[aFont fontDescriptor] fontDescriptorByAddingAttributes:
|
||||
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
|
||||
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
|
||||
break;
|
||||
|
||||
case kTypefaceChanged:
|
||||
@@ -272,12 +342,13 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
newFont = [[CPFontManager sharedFontManager] convertFont:aFont toSize:[self currentSize]];
|
||||
break;
|
||||
|
||||
case kNothingChanged:
|
||||
case kNothingChanged:
|
||||
break;
|
||||
|
||||
default:
|
||||
CPLog.trace(@"FIXME: -[" + [self className] + " " + _cmd + "] unhandled _fontChanges: " + _fontChanges);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return newFont;
|
||||
@@ -285,7 +356,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
- (void)setCurrentSize:(CGSize)aSize
|
||||
{
|
||||
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
|
||||
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
|
||||
}
|
||||
|
||||
- (CPString)currentSize
|
||||
@@ -295,7 +366,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
- (void)setCurrentFont:(CPFont)aFont
|
||||
{
|
||||
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
|
||||
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
|
||||
}
|
||||
|
||||
- (CPString)currentFont
|
||||
@@ -320,9 +391,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
case kTypefaceIndex_BoldItalic:
|
||||
row = 3;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
[_traitBrowser selectRow:row inColumn:0];
|
||||
[_traitBrowser selectRow:row inColumn:0];
|
||||
}
|
||||
|
||||
// FIXME<!> Locale support
|
||||
@@ -370,6 +442,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if ([self currentTrait] != typefaceIndex)
|
||||
[self setCurrentTrait:typefaceIndex ];
|
||||
|
||||
[_previewView setPreviewFont:font];
|
||||
|
||||
_fontChanges = kNothingChanged;
|
||||
}
|
||||
|
||||
@@ -382,23 +456,30 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// TODO: ask CPFontManager for traits //
|
||||
|
||||
- (void)browserClicked:(id)aBrowser
|
||||
{
|
||||
if (aBrowser === _fontBrowser)
|
||||
{
|
||||
_fontChanges = kFontNameChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
else if (aBrowser === _traitBrowser)
|
||||
{
|
||||
_fontChanges = kTypefaceChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
else if (aBrowser === _sizeBrowser)
|
||||
{
|
||||
_fontChanges = kSizeChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
|
||||
// Apply change immediately to manager (standard behavior)
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
|
||||
// Update our preview manually because convertFont: calls rely on selected rows
|
||||
// We construct a temporary font to update the preview view immediately
|
||||
var updatedFont = [self panelConvertFont:[_previewView font]];
|
||||
if (updatedFont)
|
||||
[_previewView setPreviewFont:updatedFont];
|
||||
}
|
||||
|
||||
- (void)dblClicked:(id)sender
|
||||
@@ -414,7 +495,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if (aBrowser === _traitBrowser)
|
||||
return [_availableTraits count];
|
||||
|
||||
return [_availableSizes count]
|
||||
return [_availableSizes count];
|
||||
}
|
||||
|
||||
- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem
|
||||
@@ -440,4 +521,116 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
@end
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// _CPFontPanelPreviewView
|
||||
// A helper class to display a font sample with metrics grid
|
||||
// -----------------------------------------------------------------------------
|
||||
@implementation _CPFontPanelPreviewView : CPView
|
||||
{
|
||||
CPTextField _sampleText;
|
||||
CPColor _gridColor;
|
||||
float _gridSize;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aRect
|
||||
{
|
||||
self = [super initWithFrame:aRect];
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self setBackgroundColor:[CPColor whiteColor]];
|
||||
|
||||
_gridColor = [CPColor colorWithHexString:@"e4f4ff"];
|
||||
_gridSize = 10.0;
|
||||
|
||||
_sampleText = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aRect), CGRectGetHeight(aRect))];
|
||||
[_sampleText setStringValue:@"AaYy-0123"];
|
||||
[_sampleText setAlignment:CPCenterTextAlignment];
|
||||
[_sampleText setVerticalAlignment:CPCenterVerticalTextAlignment];
|
||||
[_sampleText setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_sampleText setTextColor:[CPColor blackColor]];
|
||||
|
||||
[self addSubview:_sampleText];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setPreviewFont:(CPFont)aFont
|
||||
{
|
||||
[_sampleText setFont:aFont];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (CPFont)font
|
||||
{
|
||||
return [_sampleText font];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)dirtyRect
|
||||
{
|
||||
// Draw Grid (from MetricsView inspiration)
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort],
|
||||
bounds = [self bounds],
|
||||
maxX = CGRectGetMaxX(bounds),
|
||||
maxY = CGRectGetMaxY(bounds);
|
||||
|
||||
CGContextSetLineWidth(context, 1.0);
|
||||
CGContextSetStrokeColor(context, _gridColor);
|
||||
CGContextBeginPath(context);
|
||||
|
||||
for (var y = 0.5; y <= maxY; y += _gridSize)
|
||||
{
|
||||
CGContextMoveToPoint(context, 0.0, y);
|
||||
CGContextAddLineToPoint(context, maxX, y);
|
||||
}
|
||||
|
||||
for (var x = 0.5; x <= maxX; x += _gridSize)
|
||||
{
|
||||
CGContextMoveToPoint(context, x, 0.0);
|
||||
CGContextAddLineToPoint(context, x, maxY);
|
||||
}
|
||||
CGContextStrokePath(context);
|
||||
|
||||
// Draw Baseline/Ascender/Descender (from BaselineView inspiration)
|
||||
var font = [_sampleText font];
|
||||
if (!font) return;
|
||||
|
||||
var ascender = [font ascender],
|
||||
descender = [font descender],
|
||||
lineHeight = [font defaultLineHeightForFont];
|
||||
|
||||
// Calculate the baseline.
|
||||
// CPTextField with CPCenterVerticalTextAlignment usually centers the line height.
|
||||
// Top of line = midY - (lineHeight / 2.0)
|
||||
// Baseline = Top of line + ascender
|
||||
var midY = maxY / 2.0,
|
||||
baselineY = midY - (lineHeight / 2.0) + ascender;
|
||||
|
||||
CGContextSetStrokeColor(context, [CPColor redColor]);
|
||||
CGContextBeginPath(context);
|
||||
|
||||
// Baseline
|
||||
CGContextMoveToPoint(context, 0, baselineY);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY);
|
||||
|
||||
// Ascender Line
|
||||
CGContextMoveToPoint(context, 0, baselineY - ascender);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY - ascender);
|
||||
|
||||
// Descender Line
|
||||
CGContextMoveToPoint(context, 0, baselineY - descender);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY - descender);
|
||||
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
var text = prompt("Enter sample text:", [_sampleText stringValue]);
|
||||
if (text)
|
||||
[_sampleText setStringValue:text];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
[CPFontManager setFontPanelFactory:[CPFontPanel class]];
|
||||
@@ -25,14 +25,15 @@
|
||||
*/
|
||||
|
||||
@import "CPText.j"
|
||||
@import "CPTextContainer.j"
|
||||
@import "CGContext.j"
|
||||
@import "CPTypesetter.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@global _MakeRangeFromAbs
|
||||
|
||||
@global document
|
||||
@global CPBaselineOffsetAttributeName
|
||||
@global CPSuperscriptAttributeName
|
||||
|
||||
@class CPTextContainer
|
||||
@class CPTextView
|
||||
@@ -71,11 +72,12 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
BOOL _isValidatingLayoutAndGlyphs;
|
||||
CPRange _removeInvalidLineFragmentsRange;
|
||||
CPRange _lastEditedRange;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)init
|
||||
{
|
||||
@@ -95,13 +97,14 @@ _oncontextmenuhandler = function () { return false; };
|
||||
_textContainers = [[CPMutableArray alloc] init];
|
||||
_textStorage = [[CPTextStorage alloc] init];
|
||||
_typesetter = [CPTypesetter sharedSystemTypesetter];
|
||||
_lastEditedRange = nil;
|
||||
|
||||
[_textStorage addLayoutManager:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Text containes method
|
||||
// MARK: -
|
||||
// MARK: Text containes method
|
||||
|
||||
- (void)insertTextContainer:(CPTextContainer)aContainer atIndex:(int)index
|
||||
{
|
||||
@@ -286,30 +289,36 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
if (removeRange.length)
|
||||
_removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange);
|
||||
else
|
||||
_removeInvalidLineFragmentsRange = nil;
|
||||
|
||||
// We erased all lines
|
||||
if (!startIndex)
|
||||
[self setExtraLineFragmentRect:CGRectMake(0, 0) usedRect:CGRectMake(0, 0) textContainer:nil];
|
||||
// document.title=startIndex;
|
||||
|
||||
[_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
[self _cleanUpDOM];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _cleanUpDOM];
|
||||
}
|
||||
|
||||
_isValidatingLayoutAndGlyphs = NO;
|
||||
}
|
||||
|
||||
- (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange
|
||||
{
|
||||
var l = _lineFragments.length,
|
||||
location = aRange.location,
|
||||
found = NO,
|
||||
targetLine = 0;
|
||||
// 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing.
|
||||
if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0)
|
||||
return NO;
|
||||
|
||||
// try to find the first linefragment of the desired range
|
||||
for (; targetLine < l; targetLine++)
|
||||
var l = _lineFragments.length,
|
||||
location = aRange.location,
|
||||
found = NO,
|
||||
targetLine = l - 1; // Start from the END of the array
|
||||
|
||||
// 2. REVERSE SEARCH: The fragment we want is almost always at the end.
|
||||
for (; targetLine >= 0; targetLine--)
|
||||
{
|
||||
if (CPLocationInRange(location, _lineFragments[targetLine]._range))
|
||||
{
|
||||
@@ -332,9 +341,6 @@ _oncontextmenuhandler = function () { return false; };
|
||||
newLength = [[_textStorage string].length],
|
||||
removalSkip = 1;
|
||||
|
||||
// if (ABS(newLength - oldLength) > 1)
|
||||
// return NO;
|
||||
|
||||
if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment])
|
||||
{
|
||||
isIdentical = NO;
|
||||
@@ -367,6 +373,16 @@ _oncontextmenuhandler = function () { return false; };
|
||||
l = _lineFragmentsForRescue.length,
|
||||
newTargetLine = startLineForDOMRemoval + removalSkip;
|
||||
|
||||
// Ensure that the remaining lines we are attempting to rescue
|
||||
// start after the end of the edited region.
|
||||
if (newTargetLine < l && _lastEditedRange)
|
||||
{
|
||||
var firstRescuedLineNewLocation = _lineFragmentsForRescue[newTargetLine]._range.location + rangeOffset;
|
||||
|
||||
if (firstRescuedLineNewLocation < CPMaxRange(_lastEditedRange))
|
||||
return NO;
|
||||
}
|
||||
|
||||
for (; newTargetLine < l; newTargetLine++)
|
||||
{
|
||||
_lineFragmentsForRescue[newTargetLine]._isInvalid = NO; // protect them from final removal
|
||||
@@ -438,6 +454,8 @@ _oncontextmenuhandler = function () { return false; };
|
||||
{
|
||||
var actualRange = CPMakeRange(CPNotFound,0);
|
||||
|
||||
_lastEditedRange = CPMakeRangeCopy(charRange);
|
||||
|
||||
[self invalidateLayoutForCharacterRange:invalidatedRange isSoft:NO actualCharacterRange:actualRange];
|
||||
[self invalidateDisplayForGlyphRange:actualRange];
|
||||
[self _validateLayoutAndGlyphs];
|
||||
@@ -529,20 +547,26 @@ _oncontextmenuhandler = function () { return false; };
|
||||
var frames = [fragment glyphFrames],
|
||||
len = fragment._range.length;
|
||||
|
||||
for (var j = 0; j < len; j++)
|
||||
if (frames)
|
||||
{
|
||||
if (CGRectContainsPoint(frames[j], point))
|
||||
{
|
||||
if (partialFraction)
|
||||
partialFraction[0] = (point.x - frames[j].origin.x) / frames[j].size.width;
|
||||
var maxLen = MIN(len, frames.length);
|
||||
|
||||
return fragment._range.location + j;
|
||||
for (var j = 0; j < maxLen; j++)
|
||||
{
|
||||
var frame = frames[j];
|
||||
|
||||
if (frame && CGRectContainsPoint(frame, point))
|
||||
{
|
||||
if (partialFraction)
|
||||
partialFraction[0] = (point.x - frame.origin.x) / frame.size.width;
|
||||
|
||||
return fragment._range.location + j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found, maybe a point left to the last character was clicked -> search again with broader constraints
|
||||
if ([[_textStorage string] length])
|
||||
{
|
||||
for (var i = 0; i < c; i++)
|
||||
@@ -551,30 +575,33 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
if (fragment._textContainer === container)
|
||||
{
|
||||
// Within the horizontal territory of the current (not-empty) line?
|
||||
if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y &&
|
||||
point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height)
|
||||
if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y &&
|
||||
point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height)
|
||||
{
|
||||
if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y)
|
||||
continue;
|
||||
|
||||
var nlLoc = CPMaxRange(fragment._range),
|
||||
frames = [fragment glyphFrames];
|
||||
|
||||
if (frames && frames.length > 0)
|
||||
{
|
||||
// Skip tabs and move on the last fragment in this line
|
||||
if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y)
|
||||
continue;
|
||||
var lastFrame = frames[frames.length - 1],
|
||||
firstFrame = frames[0];
|
||||
|
||||
var nlLoc = CPMaxRange(fragment._range),
|
||||
lastFrame = [fragment glyphFrames][fragment._range.length - 1],
|
||||
firstFrame = [fragment glyphFrames][0];
|
||||
if (lastFrame && firstFrame)
|
||||
{
|
||||
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0]))
|
||||
nlLoc--;
|
||||
|
||||
// stay on the line the newline character belongs to
|
||||
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0]))
|
||||
nlLoc--;
|
||||
|
||||
// Clicked right to the last character
|
||||
if (point.x > CGRectGetMaxX(lastFrame))
|
||||
return nlLoc;
|
||||
// Clicked left to the last character
|
||||
else if (point.x <= CGRectGetMinX(firstFrame))
|
||||
return fragment._range.location;
|
||||
else
|
||||
return nlLoc;
|
||||
if (point.x > CGRectGetMaxX(lastFrame))
|
||||
return nlLoc;
|
||||
else if (point.x <= CGRectGetMinX(firstFrame))
|
||||
return fragment._range.location;
|
||||
else
|
||||
return nlLoc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,7 +731,11 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
var index = location - lineFragment._range.location;
|
||||
|
||||
return lineFragment._glyphsOffsets[index];
|
||||
if (index < 0 || !lineFragment._glyphsOffsets || index >= lineFragment._glyphsOffsets.length)
|
||||
return 0.0;
|
||||
|
||||
var offset = lineFragment._glyphsOffsets[index];
|
||||
return (offset === undefined) ? 0.0 : offset;
|
||||
}
|
||||
|
||||
- (double)_descentAtLocation:(unsigned)location
|
||||
@@ -716,7 +747,11 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
var index = location - lineFragment._range.location;
|
||||
|
||||
return lineFragment._glyphsFrames[index]._descent;
|
||||
if (index < 0 || !lineFragment._glyphsFrames || index >= lineFragment._glyphsFrames.length)
|
||||
return 0.0;
|
||||
|
||||
var frame = lineFragment._glyphsFrames[index];
|
||||
return (frame && frame._descent !== undefined) ? frame._descent : 0.0;
|
||||
}
|
||||
|
||||
- (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect
|
||||
@@ -840,11 +875,16 @@ _oncontextmenuhandler = function () { return false; };
|
||||
{
|
||||
if (_lineFragments.length > 0 && index >= [self numberOfGlyphs] - 1)
|
||||
{
|
||||
var lineFragment= _lineFragments[_lineFragments.length - 1],
|
||||
var lineFragment = _lineFragments[_lineFragments.length - 1],
|
||||
glyphFrames = [lineFragment glyphFrames];
|
||||
|
||||
if (glyphFrames.length > 0)
|
||||
return CGPointCreateCopy(glyphFrames[glyphFrames.length - 1].origin);
|
||||
if (glyphFrames && glyphFrames.length > 0)
|
||||
{
|
||||
var frame = glyphFrames[glyphFrames.length - 1];
|
||||
|
||||
if (frame)
|
||||
return CGPointCreateCopy(frame.origin);
|
||||
}
|
||||
}
|
||||
|
||||
var lineFragment = _objectWithLocationInRange(_lineFragments, index);
|
||||
@@ -855,8 +895,17 @@ _oncontextmenuhandler = function () { return false; };
|
||||
return CGPointCreateCopy(lineFragment._location);
|
||||
|
||||
var glyphFrames = [lineFragment glyphFrames];
|
||||
var relativeIndex = index - lineFragment._range.location;
|
||||
|
||||
return CGPointCreateCopy(glyphFrames[index - lineFragment._range.location].origin);
|
||||
if (glyphFrames && relativeIndex >= 0 && relativeIndex < glyphFrames.length)
|
||||
{
|
||||
var frame = glyphFrames[relativeIndex];
|
||||
|
||||
if (frame)
|
||||
return CGPointCreateCopy(frame.origin);
|
||||
}
|
||||
|
||||
return CGPointCreateCopy(lineFragment._location);
|
||||
}
|
||||
|
||||
return CGPointMakeZero();
|
||||
@@ -902,7 +951,6 @@ _oncontextmenuhandler = function () { return false; };
|
||||
inTextContainer:(CPTextContainer)container
|
||||
rectCount:(CGRectPointer)rectCount
|
||||
{
|
||||
|
||||
var rectArray = [],
|
||||
lineFragments = _objectsInRange(_lineFragments, selectedCharRange);
|
||||
|
||||
@@ -921,21 +969,24 @@ _oncontextmenuhandler = function () { return false; };
|
||||
rect = nil,
|
||||
len = fragment._range.length;
|
||||
|
||||
for (var j = 0; j < len; j++)
|
||||
if (frames)
|
||||
{
|
||||
if (CPLocationInRange(fragment._range.location + j, selectedCharRange))
|
||||
for (var j = 0; j < len; j++)
|
||||
{
|
||||
var correctedRect = CGRectCreateCopy(frames[j]);
|
||||
correctedRect.size.height -= frames[j]._descent;
|
||||
correctedRect.origin.y -= frames[j]._descent;
|
||||
if (j < frames.length && CPLocationInRange(fragment._range.location + j, selectedCharRange))
|
||||
{
|
||||
var frame = frames[j];
|
||||
|
||||
if (!rect)
|
||||
rect = CGRectCreateCopy(correctedRect);
|
||||
else
|
||||
rect = CGRectUnion(rect, correctedRect);
|
||||
if (frame)
|
||||
{
|
||||
var correctedRect = CGRectCreateCopy(frame);
|
||||
|
||||
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)]))
|
||||
rect.size.width = containerSize.width - rect.origin.x;
|
||||
if (!rect)
|
||||
rect = CGRectCreateCopy(correctedRect);
|
||||
else
|
||||
rect = CGRectUnion(rect, correctedRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,7 +997,7 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
var len = rectArray.length;
|
||||
|
||||
for (var i = 0; i < len - 1; i++) // extend the width of all but the last one
|
||||
for (var i = 0; i < len - 1; i++)
|
||||
{
|
||||
if (FLOOR(CGRectGetMaxY(rectArray[i])) == FLOOR(CGRectGetMaxY(rectArray[i + 1])))
|
||||
continue;
|
||||
@@ -1074,6 +1125,7 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
BOOL _isInvalid;
|
||||
BOOL _isLast;
|
||||
float _wordSpacing;
|
||||
CGRect _fragmentRect;
|
||||
CGRect _usedRect;
|
||||
CGPoint _location;
|
||||
@@ -1082,8 +1134,8 @@ var _objectsInRange = function(aList, aRange)
|
||||
CPMutableArray _runs;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)aColor
|
||||
{
|
||||
@@ -1092,7 +1144,12 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)fgColor andBackgroundColor:(CPColor)bgColor andUnderline:(CPUnderlineStyle)aUnderline
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
if (!aString || aString.length === 0)
|
||||
return nil;
|
||||
|
||||
if (!CPDOMAvailable) return nil;
|
||||
|
||||
var style,
|
||||
span = document.createElement("span");
|
||||
|
||||
@@ -1106,6 +1163,10 @@ var _objectsInRange = function(aList, aRange)
|
||||
style.whiteSpace = "pre";
|
||||
style.backgroundColor = "transparent";
|
||||
style.font = [aFont cssString];
|
||||
|
||||
// A justified line widens its spaces; the typesetter has added the same width to their advancements.
|
||||
if (_wordSpacing)
|
||||
style.wordSpacing = _wordSpacing + "px";
|
||||
|
||||
if (aUnderline)
|
||||
{
|
||||
@@ -1143,9 +1204,6 @@ var _objectsInRange = function(aList, aRange)
|
||||
span.textContent = aString;
|
||||
|
||||
return span;
|
||||
#else
|
||||
return nil;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage
|
||||
@@ -1161,6 +1219,7 @@ var _objectsInRange = function(aList, aRange)
|
||||
_range = CPMakeRangeCopy(aRange);
|
||||
_textContainer = aContainer;
|
||||
_isInvalid = NO;
|
||||
_wordSpacing = 0.0;
|
||||
_runs = [];
|
||||
_glyphsFrames = [];
|
||||
_glyphsOffsets = [];
|
||||
@@ -1172,18 +1231,16 @@ var _objectsInRange = function(aList, aRange)
|
||||
effectiveRange = attributes ? CPIntersectionRange(aRange, effectiveRange) : aRange;
|
||||
|
||||
var string = [textStorage._string substringWithRange:effectiveRange],
|
||||
underline = [attributes objectForKey:CPUnderlineStyleAttributeName] || CPUnderlineStyleNone;
|
||||
underline = [attributes objectForKey:CPUnderlineStyleAttributeName] || CPUnderlineStyleNone,
|
||||
paragraphStyle = [attributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle];
|
||||
|
||||
// this is an attachment -> create a run for it
|
||||
if (string === _CPAttachmentCharacterAsString)
|
||||
{
|
||||
if (![attributes objectForKey:_CPAttachmentInvisible])
|
||||
{
|
||||
var view = [attributes objectForKey:_CPAttachmentView],
|
||||
viewCopy = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:view]],
|
||||
elem = viewCopy._DOMElement,
|
||||
run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:elem, string:nil, view:viewCopy};
|
||||
|
||||
var view = [attributes objectForKey:_CPAttachmentView];
|
||||
var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle, underline:underline, baselineOffset:0.0};
|
||||
_runs.push(run);
|
||||
}
|
||||
}
|
||||
@@ -1191,10 +1248,97 @@ var _objectsInRange = function(aList, aRange)
|
||||
{
|
||||
var color = [attributes objectForKey:CPForegroundColorAttributeName],
|
||||
bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName],
|
||||
font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0],
|
||||
run = {_range:CPMakeRangeCopy(effectiveRange), color:color, font:font, elem:nil, string:string, bgcolor:bgcolor};
|
||||
font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0];
|
||||
|
||||
_runs.push(run);
|
||||
var baselineOffset = [attributes objectForKey:CPBaselineOffsetAttributeName],
|
||||
superscript = [attributes objectForKey:CPSuperscriptAttributeName];
|
||||
|
||||
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
|
||||
baselineOffset = 0.0;
|
||||
|
||||
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
|
||||
superscript = 0;
|
||||
|
||||
if (superscript !== 0)
|
||||
{
|
||||
var size = [font size],
|
||||
scaledSize = size * 0.65,
|
||||
fontName = [font familyName],
|
||||
isBold = [font isBold],
|
||||
isItalic = [font isItalic];
|
||||
|
||||
font = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic];
|
||||
|
||||
if (baselineOffset === 0.0)
|
||||
{
|
||||
if (superscript > 0)
|
||||
baselineOffset = size * 0.35;
|
||||
else
|
||||
baselineOffset = -size * 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
var currentLoc = effectiveRange.location,
|
||||
strLen = string.length,
|
||||
startIdx = 0;
|
||||
|
||||
for (var i = 0; i < strLen; i++)
|
||||
{
|
||||
if (string.charCodeAt(i) === 9) // Tabulator-Zeichen '\t'
|
||||
{
|
||||
if (i > startIdx)
|
||||
{
|
||||
var subString = string.substring(startIdx, i),
|
||||
subRange = CPMakeRange(currentLoc + startIdx, i - startIdx),
|
||||
run = {
|
||||
_range: subRange,
|
||||
color: color,
|
||||
font: font,
|
||||
elem: nil,
|
||||
string: subString,
|
||||
bgcolor: bgcolor,
|
||||
paragraphStyle: paragraphStyle,
|
||||
underline: underline,
|
||||
baselineOffset: baselineOffset
|
||||
};
|
||||
_runs.push(run);
|
||||
}
|
||||
|
||||
var tabRange = CPMakeRange(currentLoc + i, 1),
|
||||
tabRun = {
|
||||
_range: tabRange,
|
||||
color: nil,
|
||||
font: nil,
|
||||
elem: nil,
|
||||
string: nil,
|
||||
bgcolor: nil,
|
||||
paragraphStyle: paragraphStyle,
|
||||
underline: underline,
|
||||
baselineOffset: 0.0
|
||||
};
|
||||
_runs.push(tabRun);
|
||||
|
||||
startIdx = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIdx < strLen)
|
||||
{
|
||||
var subString = string.substring(startIdx, strLen),
|
||||
subRange = CPMakeRange(currentLoc + startIdx, strLen - startIdx),
|
||||
run = {
|
||||
_range: subRange,
|
||||
color: color,
|
||||
font: font,
|
||||
elem: nil,
|
||||
string: subString,
|
||||
bgcolor: bgcolor,
|
||||
paragraphStyle: paragraphStyle,
|
||||
underline: underline,
|
||||
baselineOffset: baselineOffset
|
||||
};
|
||||
_runs.push(run);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CPMaxRange(effectiveRange))
|
||||
@@ -1218,7 +1362,10 @@ var _objectsInRange = function(aList, aRange)
|
||||
{
|
||||
_glyphsFrames[i] = CGRectMake(origin.x, origin.y, someAdvancements[i].width, height);
|
||||
_glyphsFrames[i]._descent = someAdvancements[i].descent;
|
||||
_glyphsOffsets[i] = height - someAdvancements[i].height;
|
||||
|
||||
// Align the run's baseline with the common line baseline (_location.y)
|
||||
_glyphsOffsets[i] = _location.y - someAdvancements[i].height;
|
||||
|
||||
origin.x += someAdvancements[i].width;
|
||||
}
|
||||
}
|
||||
@@ -1266,13 +1413,11 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
for (var i = 0; i < l; i++)
|
||||
{
|
||||
if (_runs[i].view && _runs[i].DOMactive)
|
||||
[_runs[i].view removeFromSuperview];
|
||||
|
||||
if (_runs[i].elem && _runs[i].DOMactive)
|
||||
{
|
||||
if (_runs[i].view)
|
||||
[_runs[i].view removeFromSuperview];
|
||||
else
|
||||
_textContainer._textView._DOMElement.removeChild(_runs[i].elem);
|
||||
}
|
||||
_textContainer._textView._DOMElement.removeChild(_runs[i].elem);
|
||||
|
||||
_runs[i].elem = nil;
|
||||
_runs[i].DOMactive = NO;
|
||||
@@ -1285,6 +1430,9 @@ var _objectsInRange = function(aList, aRange)
|
||||
c = runs.length,
|
||||
orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y);
|
||||
|
||||
if (_runs.length === 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < c; i++)
|
||||
{
|
||||
var run = runs[i];
|
||||
@@ -1299,13 +1447,21 @@ var _objectsInRange = function(aList, aRange)
|
||||
continue;
|
||||
|
||||
var loc = run._range.location - _runs[0]._range.location;
|
||||
|
||||
// Safety bounds check to protect against uninitialized/empty glyph frames or offsets
|
||||
if (loc < 0 || loc >= _glyphsFrames.length || !_glyphsFrames[loc] || !_glyphsOffsets || loc >= _glyphsOffsets.length)
|
||||
continue;
|
||||
|
||||
orig.x = _glyphsFrames[loc].origin.x + aPoint.x;
|
||||
orig.y = _glyphsFrames[loc].origin.y + aPoint.y + _glyphsOffsets[loc];
|
||||
|
||||
if(run.elem)
|
||||
if(run.elem || run.view)
|
||||
{
|
||||
run.elem.style.left = (orig.x) + "px";
|
||||
run.elem.style.top = (orig.y) + "px";
|
||||
if (run.elem)
|
||||
{
|
||||
run.elem.style.left = (orig.x) + "px";
|
||||
run.elem.style.top = (orig.y) + "px";
|
||||
}
|
||||
|
||||
if (run.view)
|
||||
[run.view setFrameOrigin:orig];
|
||||
@@ -1313,8 +1469,9 @@ var _objectsInRange = function(aList, aRange)
|
||||
if (!run.DOMactive)
|
||||
{
|
||||
if (run.view)
|
||||
[self._textContainer._textView addSubview:run.view];
|
||||
else
|
||||
[_textContainer._textView addSubview:run.view];
|
||||
|
||||
if (run.elem)
|
||||
_textContainer._textView._DOMElement.appendChild(run.elem);
|
||||
}
|
||||
|
||||
@@ -1343,6 +1500,9 @@ var _objectsInRange = function(aList, aRange)
|
||||
if (!oldFragmentRuns || !newFragmentRuns || oldFragmentRuns.length !== newFragmentRuns.length)
|
||||
return NO;
|
||||
|
||||
if (newLineFragment._wordSpacing !== _wordSpacing)
|
||||
return NO;
|
||||
|
||||
var l = oldFragmentRuns.length;
|
||||
|
||||
for (var i = 0; i < l; i++)
|
||||
@@ -1353,9 +1513,17 @@ var _objectsInRange = function(aList, aRange)
|
||||
if (!_RectEqualToRectHorizontally(newLineFragment._fragmentRect, _fragmentRect))
|
||||
return NO;
|
||||
|
||||
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || newFragmentRuns[i].font !== oldFragmentRuns[i].font)
|
||||
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color ||
|
||||
newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor ||
|
||||
newFragmentRuns[i].font !== oldFragmentRuns[i].font ||
|
||||
newFragmentRuns[i].baselineOffset !== oldFragmentRuns[i].baselineOffset)
|
||||
return NO;
|
||||
|
||||
var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle],
|
||||
newStyle = newFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle];
|
||||
|
||||
if (![oldStyle isEqual:newStyle])
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
@@ -1371,12 +1539,22 @@ var _objectsInRange = function(aList, aRange)
|
||||
{
|
||||
_runs[i]._range.location += rangeOffset;
|
||||
|
||||
if (verticalOffset && _runs[i].elem)
|
||||
if (verticalOffset)
|
||||
{
|
||||
// An attachment view is moved through setFrameOrigin:, which also moves it on the
|
||||
// screen. Changing its frame directly would leave it in place, because the next draw's
|
||||
// setFrameOrigin: would then find it already at the new origin and do nothing.
|
||||
if (_runs[i].view)
|
||||
_runs[i].view._frame.origin.y += verticalOffset;
|
||||
{
|
||||
var viewOrigin = [_runs[i].view frameOrigin];
|
||||
|
||||
[_runs[i].view setFrameOrigin:CGPointMake(viewOrigin.x, viewOrigin.y + verticalOffset)];
|
||||
}
|
||||
|
||||
// A span that has been placed moves with its line. One that has not is placed when drawn.
|
||||
if (_runs[i].elem && _runs[i].elem.style.top)
|
||||
_runs[i].elem.style.top = (parseFloat(_runs[i].elem.style.top) + verticalOffset) + 'px';
|
||||
|
||||
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
|
||||
_runs[i].DOMpatched = YES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
* CPParagraphStyle.j
|
||||
* AppKit
|
||||
*
|
||||
* FIXME
|
||||
* This is basically a stub.
|
||||
* We need to store all the spacing informations as well as writing direction (among others)
|
||||
*
|
||||
* Created by Daniel Boehringer on 11/01/2014
|
||||
* Copyright Daniel Boehringer 2014.
|
||||
*
|
||||
@@ -26,198 +22,400 @@
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPArray.j>
|
||||
|
||||
@import "CPText.j"
|
||||
|
||||
CPLeftTabStopType = 0;
|
||||
@import <Foundation/CPDictionary.j>
|
||||
|
||||
CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName";
|
||||
|
||||
var _sharedDefaultParagraphStyle,
|
||||
_defaultTabStopArray;
|
||||
@typedef CPTabStopType
|
||||
CPLeftTabStopType = 0;
|
||||
CPRightTabStopType = 1;
|
||||
CPCenterTabStopType = 2;
|
||||
CPDecimalTabStopType = 3;
|
||||
|
||||
@implementation CPParagraphStyle : CPObject
|
||||
// Standard Tab Interval (28pts is roughly 4 spaces in standard fonts)
|
||||
var kDefaultTabInterval = 28.0;
|
||||
|
||||
// MARK: - CPTextTab Implementation
|
||||
|
||||
@implementation CPTextTab : CPObject
|
||||
{
|
||||
CPArray _tabStops @accessors(property=tabStops);
|
||||
CPTextAlignment _alignment @accessors(property=alignment);
|
||||
unsigned _firstLineHeadIndent @accessors(property=firstLineHeadIndent);
|
||||
unsigned _headIndent @accessors(property=headIndent);
|
||||
unsigned _tailIndent @accessors(property=tailIndent);
|
||||
unsigned _paragraphSpacing @accessors(property=paragraphSpacing);
|
||||
unsigned _minimumLineHeight @accessors(property=minimumLineHeight);
|
||||
unsigned _maximumLineHeight @accessors(property=maximumLineHeight);
|
||||
unsigned _lineSpacing @accessors(property=lineSpacing);
|
||||
CPTextAlignment _alignment @accessors(readonly, property=alignment);
|
||||
float _location @accessors(readonly, property=location);
|
||||
CPDictionary _options @accessors(readonly, property=options);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
|
||||
+ (CPParagraphStyle)defaultParagraphStyle
|
||||
- (id)initWithTextAlignment:(CPTextAlignment)anAlignment location:(float)aLocation options:(CPDictionary)options
|
||||
{
|
||||
if (!_sharedDefaultParagraphStyle)
|
||||
_sharedDefaultParagraphStyle = [self new];
|
||||
|
||||
return _sharedDefaultParagraphStyle;
|
||||
}
|
||||
|
||||
+ (CPArray)_defaultTabStops
|
||||
{
|
||||
if (!_defaultTabStopArray)
|
||||
if (self = [super init])
|
||||
{
|
||||
var i;
|
||||
_defaultTabStopArray = [];
|
||||
|
||||
// <!> FIXME: Define constants for these magic numbers: 13, 28
|
||||
for (i = 1; i < 16 ; i++)
|
||||
{
|
||||
_defaultTabStopArray.push([[CPTextTab alloc] initWithType:CPLeftTabStopType location:i * 28]);
|
||||
}
|
||||
_alignment = anAlignment;
|
||||
_location = aLocation;
|
||||
_options = [options copy];
|
||||
}
|
||||
|
||||
return _defaultTabStopArray;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
|
||||
- (id)init
|
||||
{
|
||||
[self _initWithDefaults];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPParagraphStyle)initWithParagraphStyle:(CPParagraphStyle)other
|
||||
// Convenience initializer matching AppKit behavior
|
||||
- (id)initWithType:(CPTabStopType)aType location:(float)aLocation
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_tabStops = [other._tabStops copy];
|
||||
_alignment = other._alignment;
|
||||
_firstLineHeadIndent = other._firstLineHeadIndent;
|
||||
_headIndent = other._headIndent;
|
||||
_tailIndent = other._tailIndent;
|
||||
_paragraphSpacing = other._paragraphSpacing;
|
||||
_minimumLineHeight = other._minimumLineHeight;
|
||||
_maximumLineHeight = other._maximumLineHeight;
|
||||
_lineSpacing = other._lineSpacing;
|
||||
|
||||
return self;
|
||||
// Map old TabStopType to TextAlignment for modern compatibility
|
||||
return [self initWithTextAlignment:aType location:aLocation options:nil];
|
||||
}
|
||||
|
||||
- (void)_initWithDefaults
|
||||
// Added to resolve the unrecognized selector exception in the RTF producer
|
||||
- (CPTabStopType)tabStopType
|
||||
{
|
||||
_alignment = CPLeftTextAlignment;
|
||||
_tabStops = [[[self class] _defaultTabStops] copy];
|
||||
return _alignment;
|
||||
}
|
||||
|
||||
- (void)addTabStop:(CPTextTab)aStop
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
_tabStops.push(aStop);
|
||||
if (self === other) return YES;
|
||||
if (![other isKindOfClass:[CPTextTab class]]) return NO;
|
||||
|
||||
return _location === [other location] &&
|
||||
_alignment === [other alignment] &&
|
||||
((_options == nil && [other options] == nil) || [_options isEqualToDictionary:[other options]]);
|
||||
}
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
var other = [[self class] alloc];
|
||||
|
||||
return [other initWithParagraphStyle:self];
|
||||
return [[CPTextTab alloc] initWithTextAlignment:_alignment location:_location options:_options];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
var CPParagraphStyleTabStopsKey = @"CPParagraphStyleTabStopsKey",
|
||||
CPParagraphStyleAlignmentKey = @"CPParagraphStyleAlignmentKey",
|
||||
CPParagraphStyleFirstLineHeadIndentKey = @"CPParagraphStyleFirstLineHeadIndentKey",
|
||||
CPParagraphStyleHeadIndentKey = @"CPParagraphStyleHeadIndentKey",
|
||||
CPParagraphStyleTailIndentKey = @"CPParagraphStyleTailIndentKey",
|
||||
CPParagraphStyleParagraphSpacingKey = @"CPParagraphStyleParagraphSpacingKey",
|
||||
CPParagraphStyleMinimumLineHeightKey = @"CPParagraphStyleMinimumLineHeightKey",
|
||||
CPParagraphStyleMaximumLineHeightKey = @"CPParagraphStyleMaximumLineHeightKey",
|
||||
CPParagraphStyleLineSpacingKey = @"CPParagraphStyleLineSpacingKey";
|
||||
|
||||
@implementation CPParagraphStyle (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(id)aCoder
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [self init];
|
||||
|
||||
if (self)
|
||||
if (self = [super init])
|
||||
{
|
||||
_tabStops = [aCoder decodeObjectForKey:"CPParagraphStyleTabStopsKey"];
|
||||
_alignment = [aCoder decodeIntForKey:"CPParagraphStyleAlignmentKey"];
|
||||
_firstLineHeadIndent = [aCoder decodeIntForKey:"CPParagraphStyleFirstLineHeadIndentKey"];
|
||||
_headIndent = [aCoder decodeIntForKey:"CPParagraphStyleHeadIndentKey"];
|
||||
_tailIndent = [aCoder decodeIntForKey:"CPParagraphStyleTailIndentKey"];
|
||||
_paragraphSpacing = [aCoder decodeIntForKey:"CPParagraphStyleParagraphSpacingKey"];
|
||||
_minimumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMinimumLineHeightKey"];
|
||||
_maximumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMaximumLineHeightKey"];
|
||||
_lineSpacing = [aCoder decodeIntForKey:"CPParagraphStyleLineSpacingKey"];
|
||||
_alignment = [aCoder decodeIntForKey:@"CPTextTabAlignment"];
|
||||
_location = [aCoder decodeFloatForKey:@"CPTextTabLocation"];
|
||||
_options = [aCoder decodeObjectForKey:@"CPTextTabOptions"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(id)aCoder
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeInt:_alignment forKey:"CPParagraphStyleAlignmentKey"];
|
||||
[aCoder encodeObject:_tabStops forKey:"CPParagraphStyleTabStopsKey"];
|
||||
[aCoder encodeInt:_firstLineHeadIndent forKey:"CPParagraphStyleFirstLineHeadIndentKey"];
|
||||
[aCoder encodeInt:_headIndent forKey:"CPParagraphStyleHeadIndentKey"];
|
||||
[aCoder encodeInt:_tailIndent forKey:"CPParagraphStyleTailIndentKey"];
|
||||
[aCoder encodeInt:_paragraphSpacing forKey:"CPParagraphStyleParagraphSpacingKey"];
|
||||
[aCoder encodeInt:_minimumLineHeight forKey:"CPParagraphStyleMinimumLineHeightKey"];
|
||||
[aCoder encodeInt:_maximumLineHeight forKey:"CPParagraphStyleMaximumLineHeightKey"];
|
||||
[aCoder encodeInt:_lineSpacing forKey:"CPParagraphStyleLineSpacingKey"];
|
||||
[aCoder encodeInt:_alignment forKey:@"CPTextTabAlignment"];
|
||||
[aCoder encodeFloat:_location forKey:@"CPTextTabLocation"];
|
||||
[aCoder encodeObject:_options forKey:@"CPTextTabOptions"];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CPTextTab : CPObject
|
||||
// MARK: - CPParagraphStyle Implementation
|
||||
|
||||
/*
|
||||
Returns YES if both arrays hold the same text lists, in the same order. A text list is
|
||||
identified by the object itself, so the lists are compared by identity.
|
||||
*/
|
||||
var _CPParagraphStyleTextListsAreIdentical = function(someLists, otherLists)
|
||||
{
|
||||
int _type @accessors(property = tabStopType);
|
||||
double _location @accessors(property = location);
|
||||
var count = someLists ? someLists.length : 0;
|
||||
|
||||
if (count !== (otherLists ? otherLists.length : 0))
|
||||
return NO;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
if (someLists[i] !== otherLists[i])
|
||||
return NO;
|
||||
|
||||
return YES;
|
||||
};
|
||||
|
||||
var _sharedDefaultParagraphStyle = nil;
|
||||
|
||||
@implementation CPParagraphStyle : CPObject
|
||||
{
|
||||
float _lineSpacing @accessors(readonly, property=lineSpacing);
|
||||
float _paragraphSpacing @accessors(readonly, property=paragraphSpacing);
|
||||
CPTextAlignment _alignment @accessors(readonly, property=alignment);
|
||||
float _headIndent @accessors(readonly, property=headIndent);
|
||||
float _tailIndent @accessors(readonly, property=tailIndent);
|
||||
float _firstLineHeadIndent @accessors(readonly, property=firstLineHeadIndent);
|
||||
float _minimumLineHeight @accessors(readonly, property=minimumLineHeight);
|
||||
float _maximumLineHeight @accessors(readonly, property=maximumLineHeight);
|
||||
CPLineBreakMode _lineBreakMode @accessors(readonly, property=lineBreakMode);
|
||||
CPWritingDirection _baseWritingDirection @accessors(readonly, property=baseWritingDirection);
|
||||
float _lineHeightMultiple @accessors(readonly, property=lineHeightMultiple);
|
||||
float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore);
|
||||
float _defaultTabInterval @accessors(readonly, property=defaultTabInterval);
|
||||
CPArray _tabStops @accessors(readonly, property=tabStops);
|
||||
CPArray _textLists @accessors(readonly, property=textLists);
|
||||
}
|
||||
|
||||
- (id)initWithType:(CPTabStopType) aType location:(double) aLocation
|
||||
+ (CPParagraphStyle)defaultParagraphStyle
|
||||
{
|
||||
if ([self = [super init]])
|
||||
if (!_sharedDefaultParagraphStyle)
|
||||
{
|
||||
_type = aType;
|
||||
_location = aLocation;
|
||||
_sharedDefaultParagraphStyle = [[CPParagraphStyle alloc] init];
|
||||
// Ensure defaults are set on the shared instance internal vars
|
||||
// Since it's immutable, we rely on the init to set these.
|
||||
}
|
||||
return _sharedDefaultParagraphStyle;
|
||||
}
|
||||
|
||||
+ (CPWritingDirection)defaultWritingDirectionForLanguage:(CPString)languageName
|
||||
{
|
||||
// Simplified: Cappuccino usually assumes LTR unless specified otherwise.
|
||||
return CPWritingDirectionLeftToRight;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_lineSpacing = 0.0;
|
||||
_paragraphSpacing = 0.0;
|
||||
_alignment = CPLeftTextAlignment;
|
||||
_headIndent = 0.0;
|
||||
_tailIndent = 0.0;
|
||||
_firstLineHeadIndent = 0.0;
|
||||
_minimumLineHeight = 0.0;
|
||||
_maximumLineHeight = 0.0;
|
||||
_lineBreakMode = CPLineBreakByWordWrapping;
|
||||
_baseWritingDirection = CPWritingDirectionNatural;
|
||||
_lineHeightMultiple = 0.0;
|
||||
_paragraphSpacingBefore = 0.0;
|
||||
_defaultTabInterval = kDefaultTabInterval;
|
||||
|
||||
// Generate default tab stops
|
||||
_tabStops = [];
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
[_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment
|
||||
location:i * kDefaultTabInterval]];
|
||||
}
|
||||
|
||||
_textLists = [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
var CPTextTabTypeKey = @"CPTextTabTypeKey",
|
||||
CPTextTabLocationKey = @"CPTextTabLocationKey";
|
||||
|
||||
@implementation CPTextTab (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(id)aCoder
|
||||
- (id)initWithParagraphStyle:(CPParagraphStyle)other
|
||||
{
|
||||
self = [self init];
|
||||
|
||||
if (self)
|
||||
if (self = [super init])
|
||||
{
|
||||
_type = [aCoder decodeIntForKey:"CPTextTabTypeKey"];
|
||||
_location = [aCoder decodeDoubleForKey:"CPTextTabLocationKey"];
|
||||
_lineSpacing = [other lineSpacing];
|
||||
_paragraphSpacing = [other paragraphSpacing];
|
||||
_alignment = [other alignment];
|
||||
_headIndent = [other headIndent];
|
||||
_tailIndent = [other tailIndent];
|
||||
_firstLineHeadIndent = [other firstLineHeadIndent];
|
||||
_minimumLineHeight = [other minimumLineHeight];
|
||||
_maximumLineHeight = [other maximumLineHeight];
|
||||
_lineBreakMode = [other lineBreakMode];
|
||||
_baseWritingDirection = [other baseWritingDirection];
|
||||
_lineHeightMultiple = [other lineHeightMultiple];
|
||||
_paragraphSpacingBefore = [other paragraphSpacingBefore];
|
||||
_defaultTabInterval = [other defaultTabInterval];
|
||||
_tabStops = [[other tabStops] copy];
|
||||
_textLists = [[other textLists] copy] || [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(id)aCoder
|
||||
- (id)copy
|
||||
{
|
||||
[aCoder encodeInt:_type forKey:"CPTextTabTypeKey"];
|
||||
[aCoder encodeDouble:_location forKey:"CPTextTabLocationKey"];
|
||||
// Since this class is immutable, return self.
|
||||
// Subclasses (Mutable) will override.
|
||||
if ([self class] === [CPParagraphStyle class])
|
||||
return self;
|
||||
|
||||
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
|
||||
}
|
||||
|
||||
- (id)mutableCopy
|
||||
{
|
||||
return [[CPMutableParagraphStyle alloc] initWithParagraphStyle:self];
|
||||
}
|
||||
|
||||
// MARK: - Coding Support
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_lineSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleLineSpacing"];
|
||||
_paragraphSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacing"];
|
||||
_alignment = [aCoder decodeIntForKey:@"CPParagraphStyleAlignment"];
|
||||
_headIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleHeadIndent"];
|
||||
_tailIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleTailIndent"];
|
||||
_firstLineHeadIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleFirstLineHeadIndent"];
|
||||
_minimumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMinimumLineHeight"];
|
||||
_maximumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMaximumLineHeight"];
|
||||
_lineBreakMode = [aCoder decodeIntForKey:@"CPParagraphStyleLineBreakMode"];
|
||||
_baseWritingDirection = [aCoder decodeIntForKey:@"CPParagraphStyleBaseWritingDirection"];
|
||||
_lineHeightMultiple = [aCoder decodeFloatForKey:@"CPParagraphStyleLineHeightMultiple"];
|
||||
_paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"];
|
||||
_defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"];
|
||||
_tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"];
|
||||
_textLists = [aCoder decodeObjectForKey:@"CPParagraphStyleTextLists"] || [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeFloat:_lineSpacing forKey:@"CPParagraphStyleLineSpacing"];
|
||||
[aCoder encodeFloat:_paragraphSpacing forKey:@"CPParagraphStyleParagraphSpacing"];
|
||||
[aCoder encodeInt:_alignment forKey:@"CPParagraphStyleAlignment"];
|
||||
[aCoder encodeFloat:_headIndent forKey:@"CPParagraphStyleHeadIndent"];
|
||||
[aCoder encodeFloat:_tailIndent forKey:@"CPParagraphStyleTailIndent"];
|
||||
[aCoder encodeFloat:_firstLineHeadIndent forKey:@"CPParagraphStyleFirstLineHeadIndent"];
|
||||
[aCoder encodeFloat:_minimumLineHeight forKey:@"CPParagraphStyleMinimumLineHeight"];
|
||||
[aCoder encodeFloat:_maximumLineHeight forKey:@"CPParagraphStyleMaximumLineHeight"];
|
||||
[aCoder encodeInt:_lineBreakMode forKey:@"CPParagraphStyleLineBreakMode"];
|
||||
[aCoder encodeInt:_baseWritingDirection forKey:@"CPParagraphStyleBaseWritingDirection"];
|
||||
[aCoder encodeFloat:_lineHeightMultiple forKey:@"CPParagraphStyleLineHeightMultiple"];
|
||||
[aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"];
|
||||
[aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"];
|
||||
[aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"];
|
||||
|
||||
if ([_textLists count])
|
||||
[aCoder encodeObject:_textLists forKey:@"CPParagraphStyleTextLists"];
|
||||
}
|
||||
|
||||
// MARK: - Equality
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
{
|
||||
if (self === other) return YES;
|
||||
if (![other isKindOfClass:[CPParagraphStyle class]]) return NO;
|
||||
|
||||
return _lineSpacing === [other lineSpacing] &&
|
||||
_paragraphSpacing === [other paragraphSpacing] &&
|
||||
_alignment === [other alignment] &&
|
||||
_headIndent === [other headIndent] &&
|
||||
_tailIndent === [other tailIndent] &&
|
||||
_firstLineHeadIndent === [other firstLineHeadIndent] &&
|
||||
_lineBreakMode === [other lineBreakMode] &&
|
||||
[_tabStops isEqualToArray:[other tabStops]] &&
|
||||
_CPParagraphStyleTextListsAreIdentical(_textLists, [other textLists]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// MARK: - CPMutableParagraphStyle Implementation
|
||||
|
||||
@implementation CPMutableParagraphStyle : CPParagraphStyle
|
||||
{
|
||||
}
|
||||
|
||||
- (id)initWithParagraphStyle:(CPParagraphStyle)other
|
||||
{
|
||||
if (self = [super initWithParagraphStyle:other])
|
||||
{
|
||||
// Ensure our tab stops array is mutable in the mutable subclass
|
||||
_tabStops = [[other tabStops] mutableCopy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (self = [super initWithCoder:aCoder])
|
||||
{
|
||||
_tabStops = [_tabStops mutableCopy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setLineSpacing:(float)aLineSpacing
|
||||
{
|
||||
_lineSpacing = aLineSpacing;
|
||||
}
|
||||
|
||||
- (void)setParagraphSpacing:(float)aParagraphSpacing
|
||||
{
|
||||
_paragraphSpacing = aParagraphSpacing;
|
||||
}
|
||||
|
||||
- (void)setAlignment:(CPTextAlignment)anAlignment
|
||||
{
|
||||
_alignment = anAlignment;
|
||||
}
|
||||
|
||||
- (void)setHeadIndent:(float)aHeadIndent
|
||||
{
|
||||
_headIndent = aHeadIndent;
|
||||
}
|
||||
|
||||
- (void)setTailIndent:(float)aTailIndent
|
||||
{
|
||||
_tailIndent = aTailIndent;
|
||||
}
|
||||
|
||||
- (void)setFirstLineHeadIndent:(float)aFirstLineHeadIndent
|
||||
{
|
||||
_firstLineHeadIndent = aFirstLineHeadIndent;
|
||||
}
|
||||
|
||||
- (void)setMinimumLineHeight:(float)aMinimumLineHeight
|
||||
{
|
||||
_minimumLineHeight = aMinimumLineHeight;
|
||||
}
|
||||
|
||||
- (void)setMaximumLineHeight:(float)aMaximumLineHeight
|
||||
{
|
||||
_maximumLineHeight = aMaximumLineHeight;
|
||||
}
|
||||
|
||||
- (void)setLineBreakMode:(CPLineBreakMode)aLineBreakMode
|
||||
{
|
||||
_lineBreakMode = aLineBreakMode;
|
||||
}
|
||||
|
||||
- (void)setBaseWritingDirection:(CPWritingDirection)aBaseWritingDirection
|
||||
{
|
||||
_baseWritingDirection = aBaseWritingDirection;
|
||||
}
|
||||
|
||||
- (void)setLineHeightMultiple:(float)aLineHeightMultiple
|
||||
{
|
||||
_lineHeightMultiple = aLineHeightMultiple;
|
||||
}
|
||||
|
||||
- (void)setParagraphSpacingBefore:(float)aParagraphSpacingBefore
|
||||
{
|
||||
_paragraphSpacingBefore = aParagraphSpacingBefore;
|
||||
}
|
||||
|
||||
- (void)setDefaultTabInterval:(float)aDefaultTabInterval
|
||||
{
|
||||
_defaultTabInterval = aDefaultTabInterval;
|
||||
}
|
||||
|
||||
- (void)addTabStop:(CPTextTab)aTabStop
|
||||
{
|
||||
[_tabStops addObject:aTabStop];
|
||||
}
|
||||
|
||||
- (void)removeTabStop:(CPTextTab)aTabStop
|
||||
{
|
||||
[_tabStops removeObject:aTabStop];
|
||||
}
|
||||
|
||||
- (void)setTabStops:(CPArray)newTabStops
|
||||
{
|
||||
if (_tabStops === newTabStops) return;
|
||||
_tabStops = [newTabStops mutableCopy];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the text lists that the paragraph belongs to, outermost first.
|
||||
*/
|
||||
- (void)setTextLists:(CPArray)someTextLists
|
||||
{
|
||||
_textLists = [someTextLists copy] || [];
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(CPZone)aZone
|
||||
{
|
||||
// Return an immutable copy
|
||||
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* CPRulerMarker.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by Daniel Boehringer on 11/01/2014
|
||||
* Copyright Daniel Boehringer 2014.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
|
||||
@import "CGGeometry.j"
|
||||
@import "CPEvent.j"
|
||||
@import "CPGraphicsContext.j"
|
||||
@import "CPImage.j"
|
||||
|
||||
@class CPRulerView
|
||||
|
||||
@global CPApp
|
||||
@global CPHorizontalRuler
|
||||
|
||||
|
||||
/*!
|
||||
Distance, in points, that the pointer must move beyond the ruler before a removable
|
||||
marker is removed when it is released.
|
||||
*/
|
||||
var CPRulerMarkerRemovalDistance = 0.0;
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPRulerMarker
|
||||
|
||||
A CPRulerMarker shows a location on a CPRulerView, such as a tab stop or a paragraph
|
||||
indent. The marker is drawn by its ruler view as an image, positioned so that the
|
||||
image's \c imageOrigin lies at the marker's location on the ruler's baseline. The
|
||||
marker's location is expressed in the coordinate system of the ruler's client view.
|
||||
|
||||
The user can drag a movable marker along the ruler, and drag a removable marker off the
|
||||
ruler to remove it. The ruler's client view is consulted through the CPRulerView client
|
||||
methods before and after each change.
|
||||
*/
|
||||
@implementation CPRulerMarker : CPObject
|
||||
{
|
||||
CPRulerView _ruler;
|
||||
float _markerLocation;
|
||||
CPImage _image;
|
||||
CGPoint _imageOrigin;
|
||||
BOOL _isMovable;
|
||||
BOOL _isRemovable;
|
||||
BOOL _isDragging;
|
||||
BOOL _isOffRuler;
|
||||
id _representedObject;
|
||||
|
||||
float _trackingStartLocation;
|
||||
float _trackingStartPoint;
|
||||
float _trackingStartCrossPoint;
|
||||
float _trackingCrossOffset;
|
||||
BOOL _trackingIsAdding;
|
||||
BOOL _trackingMoved;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a marker for \c aRulerView at \c aLocation, drawn with \c anImage. The
|
||||
marker is associated with the ruler view, but not added to it. A new marker is movable
|
||||
and not removable.
|
||||
@param aRulerView the ruler view the marker belongs to
|
||||
@param aLocation the marker's location, in the client view's coordinate system
|
||||
@param anImage the image drawn for the marker
|
||||
@param anImageOrigin the point in the image, measured from its top left corner, placed
|
||||
at the marker's location on the ruler's baseline
|
||||
@return the initialized marker
|
||||
*/
|
||||
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation image:(CPImage)anImage imageOrigin:(CGPoint)anImageOrigin
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_ruler = aRulerView;
|
||||
_markerLocation = aLocation;
|
||||
_image = anImage;
|
||||
_imageOrigin = CGPointMakeCopy(anImageOrigin);
|
||||
_isMovable = YES;
|
||||
_isRemovable = NO;
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: Getting the Ruler View
|
||||
|
||||
/*!
|
||||
Returns the ruler view the marker belongs to.
|
||||
*/
|
||||
- (CPRulerView)ruler
|
||||
{
|
||||
return _ruler;
|
||||
}
|
||||
|
||||
// MARK: Setting the Image
|
||||
|
||||
/*!
|
||||
Returns the image drawn for the marker.
|
||||
*/
|
||||
- (CPImage)image
|
||||
{
|
||||
return _image;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image drawn for the marker.
|
||||
@param anImage the new image
|
||||
*/
|
||||
- (void)setImage:(CPImage)anImage
|
||||
{
|
||||
if (_image === anImage)
|
||||
return;
|
||||
|
||||
_image = anImage;
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the point in the image, measured from its top left corner, that is placed at
|
||||
the marker's location on the ruler's baseline.
|
||||
*/
|
||||
- (CGPoint)imageOrigin
|
||||
{
|
||||
return _imageOrigin;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the point in the image, measured from its top left corner, that is placed at the
|
||||
marker's location on the ruler's baseline.
|
||||
@param aPoint the new image origin
|
||||
*/
|
||||
- (void)setImageOrigin:(CGPoint)aPoint
|
||||
{
|
||||
_imageOrigin = CGPointMakeCopy(aPoint);
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the rectangle that the marker's image occupies, in the ruler view's coordinate
|
||||
system.
|
||||
*/
|
||||
- (CGRect)imageRectInRuler
|
||||
{
|
||||
var imageSize = [_image size] || CGSizeMakeZero(),
|
||||
point = [_ruler _rulerPointForLocation:_markerLocation],
|
||||
baseline = [_ruler baselineLocation];
|
||||
|
||||
// Whole-point positions keep every marker image sharp and identical, wherever it lies on the ruler.
|
||||
// While the user drags the marker outward, it follows the pointer across the ruler.
|
||||
var crossPosition = baseline + (_isDragging ? _trackingCrossOffset : 0.0);
|
||||
|
||||
if ([_ruler orientation] === CPHorizontalRuler)
|
||||
return CGRectMake(ROUND(point - _imageOrigin.x), ROUND(crossPosition - _imageOrigin.y), imageSize.width, imageSize.height);
|
||||
|
||||
return CGRectMake(ROUND(crossPosition - _imageOrigin.x), ROUND(point - _imageOrigin.y), imageSize.width, imageSize.height);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the extent of the marker's image on the marker side of the ruler's baseline:
|
||||
above the baseline on a horizontal ruler, and to its left on a vertical ruler.
|
||||
*/
|
||||
- (float)thicknessRequiredInRuler
|
||||
{
|
||||
return [_ruler orientation] === CPHorizontalRuler ? _imageOrigin.y : _imageOrigin.x;
|
||||
}
|
||||
|
||||
// MARK: Setting Movability
|
||||
|
||||
/*!
|
||||
Returns \c YES if the user can drag the marker along the ruler.
|
||||
*/
|
||||
- (BOOL)isMovable
|
||||
{
|
||||
return _isMovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the user can drag the marker along the ruler.
|
||||
@param shouldBeMovable \c YES to let the user move the marker
|
||||
*/
|
||||
- (void)setMovable:(BOOL)shouldBeMovable
|
||||
{
|
||||
_isMovable = shouldBeMovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns \c YES if the user can remove the marker by dragging it off the ruler.
|
||||
*/
|
||||
- (BOOL)isRemovable
|
||||
{
|
||||
return _isRemovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the user can remove the marker by dragging it off the ruler.
|
||||
@param shouldBeRemovable \c YES to let the user remove the marker
|
||||
*/
|
||||
- (void)setRemovable:(BOOL)shouldBeRemovable
|
||||
{
|
||||
_isRemovable = shouldBeRemovable;
|
||||
}
|
||||
|
||||
// MARK: Setting the Location
|
||||
|
||||
/*!
|
||||
Returns the marker's location, in the client view's coordinate system.
|
||||
*/
|
||||
- (float)markerLocation
|
||||
{
|
||||
return _markerLocation;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the marker's location, in the client view's coordinate system.
|
||||
@param aLocation the new location
|
||||
*/
|
||||
- (void)setMarkerLocation:(float)aLocation
|
||||
{
|
||||
if (_markerLocation === aLocation)
|
||||
return;
|
||||
|
||||
_markerLocation = aLocation;
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
// MARK: Setting the Represented Object
|
||||
|
||||
/*!
|
||||
Returns the object the marker represents, such as a CPTextTab.
|
||||
*/
|
||||
- (id)representedObject
|
||||
{
|
||||
return _representedObject;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the object the marker represents.
|
||||
@param anObject the represented object
|
||||
*/
|
||||
- (void)setRepresentedObject:(id)anObject
|
||||
{
|
||||
_representedObject = anObject;
|
||||
}
|
||||
|
||||
// MARK: Drawing and Event Handling
|
||||
|
||||
/*!
|
||||
Returns \c YES while the user is dragging the marker.
|
||||
*/
|
||||
- (BOOL)isDragging
|
||||
{
|
||||
return _isDragging;
|
||||
}
|
||||
|
||||
/*!
|
||||
Draws the marker's image if it lies within \c aRect, in the ruler view's coordinate
|
||||
system. While a removable marker is dragged off the ruler, it is drawn faded.
|
||||
@param aRect the rectangle being drawn
|
||||
*/
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
if (!_image || [_image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
return;
|
||||
|
||||
var imageRect = [self imageRectInRuler];
|
||||
|
||||
if (!CGRectIntersectsRect(imageRect, aRect))
|
||||
return;
|
||||
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort];
|
||||
|
||||
CGContextSaveGState(context);
|
||||
|
||||
// Off the ruler's outer edge the marker follows the pointer and is clipped by the ruler.
|
||||
// Off its inner edge it stays on its line, so it is drawn faded instead.
|
||||
if (_isOffRuler && _trackingCrossOffset === 0.0)
|
||||
CGContextSetAlpha(context, 0.4);
|
||||
|
||||
CGContextDrawImage(context, imageRect, _image);
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
/*!
|
||||
Starts tracking the user's manipulation of the marker, from the mouse-down event
|
||||
\c anEvent. Tracking continues until the mouse button is released.
|
||||
|
||||
If \c isAdding is \c YES, the marker is not yet in the ruler: it is added when the user
|
||||
releases it over the ruler, if the client view agrees. Otherwise the marker is moved,
|
||||
and removed if it is removable and released away from the ruler.
|
||||
@param anEvent the mouse-down event that started the manipulation
|
||||
@param isAdding \c YES if the marker is being added to the ruler
|
||||
@return \c YES if tracking started, or \c NO if the marker cannot be moved or removed,
|
||||
or the client view refused
|
||||
*/
|
||||
- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)isAdding
|
||||
{
|
||||
if (!isAdding && !_isMovable && !_isRemovable)
|
||||
return NO;
|
||||
|
||||
if (isAdding && ![_ruler _clientShouldAddMarker:self])
|
||||
return NO;
|
||||
|
||||
if (!isAdding && _isMovable && ![_ruler _clientShouldMoveMarker:self])
|
||||
return NO;
|
||||
|
||||
_trackingIsAdding = isAdding;
|
||||
_trackingMoved = NO;
|
||||
_trackingStartLocation = _markerLocation;
|
||||
_trackingStartPoint = [self _axisPointForEvent:anEvent];
|
||||
_trackingStartCrossPoint = [self _crossPointForEvent:anEvent];
|
||||
_trackingCrossOffset = 0.0;
|
||||
_isDragging = YES;
|
||||
_isOffRuler = NO;
|
||||
|
||||
if (isAdding)
|
||||
{
|
||||
_markerLocation = [_ruler _clientWillAddMarker:self atLocation:_markerLocation];
|
||||
[_ruler _setTrackedMarker:self];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
|
||||
[CPApp setTarget:self
|
||||
selector:@selector(_trackMouseWithEvent:)
|
||||
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
|
||||
untilDate:nil
|
||||
inMode:nil
|
||||
dequeue:YES];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)_trackMouseWithEvent:(CPEvent)anEvent
|
||||
{
|
||||
var type = [anEvent type];
|
||||
|
||||
if (type === CPLeftMouseDragged)
|
||||
{
|
||||
_trackingMoved = YES;
|
||||
_isOffRuler = [self _isEventOffRuler:anEvent] && (_trackingIsAdding || _isRemovable);
|
||||
|
||||
// As in Cocoa, the marker stays on its line while the pointer is on the ruler, and follows
|
||||
// the pointer out over the ruler's outer edge, where the ruler clips it.
|
||||
_trackingCrossOffset = (_trackingIsAdding || _isRemovable) ? MIN(0.0, [self _crossPointForEvent:anEvent] - _trackingStartCrossPoint) : 0.0;
|
||||
|
||||
if (_trackingIsAdding || _isMovable)
|
||||
{
|
||||
var proposedLocation = _trackingStartLocation + [self _axisPointForEvent:anEvent] - _trackingStartPoint;
|
||||
|
||||
_markerLocation = _trackingIsAdding ? [_ruler _clientWillAddMarker:self atLocation:proposedLocation]
|
||||
: [_ruler _clientWillMoveMarker:self toLocation:proposedLocation];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
|
||||
[CPApp setTarget:self
|
||||
selector:@selector(_trackMouseWithEvent:)
|
||||
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
|
||||
untilDate:nil
|
||||
inMode:nil
|
||||
dequeue:YES];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== CPLeftMouseUp)
|
||||
return;
|
||||
|
||||
var wasOffRuler = _isOffRuler;
|
||||
|
||||
_trackingCrossOffset = 0.0;
|
||||
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
|
||||
if (_trackingIsAdding)
|
||||
{
|
||||
[_ruler _setTrackedMarker:nil];
|
||||
|
||||
if (!wasOffRuler)
|
||||
{
|
||||
[_ruler addMarker:self];
|
||||
[_ruler _clientDidAddMarker:self];
|
||||
}
|
||||
}
|
||||
else if (wasOffRuler && [_ruler _clientShouldRemoveMarker:self])
|
||||
{
|
||||
[_ruler removeMarker:self];
|
||||
[_ruler _clientDidRemoveMarker:self];
|
||||
}
|
||||
else if (_trackingMoved)
|
||||
{
|
||||
if (wasOffRuler)
|
||||
_markerLocation = _trackingStartLocation;
|
||||
|
||||
if (_markerLocation !== _trackingStartLocation)
|
||||
[_ruler _clientDidMoveMarker:self];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (float)_crossPointForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
|
||||
return [_ruler orientation] === CPHorizontalRuler ? point.y : point.x;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (float)_axisPointForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
|
||||
return [_ruler orientation] === CPHorizontalRuler ? point.x : point.y;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (BOOL)_isEventOffRuler:(CPEvent)anEvent
|
||||
{
|
||||
// Only distance across the ruler counts. Dragging past either end of the ruler moves the marker.
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil],
|
||||
bounds = [_ruler bounds];
|
||||
|
||||
if ([_ruler orientation] === CPHorizontalRuler)
|
||||
return point.y < CGRectGetMinY(bounds) - CPRulerMarkerRemovalDistance || point.y > CGRectGetMaxY(bounds) + CPRulerMarkerRemovalDistance;
|
||||
|
||||
return point.x < CGRectGetMinX(bounds) - CPRulerMarkerRemovalDistance || point.x > CGRectGetMaxX(bounds) + CPRulerMarkerRemovalDistance;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
var CPRulerMarkerRulerKey = @"CPRulerMarkerRulerKey",
|
||||
CPRulerMarkerLocationKey = @"CPRulerMarkerLocationKey",
|
||||
CPRulerMarkerImageKey = @"CPRulerMarkerImageKey",
|
||||
CPRulerMarkerImageOriginKey = @"CPRulerMarkerImageOriginKey",
|
||||
CPRulerMarkerMovableKey = @"CPRulerMarkerMovableKey",
|
||||
CPRulerMarkerRemovableKey = @"CPRulerMarkerRemovableKey",
|
||||
CPRulerMarkerRepresentedObjectKey = @"CPRulerMarkerRepresentedObjectKey";
|
||||
|
||||
@implementation CPRulerMarker (CPCoding)
|
||||
|
||||
/*!
|
||||
Initializes a marker from an archive.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_ruler = [aCoder decodeObjectForKey:CPRulerMarkerRulerKey];
|
||||
_markerLocation = [aCoder decodeFloatForKey:CPRulerMarkerLocationKey];
|
||||
_image = [aCoder decodeObjectForKey:CPRulerMarkerImageKey];
|
||||
_imageOrigin = [aCoder decodePointForKey:CPRulerMarkerImageOriginKey];
|
||||
_isMovable = [aCoder decodeBoolForKey:CPRulerMarkerMovableKey];
|
||||
_isRemovable = [aCoder decodeBoolForKey:CPRulerMarkerRemovableKey];
|
||||
_representedObject = [aCoder decodeObjectForKey:CPRulerMarkerRepresentedObjectKey];
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
_trackingCrossOffset = 0.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Archives the marker. The ruler is archived conditionally.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeConditionalObject:_ruler forKey:CPRulerMarkerRulerKey];
|
||||
[aCoder encodeFloat:_markerLocation forKey:CPRulerMarkerLocationKey];
|
||||
[aCoder encodeObject:_image forKey:CPRulerMarkerImageKey];
|
||||
[aCoder encodePoint:_imageOrigin forKey:CPRulerMarkerImageOriginKey];
|
||||
[aCoder encodeBool:_isMovable forKey:CPRulerMarkerMovableKey];
|
||||
[aCoder encodeBool:_isRemovable forKey:CPRulerMarkerRemovableKey];
|
||||
[aCoder encodeObject:_representedObject forKey:CPRulerMarkerRepresentedObjectKey];
|
||||
}
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* CPTextAttachment.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 25/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPData.j>
|
||||
@import <Foundation/CPNotificationCenter.j>
|
||||
|
||||
@import "CPImage.j"
|
||||
@import "CPImageView.j"
|
||||
|
||||
/*
|
||||
Posted when an attachment's displayed size changes, for example when its image has loaded.
|
||||
A text view that contains the attachment lays its text out again.
|
||||
*/
|
||||
_CPTextAttachmentDidChangeSizeNotification = @"_CPTextAttachmentDidChangeSizeNotification";
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPTextAttachment
|
||||
|
||||
A CPTextAttachment holds an image in rich text, as NSTextAttachment does in Cocoa. The text
|
||||
holds the attachment as one attachment character (CPAttachmentCharacter), whose
|
||||
CPAttachmentAttributeName attribute is the attachment. The attachment is edited, copied and
|
||||
undone as that one character.
|
||||
|
||||
The attachment keeps the image's data and type, and shows the image in a view. As in TextKit
|
||||
2, an attachment is drawn by a view, not by a cell. The text system lays the view out as the
|
||||
character, at the view's size, with its bottom on the baseline. That size is the attachment's
|
||||
bounds, or the image's natural size, scaled down to fit the width of the text, as in TextEdit.
|
||||
*/
|
||||
@implementation CPTextAttachment : CPObject
|
||||
{
|
||||
CPData _contents;
|
||||
CPString _fileType;
|
||||
CPImage _image;
|
||||
CGRect _bounds;
|
||||
|
||||
CPImageView _imageView;
|
||||
float _maximumWidth;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises an attachment with image data.
|
||||
@param contentData the image's data
|
||||
@param aType the image's type, as a MIME type such as \c @"image/png", or a uniform type
|
||||
identifier such as \c @"public.png"
|
||||
*/
|
||||
- (id)initWithData:(CPData)contentData ofType:(CPString)aType
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_contents = contentData;
|
||||
_fileType = aType;
|
||||
_bounds = CGRectMakeZero();
|
||||
_maximumWidth = 0.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithData:nil ofType:nil];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns an attachment made from a data URL, such as the browser's FileReader produces.
|
||||
*/
|
||||
+ (CPTextAttachment)_attachmentWithDataURL:(CPString)aDataURL
|
||||
{
|
||||
var match = /^data:([^;,]+)(;base64)?,(.*)$/.exec(aDataURL || "");
|
||||
|
||||
if (!match)
|
||||
return nil;
|
||||
|
||||
var data = match[2] ? [CPData dataWithBase64:match[3]] : [CPData dataWithRawString:decodeURIComponent(match[3])];
|
||||
|
||||
return [[self alloc] initWithData:data ofType:match[1]];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image's data.
|
||||
*/
|
||||
- (CPData)contents
|
||||
{
|
||||
return _contents;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image's data. The image is made again from the data.
|
||||
*/
|
||||
- (void)setContents:(CPData)contentData
|
||||
{
|
||||
_contents = contentData;
|
||||
[self _setImage:nil];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image's type.
|
||||
*/
|
||||
- (CPString)fileType
|
||||
{
|
||||
return _fileType;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image's type.
|
||||
*/
|
||||
- (void)setFileType:(CPString)aType
|
||||
{
|
||||
_fileType = aType;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image. It is made from the attachment's data the first time it is needed.
|
||||
*/
|
||||
- (CPImage)image
|
||||
{
|
||||
if (!_image && _contents)
|
||||
[self _setImage:[[CPImage alloc] initWithContentsOfFile:[self _dataURL]]];
|
||||
|
||||
return _image;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image. An image set this way has no data of its own unless it was made from a data
|
||||
URL, so an attachment that must be archived or copied should be made with
|
||||
-initWithData:ofType:.
|
||||
*/
|
||||
- (void)setImage:(CPImage)anImage
|
||||
{
|
||||
[self _setImage:anImage];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the attachment's bounds in the text. With a zero size, the attachment takes the
|
||||
image's natural size.
|
||||
*/
|
||||
- (CGRect)bounds
|
||||
{
|
||||
return CGRectMakeCopy(_bounds);
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the attachment's bounds in the text.
|
||||
*/
|
||||
- (void)setBounds:(CGRect)aRect
|
||||
{
|
||||
_bounds = CGRectMakeCopy(aRect);
|
||||
[self _updateViewSize];
|
||||
}
|
||||
|
||||
// MARK: Display
|
||||
|
||||
/* @ignore
|
||||
Returns the view that shows the attachment in the text. The text system lays it out as the
|
||||
attachment character.
|
||||
*/
|
||||
- (CPView)_attachmentView
|
||||
{
|
||||
if (!_imageView)
|
||||
{
|
||||
_imageView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
|
||||
[_imageView setImageScaling:CPImageScaleAxesIndependently];
|
||||
[_imageView setImage:[self image]];
|
||||
[self _updateViewSize];
|
||||
}
|
||||
|
||||
return _imageView;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Sets the widest the attachment may be shown, which is the width of the text. Returns YES if
|
||||
the displayed size changes.
|
||||
*/
|
||||
- (BOOL)_setMaximumWidth:(float)aWidth
|
||||
{
|
||||
if (_maximumWidth === aWidth)
|
||||
return NO;
|
||||
|
||||
_maximumWidth = aWidth;
|
||||
|
||||
return [self _updateViewSize];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the size at which the attachment is shown: its bounds, or the image's natural size,
|
||||
scaled down in proportion to fit the maximum width. The size is zero until the image has
|
||||
loaded.
|
||||
*/
|
||||
- (CGSize)_displaySize
|
||||
{
|
||||
var size = CGSizeMakeCopy(_bounds.size);
|
||||
|
||||
if (!size.width || !size.height)
|
||||
{
|
||||
var image = [self image];
|
||||
|
||||
if (!image || [image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
return CGSizeMakeZero();
|
||||
|
||||
size = CGSizeMakeCopy([image size]);
|
||||
}
|
||||
|
||||
if (_maximumWidth > 0.0 && size.width > _maximumWidth)
|
||||
{
|
||||
size.height = ROUND(size.height * _maximumWidth / size.width);
|
||||
size.width = _maximumWidth;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Gives the view the displayed size. Returns YES if the size changes.
|
||||
*/
|
||||
- (BOOL)_updateViewSize
|
||||
{
|
||||
if (!_imageView)
|
||||
return NO;
|
||||
|
||||
var size = [self _displaySize];
|
||||
|
||||
if (CGSizeEqualToSize(size, [_imageView frameSize]))
|
||||
return NO;
|
||||
|
||||
[_imageView setFrameSize:size];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Sets the image, and waits for it to load when it has not yet loaded.
|
||||
*/
|
||||
- (void)_setImage:(CPImage)anImage
|
||||
{
|
||||
if (_image)
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
|
||||
|
||||
_image = anImage;
|
||||
|
||||
if (_image && [_image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_imageDidLoad:) name:CPImageDidLoadNotification object:_image];
|
||||
[_image load];
|
||||
}
|
||||
|
||||
if (_imageView)
|
||||
{
|
||||
[_imageView setImage:_image];
|
||||
|
||||
if ([self _updateViewSize])
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
|
||||
}
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
The image's natural size is known once it has loaded, so the view takes its size, and the
|
||||
text that contains the attachment is laid out again.
|
||||
*/
|
||||
- (void)_imageDidLoad:(CPNotification)aNotification
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
|
||||
|
||||
if (_imageView)
|
||||
[_imageView setImage:_image];
|
||||
|
||||
if ([self _updateViewSize])
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the attachment's data as a data URL, which the browser loads as an image.
|
||||
*/
|
||||
- (CPString)_dataURL
|
||||
{
|
||||
return "data:" + [self _MIMEType] + ";base64," + [_contents base64];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the image's MIME type. A uniform type identifier is mapped to its MIME type.
|
||||
*/
|
||||
- (CPString)_MIMEType
|
||||
{
|
||||
var type = _fileType || @"image/png";
|
||||
|
||||
if (type.indexOf("/") !== -1)
|
||||
return type;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case @"public.jpeg":
|
||||
return @"image/jpeg";
|
||||
|
||||
case @"com.compuserve.gif":
|
||||
return @"image/gif";
|
||||
|
||||
case @"public.svg-image":
|
||||
return @"image/svg+xml";
|
||||
|
||||
case @"org.webmproject.webp":
|
||||
return @"image/webp";
|
||||
|
||||
default:
|
||||
return @"image/png";
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTextAttachmentContentsKey = @"CPTextAttachmentContents",
|
||||
CPTextAttachmentFileTypeKey = @"CPTextAttachmentFileType",
|
||||
CPTextAttachmentBoundsKey = @"CPTextAttachmentBounds";
|
||||
|
||||
@implementation CPTextAttachment (CPCoding)
|
||||
|
||||
/*!
|
||||
Decodes the attachment. The image is made again from the archived data.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
var contentsString = [aCoder decodeObjectForKey:CPTextAttachmentContentsKey];
|
||||
|
||||
self = [self initWithData:contentsString ? [CPData dataWithBase64:contentsString] : nil
|
||||
ofType:[aCoder decodeObjectForKey:CPTextAttachmentFileTypeKey]];
|
||||
|
||||
if (self && [aCoder containsValueForKey:CPTextAttachmentBoundsKey])
|
||||
_bounds = [aCoder decodeRectForKey:CPTextAttachmentBoundsKey];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Encodes the attachment's data, type and bounds. The view is not encoded.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (_contents)
|
||||
[aCoder encodeObject:[_contents base64] forKey:CPTextAttachmentContentsKey];
|
||||
|
||||
if (_fileType)
|
||||
[aCoder encodeObject:_fileType forKey:CPTextAttachmentFileTypeKey];
|
||||
|
||||
if (!CGRectIsEmpty(_bounds))
|
||||
[aCoder encodeRect:_bounds forKey:CPTextAttachmentBoundsKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -89,8 +89,8 @@ CPLineMovesUp = 4;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithContainerSize:(CGSize)aSize
|
||||
{
|
||||
@@ -112,14 +112,15 @@ CPLineMovesUp = 4;
|
||||
|
||||
- (void)_init
|
||||
{
|
||||
_lineFragmentPadding = 0.0;
|
||||
// Cocoa's default: room at each end of a line fragment, so that text does not touch the container's edges.
|
||||
_lineFragmentPadding = 5.0;
|
||||
|
||||
_layoutManager = [[CPLayoutManager alloc] init];
|
||||
[_layoutManager addTextContainer:self];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setter methods
|
||||
// MARK: -
|
||||
// MARK: Setter methods
|
||||
|
||||
- (void)setContainerSize:(CGSize)someSize
|
||||
{
|
||||
@@ -129,6 +130,9 @@ CPLineMovesUp = 4;
|
||||
|
||||
if (oldSize.width != _size.width)
|
||||
{
|
||||
// Image attachments fit the new width before the text is laid out again.
|
||||
[_textView _setTextAttachmentsMaximumWidth:MAX(0.0, _size.width - 2.0 * _lineFragmentPadding)];
|
||||
|
||||
_inResizing = YES;
|
||||
[_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(0, [[_layoutManager textStorage] length])
|
||||
isSoft:NO
|
||||
@@ -148,27 +152,61 @@ CPLineMovesUp = 4;
|
||||
|
||||
- (void)setWidthTracksTextView:(BOOL)flag
|
||||
{
|
||||
_widthTracksTextView = flag;
|
||||
[_textView setPostsFrameChangedNotifications:flag];
|
||||
if (_widthTracksTextView === flag)
|
||||
return;
|
||||
|
||||
if (flag && _textView)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(textViewFrameChanged:)
|
||||
name:CPViewFrameDidChangeNotification
|
||||
object:_textView];
|
||||
}
|
||||
else
|
||||
_widthTracksTextView = flag;
|
||||
[self _updateFrameObserver];
|
||||
}
|
||||
|
||||
// Controls whether the receiver adjusts the height of its bounding rectangle when its text view is resized.
|
||||
- (BOOL)heightTracksTextView
|
||||
{
|
||||
return _heightTracksTextView;
|
||||
}
|
||||
|
||||
- (void)setHeightTracksTextView:(BOOL)flag
|
||||
{
|
||||
if (_heightTracksTextView === flag)
|
||||
return;
|
||||
|
||||
_heightTracksTextView = flag;
|
||||
[self _updateFrameObserver];
|
||||
}
|
||||
|
||||
- (void)_updateFrameObserver
|
||||
{
|
||||
if (_textView)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self
|
||||
name:CPViewFrameDidChangeNotification
|
||||
object:_textView];
|
||||
|
||||
var flag = _widthTracksTextView || _heightTracksTextView;
|
||||
[_textView setPostsFrameChangedNotifications:flag];
|
||||
|
||||
if (flag)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(textViewFrameChanged:)
|
||||
name:CPViewFrameDidChangeNotification
|
||||
object:_textView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)textViewFrameChanged:(CPNotification)aNotification
|
||||
{
|
||||
var newSize = CGSizeMake([_textView frame].size.width, _size.height);
|
||||
// As in Cocoa, a tracking container fills the text view inside its text container inset on both sides.
|
||||
var newSize = CGSizeMake(_size.width, _size.height),
|
||||
frameSize = [_textView frame].size,
|
||||
inset = [_textView textContainerInset];
|
||||
|
||||
if (_widthTracksTextView)
|
||||
newSize.width = MAX(0.0, frameSize.width - 2.0 * inset.width);
|
||||
|
||||
if (_heightTracksTextView)
|
||||
newSize.height = MAX(0.0, frameSize.height - 2.0 * inset.height);
|
||||
|
||||
[self setContainerSize:newSize];
|
||||
}
|
||||
@@ -177,7 +215,9 @@ CPLineMovesUp = 4;
|
||||
{
|
||||
if (_textView)
|
||||
{
|
||||
[self setWidthTracksTextView:NO]; // We only support width
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self
|
||||
name:CPViewFrameDidChangeNotification
|
||||
object:_textView];
|
||||
[_textView setTextContainer:nil];
|
||||
}
|
||||
|
||||
@@ -185,7 +225,7 @@ CPLineMovesUp = 4;
|
||||
|
||||
if (_textView)
|
||||
{
|
||||
[self setWidthTracksTextView:_widthTracksTextView]; // We only support width
|
||||
[self _updateFrameObserver];
|
||||
[_textView setTextContainer:self];
|
||||
}
|
||||
|
||||
@@ -238,7 +278,8 @@ CPLineMovesUp = 4;
|
||||
var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
CPTextContainerLayoutManagerKey = @"CPTextContainerLayoutManagerKey",
|
||||
CPTextContainerWidthTracksTextViewKey = @"CPTextContainerWidthTracksTextViewKey",
|
||||
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey";
|
||||
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey",
|
||||
CPTextContainerLineFragmentPaddingKey = @"CPTextContainerLineFragmentPaddingKey";
|
||||
|
||||
@implementation CPTextContainer (CPCoding)
|
||||
|
||||
@@ -257,6 +298,10 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
|
||||
_widthTracksTextView = [aCoder decodeBoolForKey:CPTextContainerWidthTracksTextViewKey];
|
||||
_heightTracksTextView = [aCoder decodeBoolForKey:CPTextContainerHeightTracksTextViewKey];
|
||||
|
||||
// Archives made before the padding was archived keep the default from _init.
|
||||
if ([aCoder containsValueForKey:CPTextContainerLineFragmentPaddingKey])
|
||||
_lineFragmentPadding = [aCoder decodeFloatForKey:CPTextContainerLineFragmentPaddingKey];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -268,6 +313,7 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
[aCoder encodeObject:_layoutManager forKey:CPTextContainerLayoutManagerKey];
|
||||
[aCoder encodeBool:_widthTracksTextView forKey:CPTextContainerWidthTracksTextViewKey];
|
||||
[aCoder encodeBool:_heightTracksTextView forKey:CPTextContainerHeightTracksTextViewKey];
|
||||
[aCoder encodeFloat:_lineFragmentPadding forKey:CPTextContainerLineFragmentPaddingKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* CPTextList.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 24/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPAttributedString.j>
|
||||
|
||||
@import "CPParagraphStyle.j"
|
||||
|
||||
/*
|
||||
CPTextListOptions
|
||||
*/
|
||||
CPTextListPrependEnclosingMarker = 1 << 0;
|
||||
|
||||
/*
|
||||
CPTextListMarkerFormat
|
||||
*/
|
||||
CPTextListMarkerBox = @"{box}";
|
||||
CPTextListMarkerCheck = @"{check}";
|
||||
CPTextListMarkerCircle = @"{circle}";
|
||||
CPTextListMarkerDiamond = @"{diamond}";
|
||||
CPTextListMarkerDisc = @"{disc}";
|
||||
CPTextListMarkerHyphen = @"{hyphen}";
|
||||
CPTextListMarkerSquare = @"{square}";
|
||||
CPTextListMarkerLowercaseHexadecimal = @"{lower-hexadecimal}";
|
||||
CPTextListMarkerUppercaseHexadecimal = @"{upper-hexadecimal}";
|
||||
CPTextListMarkerOctal = @"{octal}";
|
||||
CPTextListMarkerLowercaseAlpha = @"{lower-alpha}";
|
||||
CPTextListMarkerUppercaseAlpha = @"{upper-alpha}";
|
||||
CPTextListMarkerLowercaseLatin = @"{lower-latin}";
|
||||
CPTextListMarkerUppercaseLatin = @"{upper-latin}";
|
||||
CPTextListMarkerLowercaseRoman = @"{lower-roman}";
|
||||
CPTextListMarkerUppercaseRoman = @"{upper-roman}";
|
||||
CPTextListMarkerDecimal = @"{decimal}";
|
||||
|
||||
/*
|
||||
The characters that the unordered marker formats stand for.
|
||||
*/
|
||||
var CPTextListBulletCharacters = {
|
||||
"box": "□",
|
||||
"check": "✓",
|
||||
"circle": "◦",
|
||||
"diamond": "◆",
|
||||
"disc": "•",
|
||||
"hyphen": "⁃",
|
||||
"square": "▪"
|
||||
},
|
||||
CPTextListPlaceholderPattern = /\{([a-z-]+)\}/;
|
||||
|
||||
/*
|
||||
Returns anItemNumber as a letter sequence: 1 is "a", 26 is "z", 27 is "aa".
|
||||
*/
|
||||
var _CPTextListAlphaString = function(anItemNumber)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
while (anItemNumber > 0)
|
||||
{
|
||||
anItemNumber--;
|
||||
result = String.fromCharCode(97 + anItemNumber % 26) + result;
|
||||
anItemNumber = Math.floor(anItemNumber / 26);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns anItemNumber as a lowercase Roman numeral.
|
||||
*/
|
||||
var _CPTextListRomanString = function(anItemNumber)
|
||||
{
|
||||
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
|
||||
numerals = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"],
|
||||
result = "";
|
||||
|
||||
for (var i = 0; i < values.length; i++)
|
||||
{
|
||||
while (anItemNumber >= values[i])
|
||||
{
|
||||
result += numerals[i];
|
||||
anItemNumber -= values[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPTextList
|
||||
|
||||
A CPTextList describes one list in rich text: the format of its markers, its options and the
|
||||
number of its first item. A paragraph belongs to the lists in its paragraph style's
|
||||
\c textLists, outermost first. As in Cocoa, the markers are ordinary text at the start of
|
||||
each item: a tab, the marker and another tab, placed by the paragraph style's indents and
|
||||
tab stops. A list is identified by the object itself, so the paragraphs of one list share
|
||||
the same CPTextList.
|
||||
*/
|
||||
@implementation CPTextList : CPObject
|
||||
{
|
||||
CPString _markerFormat;
|
||||
unsigned _listOptions;
|
||||
int _startingItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises a list.
|
||||
@param aFormat the marker format: one of the CPTextListMarker constants, optionally with
|
||||
text around it, such as \c @"({decimal})"
|
||||
@param aMask the list options, such as CPTextListPrependEnclosingMarker
|
||||
*/
|
||||
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask
|
||||
{
|
||||
return [self initWithMarkerFormat:aFormat options:aMask startingItemNumber:1];
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises a list whose first item has the number anItemNumber.
|
||||
*/
|
||||
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask startingItemNumber:(int)anItemNumber
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_markerFormat = aFormat || CPTextListMarkerDisc;
|
||||
_listOptions = aMask;
|
||||
_startingItemNumber = anItemNumber;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the marker format.
|
||||
*/
|
||||
- (CPString)markerFormat
|
||||
{
|
||||
return _markerFormat;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the list options.
|
||||
*/
|
||||
- (unsigned)listOptions
|
||||
{
|
||||
return _listOptions;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of the list's first item. The default is 1.
|
||||
*/
|
||||
- (int)startingItemNumber
|
||||
{
|
||||
return _startingItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the number of the list's first item.
|
||||
*/
|
||||
- (void)setStartingItemNumber:(int)anItemNumber
|
||||
{
|
||||
_startingItemNumber = anItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns \c YES if the markers are numbers or letters, and \c NO if they are bullets.
|
||||
*/
|
||||
- (BOOL)isOrdered
|
||||
{
|
||||
var match = _markerFormat.match(CPTextListPlaceholderPattern);
|
||||
|
||||
return !!match && !CPTextListBulletCharacters.hasOwnProperty(match[1]);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the marker for the item with the number anItemNumber, without the tabs around it.
|
||||
The marker format's placeholder is replaced by the bullet or the formatted number, and the
|
||||
text around the placeholder is kept.
|
||||
*/
|
||||
- (CPString)markerForItemNumber:(int)anItemNumber
|
||||
{
|
||||
var match = _markerFormat.match(CPTextListPlaceholderPattern);
|
||||
|
||||
if (!match)
|
||||
return _markerFormat;
|
||||
|
||||
var name = match[1],
|
||||
marker;
|
||||
|
||||
if (CPTextListBulletCharacters.hasOwnProperty(name))
|
||||
marker = CPTextListBulletCharacters[name];
|
||||
else
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "lower-hexadecimal":
|
||||
marker = anItemNumber.toString(16);
|
||||
break;
|
||||
|
||||
case "upper-hexadecimal":
|
||||
marker = anItemNumber.toString(16).toUpperCase();
|
||||
break;
|
||||
|
||||
case "octal":
|
||||
marker = anItemNumber.toString(8);
|
||||
break;
|
||||
|
||||
case "lower-alpha":
|
||||
case "lower-latin":
|
||||
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber) : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "upper-alpha":
|
||||
case "upper-latin":
|
||||
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber).toUpperCase() : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "lower-roman":
|
||||
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber) : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "upper-roman":
|
||||
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber).toUpperCase() : String(anItemNumber);
|
||||
break;
|
||||
|
||||
default:
|
||||
marker = String(anItemNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return _markerFormat.replace(CPTextListPlaceholderPattern, marker);
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
return [super description] + " markerFormat=" + _markerFormat + " startingItemNumber=" + _startingItemNumber;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTextListMarkerFormatKey = @"CPTextListMarkerFormat",
|
||||
CPTextListOptionsKey = @"CPTextListOptions",
|
||||
CPTextListStartingItemNumberKey = @"CPTextListStartingItemNumber";
|
||||
|
||||
@implementation CPTextList (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_markerFormat = [aCoder decodeObjectForKey:CPTextListMarkerFormatKey] || CPTextListMarkerDisc;
|
||||
_listOptions = [aCoder decodeIntForKey:CPTextListOptionsKey];
|
||||
_startingItemNumber = [aCoder containsValueForKey:CPTextListStartingItemNumberKey] ? [aCoder decodeIntForKey:CPTextListStartingItemNumberKey] : 1;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:_markerFormat forKey:CPTextListMarkerFormatKey];
|
||||
[aCoder encodeInt:_listOptions forKey:CPTextListOptionsKey];
|
||||
[aCoder encodeInt:_startingItemNumber forKey:CPTextListStartingItemNumberKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
Returns YES if the character code separates paragraphs.
|
||||
*/
|
||||
var _CPTextListIsParagraphSeparator = function(aCharCode)
|
||||
{
|
||||
return aCharCode === 10 || aCharCode === 13 || aCharCode === 0x2029;
|
||||
};
|
||||
|
||||
@implementation CPAttributedString (CPTextListAdditions)
|
||||
|
||||
/* @ignore
|
||||
Returns the range of the paragraph containing anIndex, including its paragraph separator.
|
||||
*/
|
||||
- (CPRange)_paragraphRangeForIndex:(unsigned)anIndex
|
||||
{
|
||||
var string = [self string],
|
||||
length = string.length,
|
||||
start = MIN(anIndex, length),
|
||||
end = start;
|
||||
|
||||
while (start > 0 && !_CPTextListIsParagraphSeparator(string.charCodeAt(start - 1)))
|
||||
start--;
|
||||
|
||||
while (end < length && !_CPTextListIsParagraphSeparator(string.charCodeAt(end)))
|
||||
end++;
|
||||
|
||||
if (end < length)
|
||||
end++;
|
||||
|
||||
return CPMakeRange(start, end - start);
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the text lists of the paragraph that starts at aParagraphStart, outermost first.
|
||||
*/
|
||||
- (CPArray)_textListsOfParagraphAtIndex:(unsigned)aParagraphStart
|
||||
{
|
||||
if (aParagraphStart >= [self length])
|
||||
return [];
|
||||
|
||||
var paragraphStyle = [self attribute:CPParagraphStyleAttributeName atIndex:aParagraphStart effectiveRange:nil];
|
||||
|
||||
return [paragraphStyle textLists] || [];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the range of the paragraphs that belong to aList, around anIndex. The paragraphs
|
||||
are contiguous: the range ends at the first paragraph on either side that does not belong
|
||||
to the list, at any level. Returns a range with the location CPNotFound if the paragraph
|
||||
at anIndex does not belong to aList.
|
||||
*/
|
||||
- (CPRange)rangeOfTextList:(CPTextList)aList atIndex:(unsigned)anIndex
|
||||
{
|
||||
var paragraphRange = [self _paragraphRangeForIndex:anIndex];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:paragraphRange.location] containsObjectIdenticalTo:aList])
|
||||
return CPMakeRange(CPNotFound, 0);
|
||||
|
||||
var start = paragraphRange.location,
|
||||
end = CPMaxRange(paragraphRange),
|
||||
length = [self length];
|
||||
|
||||
while (start > 0)
|
||||
{
|
||||
var previousRange = [self _paragraphRangeForIndex:start - 1];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:previousRange.location] containsObjectIdenticalTo:aList])
|
||||
break;
|
||||
|
||||
start = previousRange.location;
|
||||
}
|
||||
|
||||
while (end < length)
|
||||
{
|
||||
var nextRange = [self _paragraphRangeForIndex:end];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:nextRange.location] containsObjectIdenticalTo:aList])
|
||||
break;
|
||||
|
||||
end = CPMaxRange(nextRange);
|
||||
}
|
||||
|
||||
return CPMakeRange(start, end - start);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of the item of aList at anIndex. The items of a list are the paragraphs
|
||||
whose innermost list it is; paragraphs of lists nested in it do not count. For a paragraph
|
||||
of a nested list, the result is the number of the enclosing item. Returns 0 if the
|
||||
paragraph at anIndex does not belong to aList.
|
||||
*/
|
||||
- (int)itemNumberInTextList:(CPTextList)aList atIndex:(unsigned)anIndex
|
||||
{
|
||||
var listRange = [self rangeOfTextList:aList atIndex:anIndex];
|
||||
|
||||
if (listRange.location === CPNotFound)
|
||||
return 0;
|
||||
|
||||
var target = [self _paragraphRangeForIndex:anIndex].location,
|
||||
itemNumber = [aList startingItemNumber] - 1,
|
||||
location = listRange.location;
|
||||
|
||||
while (location <= target)
|
||||
{
|
||||
var paragraphRange = [self _paragraphRangeForIndex:location];
|
||||
|
||||
if ([[self _textListsOfParagraphAtIndex:location] lastObject] === aList)
|
||||
itemNumber++;
|
||||
|
||||
if (!paragraphRange.length)
|
||||
break;
|
||||
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
|
||||
return MAX(itemNumber, [aList startingItemNumber]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
@import "CPText.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPTextAttachment.j"
|
||||
|
||||
@class CPLayoutManager;
|
||||
|
||||
@@ -70,8 +71,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithString:(CPString)aString attributes:(CPDictionary)attributes
|
||||
{
|
||||
@@ -99,8 +100,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
- (void)setDelegate:(id <CPTextStorageDelegate>)aDelegate
|
||||
{
|
||||
@@ -121,8 +122,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout manager methods
|
||||
// MARK: -
|
||||
// MARK: Layout manager methods
|
||||
|
||||
- (void)addLayoutManager:(CPLayoutManager)aManager
|
||||
{
|
||||
@@ -148,8 +149,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Editing methods
|
||||
// MARK: -
|
||||
// MARK: Editing methods
|
||||
|
||||
- (void)processEditing
|
||||
{
|
||||
@@ -263,16 +264,23 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns an instance of CPTextStorage that contains the provided instance of CPView.
|
||||
This can be used to insert arbitrary views into the text. These views are treated as individual characters during editing.
|
||||
This works only with views that conform to the CPCoding protocol
|
||||
Returns an instance of CPTextStorage that contains one attachment character for anAttachment.
|
||||
The attachment is treated as an individual character during editing.
|
||||
|
||||
anAttachment is either a CPTextAttachment, as in Cocoa, or a view. A CPTextAttachment is
|
||||
stored as the character's CPAttachmentAttributeName attribute, and its view is laid out as
|
||||
the character. A view is inserted into the text directly; this works only with views that
|
||||
conform to the CPCoding protocol.
|
||||
*/
|
||||
|
||||
+ (id)attributedStringWithAttachment:(CPView)someView
|
||||
+ (id)attributedStringWithAttachment:(id)anAttachment
|
||||
{
|
||||
var result = [[self alloc] initWithString:_CPAttachmentCharacterAsString];
|
||||
|
||||
[result setAttributes:@{_CPAttachmentView:someView} range:CPMakeRange(0, 1)];
|
||||
if ([anAttachment isKindOfClass:[CPTextAttachment class]])
|
||||
[result setAttributes:@{CPAttachmentAttributeName:anAttachment, _CPAttachmentView:[anAttachment _attachmentView]} range:CPMakeRange(0, 1)];
|
||||
else
|
||||
[result setAttributes:@{_CPAttachmentView:anAttachment} range:CPMakeRange(0, 1)];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+2630
-378
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,7 @@
|
||||
* All modifications copyright Daniel Boehringer 2013.
|
||||
* Extensive code formatting and review by Andrew Hankinson
|
||||
* Based on original work by
|
||||
* Emmanuel Maillard on 27/02/2010.
|
||||
* Created by Emmanuel Maillard on 27/02/2010.
|
||||
* Copyright Emmanuel Maillard 2010.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
@@ -30,6 +30,9 @@
|
||||
@import "CPTextStorage.j"
|
||||
@import "CPFont.j"
|
||||
|
||||
@global CPBaselineOffsetAttributeName
|
||||
@global CPSuperscriptAttributeName
|
||||
|
||||
// forward declare these classes for type matching
|
||||
@class CPLayoutManager
|
||||
@class CPTextContainer
|
||||
@@ -46,13 +49,29 @@ CPTypesetterParagraphBreakAction = 1 << 4;
|
||||
CPTypesetterContainerBreakAction = 1 << 5;
|
||||
|
||||
var CPSystemTypesetterFactory,
|
||||
_sharedSimpleTypesetter;
|
||||
_sharedSimpleTypesetter,
|
||||
_CPTypesetterCachedDecimalSeparator = nil;
|
||||
|
||||
/*
|
||||
Returns the decimal separator of the user's locale, which a decimal tab stop aligns on.
|
||||
*/
|
||||
var _CPTypesetterDecimalSeparator = function()
|
||||
{
|
||||
if (!_CPTypesetterCachedDecimalSeparator)
|
||||
{
|
||||
var formatted = (1.5).toLocaleString();
|
||||
|
||||
_CPTypesetterCachedDecimalSeparator = formatted.length === 3 ? formatted.charAt(1) : @".";
|
||||
}
|
||||
|
||||
return _CPTypesetterCachedDecimalSeparator;
|
||||
};
|
||||
|
||||
@implementation CPTypesetter : CPObject
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
@@ -119,8 +138,8 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (id)sharedInstance
|
||||
{
|
||||
@@ -135,31 +154,30 @@ var CPSystemTypesetterFactory,
|
||||
return [_layoutManager textContainers];
|
||||
}
|
||||
|
||||
// Retrieves correct CPTextTab stop accounting for custom stops and default intervals
|
||||
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
|
||||
{
|
||||
var tabStops = [_currentParagraph tabStops];
|
||||
var tabStops = [_currentParagraph tabStops],
|
||||
defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
|
||||
|
||||
if (!tabStops)
|
||||
tabStops = [CPParagraphStyle _defaultTabStops];
|
||||
var l = tabStops ? [tabStops count] : 0;
|
||||
|
||||
var l = tabStops.length;
|
||||
|
||||
if (aWidth > tabStops[l - 1]._location)
|
||||
return nil;
|
||||
|
||||
for (var i = l - 1; i >= 0; i--)
|
||||
// 1. If custom tab stops exist ahead of current position, use the first one encountered
|
||||
if (l > 0)
|
||||
{
|
||||
if (aWidth > tabStops[i]._location)
|
||||
for (var i = 0; i < l; i++)
|
||||
{
|
||||
if (i + 1 < l)
|
||||
return tabStops[i + 1];
|
||||
var tab = [tabStops objectAtIndex:i];
|
||||
|
||||
if ([tab location] > aWidth)
|
||||
return tab;
|
||||
}
|
||||
}
|
||||
|
||||
if (i === -1)
|
||||
return tabStops[0];
|
||||
// 2. Otherwise (or when all custom tab stops are behind the text), advance to the next default interval
|
||||
var nextLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval;
|
||||
|
||||
return nil;
|
||||
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
|
||||
}
|
||||
|
||||
- (BOOL)_flushRange:(CPRange)lineRange
|
||||
@@ -168,10 +186,32 @@ var CPSystemTypesetterFactory,
|
||||
advancements:(CPArray)advancements
|
||||
lineCount:(unsigned)lineCount
|
||||
sameLine:(BOOL)sameLine
|
||||
wordWrapped:(BOOL)wordWrapped
|
||||
rightMargin:(float)rightMargin
|
||||
{
|
||||
var myX = 0,
|
||||
rect = CGRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight),
|
||||
containerSize = aContainer._size;
|
||||
// Line positions are computed inside the line fragment padding. The padding is added here, where
|
||||
// the line fragment rectangle is stored, and alignment uses the width inside the padding.
|
||||
var padding = [aContainer lineFragmentPadding],
|
||||
myX = 0,
|
||||
rect = CGRectMake(lineOrigin.x + padding, lineOrigin.y, _lineWidth, _lineHeight);
|
||||
|
||||
// As in Cocoa, text is aligned between the indents, and whitespace at the end of the line hangs
|
||||
// outside the alignment: it does not count in the line's visible width.
|
||||
var theString = [_textStorage string],
|
||||
availableWidth = rightMargin - lineOrigin.x,
|
||||
visibleLength = lineRange.length,
|
||||
visibleWidth = _lineWidth;
|
||||
|
||||
while (visibleLength > 0)
|
||||
{
|
||||
var trailingCharCode = theString.charCodeAt(lineRange.location + visibleLength - 1);
|
||||
|
||||
if (trailingCharCode !== 32 && trailingCharCode !== 10 && trailingCharCode !== 13)
|
||||
break;
|
||||
|
||||
visibleLength--;
|
||||
visibleWidth -= advancements[visibleLength].width;
|
||||
}
|
||||
|
||||
[_layoutManager _appendNewLineFragmentInTextContainer:_currentTextContainer forGlyphRange:lineRange];
|
||||
|
||||
@@ -188,18 +228,48 @@ var CPSystemTypesetterFactory,
|
||||
break;
|
||||
|
||||
case CPCenterTextAlignment:
|
||||
myX = (containerSize.width - _lineWidth) / 2;
|
||||
myX = (availableWidth - visibleWidth) / 2;
|
||||
break;
|
||||
|
||||
case CPRightTextAlignment:
|
||||
myX = containerSize.width - _lineWidth;
|
||||
myX = availableWidth - visibleWidth;
|
||||
break;
|
||||
|
||||
case CPJustifiedTextAlignment:
|
||||
// Only a line that wraps is justified. The last line of a paragraph, and the text
|
||||
// before a tab on the same line, stay left aligned, as in Cocoa. The spare width is
|
||||
// spread evenly over the spaces inside the visible part of the line. The line fragment
|
||||
// draws its spans with the same extra word spacing.
|
||||
if (wordWrapped && !sameLine)
|
||||
{
|
||||
var spaceIndexes = [];
|
||||
|
||||
for (var i = 0; i < visibleLength; i++)
|
||||
{
|
||||
var charCode = theString.charCodeAt(lineRange.location + i);
|
||||
|
||||
if (charCode === 32 || charCode === 160)
|
||||
spaceIndexes.push(i);
|
||||
}
|
||||
|
||||
var spaceCount = spaceIndexes.length,
|
||||
extraWidth = spaceCount ? (availableWidth - visibleWidth) / spaceCount : 0.0;
|
||||
|
||||
if (extraWidth > 0.0)
|
||||
{
|
||||
for (var i = 0; i < spaceCount; i++)
|
||||
advancements[spaceIndexes[i]].width += extraWidth;
|
||||
|
||||
fragment._wordSpacing = extraWidth;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
[_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange];
|
||||
[_layoutManager _setAdvancements:advancements forGlyphRange:lineRange];
|
||||
|
||||
//fix the _lineFragments when fontsizes differ
|
||||
// fix the _lineFragments when fontsizes differ
|
||||
var l = _lineFragments.length;
|
||||
|
||||
for (var i = 0 ; i < l ; i++)
|
||||
@@ -235,8 +305,10 @@ var CPSystemTypesetterFactory,
|
||||
_lineBase = 0;
|
||||
_lineWidth = 0;
|
||||
|
||||
// containerSizeWidth is the width inside the line fragment padding at both ends.
|
||||
var containerSize = [_currentTextContainer containerSize],
|
||||
containerSizeWidth = containerSize.width,
|
||||
lineFragmentPadding = [_currentTextContainer lineFragmentPadding],
|
||||
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding,
|
||||
containerSizeHeight = containerSize.height,
|
||||
lineRange = CPMakeRange(glyphIndex, 0),
|
||||
wrapRange = CPMakeRange(0, 0),
|
||||
@@ -245,7 +317,7 @@ var CPSystemTypesetterFactory,
|
||||
isTabStop = NO,
|
||||
isAttachment = NO,
|
||||
isWordWrapped = NO,
|
||||
numberOfGlyphs= [_textStorage length],
|
||||
numberOfGlyphs = [_textStorage length],
|
||||
leading,
|
||||
numLines = 0,
|
||||
theString = [_textStorage string],
|
||||
@@ -263,8 +335,20 @@ var CPSystemTypesetterFactory,
|
||||
currentParagraphMaximumLineHeight,
|
||||
currentParagraphLineSpacing;
|
||||
|
||||
// Track physical line starts to prevent overwriting lineOrigin.x in tab segments
|
||||
var isStartOfPhysicalLine = YES;
|
||||
|
||||
// Track paragraph indents and margins
|
||||
var isFirstLineOfLayout = YES,
|
||||
isFirstLineOfParagraph = YES,
|
||||
rightMargin = containerSizeWidth;
|
||||
|
||||
if (glyphIndex > 0)
|
||||
{
|
||||
// Stored line fragment rectangles include the padding, which the layout below does not.
|
||||
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
|
||||
lineOrigin.x -= lineFragmentPadding;
|
||||
}
|
||||
else if ([_layoutManager extraLineFragmentTextContainer])
|
||||
lineOrigin = CGPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y);
|
||||
else
|
||||
@@ -279,7 +363,7 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++)
|
||||
{
|
||||
// check whether there any change in the attributes from here on
|
||||
// check whether there is any change in the attributes from here on
|
||||
if (!CPLocationInRange(glyphIndex, _attributesRange))
|
||||
{
|
||||
_currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange];
|
||||
@@ -289,12 +373,85 @@ var CPSystemTypesetterFactory,
|
||||
currentParagraphMaximumLineHeight = [_currentParagraph maximumLineHeight];
|
||||
currentParagraphLineSpacing = [_currentParagraph lineSpacing];
|
||||
|
||||
// Recalculate right margin on paragraph style change
|
||||
var tailIndent = [_currentParagraph tailIndent];
|
||||
if (tailIndent > 0.0)
|
||||
rightMargin = tailIndent;
|
||||
else if (tailIndent < 0.0)
|
||||
rightMargin = containerSizeWidth + tailIndent;
|
||||
else
|
||||
rightMargin = containerSizeWidth;
|
||||
|
||||
// If we are at the start of a physical line, we update lineOrigin.x
|
||||
if (isStartOfPhysicalLine)
|
||||
{
|
||||
if (glyphIndex > 0)
|
||||
{
|
||||
var prevChar = theString.charCodeAt(glyphIndex - 1);
|
||||
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
|
||||
}
|
||||
else
|
||||
{
|
||||
isFirstLineOfParagraph = YES;
|
||||
}
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
isFirstLineOfLayout = NO;
|
||||
}
|
||||
|
||||
// Handle the layout's very first line indentation
|
||||
if (isFirstLineOfLayout)
|
||||
{
|
||||
if (glyphIndex > 0)
|
||||
{
|
||||
var prevChar = theString.charCodeAt(glyphIndex - 1);
|
||||
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
|
||||
}
|
||||
else
|
||||
{
|
||||
isFirstLineOfParagraph = YES;
|
||||
}
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
isFirstLineOfLayout = NO;
|
||||
}
|
||||
|
||||
if (!currentFont)
|
||||
currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
|
||||
|
||||
ascent = [currentFont ascender];
|
||||
descent = [currentFont descender];
|
||||
leading = (ascent - descent) * 0.2; // FAKE leading
|
||||
// Safely retrieve and validate CPBaselineOffsetAttributeName
|
||||
var baselineOffset = [_currentAttributes objectForKey:CPBaselineOffsetAttributeName];
|
||||
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
|
||||
baselineOffset = 0.0;
|
||||
|
||||
// Safely retrieve and validate CPSuperscriptAttributeName
|
||||
var superscript = [_currentAttributes objectForKey:CPSuperscriptAttributeName];
|
||||
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
|
||||
superscript = 0;
|
||||
|
||||
if (superscript !== 0)
|
||||
{
|
||||
var size = [currentFont size],
|
||||
scaledSize = size * 0.65,
|
||||
fontName = [currentFont familyName],
|
||||
isBold = [currentFont isBold],
|
||||
isItalic = [currentFont isItalic];
|
||||
|
||||
currentFont = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic];
|
||||
|
||||
if (baselineOffset === 0.0)
|
||||
{
|
||||
if (superscript > 0)
|
||||
baselineOffset = size * 0.35;
|
||||
else
|
||||
baselineOffset = -size * 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
var fontAscent = [currentFont ascender] || 0.0,
|
||||
fontDescent = [currentFont descender] || 0.0;
|
||||
|
||||
ascent = fontAscent + baselineOffset;
|
||||
descent = fontDescent + baselineOffset;
|
||||
leading = (fontAscent - fontDescent) * 0.2; // FAKE leading
|
||||
|
||||
currentFontLineHeight = ascent - descent + leading;
|
||||
|
||||
@@ -307,19 +464,26 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
}
|
||||
|
||||
if (currentFontLineHeight > _lineHeight)
|
||||
_lineHeight = currentFontLineHeight;
|
||||
// Clean bounds logic to prevent NaN and layout calculation overhead
|
||||
var currentAscent = (ascent === undefined || isNaN(ascent)) ? 0.0 : ascent,
|
||||
currentLineHeight = (currentFontLineHeight === undefined || isNaN(currentFontLineHeight)) ? 12.0 : currentFontLineHeight;
|
||||
|
||||
if (ascent > _lineBase)
|
||||
_lineBase = ascent;
|
||||
if (currentLineHeight > _lineHeight)
|
||||
_lineHeight = currentLineHeight;
|
||||
|
||||
if (currentAscent > _lineBase)
|
||||
_lineBase = currentAscent;
|
||||
|
||||
lineRange.length++;
|
||||
measuringRange.length++;
|
||||
|
||||
var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
|
||||
// We are processing characters, so we are no longer at the start of a physical line
|
||||
isStartOfPhysicalLine = NO;
|
||||
|
||||
var currentCharCode = theString.charCodeAt(glyphIndex),
|
||||
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
|
||||
|
||||
switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char.
|
||||
switch (currentCharCode)
|
||||
{
|
||||
case CPAttachmentCharacter:
|
||||
{
|
||||
@@ -346,25 +510,94 @@ var CPSystemTypesetterFactory,
|
||||
if (viewSize.height > _lineHeight)
|
||||
_lineHeight = viewSize.height - descent + leading;
|
||||
|
||||
// An attachment wider than the container stays on the line it overflows, so a break
|
||||
// there keeps the line's height and baseline with the attachment's size applied.
|
||||
// Otherwise the attachment would be placed on the baseline of the text before it,
|
||||
// and drawn above its line.
|
||||
if (CPMaxRange(wrapRange) === CPMaxRange(lineRange))
|
||||
{
|
||||
wrapRange._height = _lineHeight;
|
||||
wrapRange._base = _lineBase;
|
||||
}
|
||||
|
||||
ascent = viewSize.height;
|
||||
break;
|
||||
}
|
||||
case 9: // '\t'
|
||||
{
|
||||
var nextTab = [self textTabForWidth:rangeWidth + lineOrigin.x writingDirection:0];
|
||||
// Measure against the actual text position before the tab stop
|
||||
var nextTab = [self textTabForWidth:prevRangeWidth + lineOrigin.x writingDirection:0];
|
||||
|
||||
isTabStop = YES;
|
||||
|
||||
if (nextTab)
|
||||
rangeWidth = nextTab._location - lineOrigin.x;
|
||||
{
|
||||
// Look-ahead to measure the width of the incoming text segment for alignment
|
||||
var nextSegmentWidth = 0.0,
|
||||
tempIndex = glyphIndex + 1,
|
||||
segmentString = "";
|
||||
|
||||
while (tempIndex < numberOfGlyphs)
|
||||
{
|
||||
var nextCharCode = theString.charCodeAt(tempIndex);
|
||||
if (nextCharCode === 9 || nextCharCode === 10 || nextCharCode === 13)
|
||||
break;
|
||||
segmentString += theString.charAt(tempIndex);
|
||||
tempIndex++;
|
||||
}
|
||||
|
||||
if (segmentString.length > 0)
|
||||
nextSegmentWidth = [segmentString sizeWithFont:currentFont inWidth:NULL].width;
|
||||
|
||||
var tabLocation = [nextTab location],
|
||||
tabAlignment = [nextTab alignment];
|
||||
|
||||
// Mathematically offset the tab character's right boundary
|
||||
if ([nextTab tabStopType] === CPDecimalTabStopType)
|
||||
{
|
||||
// As in Cocoa, the first decimal separator of the user's locale starts at the stop.
|
||||
// A segment without one ends at the stop, like a right tab.
|
||||
var separatorIndex = segmentString.indexOf(_CPTypesetterDecimalSeparator()),
|
||||
widthBeforeSeparator = separatorIndex === -1 ? nextSegmentWidth : [segmentString.substring(0, separatorIndex) sizeWithFont:currentFont inWidth:NULL].width;
|
||||
|
||||
rangeWidth = (tabLocation - widthBeforeSeparator) - lineOrigin.x;
|
||||
}
|
||||
else if (tabAlignment === CPCenterTextAlignment)
|
||||
{
|
||||
rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x;
|
||||
}
|
||||
else if (tabAlignment === CPRightTextAlignment)
|
||||
{
|
||||
rangeWidth = (tabLocation - nextSegmentWidth) - lineOrigin.x;
|
||||
}
|
||||
else // Left align tab stop
|
||||
{
|
||||
rangeWidth = tabLocation - lineOrigin.x;
|
||||
}
|
||||
|
||||
// Enforce a minimum safety spacer width to avoid character overlapping
|
||||
var minRangeWidth = prevRangeWidth + 5.0;
|
||||
if (rangeWidth < minRangeWidth)
|
||||
rangeWidth = minRangeWidth;
|
||||
}
|
||||
else
|
||||
rangeWidth += 28; //FIXME
|
||||
} // fallthrough intentional
|
||||
{
|
||||
rangeWidth = prevRangeWidth + 28.0; // standard fallback spacer
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 32: // ' '
|
||||
wrapRange = CPMakeRangeCopy(lineRange);
|
||||
wrapWidth = rangeWidth;
|
||||
wrapRange._height = _lineHeight;
|
||||
wrapRange._base = _lineBase;
|
||||
|
||||
if (theString.charCodeAt(glyphIndex + 1) !== 32)
|
||||
{
|
||||
currentAnchor = rangeWidth;
|
||||
measuringRange = CPMakeRange(glyphIndex + 1, 0);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 10:
|
||||
@@ -375,7 +608,8 @@ var CPSystemTypesetterFactory,
|
||||
advancements.push({width: rangeWidth - prevRangeWidth, height: ascent, descent: descent});
|
||||
prevRangeWidth = _lineWidth = rangeWidth;
|
||||
|
||||
if (lineOrigin.x + rangeWidth > containerSizeWidth)
|
||||
// Wrap lines against the tail indent (rightMargin) instead of container boundaries
|
||||
if (lineOrigin.x + rangeWidth > rightMargin)
|
||||
{
|
||||
if (wrapWidth)
|
||||
{
|
||||
@@ -387,12 +621,12 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
isNewline = YES;
|
||||
isWordWrapped = YES;
|
||||
glyphIndex = CPMaxRange(lineRange) - 1; // start the line starts directly at current character
|
||||
glyphIndex = CPMaxRange(lineRange) - 1;
|
||||
}
|
||||
|
||||
if (isNewline || isTabStop || isAttachment)
|
||||
{
|
||||
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline])
|
||||
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline wordWrapped:isWordWrapped rightMargin:rightMargin])
|
||||
return;
|
||||
|
||||
if (isTabStop || isAttachment)
|
||||
@@ -415,16 +649,20 @@ var CPSystemTypesetterFactory,
|
||||
{
|
||||
_currentTextContainer = textContainers[++_indexOfCurrentContainer];
|
||||
containerSize = [_currentTextContainer containerSize];
|
||||
containerSizeWidth = containerSize.width;
|
||||
lineFragmentPadding = [_currentTextContainer lineFragmentPadding];
|
||||
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding;
|
||||
containerSizeHeight = containerSize.height;
|
||||
}
|
||||
|
||||
lineOrigin.x = 0;
|
||||
isFirstLineOfParagraph = !isWordWrapped;
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
|
||||
numLines++;
|
||||
isNewline = NO;
|
||||
_lineFragments = [];
|
||||
_lineHeight = 0;
|
||||
_lineBase = ascent;
|
||||
_lineBase = 0;
|
||||
isStartOfPhysicalLine = YES;
|
||||
}
|
||||
|
||||
isTabStop = NO;
|
||||
@@ -441,11 +679,12 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// this is to "flush" the remaining characters
|
||||
// Flush remaining characters
|
||||
if (lineRange.length)
|
||||
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];
|
||||
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO wordWrapped:NO rightMargin:rightMargin];
|
||||
|
||||
var rect = CGRectMake(1, lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
|
||||
// The empty line after the text starts inside the padding, and at least 1 point in, so the insertion point stays visible.
|
||||
var rect = CGRectMake(MAX(1.0, lineFragmentPadding), lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
|
||||
[_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer];
|
||||
|
||||
var fragment = [_layoutManager._lineFragments lastObject];
|
||||
|
||||
+1053
-92
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
_CPRTFProducer.j
|
||||
|
||||
Serialize CPAttributedString to a RTF String
|
||||
|
||||
Copyright (C) 2014 Daniel Boehringer
|
||||
This file is based on the RTFProducer from GNUStep
|
||||
(which i co-authored with Fred Kiefer in 1999)
|
||||
|
||||
* _CPRTFProducer.j
|
||||
*
|
||||
* Serialize CPAttributedString to a RTF String
|
||||
*
|
||||
* Copyright (C) 2014 Daniel Boehringer
|
||||
* This file is based on the RTFProducer from GNUStep
|
||||
* (which I co-authored with Fred Kiefer in 1999)
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
@@ -27,6 +27,8 @@
|
||||
@import "CPColor.j"
|
||||
@import "CPGraphics.j"
|
||||
@import "CPFontManager.j"
|
||||
@import "_CPTableTextAttachment.j"
|
||||
@import "CPTextList.j"
|
||||
|
||||
@global CPForegroundColorAttributeName
|
||||
@global CPBackgroundColorAttributeName
|
||||
@@ -43,6 +45,11 @@
|
||||
@global CPJustifiedTextAlignment
|
||||
@global CPNaturalTextAlignment
|
||||
|
||||
@global CPLeftTabStopType
|
||||
@global CPRightTabStopType
|
||||
@global CPCenterTabStopType
|
||||
@global CPDecimalTabStopType
|
||||
|
||||
var PAPERSIZE = @"PaperSize",
|
||||
LEFTMARGIN = @"LeftMargin",
|
||||
RIGHTMARGIN = @"RightMargin",
|
||||
@@ -51,6 +58,44 @@ var PAPERSIZE = @"PaperSize",
|
||||
|
||||
function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
/*
|
||||
Escapes text for RTF. Backslashes and braces are escaped, and every character outside ASCII
|
||||
is written as a Unicode control word, \uN, with no fallback characters (\uc0), as Cocoa
|
||||
writes it. Values above 32767 are written as negative numbers, as RTF requires.
|
||||
*/
|
||||
var _CPRTFEscapeUnicode = function(aString)
|
||||
{
|
||||
return aString.replace(/[^\x00-\x7f]/g, function(aCharacter)
|
||||
{
|
||||
var code = aCharacter.charCodeAt(0);
|
||||
|
||||
return "\\uc0\\u" + (code > 32767 ? code - 65536 : code) + " ";
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Returns aNumber as two hexadecimal digits, for the \'hh escapes of a list level's text.
|
||||
*/
|
||||
var _CPRTFHexByte = function(aNumber)
|
||||
{
|
||||
return ("0" + (aNumber & 0xff).toString(16)).slice(-2);
|
||||
};
|
||||
|
||||
/*
|
||||
The RTF number format (\levelnfc) of each marker format name. Bullets are 23. Hexadecimal and
|
||||
octal have no RTF number format, so other readers show them as decimal numbers; Cocoa reads
|
||||
the marker format itself from \levelmarker.
|
||||
*/
|
||||
var _CPRTFLevelNumberFormats = {
|
||||
"decimal": 0,
|
||||
"upper-roman": 1,
|
||||
"lower-roman": 2,
|
||||
"upper-alpha": 3,
|
||||
"upper-latin": 3,
|
||||
"lower-alpha": 4,
|
||||
"lower-latin": 4
|
||||
};
|
||||
|
||||
@implementation _CPRTFProducer : CPObject
|
||||
{
|
||||
CPAttributedString text;
|
||||
@@ -62,10 +107,15 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
CPColor fgColor;
|
||||
CPColor bgColor;
|
||||
CPColor ulColor;
|
||||
|
||||
// The outermost text lists in the text, each written as one RTF list, and for each, the text
|
||||
// list of every level, as first met.
|
||||
CPArray textLists;
|
||||
CPArray textListLevels;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
|
||||
+ (CPString)produceRTF:(CPAttributedString)aText documentAttributes:(CPDictionary)dict
|
||||
@@ -76,29 +126,26 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark init methods
|
||||
// MARK: -
|
||||
// MARK: init methods
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
// maintain a dictionary for the used colours
|
||||
// (for rtf-header generation)
|
||||
colorDict = [CPMutableDictionary new];
|
||||
|
||||
//maintain a dictionary for the used fonts
|
||||
//(for rtf-header generation)
|
||||
fontDict = [CPMutableDictionary new];
|
||||
|
||||
fgColor = [CPColor blackColor];
|
||||
bgColor= [CPColor whiteColor];
|
||||
|
||||
textLists = [];
|
||||
textListLevels = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// private stuff follows
|
||||
- (CPString)fontTable
|
||||
{
|
||||
if (![fontDict count])
|
||||
@@ -246,6 +293,7 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
result = [CPString stringWithString:@"{\\rtf1\\ansi"];
|
||||
result += [self fontTable];
|
||||
result += [self colorTable];
|
||||
result += [self listTables];
|
||||
result += [self documentAttributes];
|
||||
|
||||
return result;
|
||||
@@ -277,7 +325,7 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
if (!num)
|
||||
[colorDict setObject:num = [CPNumber numberWithInt:[colorDict count] + 1]
|
||||
forKey:[color cssString]];
|
||||
forKey:[color cssString]];
|
||||
|
||||
return [num intValue];
|
||||
}
|
||||
@@ -313,8 +361,8 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
break;
|
||||
}
|
||||
|
||||
// write first line indent and left indent
|
||||
var twips = _points2twips([paraStyle firstLineHeadIndent]);
|
||||
// In RTF the first line indent is an offset from the left indent.
|
||||
var twips = _points2twips([paraStyle firstLineHeadIndent] - [paraStyle headIndent]);
|
||||
|
||||
if (twips != 0.0)
|
||||
headerString += [CPString stringWithFormat:@"\\fi%d", twips];
|
||||
@@ -351,35 +399,390 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
while ((tab = [enumerator nextObject]))
|
||||
{
|
||||
switch ([tab tabStopType])
|
||||
var tabType = [tab respondsToSelector:@selector(tabStopType)] ? [tab tabStopType] : nil;
|
||||
if (tabType === nil && [tab respondsToSelector:@selector(alignment)])
|
||||
tabType = [tab alignment];
|
||||
|
||||
switch (tabType)
|
||||
{
|
||||
case CPLeftTabStopType:
|
||||
// no tabkind emission needed
|
||||
case CPLeftTextAlignment:
|
||||
break;
|
||||
/* case NSRightTabStopType:
|
||||
case CPRightTabStopType:
|
||||
case CPRightTextAlignment:
|
||||
headerString += @"\\tqr";
|
||||
break;
|
||||
case NSCenterTabStopType:
|
||||
headerString += @"\\tqc";
|
||||
break;
|
||||
case NSDecimalTabStopType:
|
||||
break;
|
||||
case CPCenterTabStopType:
|
||||
case CPCenterTextAlignment:
|
||||
headerString += @"\\tqc";
|
||||
break;
|
||||
case CPDecimalTabStopType:
|
||||
headerString += @"\\tqdec";
|
||||
break;
|
||||
default:
|
||||
NSLog(@"Unknown tab stop type.");
|
||||
*/
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
|
||||
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
|
||||
}
|
||||
|
||||
// A list item names its RTF list and its level.
|
||||
var lists = [paraStyle textLists];
|
||||
|
||||
if ([lists count])
|
||||
headerString += [CPString stringWithFormat:@"\\ls%d\\ilvl%d", [self listNumberForTextLists:lists], [lists count] - 1];
|
||||
|
||||
return headerString;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: Text lists
|
||||
|
||||
/*
|
||||
Returns the number of the RTF list for a paragraph's text lists, and records the list of each
|
||||
level. Each outermost text list becomes one RTF list, with one level for each nesting depth.
|
||||
*/
|
||||
- (int)listNumberForTextLists:(CPArray)someLists
|
||||
{
|
||||
var index = [textLists indexOfObjectIdenticalTo:someLists[0]];
|
||||
|
||||
if (index === CPNotFound)
|
||||
{
|
||||
index = textLists.length;
|
||||
textLists.push(someLists[0]);
|
||||
textListLevels.push([]);
|
||||
}
|
||||
|
||||
var levels = textListLevels[index];
|
||||
|
||||
for (var level = 0; level < someLists.length; level++)
|
||||
if (!levels[level])
|
||||
levels[level] = someLists[level];
|
||||
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the list table and the list override table for the text lists in the text. Each RTF
|
||||
list is a hybrid list with one level for each nesting depth, and one override, \lsN, that
|
||||
paragraphs refer to.
|
||||
*/
|
||||
- (CPString)listTables
|
||||
{
|
||||
if (!textLists.length)
|
||||
return @"";
|
||||
|
||||
var listTable = @"{\\*\\listtable",
|
||||
overrideTable = @"{\\*\\listoverridetable";
|
||||
|
||||
for (var i = 0; i < textLists.length; i++)
|
||||
{
|
||||
var listNumber = i + 1,
|
||||
levels = textListLevels[i];
|
||||
|
||||
listTable += "{\\list\\listtemplateid" + listNumber + "\\listhybrid";
|
||||
|
||||
for (var level = 0; level < levels.length; level++)
|
||||
listTable += [self listLevelString:levels atLevel:level templateID:listNumber * 100 + level + 1];
|
||||
|
||||
listTable += "{\\listname ;}\\listid" + listNumber + "}";
|
||||
overrideTable += "{\\listoverride\\listid" + listNumber + "\\listoverridecount0\\ls" + listNumber + "}";
|
||||
}
|
||||
|
||||
return listTable + "}\n" + overrideTable + "}\n";
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the RTF definition of one list level. Cocoa reads the marker format from the
|
||||
\levelmarker destination and the prepending of the enclosing marker from \levelprepend. Other
|
||||
readers use the number format (\levelnfc), the starting number (\levelstartat) and the
|
||||
level's text (\leveltext), in which \'0N stands for the number of level N.
|
||||
*/
|
||||
- (CPString)listLevelString:(CPArray)someLevels atLevel:(int)aLevel templateID:(int)aTemplateID
|
||||
{
|
||||
var list = someLevels[aLevel],
|
||||
format = [list markerFormat],
|
||||
match = format.match(/\{([a-z-]+)\}/),
|
||||
name = match ? match[1] : "",
|
||||
numberFormat = [list isOrdered] ? (_CPRTFLevelNumberFormats[name] || 0) : 23,
|
||||
parts = [self levelTextParts:someLevels atLevel:aLevel],
|
||||
levelText = "",
|
||||
levelNumbers = "",
|
||||
length = 0,
|
||||
indent = 720 * (aLevel + 1);
|
||||
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (typeof part === "number")
|
||||
{
|
||||
length++;
|
||||
levelText += "\\'" + _CPRTFHexByte(part);
|
||||
levelNumbers += "\\'" + _CPRTFHexByte(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
length += part.length;
|
||||
levelText += _CPRTFEscapeUnicode(part.replace(/[\\{};]/g, function(aCharacter)
|
||||
{
|
||||
return "\\'" + _CPRTFHexByte(aCharacter.charCodeAt(0));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return "{\\listlevel\\levelnfc" + numberFormat + "\\levelnfcn" + numberFormat +
|
||||
"\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat" + [list startingItemNumber] +
|
||||
"\\levelspace360\\levelindent0{\\*\\levelmarker " + _CPRTFEscapeUnicode(format.replace(/([\\{}])/g, "\\$1")) + "}" +
|
||||
(([list listOptions] & CPTextListPrependEnclosingMarker) ? "\\levelprepend" : "") +
|
||||
"{\\leveltext\\leveltemplateid" + aTemplateID + "\\'" + _CPRTFHexByte(length) + levelText + ";}" +
|
||||
"{\\levelnumbers" + levelNumbers + ";}\\fi-720\\li" + indent + "\\lin" + indent + " }";
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the parts of a list level's text: strings, and the level numbers whose item numbers
|
||||
take their place. A bullet level is its bullet. A numbered level is the text around its number,
|
||||
after the enclosing level's text when the list prepends the enclosing marker.
|
||||
*/
|
||||
- (CPArray)levelTextParts:(CPArray)someLevels atLevel:(int)aLevel
|
||||
{
|
||||
var list = someLevels[aLevel],
|
||||
format = [list markerFormat];
|
||||
|
||||
if (![list isOrdered])
|
||||
return [[list markerForItemNumber:1]];
|
||||
|
||||
var match = format.match(/\{[a-z-]+\}/),
|
||||
parts = [];
|
||||
|
||||
if (aLevel > 0 && ([list listOptions] & CPTextListPrependEnclosingMarker))
|
||||
parts = [self levelTextParts:someLevels atLevel:aLevel - 1];
|
||||
|
||||
if (match.index > 0)
|
||||
parts.push(format.substring(0, match.index));
|
||||
|
||||
parts.push(aLevel);
|
||||
|
||||
if (match.index + match[0].length < format.length)
|
||||
parts.push(format.substring(match.index + match[0].length));
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
- (CPString)runStringForString:(CPString) substring
|
||||
attributes:(CPDictionary) attributes
|
||||
paragraphStart:(BOOL) first
|
||||
{
|
||||
return [self runStringForString:substring attributes:attributes paragraphStart:first listMarker:NO];
|
||||
}
|
||||
|
||||
/*
|
||||
As -runStringForString:attributes:paragraphStart:. When isListMarker is YES, the run is the
|
||||
marker at the start of a list item, and is written in a \listtext group, as Cocoa writes it.
|
||||
Readers that understand lists number the items themselves; others show the marker text.
|
||||
*/
|
||||
- (CPString)runStringForString:(CPString) substring
|
||||
attributes:(CPDictionary) attributes
|
||||
paragraphStart:(BOOL) first
|
||||
listMarker:(BOOL) isListMarker
|
||||
{
|
||||
var unwrap = function(obj) {
|
||||
if (!obj) return null;
|
||||
|
||||
if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) ||
|
||||
obj.headers || obj._headers || obj.rows || obj._rows) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
var unwrapped = null;
|
||||
if (typeof obj.respondsToSelector === "function") {
|
||||
if ([obj respondsToSelector:@selector(attachmentCell)]) {
|
||||
unwrapped = [obj attachmentCell];
|
||||
} else if ([obj respondsToSelector:@selector(content)]) {
|
||||
unwrapped = [obj content];
|
||||
} else if ([obj respondsToSelector:@selector(view)]) {
|
||||
unwrapped = [obj view];
|
||||
}
|
||||
}
|
||||
if (!unwrapped) {
|
||||
unwrapped = obj._attachmentCell || obj._content || obj._view || obj.attachmentCell || obj.content || obj.view;
|
||||
}
|
||||
return unwrapped ? unwrapped : obj;
|
||||
};
|
||||
|
||||
var tableAttachment = null;
|
||||
|
||||
if (typeof CPAttachmentAttributeName !== "undefined") {
|
||||
tableAttachment = [attributes objectForKey:CPAttachmentAttributeName];
|
||||
}
|
||||
if (!tableAttachment) {
|
||||
tableAttachment = [attributes objectForKey:@"CPAttachmentAttributeName"];
|
||||
}
|
||||
if (!tableAttachment) {
|
||||
tableAttachment = [attributes objectForKey:@"TableAttachmentAttribute"];
|
||||
}
|
||||
if (!tableAttachment) {
|
||||
tableAttachment = [attributes objectForKey:@"_CPAttachmentView"];
|
||||
}
|
||||
if (!tableAttachment && typeof _CPAttachmentView !== "undefined") {
|
||||
tableAttachment = [attributes objectForKey:_CPAttachmentView];
|
||||
}
|
||||
|
||||
tableAttachment = unwrap(tableAttachment);
|
||||
|
||||
if (!tableAttachment && (substring === "\uFFFC" || substring === ""))
|
||||
{
|
||||
var keys = [attributes allKeys],
|
||||
count = [keys count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var key = [keys objectAtIndex:i],
|
||||
val = unwrap([attributes objectForKey:key]);
|
||||
|
||||
if (val && (
|
||||
(typeof val.respondsToSelector === "function" && [val respondsToSelector:@selector(headers)]) ||
|
||||
val._headers ||
|
||||
val.headers ||
|
||||
(typeof _CPTableTextAttachment !== "undefined" && [val isKindOfClass:[_CPTableTextAttachment class]])
|
||||
)) {
|
||||
tableAttachment = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tableAttachment)
|
||||
{
|
||||
var headers = null,
|
||||
rows = null;
|
||||
|
||||
// Try to fetch from the active live view of the attachment first to capture user edits
|
||||
var activeView = null;
|
||||
if (typeof tableAttachment.respondsToSelector === "function" && [tableAttachment respondsToSelector:@selector(view)]) {
|
||||
activeView = [tableAttachment view];
|
||||
}
|
||||
if (!activeView) {
|
||||
activeView = tableAttachment._view || tableAttachment.view;
|
||||
}
|
||||
|
||||
if (activeView) {
|
||||
if (typeof activeView.respondsToSelector === "function") {
|
||||
if ([activeView respondsToSelector:@selector(headers)]) {
|
||||
headers = [activeView headers];
|
||||
}
|
||||
if ([activeView respondsToSelector:@selector(rows)]) {
|
||||
rows = [activeView rows];
|
||||
}
|
||||
}
|
||||
if (!headers) {
|
||||
headers = activeView._headers || activeView.headers;
|
||||
}
|
||||
if (!rows) {
|
||||
rows = activeView._rows || activeView.rows;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the attachment's parsed properties if the view is nil or lacks the properties
|
||||
if (!headers || !rows) {
|
||||
if (typeof tableAttachment.respondsToSelector === "function") {
|
||||
if ([tableAttachment respondsToSelector:@selector(headers)]) {
|
||||
headers = [tableAttachment headers];
|
||||
}
|
||||
if ([tableAttachment respondsToSelector:@selector(rows)]) {
|
||||
rows = [tableAttachment rows];
|
||||
}
|
||||
}
|
||||
if (!headers) {
|
||||
headers = tableAttachment._headers || tableAttachment.headers;
|
||||
}
|
||||
if (!rows) {
|
||||
rows = tableAttachment._rows || tableAttachment.rows;
|
||||
}
|
||||
}
|
||||
|
||||
var getCount = function(arr) {
|
||||
if (!arr) return 0;
|
||||
if (typeof arr.count === "function") return [arr count];
|
||||
return arr.length;
|
||||
};
|
||||
|
||||
var getObjectAtIndex = function(arr, idx) {
|
||||
if (!arr) return null;
|
||||
if (typeof arr.objectAtIndex === "function") return [arr objectAtIndex:idx];
|
||||
return arr[idx];
|
||||
};
|
||||
|
||||
var numCols = getCount(headers);
|
||||
if (numCols == 0 && getCount(rows) > 0) {
|
||||
numCols = getCount(getObjectAtIndex(rows, 0));
|
||||
}
|
||||
|
||||
if (numCols > 0)
|
||||
{
|
||||
var totalWidthTwips = 10000;
|
||||
var colWidthTwips = Math.floor(totalWidthTwips / numCols);
|
||||
var cellBoundaries = [];
|
||||
var currentBoundary = 0;
|
||||
for (var i = 0; i < numCols; i++)
|
||||
{
|
||||
currentBoundary += colWidthTwips;
|
||||
cellBoundaries.push(currentBoundary);
|
||||
}
|
||||
|
||||
var tableRTF = "";
|
||||
var writeRow = function(rowData, isHeaderRow) {
|
||||
var rowRTF = "\\trowd\\trgaph115\\trleft0";
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
rowRTF += "\\clbrdrt\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10";
|
||||
rowRTF += "\\cellx" + cellBoundaries[c];
|
||||
}
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var cellText = "";
|
||||
if (c < getCount(rowData)) {
|
||||
cellText = getObjectAtIndex(rowData, c);
|
||||
}
|
||||
if (cellText === null || cellText === undefined) {
|
||||
cellText = "";
|
||||
}
|
||||
|
||||
// Safely extract text representation from CPAttributedString / CPTextStorage if present
|
||||
if (cellText && typeof cellText === "object") {
|
||||
if (typeof cellText.string === "function") {
|
||||
cellText = [cellText string];
|
||||
} else if (cellText._string !== undefined) {
|
||||
cellText = cellText._string;
|
||||
} else if (cellText.string !== undefined) {
|
||||
cellText = cellText.string;
|
||||
}
|
||||
}
|
||||
|
||||
cellText = String(cellText);
|
||||
cellText = cellText.replace(/\\/g, '\\\\');
|
||||
cellText = cellText.replace(/{/g, '\\{');
|
||||
cellText = cellText.replace(/}/g, '\\}');
|
||||
cellText = cellText.replace(/\n/g, '\\line ');
|
||||
|
||||
if (isHeaderRow) {
|
||||
rowRTF += "{\\intbl\\b " + cellText + "\\b0\\cell}";
|
||||
} else {
|
||||
rowRTF += "{\\intbl " + cellText + "\\cell}";
|
||||
}
|
||||
}
|
||||
rowRTF += "\\row\n";
|
||||
return rowRTF;
|
||||
};
|
||||
|
||||
if (getCount(headers) > 0) {
|
||||
tableRTF += writeRow(headers, YES);
|
||||
}
|
||||
var rowCount = getCount(rows);
|
||||
for (var r = 0; r < rowCount; r++) {
|
||||
tableRTF += writeRow(getObjectAtIndex(rows, r), NO);
|
||||
}
|
||||
|
||||
return tableRTF;
|
||||
}
|
||||
}
|
||||
|
||||
var result = "",
|
||||
headerString = "",
|
||||
trailerString = "",
|
||||
@@ -392,23 +795,12 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
headerString += [self paragraphStyle:paraStyle];
|
||||
}
|
||||
|
||||
/*
|
||||
* analyze attributes of current run
|
||||
*
|
||||
* FIXME: All the character attributes should be output relative to the font
|
||||
* attributes of the paragraph. So if the paragraph has underline on it should
|
||||
* still be possible to switch it off for some characters, which currently is
|
||||
* not possible.
|
||||
*/
|
||||
attribEnum = [attributes keyEnumerator];
|
||||
|
||||
while ((currAttrib = [attribEnum nextObject]) != nil)
|
||||
{
|
||||
if ([currAttrib isEqualToString:CPFontAttributeName])
|
||||
{
|
||||
/*
|
||||
* handle fonts
|
||||
*/
|
||||
var font,
|
||||
fontName,
|
||||
traits;
|
||||
@@ -417,15 +809,9 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
fontName = [font familyName];
|
||||
traits = [[CPFontManager sharedFontManager] traitsOfFont:font];
|
||||
|
||||
/*
|
||||
* font name
|
||||
*/
|
||||
if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]])
|
||||
headerString += [self fontToken:fontName];
|
||||
|
||||
/*
|
||||
* font size
|
||||
*/
|
||||
if (currentFont == nil || [font size] != [currentFont size])
|
||||
{
|
||||
var points = [font size] * 2,
|
||||
@@ -434,9 +820,7 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
pString = [CPString stringWithFormat:@"\\fs%d", points];
|
||||
headerString += pString;
|
||||
}
|
||||
/*
|
||||
* font attributes
|
||||
*/
|
||||
|
||||
if (traits & CPItalicFontMask)
|
||||
{
|
||||
headerString += @"\\i";
|
||||
@@ -475,28 +859,29 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
else if ([currAttrib isEqualToString:CPUnderlineStyleAttributeName])
|
||||
{
|
||||
headerString += @"\\ul";
|
||||
trailerString += @"\\ulnone";
|
||||
trailerString += @"\\ulnone "; // trailing space important!
|
||||
}
|
||||
else if ([currAttrib isEqualToString:CPSuperscriptAttributeName])
|
||||
{
|
||||
var value = [attributes objectForKey:CPSuperscriptAttributeName],
|
||||
svalue = [value intValue] * 6;
|
||||
ivalue = [value intValue];
|
||||
|
||||
if (svalue > 0)
|
||||
if (ivalue > 0)
|
||||
{
|
||||
headerString += [CPString stringWithFormat:@"\\up%d", svalue];
|
||||
trailerString += @"\\up0";
|
||||
headerString += @"\\super";
|
||||
trailerString += @"\\nosupersub "; // trailing space important!
|
||||
}
|
||||
else if (svalue < 0)
|
||||
else if (ivalue < 0)
|
||||
{
|
||||
headerString += [CPString stringWithFormat:@"\\dn-%d", svalue];
|
||||
trailerString += @"\\dn0";
|
||||
headerString += @"\\sub";
|
||||
trailerString += @"\\nosupersub "; // trailing space important!
|
||||
}
|
||||
}
|
||||
else if ([currAttrib isEqualToString:CPBaselineOffsetAttributeName])
|
||||
{
|
||||
var value = [attributes objectForKey:CPBaselineOffsetAttributeName],
|
||||
svalue = [value floatValue] * 2;
|
||||
fvalue = [value floatValue],
|
||||
svalue = Math.round(fvalue * 2.0); // Convert standard points to RTF half-points
|
||||
|
||||
if (svalue > 0)
|
||||
{
|
||||
@@ -505,7 +890,8 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
}
|
||||
else if (svalue < 0)
|
||||
{
|
||||
headerString += [CPString stringWithFormat:@"\\dn-%d", svalue];
|
||||
// Correct negative formatting using safe positive boundary
|
||||
headerString += [CPString stringWithFormat:@"\\dn%d", Math.abs(svalue)];
|
||||
trailerString += @"\\dn0";
|
||||
}
|
||||
}
|
||||
@@ -520,13 +906,18 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
}
|
||||
}
|
||||
|
||||
// An image attachment is not written yet, so its attachment character is left out.
|
||||
substring = substring.replace(/\uFFFC/g, '');
|
||||
|
||||
substring = substring.replace(/\\/g, '\\\\');
|
||||
substring = substring.replace(/\n/g, '\\par\n');
|
||||
substring = substring.replace(/\t/g, '\\tab');
|
||||
substring = substring.replace(/\t/g, '\\tab ');
|
||||
substring = substring.replace(/{/g, '\\{');
|
||||
substring = substring.replace(/}/g, '\\}');
|
||||
// FIXME: All characters not in the standard encoding must be
|
||||
// replaced by \'xx
|
||||
substring = _CPRTFEscapeUnicode(substring);
|
||||
|
||||
if (isListMarker)
|
||||
substring = "{\\listtext " + substring + "}";
|
||||
|
||||
if (!first)
|
||||
{
|
||||
@@ -562,31 +953,75 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
length = [string length],
|
||||
currRange = CPMakeRange(loc, 0),
|
||||
completeRange = CPMakeRange(0, length),
|
||||
first = YES;
|
||||
paragraphStart = YES;
|
||||
|
||||
// FIXME <!> split along newline characters and run as outer loop
|
||||
while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs"
|
||||
while (CPMaxRange(currRange) < CPMaxRange(completeRange))
|
||||
{
|
||||
var attributes,
|
||||
substring,
|
||||
runString;
|
||||
|
||||
attributes = [text attributesAtIndex:CPMaxRange(currRange)
|
||||
var runStart = CPMaxRange(currRange);
|
||||
|
||||
attributes = [text attributesAtIndex:runStart
|
||||
longestEffectiveRange:currRange
|
||||
inRange:completeRange];
|
||||
|
||||
// The longest effective range can reach back before the run's start, after a list marker
|
||||
// that shares its attributes was written as a run of its own.
|
||||
if (currRange.location < runStart)
|
||||
currRange = CPMakeRange(runStart, CPMaxRange(currRange) - runStart);
|
||||
|
||||
// A list item's marker, a tab, the marker and a tab, is written as a run of its own.
|
||||
var markerLength = paragraphStart ? [self listMarkerLengthAtIndex:currRange.location attributes:attributes] : 0,
|
||||
isListMarker = markerLength > 0 && markerLength <= currRange.length;
|
||||
|
||||
if (isListMarker)
|
||||
currRange.length = markerLength;
|
||||
|
||||
substring = [string substringWithRange:currRange];
|
||||
runString = [self runStringForString:substring
|
||||
attributes:attributes
|
||||
paragraphStart:YES];
|
||||
paragraphStart:paragraphStart
|
||||
listMarker:isListMarker];
|
||||
|
||||
result += runString;
|
||||
first = NO;
|
||||
|
||||
if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n')
|
||||
paragraphStart = YES;
|
||||
else
|
||||
paragraphStart = NO;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Returns the length of the marker at the start of a list item, a tab, the marker and a tab, or
|
||||
0 when the paragraph is not a list item or does not start with a marker.
|
||||
*/
|
||||
- (unsigned)listMarkerLengthAtIndex:(unsigned)anIndex attributes:(CPDictionary)attributes
|
||||
{
|
||||
var string = [text string];
|
||||
|
||||
if (![[[attributes objectForKey:CPParagraphStyleAttributeName] textLists] count] || string.charAt(anIndex) !== "\t")
|
||||
return 0;
|
||||
|
||||
for (var i = anIndex + 1; i < string.length; i++)
|
||||
{
|
||||
var character = string.charAt(i);
|
||||
|
||||
if (character === "\t")
|
||||
return i - anIndex + 1;
|
||||
|
||||
if (character === "\n")
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CPString)RTFDStringFromAttributedString:(CPAttributedString)aText
|
||||
documentAttributes:(CPDictionary)dict
|
||||
{
|
||||
@@ -598,9 +1033,6 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
text = aText;
|
||||
docDict = dict;
|
||||
|
||||
/*
|
||||
* do not change order! (esp. body has to be generated first; builds context)
|
||||
*/
|
||||
bodyString = [self bodyString];
|
||||
trailerString = [self trailerString];
|
||||
headerString = [self headerString];
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
/* _CPTableTextAttachment.j
|
||||
* A self-contained, renderable text attachment representation of a table.
|
||||
*
|
||||
* Copyright (C) 2026 Daniel Boehringer
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "CPView.j"
|
||||
@class CPTextView;
|
||||
@class CPTextContainer;
|
||||
@import "CPTextField.j"
|
||||
@import <Foundation/CPAttributedString.j>
|
||||
|
||||
@implementation _CPTableTextAttachment : CPView
|
||||
{
|
||||
CPArray _headers;
|
||||
CPArray _rows;
|
||||
BOOL _isResizing;
|
||||
BOOL _isEditable;
|
||||
BOOL _acceptsRichText;
|
||||
}
|
||||
|
||||
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows
|
||||
{
|
||||
return [self initWithHeaders:headers rows:rows width:500.0 isEditable:YES acceptsRichText:YES];
|
||||
}
|
||||
|
||||
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth
|
||||
{
|
||||
return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:YES];
|
||||
}
|
||||
|
||||
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth isEditable:(BOOL)isEditable acceptsRichText:(BOOL)acceptsRichText
|
||||
{
|
||||
self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)];
|
||||
if (self)
|
||||
{
|
||||
_headers = headers;
|
||||
_rows = rows;
|
||||
_isEditable = isEditable;
|
||||
_acceptsRichText = acceptsRichText;
|
||||
_isResizing = NO;
|
||||
|
||||
[self _rebuildTableWithWidth:totalWidth];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_rebuildTableWithWidth:(float)totalWidth
|
||||
{
|
||||
var numCols = _headers ? [_headers count] : 0;
|
||||
|
||||
if (numCols == 0 && _rows && [_rows count] > 0)
|
||||
numCols = [[_rows objectAtIndex:0] count];
|
||||
|
||||
// Apply borders on the outer left and top container edges
|
||||
if (self._DOMElement) {
|
||||
self._DOMElement.style.borderTop = "1px solid #e0e0e0";
|
||||
self._DOMElement.style.borderLeft = "1px solid #e0e0e0";
|
||||
self._DOMElement.style.boxSizing = "border-box";
|
||||
}
|
||||
|
||||
// Initialize header cells
|
||||
if (_headers && [_headers count] > 0) {
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var headerText = [_headers objectAtIndex:c];
|
||||
var cellView = [self createCellWithText:headerText frame:CGRectMakeZero() isHeader:YES];
|
||||
[self addSubview:cellView];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize body rows
|
||||
if (_rows) {
|
||||
for (var r = 0; r < [_rows count]; r++) {
|
||||
var rowData = [_rows objectAtIndex:r];
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var cellText = @"";
|
||||
|
||||
if (c < [rowData count])
|
||||
cellText = [rowData objectAtIndex:c];
|
||||
|
||||
var cellView = [self createCellWithText:cellText frame:CGRectMakeZero() isHeader:NO];
|
||||
[self addSubview:cellView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self resizeToWidth:totalWidth];
|
||||
|
||||
// Trigger parent layout engine re-calculation
|
||||
var textView = [self superview];
|
||||
|
||||
if (textView && [textView isKindOfClass:[CPTextView class]])
|
||||
{
|
||||
var layoutManager = [textView layoutManager];
|
||||
|
||||
if (layoutManager)
|
||||
{
|
||||
var charRange = [self _findCharacterRangeInLayoutManager:layoutManager];
|
||||
if (charRange && charRange.location !== CPNotFound)
|
||||
{
|
||||
[layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil];
|
||||
[layoutManager invalidateDisplayForGlyphRange:charRange];
|
||||
[layoutManager _validateLayoutAndGlyphs];
|
||||
[textView sizeToFit];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CPRange)_findCharacterRangeInLayoutManager:(CPLayoutManager)layoutManager
|
||||
{
|
||||
var lineFragments = layoutManager._lineFragments;
|
||||
if (lineFragments)
|
||||
{
|
||||
var l = lineFragments.length;
|
||||
for (var i = 0; i < l; i++)
|
||||
{
|
||||
var fragment = lineFragments[i];
|
||||
var runs = fragment._runs;
|
||||
if (runs)
|
||||
{
|
||||
var rc = runs.length;
|
||||
for (var j = 0; j < rc; j++)
|
||||
{
|
||||
if (runs[j].view === self)
|
||||
{
|
||||
return runs[j]._range;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return CPMakeRange(CPNotFound, 0);
|
||||
}
|
||||
|
||||
- (CPArray)headers
|
||||
{
|
||||
var numCols = _headers ? [_headers count] : 0;
|
||||
if (numCols == 0 && _rows && [_rows count] > 0)
|
||||
numCols = [[_rows objectAtIndex:0] count];
|
||||
|
||||
if (numCols == 0)
|
||||
return _headers;
|
||||
|
||||
var subviews = [self subviews];
|
||||
if ([subviews count] < numCols)
|
||||
return _headers; // Subviews are not yet rendered, return cached fallback
|
||||
|
||||
var currentHeaders = [CPMutableArray array];
|
||||
for (var c = 0; c < numCols; c++)
|
||||
{
|
||||
var cellView = [subviews objectAtIndex:c];
|
||||
var textView = [self getTextViewFromCell:cellView];
|
||||
var text = @"";
|
||||
if (textView)
|
||||
{
|
||||
text = _acceptsRichText ? [[textView textStorage] copy] : [textView string];
|
||||
}
|
||||
[currentHeaders addObject:text];
|
||||
}
|
||||
|
||||
_headers = currentHeaders;
|
||||
return _headers;
|
||||
}
|
||||
|
||||
- (CPArray)rows
|
||||
{
|
||||
var numCols = _headers ? [_headers count] : 0;
|
||||
if (numCols == 0 && _rows && [_rows count] > 0)
|
||||
numCols = [[_rows objectAtIndex:0] count];
|
||||
|
||||
if (numCols == 0 || !_rows)
|
||||
return _rows;
|
||||
|
||||
var subviews = [self subviews];
|
||||
var headerOffset = (_headers && [_headers count] > 0) ? numCols : 0;
|
||||
var expectedCount = headerOffset + ([_rows count] * numCols);
|
||||
|
||||
if ([subviews count] < expectedCount)
|
||||
return _rows; // Subviews are not yet rendered, return cached fallback
|
||||
|
||||
var currentRows = [CPMutableArray array];
|
||||
var cellIndex = headerOffset;
|
||||
|
||||
for (var r = 0; r < [_rows count]; r++)
|
||||
{
|
||||
var rowData = [CPMutableArray array];
|
||||
for (var c = 0; c < numCols; c++)
|
||||
{
|
||||
var cellView = [subviews objectAtIndex:cellIndex++];
|
||||
var textView = [self getTextViewFromCell:cellView];
|
||||
var text = @"";
|
||||
if (textView)
|
||||
{
|
||||
text = _acceptsRichText ? [[textView textStorage] copy] : [textView string];
|
||||
}
|
||||
[rowData addObject:text];
|
||||
}
|
||||
[currentRows addObject:rowData];
|
||||
}
|
||||
|
||||
_rows = currentRows;
|
||||
return _rows;
|
||||
}
|
||||
|
||||
- (CPView)viewForWidth:(float)width
|
||||
{
|
||||
[self resizeToWidth:width];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPView)createCellWithText:(id)text frame:(CGRect)frame isHeader:(BOOL)isHeader
|
||||
{
|
||||
var initialWidth = (frame.size.width > 0) ? frame.size.width : 120.0;
|
||||
var initialHeight = (frame.size.height > 0) ? frame.size.height : 28.0;
|
||||
|
||||
var cellContainer = [[CPView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, initialWidth, initialHeight)];
|
||||
[cellContainer setBackgroundColor:isHeader ? [CPColor colorWithWhite:0.92 alpha:1.0] : [CPColor whiteColor]];
|
||||
|
||||
// Bottom and right edge borders to construct the grid
|
||||
var borderView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, initialWidth, initialHeight)];
|
||||
[borderView setBackgroundColor:[CPColor clearColor]];
|
||||
[borderView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
|
||||
if (borderView._DOMElement)
|
||||
{
|
||||
borderView._DOMElement.style.borderBottom = "1px solid #e0e0e0";
|
||||
borderView._DOMElement.style.borderRight = "1px solid #e0e0e0";
|
||||
borderView._DOMElement.style.boxSizing = "border-box";
|
||||
}
|
||||
[cellContainer addSubview:borderView];
|
||||
|
||||
var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)];
|
||||
var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer];
|
||||
|
||||
// Set explicit zero margins inside the text view to keep height measurements aligned with usedRect
|
||||
[textView setTextContainerInset:CGSizeMake(0, 0)];
|
||||
|
||||
[textView setEditable:_isEditable];
|
||||
[textView setSelectable:YES];
|
||||
[textView setBackgroundColor:[CPColor clearColor]];
|
||||
[textView setVerticallyResizable:YES];
|
||||
[textView setHorizontallyResizable:NO];
|
||||
[[textView textContainer] setWidthTracksTextView:YES];
|
||||
|
||||
// Configure cell rich text mode
|
||||
[textView setRichText:_acceptsRichText];
|
||||
|
||||
// Intercept changes within cell TextViews to notify the table
|
||||
[textView setDelegate:self];
|
||||
|
||||
// Configure cell text style using standard CPTextView APIs
|
||||
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
|
||||
[textView setFont:cellFont];
|
||||
[textView setTextColor:[CPColor blackColor]];
|
||||
|
||||
if (text)
|
||||
[textView insertText:text];
|
||||
|
||||
[cellContainer addSubview:textView];
|
||||
|
||||
return cellContainer;
|
||||
}
|
||||
|
||||
- (void)textDidChange:(CPNotification)aNotification
|
||||
{
|
||||
// Live update cell layouts and heights when changes are typed
|
||||
[self resizeToWidth:CGRectGetWidth([self frame])];
|
||||
}
|
||||
|
||||
- (CPTextView)getTextViewFromCell:(CPView)cellView
|
||||
{
|
||||
var subviews = [cellView subviews];
|
||||
for (var i = 0; i < [subviews count]; i++) {
|
||||
var sub = [subviews objectAtIndex:i];
|
||||
if ([sub isKindOfClass:[CPTextView class]]) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)resizeToWidth:(float)newWidth
|
||||
{
|
||||
if (_isResizing)
|
||||
return;
|
||||
|
||||
_isResizing = YES;
|
||||
|
||||
// Use dynamic getters to fetch live edited strings
|
||||
var currentHeaders = [self headers];
|
||||
var currentRows = [self rows];
|
||||
|
||||
var numCols = currentHeaders ? [currentHeaders count] : 0;
|
||||
if (numCols == 0 && currentRows && [currentRows count] > 0) {
|
||||
numCols = [[currentRows objectAtIndex:0] count];
|
||||
}
|
||||
if (numCols == 0) {
|
||||
_isResizing = NO;
|
||||
return;
|
||||
}
|
||||
|
||||
var subviews = [self subviews];
|
||||
var colNaturalWidths = [];
|
||||
var colMinWidths = [];
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
colNaturalWidths[c] = 80.0;
|
||||
colMinWidths[c] = 60.0;
|
||||
}
|
||||
|
||||
var measureTextField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 10000.0, 24.0)];
|
||||
[measureTextField setFont:[CPFont systemFontOfSize:13.0]];
|
||||
|
||||
var measureCell = function(cellText, isHeader, colIndex) {
|
||||
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
|
||||
[measureTextField setFont:cellFont];
|
||||
|
||||
var plainText = (cellText && typeof cellText.string === "function") ? [cellText string] : (cellText || @"");
|
||||
plainText = String(plainText);
|
||||
|
||||
if (cellText && typeof cellText.string === "function") {
|
||||
if ([measureTextField respondsToSelector:@selector(setAttributedStringValue:)]) {
|
||||
[measureTextField setAttributedStringValue:cellText];
|
||||
} else {
|
||||
[measureTextField setStringValue:plainText];
|
||||
}
|
||||
} else {
|
||||
[measureTextField setStringValue:plainText];
|
||||
}
|
||||
|
||||
[measureTextField sizeToFit];
|
||||
var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0;
|
||||
if (naturalW > colNaturalWidths[colIndex]) {
|
||||
colNaturalWidths[colIndex] = naturalW;
|
||||
}
|
||||
|
||||
var words = plainText.split(/[\s\-]/);
|
||||
var maxWordW = 50.0;
|
||||
for (var w = 0; w < words.length; w++) {
|
||||
var word = words[w].trim();
|
||||
if (word.length === 0) continue;
|
||||
[measureTextField setStringValue:word];
|
||||
[measureTextField sizeToFit];
|
||||
var wordW = CGRectGetWidth([measureTextField frame]) + 30.0;
|
||||
if (wordW > maxWordW) {
|
||||
maxWordW = wordW;
|
||||
}
|
||||
}
|
||||
if (maxWordW > colMinWidths[colIndex]) {
|
||||
colMinWidths[colIndex] = maxWordW;
|
||||
}
|
||||
};
|
||||
|
||||
if (currentHeaders) {
|
||||
for (var c = 0; c < [currentHeaders count]; c++) {
|
||||
measureCell([currentHeaders objectAtIndex:c], YES, c);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRows) {
|
||||
for (var r = 0; r < [currentRows count]; r++) {
|
||||
var rowData = [currentRows objectAtIndex:r];
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var cellText = @"";
|
||||
if (c < [rowData count]) {
|
||||
cellText = [rowData objectAtIndex:c];
|
||||
}
|
||||
measureCell(cellText, NO, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalMinWidth = 0.0;
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
totalMinWidth += colMinWidths[c];
|
||||
}
|
||||
|
||||
var colWidths = [];
|
||||
|
||||
if (newWidth <= totalMinWidth) {
|
||||
var remainingWidth = newWidth;
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var w = Math.floor((colMinWidths[c] / totalMinWidth) * newWidth);
|
||||
colWidths[c] = w;
|
||||
remainingWidth -= w;
|
||||
}
|
||||
if (numCols > 0) colWidths[numCols - 1] += remainingWidth;
|
||||
} else {
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
colWidths[c] = colMinWidths[c];
|
||||
}
|
||||
|
||||
var totalGrowthCapacity = 0.0;
|
||||
var growthCapacities = [];
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var capacity = Math.max(0.0, colNaturalWidths[c] - colMinWidths[c]);
|
||||
growthCapacities[c] = capacity;
|
||||
totalGrowthCapacity += capacity;
|
||||
}
|
||||
|
||||
var extraWidth = newWidth - totalMinWidth;
|
||||
var remainingExtra = extraWidth;
|
||||
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
if (totalGrowthCapacity > 0) {
|
||||
var w = Math.floor((growthCapacities[c] / totalGrowthCapacity) * extraWidth);
|
||||
colWidths[c] += w;
|
||||
remainingExtra -= w;
|
||||
}
|
||||
}
|
||||
if (numCols > 0) {
|
||||
colWidths[numCols - 1] += remainingExtra;
|
||||
}
|
||||
}
|
||||
|
||||
var cellIndex = 0;
|
||||
var currentY = 0;
|
||||
|
||||
var layoutRow = function(startIndex) {
|
||||
var maxCellHeight = 28.0;
|
||||
|
||||
for (var c = 0; c < numCols; c++) {
|
||||
var idx = startIndex + c;
|
||||
|
||||
if (idx < [subviews count]) {
|
||||
var cellView = [subviews objectAtIndex:idx];
|
||||
var textView = [self getTextViewFromCell:cellView];
|
||||
|
||||
if (textView)
|
||||
{
|
||||
var targetWidth = Math.max(10.0, colWidths[c] - 8);
|
||||
|
||||
// Update frame size directly so that textContainer auto-resizes.
|
||||
// Set a large temporary height to allow accurate wrapping measurements.
|
||||
[textView setFrameSize:CGSizeMake(targetWidth, 1e7)];
|
||||
|
||||
var layoutManager = [textView layoutManager];
|
||||
if (layoutManager)
|
||||
{
|
||||
// FORCE LAYOUT RECALCULATION:
|
||||
// Because the width changed, we must force the lazy layout manager
|
||||
// to synchronously calculate glyphs and wraps at the new width.
|
||||
[layoutManager glyphRangeForTextContainer:[textView textContainer]];
|
||||
}
|
||||
|
||||
var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil;
|
||||
var textHeight = usedRect ? CGRectGetHeight(usedRect) : 0.0;
|
||||
|
||||
// Exact height + 8.0px padding (4.0px top, 4.0px bottom margin) inside the cell
|
||||
var wrappedHeight = textHeight + 8.0;
|
||||
|
||||
if (wrappedHeight > maxCellHeight)
|
||||
maxCellHeight = wrappedHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentX = 0;
|
||||
|
||||
for (var c = 0; c < numCols; c++)
|
||||
{
|
||||
var idx = startIndex + c;
|
||||
|
||||
if (idx < [subviews count])
|
||||
{
|
||||
var cellView = [subviews objectAtIndex:idx];
|
||||
[cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)];
|
||||
|
||||
var textView = [self getTextViewFromCell:cellView];
|
||||
|
||||
if (textView)
|
||||
{
|
||||
var targetWidth = Math.max(10.0, colWidths[c] - 8);
|
||||
var textY = 4.0;
|
||||
var finalTextViewHeight = maxCellHeight - 8.0;
|
||||
[textView setFrame:CGRectMake(4, textY, targetWidth, finalTextViewHeight)];
|
||||
}
|
||||
|
||||
var cellSubviews = [cellView subviews];
|
||||
|
||||
if ([cellSubviews count] > 0)
|
||||
[[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)];
|
||||
}
|
||||
currentX += colWidths[c];
|
||||
}
|
||||
|
||||
return maxCellHeight;
|
||||
};
|
||||
|
||||
if (currentHeaders && [currentHeaders count] > 0)
|
||||
{
|
||||
var headerHeight = layoutRow(cellIndex);
|
||||
cellIndex += numCols;
|
||||
currentY += headerHeight;
|
||||
}
|
||||
|
||||
if (currentRows)
|
||||
{
|
||||
for (var r = 0; r < [currentRows count]; r++)
|
||||
{
|
||||
var rowHeight = layoutRow(cellIndex);
|
||||
cellIndex += numCols;
|
||||
currentY += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// GUARD FRAME SIZE MUTATIONS
|
||||
var currentSize = [self frame].size;
|
||||
|
||||
if (ABS(currentSize.width - newWidth) > 0.1 || ABS(currentSize.height - currentY) > 0.1)
|
||||
{
|
||||
[self setFrameSize:CGSizeMake(newWidth, currentY)];
|
||||
}
|
||||
|
||||
// we need to re-layout the textview here.
|
||||
// but this is not easy as the layout engine is not re-entrant
|
||||
// this does not work: (delay does not matter), layout is always off
|
||||
// setTimeout(function() {
|
||||
// var textView = [self superview];
|
||||
//
|
||||
// if (textView && [textView isKindOfClass:[CPTextView class]])
|
||||
// {
|
||||
// var layoutManager = [textView layoutManager];
|
||||
//
|
||||
// if (layoutManager)
|
||||
// {
|
||||
// var charRange = [self _findCharacterRangeInLayoutManager:layoutManager];
|
||||
//
|
||||
// if (charRange && charRange.location !== CPNotFound)
|
||||
// {
|
||||
// [layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil];
|
||||
// [layoutManager invalidateDisplayForGlyphRange:charRange];
|
||||
// [layoutManager _validateLayoutAndGlyphs];
|
||||
// [textView sizeToFit];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }, 0);
|
||||
|
||||
_isResizing = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var _CPTableTextAttachmentHeadersKey = @"_CPTableTextAttachmentHeaders",
|
||||
_CPTableTextAttachmentRowsKey = @"_CPTableTextAttachmentRows",
|
||||
_CPTableTextAttachmentWidthKey = @"_CPTableTextAttachmentWidth",
|
||||
_CPTableTextAttachmentIsEditableKey = @"_CPTableTextAttachmentIsEditable",
|
||||
_CPTableTextAttachmentAcceptsRichTextKey = @"_CPTableTextAttachmentAcceptsRichText";
|
||||
|
||||
@implementation _CPTableTextAttachment (CPCoding)
|
||||
|
||||
/*
|
||||
Decodes the table from its headers, rows, width and settings, and builds its cells again.
|
||||
The cells are not decoded as subviews; they are made from the decoded text.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
return [self initWithHeaders:[aCoder decodeObjectForKey:_CPTableTextAttachmentHeadersKey]
|
||||
rows:[aCoder decodeObjectForKey:_CPTableTextAttachmentRowsKey]
|
||||
width:[aCoder decodeFloatForKey:_CPTableTextAttachmentWidthKey] || 500.0
|
||||
isEditable:[aCoder decodeBoolForKey:_CPTableTextAttachmentIsEditableKey]
|
||||
acceptsRichText:[aCoder decodeBoolForKey:_CPTableTextAttachmentAcceptsRichTextKey]];
|
||||
}
|
||||
|
||||
/*
|
||||
Encodes the table's current text, including edits made in its cells, its width and its
|
||||
settings. The cell views are not encoded.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:[self headers] forKey:_CPTableTextAttachmentHeadersKey];
|
||||
[aCoder encodeObject:[self rows] forKey:_CPTableTextAttachmentRowsKey];
|
||||
[aCoder encodeFloat:CGRectGetWidth([self frame]) forKey:_CPTableTextAttachmentWidthKey];
|
||||
[aCoder encodeBool:_isEditable forKey:_CPTableTextAttachmentIsEditableKey];
|
||||
[aCoder encodeBool:_acceptsRichText forKey:_CPTableTextAttachmentAcceptsRichTextKey];
|
||||
}
|
||||
|
||||
@end
|
||||
+6
-4
@@ -319,8 +319,8 @@ var CPThemeNameKey = @"CPThemeNameKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CSS Theming
|
||||
// MARK: -
|
||||
// MARK: CSS Theming
|
||||
|
||||
// The code below adds support for CSS theming with 100% compatibility with current theming system.
|
||||
// The idea is to extend CPColor and CPImage with CSS components and adapt low level UI components to
|
||||
@@ -556,7 +556,7 @@ var _savedThemesByName = { };
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*!
|
||||
* ThemeStates are immutable objects representing a particular ThemeState. Applications should never be creating
|
||||
@@ -814,10 +814,12 @@ CPThemeStateKeyWindow = CPThemeState("keyWindow");
|
||||
CPThemeStateControlSizeRegular = CPThemeState("controlSizeRegular");
|
||||
CPThemeStateControlSizeSmall = CPThemeState("controlSizeSmall");
|
||||
CPThemeStateControlSizeMini = CPThemeState("controlSizeMini");
|
||||
CPThemeStateControlSizeLarge = CPThemeState("controlSizeLarge");
|
||||
CPThemeStateAlternateState = CPThemeState("alternate");
|
||||
CPThemeStateComposedControl = CPThemeState("composed");
|
||||
|
||||
CPThemeStateWindowsPlatform = CPThemeState("windowsPlatform");
|
||||
CPThemeStateNormalString = String(CPThemeStateNormal);
|
||||
CPThemeStateHUD = CPThemeState("hud");
|
||||
|
||||
|
||||
@implementation _CPThemeAttribute : CPObject
|
||||
|
||||
+90
-77
@@ -32,6 +32,7 @@
|
||||
@import "CPText.j"
|
||||
@import "CPTextField.j"
|
||||
@import "CPWindow_Constants.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@class _CPTokenFieldTokenCloseButton
|
||||
@class _CPTokenFieldTokenDisclosureButton
|
||||
@@ -63,8 +64,6 @@ var CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_
|
||||
CPTokenFieldDelegate_tokenField_representedObjectForEditingString_ = 1 << 6;
|
||||
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
var CPTokenFieldDOMInputElement = nil,
|
||||
CPTokenFieldDOMPasswordInputElement = nil,
|
||||
CPTokenFieldDOMStandardInputElement = nil,
|
||||
@@ -79,8 +78,6 @@ var CPTokenFieldDOMInputElement = nil,
|
||||
|
||||
CPTokenFieldBlurHandler = nil;
|
||||
|
||||
#endif
|
||||
|
||||
var CPScrollDestinationNone = 0,
|
||||
CPScrollDestinationLeft = 1,
|
||||
CPScrollDestinationRight = 2;
|
||||
@@ -169,8 +166,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
[self addSubview:_tokenScrollView];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
/*!
|
||||
Set the delegate of the receiver
|
||||
@@ -349,6 +346,12 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
if (theBinding)
|
||||
[theBinding reverseSetValueFor:@"objectValue"];
|
||||
|
||||
if (!_isEditing)
|
||||
{
|
||||
_isEditing = YES;
|
||||
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
}
|
||||
|
||||
[self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
|
||||
|
||||
_shouldNotifyTarget = YES;
|
||||
@@ -388,10 +391,10 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
if (![super becomeFirstResponder])
|
||||
return NO;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window])
|
||||
if (CPDOMAvailable && CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window])
|
||||
{
|
||||
[[CPTokenFieldInputOwner window] makeFirstResponder:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
// As long as we are the first responder we need to monitor the key status of our window.
|
||||
[self _setObserveWindowKeyNotifications:YES];
|
||||
@@ -417,75 +420,74 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
[self setNeedsLayout];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
var string = [self stringValue],
|
||||
element = [self _inputElement],
|
||||
font = [self currentValueForThemeAttribute:@"font"];
|
||||
|
||||
element.value = nil;
|
||||
element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
|
||||
element.style.font = [font cssString];
|
||||
element.style.zIndex = 1000;
|
||||
|
||||
switch ([self alignment])
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
case CPCenterTextAlignment:
|
||||
element.style.textAlign = "center";
|
||||
break;
|
||||
var string = [self stringValue],
|
||||
element = [self _inputElement],
|
||||
font = [self currentValueForThemeAttribute:@"font"];
|
||||
|
||||
case CPRightTextAlignment:
|
||||
element.style.textAlign = "right";
|
||||
break;
|
||||
element.value = nil;
|
||||
element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString];
|
||||
element.style.font = [font cssString];
|
||||
element.style.zIndex = 1000;
|
||||
|
||||
default:
|
||||
element.style.textAlign = "left";
|
||||
}
|
||||
switch ([self alignment])
|
||||
{
|
||||
case CPCenterTextAlignment:
|
||||
element.style.textAlign = "center";
|
||||
break;
|
||||
|
||||
var contentRect = [self contentRectForBounds:[self bounds]];
|
||||
case CPRightTextAlignment:
|
||||
element.style.textAlign = "right";
|
||||
break;
|
||||
|
||||
element.style.top = CGRectGetMinY(contentRect) + "px";
|
||||
element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // <input> element effectively imposes a 1px left margin
|
||||
element.style.width = CGRectGetWidth(contentRect) + "px";
|
||||
element.style.height = [font defaultLineHeightForFont] + "px";
|
||||
default:
|
||||
element.style.textAlign = "left";
|
||||
}
|
||||
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
[_tokenScrollView documentView]._DOMElement.appendChild(element);
|
||||
var contentRect = [self contentRectForBounds:[self bounds]];
|
||||
|
||||
//post CPControlTextDidBeginEditingNotification
|
||||
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
element.style.top = CGRectGetMinY(contentRect) + "px";
|
||||
element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // <input> element effectively imposes a 1px left margin
|
||||
element.style.width = CGRectGetWidth(contentRect) + "px";
|
||||
element.style.height = [font defaultLineHeightForFont] + "px";
|
||||
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
[_tokenScrollView documentView]._DOMElement.appendChild(element);
|
||||
|
||||
element.focus();
|
||||
// Removed so CPTokenField doesn't fire the notification the moment it becomes the first responder, but instead defers to the first keystroke, just like Cocoa (see keyDown: in CPTextField).
|
||||
// [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
// This will prevent to jump to the focused element
|
||||
var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
|
||||
|
||||
CPTokenFieldInputOwner = self;
|
||||
element.focus();
|
||||
|
||||
[self _restorePreviousScrollingOrigin:previousScrollingOrigin];
|
||||
|
||||
CPTokenFieldInputOwner = self;
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
|
||||
[self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
|
||||
[self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
|
||||
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
|
||||
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
|
||||
CPTokenFieldInputIsActive = YES;
|
||||
|
||||
CPTokenFieldInputIsActive = YES;
|
||||
if (document.attachEvent)
|
||||
{
|
||||
CPTokenFieldCachedSelectStartFunction = document.body.onselectstart;
|
||||
CPTokenFieldCachedDragFunction = document.body.ondrag;
|
||||
|
||||
if (document.attachEvent)
|
||||
{
|
||||
CPTokenFieldCachedSelectStartFunction = document.body.onselectstart;
|
||||
CPTokenFieldCachedDragFunction = document.body.ondrag;
|
||||
|
||||
document.body.ondrag = function () {};
|
||||
document.body.onselectstart = function () {};
|
||||
document.body.ondrag = function () {};
|
||||
document.body.onselectstart = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -498,6 +500,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
[self _resignFirstKeyResponder];
|
||||
|
||||
_isEditing = NO;
|
||||
|
||||
if (_shouldNotifyTarget)
|
||||
{
|
||||
_shouldNotifyTarget = NO;
|
||||
@@ -519,7 +523,7 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
[self _updatePlaceholderState];
|
||||
[self setNeedsLayout];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return;
|
||||
|
||||
var element = [self _inputElement];
|
||||
|
||||
@@ -551,10 +555,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
CPTokenFieldCachedDragFunction = nil;
|
||||
|
||||
document.body.ondrag = CPTokenFieldCachedDragFunction;
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
@@ -585,6 +587,12 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
// Snap to the token if it's only half visible due to mouse wheel scrolling.
|
||||
_shouldScrollTo = aToken;
|
||||
}
|
||||
|
||||
// this is a hack to compensate for a recent adoption in FR management as introduced by commit #26aab29
|
||||
// this PR makes the tokenfield loose FR status prematurely, so we have to regain here in order to make deleteForward: and friends work
|
||||
setTimeout(function(){
|
||||
[[self window] makeFirstResponder:self];
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ===========
|
||||
@@ -616,16 +624,12 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
[objectValue addObject:[token representedObject]];
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
|
||||
if ([self _editorValue])
|
||||
if (CPDOMAvailable && [self _editorValue])
|
||||
{
|
||||
var token = [self _representedObjectForEditingString:[self _editorValue]];
|
||||
[objectValue insertObject:token atIndex:_selectedRange.location];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return objectValue;
|
||||
}
|
||||
|
||||
@@ -741,9 +745,10 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
// =============
|
||||
// = TEXTFIELD =
|
||||
// =============
|
||||
#if PLATFORM(DOM)
|
||||
- (DOMElement)_inputElement
|
||||
{
|
||||
if (!CPDOMAvailable) return nil;
|
||||
|
||||
if (!CPTokenFieldDOMInputElement)
|
||||
{
|
||||
CPTokenFieldDOMInputElement = document.createElement("input");
|
||||
@@ -784,7 +789,6 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
return CPTokenFieldDOMInputElement;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (CPString)_editorValue
|
||||
{
|
||||
@@ -965,8 +969,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
[self _selectToken:tokenView byExtendingSelection:NO];
|
||||
}
|
||||
}
|
||||
else
|
||||
[self _removeSelectedTokens:nil];
|
||||
// we have to remove unconditionally because the backspace is not propagated anymore starting from commit #26aab29
|
||||
[self _removeSelectedTokens:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1022,9 +1026,20 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
- (void)keyDown:(CPEvent)anEvent
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
CPTokenFieldTextDidChangeValue = [self stringValue];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
CPTokenFieldTextDidChangeValue = [self stringValue];
|
||||
}
|
||||
|
||||
// Has to be enabled, and it also has to be editable or selectable.
|
||||
if (![self isEnabled] || !([self isEditable] || [self isSelectable]))
|
||||
return;
|
||||
|
||||
if ([self isEditable] && !_isEditing)
|
||||
{
|
||||
_isEditing = YES;
|
||||
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
}
|
||||
|
||||
// Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending
|
||||
// on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload.
|
||||
@@ -1038,12 +1053,10 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
- (void)keyUp:(CPEvent)anEvent
|
||||
{
|
||||
#if PLATFORM(DOM)
|
||||
if ([self stringValue] !== CPTokenFieldTextDidChangeValue)
|
||||
if (CPDOMAvailable && [self stringValue] !== CPTokenFieldTextDidChangeValue)
|
||||
{
|
||||
[self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
|
||||
}
|
||||
#endif
|
||||
|
||||
[[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
|
||||
}
|
||||
|
||||
@@ -1014,12 +1014,29 @@ var LABEL_MARGIN = 2.0;
|
||||
|
||||
CPImageView _imageView;
|
||||
CPView _view;
|
||||
CPView _highlightView;
|
||||
|
||||
CPTextField _labelField;
|
||||
|
||||
BOOL _FIXME_isHUD;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
if (_view && [_view acceptsFirstResponder])
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
if (_view && [_view acceptsFirstResponder])
|
||||
return [[self window] makeFirstResponder:_view];
|
||||
|
||||
return [super becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (id)initWithToolbarItem:(CPToolbarItem)aToolbarItem toolbar:(CPToolbar)aToolbar
|
||||
{
|
||||
self = [super init];
|
||||
@@ -1243,6 +1260,18 @@ var LABEL_MARGIN = 2.0;
|
||||
|
||||
if (alternateImage)
|
||||
[_imageView setImage:alternateImage];
|
||||
else
|
||||
{
|
||||
if (!_highlightView)
|
||||
{
|
||||
_highlightView = [[CPView alloc] initWithFrame:[_imageView bounds]];
|
||||
[_highlightView setBackgroundColor:[CPColor blackColor]];
|
||||
[_highlightView setAlphaValue:0.3];
|
||||
[_highlightView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
}
|
||||
|
||||
[_imageView addSubview:_highlightView];
|
||||
}
|
||||
|
||||
[_labelField setTextShadowOffset:CGSizeMakeZero()];
|
||||
}
|
||||
@@ -1253,6 +1282,8 @@ var LABEL_MARGIN = 2.0;
|
||||
if (image)
|
||||
[_imageView setImage:image];
|
||||
|
||||
[_highlightView removeFromSuperview];
|
||||
|
||||
[_labelField setTextShadowOffset:CGSizeMake(0.0, 1.0)];
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ CPTrackingOwnerImplementsCursorUpdate = 1 << 4;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*!
|
||||
Initializes and returns an object defining a region of a view to receive mouse-tracking events, mouse-moved events, cursor-update events, or possibly
|
||||
@@ -121,8 +121,8 @@ CPTrackingOwnerImplementsCursorUpdate = 1 << 4;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Implementation
|
||||
// MARK: -
|
||||
// MARK: Implementation
|
||||
|
||||
- (void)_updateWindowRect
|
||||
{
|
||||
@@ -140,8 +140,8 @@ CPTrackingOwnerImplementsCursorUpdate = 1 << 4;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CPCoding
|
||||
// MARK: -
|
||||
// MARK: CPCoding
|
||||
|
||||
@implementation CPTrackingArea (CPCoding)
|
||||
|
||||
|
||||
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
* CPTreeController.j
|
||||
* AppKit
|
||||
*
|
||||
* Daniel Boehringer Mar/2026
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
@import "CPObjectController.j"
|
||||
@import "CPKeyValueBinding.j"
|
||||
@import "CPTreeNode.j"
|
||||
|
||||
@implementation CPTreeController : CPObjectController
|
||||
{
|
||||
BOOL _avoidsEmptySelection;
|
||||
BOOL _preservesSelection;
|
||||
BOOL _selectsInsertedObjects;
|
||||
BOOL _alwaysUsesMultipleValuesMarker;
|
||||
|
||||
CPString _childrenKeyPath;
|
||||
CPString _countKeyPath;
|
||||
CPString _leafKeyPath;
|
||||
|
||||
CPArray _sortDescriptors;
|
||||
id _arrangedObjects;
|
||||
|
||||
CPArray _selectionIndexPaths;
|
||||
BOOL _disableSetContent;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self !== [CPTreeController class])
|
||||
return;
|
||||
|
||||
[self exposeBinding:@"contentArray"];
|
||||
[self exposeBinding:@"sortDescriptors"];
|
||||
[self exposeBinding:@"selectionIndexPaths"];
|
||||
[self exposeBinding:@"selectionIndexPath"];
|
||||
[self exposeBinding:@"selectedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingContentArray
|
||||
{
|
||||
return [CPSet setWithObjects:@"content"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"content", @"contentArray", @"sortDescriptors", @"childrenKeyPath"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsert
|
||||
{
|
||||
return [CPSet setWithObjects:@"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_preservesSelection = YES;
|
||||
_selectsInsertedObjects = YES;
|
||||
_avoidsEmptySelection = YES;
|
||||
_alwaysUsesMultipleValuesMarker = NO;
|
||||
_childrenKeyPath = @"children";
|
||||
|
||||
[self _init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_init
|
||||
{
|
||||
_sortDescriptors = [CPArray array];
|
||||
_selectionIndexPaths = [CPArray array];
|
||||
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
|
||||
}
|
||||
|
||||
- (void)prepareContent
|
||||
{
|
||||
[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
}
|
||||
|
||||
- (BOOL)preservesSelection { return _preservesSelection; }
|
||||
- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; }
|
||||
|
||||
- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; }
|
||||
- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; }
|
||||
|
||||
- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; }
|
||||
- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; }
|
||||
|
||||
- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; }
|
||||
- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; }
|
||||
|
||||
- (CPArray)sortDescriptors { return _sortDescriptors; }
|
||||
- (void)setSortDescriptors:(CPArray)value
|
||||
{
|
||||
if (_sortDescriptors === value)
|
||||
return;
|
||||
|
||||
_sortDescriptors = [value copy];
|
||||
[self _rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)childrenKeyPath { return _childrenKeyPath; }
|
||||
- (void)setChildrenKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (_childrenKeyPath === aKeyPath) return;
|
||||
_childrenKeyPath = aKeyPath;
|
||||
[self rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)countKeyPath { return _countKeyPath; }
|
||||
- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; }
|
||||
|
||||
- (CPString)leafKeyPath { return _leafKeyPath; }
|
||||
- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; }
|
||||
|
||||
- (CPString)childrenKeyPathForNode:(CPTreeNode)node { return [self childrenKeyPath]; }
|
||||
- (CPString)countKeyPathForNode:(CPTreeNode)node { return [self countKeyPath]; }
|
||||
- (CPString)leafKeyPathForNode:(CPTreeNode)node { return [self leafKeyPath]; }
|
||||
|
||||
- (void)setContent:(id)value
|
||||
{
|
||||
if (_disableSetContent) return;
|
||||
|
||||
if (!value)
|
||||
value = [CPArray array];
|
||||
if (![value isKindOfClass:[CPArray class]])
|
||||
value = [CPArray arrayWithObject:value];
|
||||
|
||||
if (_contentObject === value)
|
||||
return;
|
||||
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
[self willChangeValueForKey:@"contentArray"];
|
||||
|
||||
_contentObject = value;
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self preservesSelection])
|
||||
[self __setSelectedObjects:oldSelectedObjects];
|
||||
else
|
||||
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
|
||||
[self didChangeValueForKey:@"contentArray"];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
|
||||
- (void)_setContentArray:(id)anArray { [self setContent:anArray]; }
|
||||
- (id)contentArray { return _contentObject; }
|
||||
- (id)arrangedObjects { return _arrangedObjects; }
|
||||
|
||||
- (void)rearrangeObjects
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"arrangedObjects"];
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
[self didChangeValueForKey:@"arrangedObjects"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)_rearrangeObjects
|
||||
{
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
[self __rebuildArrangedObjectsTree];
|
||||
|
||||
if ([self preservesSelection])
|
||||
[self __setSelectedObjects:oldSelectedObjects];
|
||||
else
|
||||
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
}
|
||||
|
||||
- (void)__rebuildArrangedObjectsTree
|
||||
{
|
||||
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
|
||||
contentArray = [self contentArray];
|
||||
|
||||
if (contentArray && [contentArray count] > 0)
|
||||
{
|
||||
var children = [self _buildTreeNodesForObjects:contentArray];
|
||||
[[rootNode mutableChildNodes] addObjectsFromArray:children];
|
||||
}
|
||||
|
||||
_arrangedObjects = rootNode;
|
||||
}
|
||||
|
||||
- (CPArray)_buildTreeNodesForObjects:(CPArray)objects
|
||||
{
|
||||
var count = [objects count];
|
||||
|
||||
if (count === 0)
|
||||
return [];
|
||||
|
||||
var sortedObjects = objects;
|
||||
|
||||
if (_sortDescriptors && [_sortDescriptors count] > 0)
|
||||
sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors];
|
||||
|
||||
var nodes = [CPMutableArray arrayWithCapacity:count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var obj = [sortedObjects objectAtIndex:i],
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
|
||||
if (_childrenKeyPath)
|
||||
{
|
||||
var childObjects = [obj valueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (childObjects && [childObjects count] > 0)
|
||||
{
|
||||
var childNodes = [self _buildTreeNodesForObjects:childObjects];
|
||||
[[node mutableChildNodes] addObjectsFromArray:childNodes];
|
||||
}
|
||||
}
|
||||
|
||||
[nodes addObject:node];
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
- (CPIndexPath)selectionIndexPath
|
||||
{
|
||||
return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil;
|
||||
}
|
||||
|
||||
- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array];
|
||||
return [self setSelectionIndexPaths:paths];
|
||||
}
|
||||
|
||||
- (CPArray)selectionIndexPaths { return _selectionIndexPaths; }
|
||||
|
||||
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
return [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
}
|
||||
|
||||
- (void)_ensureTreeNodesExistForIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
if (!_childrenKeyPath)
|
||||
return;
|
||||
|
||||
var count = [indexPaths count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var path = [indexPaths objectAtIndex:i];
|
||||
var length = [path length];
|
||||
var currentNode = [self arrangedObjects];
|
||||
|
||||
for (var j = 0; j < length; j++)
|
||||
{
|
||||
var index = [path indexAtPosition:j];
|
||||
|
||||
var obj = [currentNode representedObject];
|
||||
var expectedChildObjects = nil;
|
||||
|
||||
if (!obj && currentNode === [self arrangedObjects])
|
||||
expectedChildObjects = [self contentArray];
|
||||
else if (obj)
|
||||
expectedChildObjects = [obj valueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (expectedChildObjects)
|
||||
{
|
||||
var childNodes = [currentNode mutableChildNodes];
|
||||
var needsRebuild = NO;
|
||||
|
||||
if (!childNodes || [childNodes count] !== [expectedChildObjects count])
|
||||
{
|
||||
needsRebuild = YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var k = 0, kCount = [childNodes count]; k < kCount; k++)
|
||||
{
|
||||
if ([[childNodes objectAtIndex:k] representedObject] !== [expectedChildObjects objectAtIndex:k])
|
||||
{
|
||||
needsRebuild = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRebuild)
|
||||
{
|
||||
[childNodes removeAllObjects];
|
||||
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];
|
||||
[childNodes addObjectsFromArray:newNodes];
|
||||
}
|
||||
}
|
||||
|
||||
var children = [currentNode childNodes];
|
||||
if (children && index < [children count])
|
||||
currentNode = [children objectAtIndex:index];
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)__setSelectionIndexPaths:(CPArray)indexPaths avoidEmpty:(BOOL)avoidEmpty
|
||||
{
|
||||
var newPaths = indexPaths;
|
||||
|
||||
if (!newPaths)
|
||||
newPaths = [CPArray array];
|
||||
|
||||
if ([newPaths count] > 0)
|
||||
[self _ensureTreeNodesExistForIndexPaths:newPaths];
|
||||
|
||||
if (![newPaths count] && avoidEmpty)
|
||||
{
|
||||
if ([[[self arrangedObjects] childNodes] count] > 0)
|
||||
newPaths = [CPArray arrayWithObject:[CPIndexPath indexPathWithIndex:0]];
|
||||
}
|
||||
|
||||
if ([_selectionIndexPaths isEqualToArray:newPaths])
|
||||
return NO;
|
||||
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"selectionIndexPaths"];
|
||||
|
||||
_selectionIndexPaths = [newPaths copy];
|
||||
|
||||
[self didChangeValueForKey:@"selectionIndexPaths"];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
var newPaths = [_selectionIndexPaths mutableCopy];
|
||||
[newPaths addObjectsFromArray:indexPaths];
|
||||
return [self setSelectionIndexPaths:newPaths];
|
||||
}
|
||||
|
||||
- (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
var newPaths = [_selectionIndexPaths mutableCopy];
|
||||
[newPaths removeObjectsInArray:indexPaths];
|
||||
return [self setSelectionIndexPaths:newPaths];
|
||||
}
|
||||
|
||||
- (CPArray)selectedNodes
|
||||
{
|
||||
var nodes = [CPMutableArray array],
|
||||
count = [_selectionIndexPaths count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var node = [[self arrangedObjects] descendantNodeAtIndexPath:[_selectionIndexPaths objectAtIndex:i]];
|
||||
if (node)
|
||||
[nodes addObject:node];
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
- (CPArray)selectedObjects
|
||||
{
|
||||
var objects = [CPMutableArray array],
|
||||
nodes = [self selectedNodes],
|
||||
count = [nodes count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var representedObject = [[nodes objectAtIndex:i] representedObject];
|
||||
if (representedObject)
|
||||
[objects addObject:representedObject];
|
||||
}
|
||||
|
||||
return [_CPObservableArray arrayWithArray:objects];
|
||||
}
|
||||
|
||||
- (BOOL)__setSelectedObjects:(CPArray)objects
|
||||
{
|
||||
if (!objects || [objects count] === 0)
|
||||
return [self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection];
|
||||
|
||||
var newPaths = [CPMutableArray array];
|
||||
for (var i = 0, count = [objects count]; i < count; i++)
|
||||
{
|
||||
var path = [self _indexPathForObject:[objects objectAtIndex:i] inNode:[self arrangedObjects]];
|
||||
if (path)
|
||||
[newPaths addObject:path];
|
||||
}
|
||||
|
||||
return [self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection];
|
||||
}
|
||||
|
||||
- (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node
|
||||
{
|
||||
if ([node representedObject] === anObject && [node parentNode] != nil)
|
||||
return [node indexPath];
|
||||
|
||||
var children = [node childNodes];
|
||||
if (children)
|
||||
{
|
||||
for (var i = 0, count = [children count]; i < count; i++)
|
||||
{
|
||||
var found = [self _indexPathForObject:anObject inNode:[children objectAtIndex:i]];
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)canInsert { return [self isEditable]; }
|
||||
- (BOOL)canInsertChild { return [self isEditable] && [_selectionIndexPaths count] > 0; }
|
||||
- (BOOL)canAddChild { return [self canInsertChild]; }
|
||||
|
||||
- (void)add:(id)sender
|
||||
{
|
||||
if (![self canInsert]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
selectionPath = [self selectionIndexPath];
|
||||
|
||||
if (!selectionPath)
|
||||
selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]];
|
||||
|
||||
var length = [selectionPath length],
|
||||
lastIndex = [selectionPath indexAtPosition:length - 1],
|
||||
insertPath = [selectionPath indexPathByRemovingLastIndex];
|
||||
|
||||
insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
|
||||
- (void)addChild:(id)sender
|
||||
{
|
||||
if (![self canAddChild])
|
||||
return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]],
|
||||
childCount = [[parentNode childNodes] count],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
|
||||
- (void)insert:(id)sender
|
||||
{
|
||||
if (![self canInsert]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:indexPath];
|
||||
}
|
||||
|
||||
- (void)insertChild:(id)sender
|
||||
{
|
||||
if (![self canInsertChild]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
|
||||
- (void)insertObject:(id)anObject atArrangedObjectIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
[self insertObjects:[CPArray arrayWithObject:anObject] atArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
|
||||
}
|
||||
|
||||
- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
var count = [objects count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var object = [objects objectAtIndex:i],
|
||||
path = [indexPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{
|
||||
[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1];
|
||||
|
||||
var children = [parentObj valueForKeyPath:_childrenKeyPath];
|
||||
if (!children)
|
||||
{
|
||||
children = [CPMutableArray array];
|
||||
[parentObj setValue:children forKeyPath:_childrenKeyPath];
|
||||
}
|
||||
|
||||
var mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
|
||||
[mutableChildren insertObject:object atIndex:childIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var binding = [[self class] _binderClassForBinding:@"contentArray"];
|
||||
if (binding)
|
||||
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
|
||||
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self selectsInsertedObjects])
|
||||
[self setSelectionIndexPaths:indexPaths];
|
||||
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)remove:(id)sender
|
||||
{
|
||||
[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths];
|
||||
}
|
||||
|
||||
- (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
|
||||
}
|
||||
|
||||
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
|
||||
count = [sortedPaths count];
|
||||
|
||||
for (var i = count - 1; i >= 0; i--)
|
||||
{
|
||||
var path = [sortedPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{
|
||||
[_contentObject removeObjectAtIndex:[path indexAtPosition:0]];
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1],
|
||||
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (mutableChildren && childIndex < [mutableChildren count])
|
||||
[mutableChildren removeObjectAtIndex:childIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var binding = [[self class] _binderClassForBinding:@"contentArray"];
|
||||
if (binding)
|
||||
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
|
||||
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
[self moveNodes:[CPArray arrayWithObject:node] toIndexPath:indexPath];
|
||||
}
|
||||
|
||||
- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
|
||||
{
|
||||
[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTreeControllerAvoidsEmptySelection = @"CPTreeControllerAvoidsEmptySelection",
|
||||
CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
|
||||
CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
|
||||
CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
|
||||
CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
|
||||
CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
|
||||
CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
|
||||
|
||||
@implementation CPTreeController (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super initWithCoder:aCoder];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_avoidsEmptySelection = [aCoder decodeBoolForKey:CPTreeControllerAvoidsEmptySelection];
|
||||
_preservesSelection = [aCoder decodeBoolForKey:CPTreeControllerPreservesSelection];
|
||||
_selectsInsertedObjects = [aCoder decodeBoolForKey:CPTreeControllerSelectsInsertedObjects];
|
||||
_alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
|
||||
|
||||
_childrenKeyPath = [aCoder decodeObjectForKey:CPTreeControllerChildrenKeyPath] || @"children";
|
||||
_countKeyPath = [aCoder decodeObjectForKey:CPTreeControllerCountKeyPath];
|
||||
_leafKeyPath = [aCoder decodeObjectForKey:CPTreeControllerLeafKeyPath];
|
||||
|
||||
_sortDescriptors = [CPArray array];
|
||||
_selectionIndexPaths = [CPArray array];
|
||||
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
|
||||
|
||||
if (![self content] && [self automaticallyPreparesContent])
|
||||
[self prepareContent];
|
||||
else if (![self content])
|
||||
[self _setContentArray:[CPArray array]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[super encodeWithCoder:aCoder];
|
||||
|
||||
[aCoder encodeBool:_avoidsEmptySelection forKey:CPTreeControllerAvoidsEmptySelection];
|
||||
[aCoder encodeBool:_preservesSelection forKey:CPTreeControllerPreservesSelection];
|
||||
[aCoder encodeBool:_selectsInsertedObjects forKey:CPTreeControllerSelectsInsertedObjects];
|
||||
[aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
|
||||
[aCoder encodeObject:_childrenKeyPath forKey:CPTreeControllerChildrenKeyPath];
|
||||
[aCoder encodeObject:_countKeyPath forKey:CPTreeControllerCountKeyPath];
|
||||
[aCoder encodeObject:_leafKeyPath forKey:CPTreeControllerLeafKeyPath];
|
||||
}
|
||||
|
||||
- (void)awakeFromCib
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
@end
|
||||
Loaded 100 of 1299 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user