mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-09 12:17:13 +00:00
Compare commits
513
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf36f12cb | ||
|
|
fa79df4309 | ||
|
|
87c1727ea0 | ||
|
|
1a9a42ff54 | ||
|
|
5592056351 | ||
|
|
98bfd87446 | ||
|
|
3f63be7290 | ||
|
|
b23d0cb8eb | ||
|
|
0da009d1b9 | ||
|
|
b1fade86fe | ||
|
|
04bad947e3 | ||
|
|
f2cdb08e76 | ||
|
|
da3b0b5e21 | ||
|
|
a240d312d0 | ||
|
|
bd48e2055e | ||
|
|
42c212df64 | ||
|
|
59e811328f | ||
|
|
317c1a1475 | ||
|
|
a93e67662b | ||
|
|
14379b1be9 | ||
|
|
2e8767509e | ||
|
|
429dfd7467 | ||
|
|
71a7b54edd | ||
|
|
ad05fbe97b | ||
|
|
0fed962b37 | ||
|
|
ee36796180 | ||
|
|
535315485a | ||
|
|
88146a5389 | ||
|
|
c8789c5450 | ||
|
|
766035f4b7 | ||
|
|
a90b504dc7 | ||
|
|
26b2280fc2 | ||
|
|
15bbca8d77 | ||
|
|
319bcdaba9 | ||
|
|
89445f94a2 | ||
|
|
beb4aef424 | ||
|
|
ef680c080d | ||
|
|
c187f5de1d | ||
|
|
d40a151e05 | ||
|
|
e67c141921 | ||
|
|
de2ffd3aad | ||
|
|
4c8fd58471 | ||
|
|
873a102872 | ||
|
|
6dc11de431 | ||
|
|
672f25ce27 | ||
|
|
9731c4b394 | ||
|
|
bc0380e1cb | ||
|
|
1223cfc965 | ||
|
|
361a4203da | ||
|
|
8de928d905 | ||
|
|
5a3d6e30cb | ||
|
|
50bac0a470 | ||
|
|
cfff603508 | ||
|
|
894a60ad45 | ||
|
|
acbe5885f1 | ||
|
|
a981b8b04d | ||
|
|
d75c0ac452 | ||
|
|
da061aac1d | ||
|
|
77433a3a1c | ||
|
|
4aaa32833a | ||
|
|
2a0cebb851 | ||
|
|
1b23bb8a67 | ||
|
|
5dc66c5b4e | ||
|
|
49b1800027 | ||
|
|
1cad83aba8 | ||
|
|
e1f5238063 | ||
|
|
5897d76c04 | ||
|
|
a0fc7723fc | ||
|
|
ec4a1d1724 | ||
|
|
861c504c41 | ||
|
|
c9df6e4d25 | ||
|
|
c9d3ccdf0d | ||
|
|
d88f2ef7c0 | ||
|
|
14b5a67bf5 | ||
|
|
210133d9e2 | ||
|
|
524b1df4c9 | ||
|
|
9115949169 | ||
|
|
0b3aa6aac2 | ||
|
|
e03e0c6fc4 | ||
|
|
613a2722be | ||
|
|
e9b9d9702e | ||
|
|
750c123d1e | ||
|
|
5e0fc1cbb5 | ||
|
|
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 | ||
|
|
c0b875de5d | ||
|
|
141eb7b98e | ||
|
|
6099311cbd | ||
|
|
dcb06fbc4f | ||
|
|
8258c8ef0b | ||
|
|
9312c96154 | ||
|
|
c27b48d27a | ||
|
|
f3039fb77c | ||
|
|
abe159eceb | ||
|
|
ffb6b3e6fe | ||
|
|
7f25de0fad | ||
|
|
d6c5d8b4f2 | ||
|
|
78e1ea43c8 | ||
|
|
9d24c855e2 | ||
|
|
872af9ce2a | ||
|
|
18b2a668c5 | ||
|
|
72be296713 | ||
|
|
b620991770 | ||
|
|
732fc2d1e1 | ||
|
|
79a9ae31d3 | ||
|
|
e0e16eeca5 | ||
|
|
a784435c2b | ||
|
|
57987b6f67 | ||
|
|
480917eeab | ||
|
|
013e7bdf7e | ||
|
|
c4032ecd1d | ||
|
|
2ea11eb5ab | ||
|
|
7fad67484c | ||
|
|
f34cbbdf29 | ||
|
|
1c0cc2acef | ||
|
|
14a4b04d9b | ||
|
|
8aee2a511f | ||
|
|
65cb048b1b | ||
|
|
cf10b826d7 | ||
|
|
b7ac5272d0 | ||
|
|
dc893f7ca3 | ||
|
|
206b1d4a05 | ||
|
|
0bd83b6635 | ||
|
|
4dcfe992de | ||
|
|
ef9d6662cb | ||
|
|
0e5f3b1697 | ||
|
|
7fbea7913a | ||
|
|
4b81001330 | ||
|
|
043029cf41 | ||
|
|
81988f1ec7 | ||
|
|
9ecc739e8d | ||
|
|
3d5f4eec18 | ||
|
|
c7e89d3018 | ||
|
|
63e3f6c3fc | ||
|
|
d39a583127 | ||
|
|
f568427dc2 | ||
|
|
6e486bb30b | ||
|
|
5e297dd293 | ||
|
|
63cbb04acc | ||
|
|
6f55c7bbba | ||
|
|
ea4adf1039 | ||
|
|
3ff7818d58 | ||
|
|
3e4ebe8e47 | ||
|
|
b77a374f48 | ||
|
|
43936f640c | ||
|
|
6671d371e9 | ||
|
|
4fd99d2c52 | ||
|
|
6563e730a0 | ||
|
|
b39c3e8d4b | ||
|
|
9f092a3c2e | ||
|
|
5b820b270c | ||
|
|
da2c89daf3 | ||
|
|
ea425ae4c4 | ||
|
|
37a6e8fde5 | ||
|
|
0274d9fee7 | ||
|
|
f223644640 | ||
|
|
0aa4607d8e | ||
|
|
37b68bb166 | ||
|
|
ae4eef5d16 | ||
|
|
fdba5f55c5 | ||
|
|
bfeaabc038 | ||
|
|
a32b2d5f73 | ||
|
|
9f4db09bec | ||
|
|
58fe3d3d27 | ||
|
|
28303a7893 | ||
|
|
031e5e7ca3 | ||
|
|
c70079ae71 | ||
|
|
8ac2b94400 | ||
|
|
b1f2fb44a8 | ||
|
|
cd4c3cffe0 | ||
|
|
98c01b88e2 | ||
|
|
4970a2742d | ||
|
|
10d18e35ed | ||
|
|
12960f8160 | ||
|
|
622d738b44 | ||
|
|
4e5c0aed67 | ||
|
|
96d3ad2c47 | ||
|
|
c519784ecc | ||
|
|
d0d7033f63 | ||
|
|
9aaa565657 | ||
|
|
50e98d1335 | ||
|
|
837df80b64 | ||
|
|
f7a27cf905 | ||
|
|
da35d6b6ed | ||
|
|
fca30325e4 | ||
|
|
8f48d01c33 | ||
|
|
89d345dfdc | ||
|
|
1bbf87e2cb | ||
|
|
ec9c658565 | ||
|
|
8db24fcaf6 | ||
|
|
bce326de9d | ||
|
|
676ddc4601 | ||
|
|
19cceb4dd9 | ||
|
|
f31ac7cf21 | ||
|
|
802c644d94 | ||
|
|
749909b23c | ||
|
|
df594ae48e | ||
|
|
980e2f2c91 | ||
|
|
ea47767bb8 | ||
|
|
0b83c20648 | ||
|
|
0f9cadf8d3 | ||
|
|
c82e5750f0 | ||
|
|
2ff5eb39eb | ||
|
|
e216f0a355 | ||
|
|
0e86264837 | ||
|
|
57d0750f3c | ||
|
|
3be864e50f | ||
|
|
d583403195 | ||
|
|
946362f0d1 | ||
|
|
7ee4003840 | ||
|
|
f2bebcb2e6 | ||
|
|
e413f3a9fd | ||
|
|
0c4d33dccc | ||
|
|
4af8fcc008 | ||
|
|
6800472725 | ||
|
|
e6b9b9cfbd | ||
|
|
9dfaf293f2 | ||
|
|
fffcc85486 | ||
|
|
b349b23b81 | ||
|
|
886a4a422f | ||
|
|
d8dd3d0660 | ||
|
|
f19901c214 | ||
|
|
e198fbdc40 | ||
|
|
3dd97c0b49 | ||
|
|
b93aa732d2 | ||
|
|
16d8780812 | ||
|
|
febd1260cd | ||
|
|
45162a3fcb | ||
|
|
b598e80c58 | ||
|
|
41648b8605 | ||
|
|
6b285cd7f0 | ||
|
|
a5e9d65852 | ||
|
|
94195237d9 | ||
|
|
b8f63166cd | ||
|
|
fc91326a5f | ||
|
|
0fc2ed7081 | ||
|
|
8433d4a1d1 | ||
|
|
2615577173 | ||
|
|
5aa315e190 | ||
|
|
d180a25f2e | ||
|
|
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 | ||
|
|
668567d6e2 | ||
|
|
09f92407ec | ||
|
|
9dc12c0142 | ||
|
|
2fd821ca00 | ||
|
|
a3d906165a | ||
|
|
4c1ce50dd2 | ||
|
|
b638242e6f | ||
|
|
966d9fe0d2 | ||
|
|
88c1846e1a | ||
|
|
be3d559d8a | ||
|
|
c528421a8e | ||
|
|
0cfb53cea4 | ||
|
|
b349f0a2fb |
@@ -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
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [17.x, 18.x, 19.x, 20.x, 21.x]
|
||||
node-version: [20.x, 21.x, 22.x, 23.x, 24.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
});
|
||||
+1
-1
@@ -25,4 +25,4 @@ node_modules
|
||||
/dist/objective-j/lib
|
||||
/dist/cappuccino/package.json
|
||||
/dist/cappuccino/lib
|
||||
|
||||
/dist/cappuccino/bin
|
||||
|
||||
@@ -115,3 +115,5 @@
|
||||
@import "CPWindow.j"
|
||||
@import "CPWindowController.j"
|
||||
@import "CPWorkspace.j"
|
||||
@import "CPFontPanel.j"
|
||||
@import "CPTreeController.j"
|
||||
|
||||
+8
-8
@@ -172,7 +172,7 @@ var bottomHeight = 71;
|
||||
BOOL _needsLayout;
|
||||
}
|
||||
|
||||
#pragma mark Creating Alerts
|
||||
// MARK: Creating Alerts
|
||||
|
||||
/*!
|
||||
Returns a CPAlert object with the provided info
|
||||
@@ -249,8 +249,8 @@ var bottomHeight = 71;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate
|
||||
// MARK: -
|
||||
// MARK: Delegate
|
||||
|
||||
/*!
|
||||
Set the delegate of the receiver
|
||||
@@ -272,7 +272,7 @@ var bottomHeight = 71;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Accessors
|
||||
// MARK: Accessors
|
||||
|
||||
- (CPTheme)theme
|
||||
{
|
||||
@@ -402,7 +402,7 @@ var bottomHeight = 71;
|
||||
_needsLayout = YES;
|
||||
}
|
||||
|
||||
#pragma mark Accessing Buttons
|
||||
// MARK: Accessing Buttons
|
||||
|
||||
/*!
|
||||
Adds a button with a given title to the receiver.
|
||||
@@ -438,7 +438,7 @@ var bottomHeight = 71;
|
||||
[_buttons insertObject:button atIndex:0];
|
||||
}
|
||||
|
||||
#pragma mark Layout
|
||||
// MARK: Layout
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -664,7 +664,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 +742,7 @@ var bottomHeight = 71;
|
||||
[self beginSheetModalForWindow:aWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
// MARK: Private
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+8
-8
@@ -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,7 +282,7 @@ CPBelowBottom = 6;
|
||||
*/
|
||||
|
||||
// See discussion above.
|
||||
#pragma mark borderColor
|
||||
// MARK: borderColor
|
||||
- (CPColor)borderColor
|
||||
{
|
||||
return [self valueForThemeAttribute:@"border-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)
|
||||
|
||||
@@ -829,7 +829,7 @@ CPBelowBottom = 6;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
CPBoxBorderTypeKey = @"CPBoxBorderTypeKey",
|
||||
|
||||
+3
-3
@@ -220,8 +220,8 @@ CPButtonImageOffset = 3.0;
|
||||
[self setButtonType:CPMomentaryPushInButton];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -232,7 +232,7 @@ CPButtonImageOffset = 3.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
// Setting the state
|
||||
/*!
|
||||
|
||||
+72
-16
@@ -36,6 +36,7 @@
|
||||
BOOL _hasResizeControl;
|
||||
BOOL _resizeControlIsLeftAligned;
|
||||
CPArray _buttons;
|
||||
CPArray _rightButtons;
|
||||
}
|
||||
|
||||
+ (id)plusButton
|
||||
@@ -104,6 +105,7 @@
|
||||
if (self)
|
||||
{
|
||||
_buttons = [];
|
||||
_rightButtons = [];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
@@ -157,6 +159,30 @@
|
||||
return [CPArray arrayWithArray:_buttons];
|
||||
}
|
||||
|
||||
- (void)setRightButtons:(CPArray)buttons
|
||||
{
|
||||
for (var i = [_rightButtons count] - 1; i >= 0; i--)
|
||||
{
|
||||
[_rightButtons[i] removeFromSuperview];
|
||||
[_rightButtons[i] removeObserver:self forKeyPath:@"hidden"];
|
||||
}
|
||||
|
||||
_rightButtons = [CPArray arrayWithArray:buttons];
|
||||
|
||||
for (var i = [_rightButtons count] - 1; i >= 0; i--)
|
||||
{
|
||||
[_rightButtons[i] addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
|
||||
[_rightButtons[i] setBordered:YES];
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (CPArray)rightButtons
|
||||
{
|
||||
return [CPArray arrayWithArray:_rightButtons];
|
||||
}
|
||||
|
||||
- (void)setHasResizeControl:(BOOL)shouldHaveResizeControl
|
||||
{
|
||||
if (_hasResizeControl === shouldHaveResizeControl)
|
||||
@@ -243,46 +269,73 @@
|
||||
}
|
||||
}
|
||||
|
||||
var currentButtonOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX([self bounds]) + 1 : -1,
|
||||
bounds = [self bounds],
|
||||
var rightButtonsNotHidden = [CPArray arrayWithArray:_rightButtons],
|
||||
rightCount = [rightButtonsNotHidden count];
|
||||
|
||||
while (rightCount--)
|
||||
{
|
||||
var button = rightButtonsNotHidden[rightCount];
|
||||
|
||||
if ([button isHidden])
|
||||
{
|
||||
[button removeFromSuperview];
|
||||
[rightButtonsNotHidden removeObject:button];
|
||||
}
|
||||
}
|
||||
|
||||
var bounds = [self bounds],
|
||||
height = CGRectGetHeight(bounds) - 1,
|
||||
frameWidth = CGRectGetWidth(bounds),
|
||||
resizeRect = _hasResizeControl ? [self rectForEphemeralSubviewNamed:"resize-control-view"] : CGRectMakeZero(),
|
||||
resizeWidth = CGRectGetWidth(resizeRect),
|
||||
availableWidth = frameWidth - resizeWidth - 1;
|
||||
|
||||
for (var i = 0, count = [buttonsNotHidden count]; i < count; i++)
|
||||
var currentLeftOffset = _resizeControlIsLeftAligned ? resizeWidth - 1 : -1,
|
||||
currentRightOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX(bounds) + 1 : CGRectGetMaxX(bounds) - resizeWidth + 1;
|
||||
|
||||
var setupButton = function(button, isRightAligned)
|
||||
{
|
||||
var button = buttonsNotHidden[i],
|
||||
width = CGRectGetWidth([button frame]);
|
||||
var width = CGRectGetWidth([button frame]);
|
||||
|
||||
if (availableWidth > width)
|
||||
availableWidth -= width;
|
||||
else
|
||||
break;
|
||||
return NO;
|
||||
|
||||
if (_resizeControlIsLeftAligned)
|
||||
if (isRightAligned)
|
||||
{
|
||||
[button setFrame:CGRectMake(currentButtonOffset - width, 1, width, height)];
|
||||
currentButtonOffset -= width - 1;
|
||||
[button setFrame:CGRectMake(currentRightOffset - width, 1, width, height)];
|
||||
currentRightOffset -= width - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
[button setFrame:CGRectMake(currentButtonOffset, 1, width, height)];
|
||||
currentButtonOffset += width - 1;
|
||||
{[button setFrame:CGRectMake(currentLeftOffset, 1, width, height)];
|
||||
currentLeftOffset += width - 1;
|
||||
}
|
||||
|
||||
[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered]];
|
||||
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, ]];
|
||||
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered]];
|
||||
[button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered]];
|
||||
[button setValue:textColor forThemeAttribute:@"text-color" inState:CPThemeStateBordered];
|
||||
|
||||
// FIXME shouldn't need this
|
||||
[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
|
||||
// FIXME shouldn't need this[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
|
||||
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
|
||||
[button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
|
||||
|
||||
[self addSubview:button];
|
||||
|
||||
return YES;
|
||||
};
|
||||
|
||||
for (var i = 0, count = [buttonsNotHidden count]; i < count; i++)
|
||||
{
|
||||
if (!setupButton(buttonsNotHidden[i], _resizeControlIsLeftAligned))
|
||||
break;
|
||||
}
|
||||
|
||||
for (var i = 0, count = [rightButtonsNotHidden count]; i < count; i++)
|
||||
{
|
||||
if (!setupButton(rightButtonsNotHidden[i], YES))
|
||||
break;
|
||||
}
|
||||
|
||||
if (_hasResizeControl)
|
||||
@@ -314,7 +367,8 @@
|
||||
|
||||
var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
|
||||
CPButtonBarResizeControlIsLeftAlignedKey = @"CPButtonBarResizeControlIsLeftAlignedKey",
|
||||
CPButtonBarButtonsKey = @"CPButtonBarButtonsKey";
|
||||
CPButtonBarButtonsKey = @"CPButtonBarButtonsKey",
|
||||
CPButtonBarRightButtonsKey = @"CPButtonBarRightButtonsKey";
|
||||
|
||||
@implementation CPButtonBar (CPCoding)
|
||||
|
||||
@@ -325,6 +379,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
|
||||
[aCoder encodeBool:_hasResizeControl forKey:CPButtonBarHasResizeControlKey];
|
||||
[aCoder encodeBool:_resizeControlIsLeftAligned forKey:CPButtonBarResizeControlIsLeftAlignedKey];
|
||||
[aCoder encodeObject:_buttons forKey:CPButtonBarButtonsKey];
|
||||
[aCoder encodeObject:_rightButtons forKey:CPButtonBarRightButtonsKey];
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
@@ -332,6 +387,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
|
||||
if (self = [super initWithCoder:aCoder])
|
||||
{
|
||||
_buttons = [aCoder decodeObjectForKey:CPButtonBarButtonsKey] || [];
|
||||
_rightButtons = [aCoder decodeObjectForKey:CPButtonBarRightButtonsKey] || [];
|
||||
_hasResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasResizeControlKey];
|
||||
_resizeControlIsLeftAligned = [aCoder decodeBoolForKey:CPButtonBarResizeControlIsLeftAlignedKey];
|
||||
}
|
||||
|
||||
+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)];
|
||||
|
||||
+7
-7
@@ -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.
|
||||
@@ -884,8 +884,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
|
||||
@@ -1091,7 +1091,7 @@ url("data:image/png;base64,BASE64ENCODEDDATA") // if there is a pattern image
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/// @cond IGNORE
|
||||
var CPColorComponentsKey = @"CPColorComponentsKey",
|
||||
|
||||
+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"
|
||||
|
||||
+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;
|
||||
|
||||
+12
-12
@@ -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.
|
||||
@@ -594,7 +594,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 +646,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[self selectItemAtIndex:index];
|
||||
}
|
||||
|
||||
#pragma mark Completing the Text Field
|
||||
// MARK: Completing the Text Field
|
||||
|
||||
- (BOOL)completes
|
||||
{
|
||||
@@ -693,7 +693,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
|
||||
@@ -893,7 +893,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
[_listDelegate setAlignment:alignment];
|
||||
}
|
||||
|
||||
#pragma mark Pop Up Button Layout
|
||||
// MARK: Pop Up Button Layout
|
||||
|
||||
- (CGRect)popupButtonRectForBounds:(CGRect)bounds
|
||||
{
|
||||
@@ -938,7 +938,7 @@ var CPComboBoxTextSubview = @"text",
|
||||
relativeToEphemeralSubviewNamed:@"content-view"];
|
||||
}
|
||||
|
||||
#pragma mark Internal Helpers
|
||||
// MARK: Internal Helpers
|
||||
|
||||
/*! @ignore */
|
||||
- (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag
|
||||
@@ -1016,8 +1016,8 @@ var CPComboBoxTextSubview = @"text",
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Observers method
|
||||
// MARK: -
|
||||
// MARK: Observers method
|
||||
|
||||
- (void)_addObservers
|
||||
{
|
||||
|
||||
+5
-5
@@ -207,8 +207,8 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
/*!
|
||||
Returns the control's control size
|
||||
@@ -286,7 +286,7 @@ var CPControlBlackColor = [CPColor blackColor];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*!
|
||||
Sets the receiver's target action.
|
||||
@@ -1023,8 +1023,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
|
||||
|
||||
@@ -82,8 +82,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme methods
|
||||
// MARK: -
|
||||
// MARK: Theme methods
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -174,8 +174,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Binding methods
|
||||
// MARK: -
|
||||
// MARK: Binding methods
|
||||
|
||||
+ (Class)_binderClassForBinding:(CPString)theBinding
|
||||
{
|
||||
@@ -200,8 +200,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -271,8 +271,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -285,8 +285,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 +302,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout method
|
||||
// MARK: -
|
||||
// MARK: Layout method
|
||||
|
||||
/*! Layout the subviews
|
||||
*/
|
||||
@@ -313,8 +313,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 +651,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 +689,8 @@ CPEraDatePickerElementFlag = 0x0100;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark getter
|
||||
// MARK: -
|
||||
// MARK: getter
|
||||
|
||||
/*!
|
||||
Returns \c YES if the textfield is bezeled.
|
||||
@@ -723,8 +723,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
|
||||
|
||||
@@ -71,8 +71,8 @@ _CPDatePickerClockSeconds = 3;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame datePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -200,7 +200,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark Layout methods
|
||||
// MARK: Layout methods
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
@@ -246,7 +246,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
// [_middleHandLayer setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark Accessors
|
||||
// MARK: Accessors
|
||||
|
||||
- (void)setEnabled:(BOOL)shouldEnable
|
||||
{
|
||||
@@ -261,7 +261,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark Mouse actions
|
||||
// MARK: Mouse actions
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
@@ -425,7 +425,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation HandLayer : CALayer
|
||||
{
|
||||
@@ -434,7 +434,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
float _rotationRadians;
|
||||
}
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithSize:(CGSize)aSize
|
||||
{
|
||||
@@ -453,7 +453,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 +516,7 @@ _CPDatePickerClockSeconds = 3;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation HandImageLayer : CALayer
|
||||
{
|
||||
@@ -550,7 +550,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
|
||||
*/
|
||||
|
||||
@@ -27,12 +27,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 +185,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 +237,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;
|
||||
@@ -466,8 +481,8 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Mouse event
|
||||
// MARK: -
|
||||
// MARK: Mouse event
|
||||
|
||||
/*! Mouse down event. Launch a notification to notif the new first responder textField
|
||||
*/
|
||||
@@ -481,8 +496,8 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme functions
|
||||
// MARK: -
|
||||
// MARK: Theme functions
|
||||
|
||||
/*! Set the theme CPThemeStateSelected
|
||||
*/
|
||||
@@ -502,8 +517,8 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override
|
||||
// MARK: -
|
||||
// MARK: Override
|
||||
|
||||
/*!
|
||||
We override this method to get all the time the good width
|
||||
@@ -569,7 +584,7 @@ CPAMPMDateType = 6;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPDatePickerElementSeparator : CPTextField
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init
|
||||
// MARK: Init
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame withDatePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -141,8 +141,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Responder methods
|
||||
// MARK: -
|
||||
// MARK: Responder methods
|
||||
|
||||
/*! @ignore */
|
||||
- (BOOL)acceptsFirstResponder
|
||||
@@ -154,8 +154,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override observers
|
||||
// MARK: -
|
||||
// MARK: Override observers
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
@@ -177,8 +177,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 +229,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 +378,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Notification methods
|
||||
// MARK: -
|
||||
// MARK: Notification methods
|
||||
|
||||
/*! Called when changing AM or PM
|
||||
@param aNotification
|
||||
@@ -413,8 +413,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
|
||||
{
|
||||
@@ -886,8 +886,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
|
||||
*/
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init methods
|
||||
// MARK: Init methods
|
||||
|
||||
/*! Init a _CPDatePickerMonthView
|
||||
@param aFrame
|
||||
@@ -77,8 +77,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter Setter methods
|
||||
// MARK: -
|
||||
// MARK: Getter Setter methods
|
||||
|
||||
/*! Set the monthDate of the component
|
||||
@param aDate
|
||||
@@ -194,8 +194,8 @@
|
||||
return tileIndex;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Reload data
|
||||
// MARK: -
|
||||
// MARK: Reload data
|
||||
|
||||
/*! Reload the data
|
||||
*/
|
||||
@@ -235,8 +235,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 +297,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout methods
|
||||
// MARK: -
|
||||
// MARK: Layout methods
|
||||
|
||||
/*! Tile the view
|
||||
*/
|
||||
@@ -438,8 +438,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Mouse event
|
||||
// MARK: -
|
||||
// MARK: Mouse event
|
||||
|
||||
/*! Mouse down event
|
||||
*/
|
||||
@@ -596,8 +596,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Timer
|
||||
// MARK: -
|
||||
// MARK: Timer
|
||||
|
||||
- (void)_timerNextMonthEvent:(CPEvent)anEvent
|
||||
{
|
||||
@@ -618,8 +618,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Date methods
|
||||
// MARK: -
|
||||
// MARK: Date methods
|
||||
|
||||
- (CPDate)_hoursMinutesSecondsFromDatePickerForDate:(CPDate)aDate
|
||||
{
|
||||
|
||||
@@ -48,13 +48,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 +62,7 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Init
|
||||
// MARK: Init
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame withDatePicker:(CPDatePicker)aDatePicker
|
||||
{
|
||||
@@ -110,8 +103,8 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override responder methods
|
||||
// MARK: -
|
||||
// MARK: Override responder methods
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
@@ -131,7 +124,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 +138,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 +188,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 +207,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 +273,8 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Events
|
||||
// MARK: -
|
||||
// MARK: Events
|
||||
|
||||
/*! Called when the user click on the stepper
|
||||
*/
|
||||
@@ -334,23 +344,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 +437,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 +485,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 +523,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 +532,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 +607,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);
|
||||
}
|
||||
|
||||
|
||||
+56
-2
@@ -26,6 +26,7 @@
|
||||
@import "CPPasteboard.j"
|
||||
@import "CPView.j"
|
||||
@import "CPWindow_Constants.j"
|
||||
@import "CPViewAnimation.j"
|
||||
|
||||
@class CPWindow // This file is imported by CPWindow.j
|
||||
@class _CPDOMDataTransferPasteboard
|
||||
@@ -132,6 +133,10 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
|
||||
unsigned _dragOperation;
|
||||
|
||||
CPTimer _draggingUpdateTimer;
|
||||
|
||||
// Animation State
|
||||
CGPoint _pendingEndLocation;
|
||||
CPDragOperation _pendingEndOperation;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -261,10 +266,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 +330,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
-3
@@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
BOOL _isARepeat;
|
||||
unsigned _keyCode;
|
||||
DOMEvent _DOMEvent;
|
||||
BOOL _isActionKey;
|
||||
int _data1;
|
||||
int _data2;
|
||||
short _subtype;
|
||||
@@ -110,17 +111,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 +264,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 +276,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
_charactersIgnoringModifiers = unmodCharacters;
|
||||
_isARepeat = isARepeat;
|
||||
_keyCode = code;
|
||||
_isActionKey = isAnActionKey;
|
||||
_windowNumber = aWindowNumber;
|
||||
}
|
||||
|
||||
@@ -571,6 +584,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
|
||||
|
||||
@@ -464,6 +464,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
|
||||
*/
|
||||
|
||||
@@ -208,7 +208,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;
|
||||
|
||||
|
||||
+45
-6
@@ -31,6 +31,7 @@
|
||||
|
||||
@import "CGGeometry.j"
|
||||
@import "CPCompatibility.j"
|
||||
@import "CPGraphicsContext.j"
|
||||
|
||||
@class CPColor
|
||||
@global document
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ CPStandardKeyBindings = {
|
||||
@"@.": @"cancelOperation:",
|
||||
|
||||
@"@a": @"selectAll:",
|
||||
@"@~$v": @"pasteAsPlainText:",
|
||||
@"^a": @"moveToBeginningOfParagraph:",
|
||||
@"^$a": @"moveToBeginningOfParagraphAndModifySelection:",
|
||||
@"^b": @"moveBackward:",
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
+65
-3
@@ -27,6 +27,7 @@
|
||||
|
||||
@import "CPKeyValueBinding.j"
|
||||
@import "CPMenuItem.j"
|
||||
@import "CALayer.j"
|
||||
|
||||
@global CPApp
|
||||
|
||||
@@ -268,7 +269,8 @@ 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;
|
||||
@@ -373,6 +375,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 +395,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]
|
||||
@@ -1016,6 +1025,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 +1070,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];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
// 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 (characters != "c" && characters != "x" && characters != "v")
|
||||
_CPDOMEventStop(anEvent._DOMEvent);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
//beep?
|
||||
@@ -1067,7 +1096,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 +1182,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 +1272,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 +1292,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
|
||||
@@ -1293,7 +1356,7 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPMenu (CSSTheming)
|
||||
|
||||
@@ -1326,4 +1389,3 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
|
||||
|
||||
@import "_CPMenuBarWindow.j"
|
||||
@import "_CPMenuWindow.j"
|
||||
|
||||
|
||||
@@ -370,12 +370,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 +419,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 +445,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 +485,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,19 @@ _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 && (CGRectGetMaxX(_unconstrainedFrame) > CGRectGetMaxX(_constraintRect)))
|
||||
{
|
||||
var supermenuWindow = supermenu._menuWindow;
|
||||
if (supermenuWindow)
|
||||
{
|
||||
var supermenuFrame = [supermenuWindow frame];
|
||||
_unconstrainedFrame.origin.x = CGRectGetMinX(supermenuFrame) - CGRectGetWidth(_unconstrainedFrame);
|
||||
}
|
||||
}
|
||||
|
||||
var constrainedFrame = CGRectIntersection(_unconstrainedFrame, _constraintRect),
|
||||
marginInset = [_menuView valueForThemeAttribute:@"menu-window-margin-inset"],
|
||||
scrollIndicatorHeight = [_menuView valueForThemeAttribute:@"menu-window-scroll-indicator-height"];
|
||||
@@ -420,7 +433,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuWindow (CSSTheming)
|
||||
|
||||
@@ -433,7 +446,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
/*
|
||||
@ignore
|
||||
@@ -604,7 +617,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
|
||||
@@ -897,7 +897,7 @@ CPControlKeyMask
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPMenuItem (CSSTheming)
|
||||
|
||||
@@ -909,7 +909,7 @@ CPControlKeyMask
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey",
|
||||
|
||||
|
||||
@@ -406,11 +406,11 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemStandardView (CSSTheming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -420,7 +420,7 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemSubmenuIndicatorView : CPView
|
||||
{
|
||||
|
||||
@@ -252,11 +252,11 @@
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation _CPMenuItemView (CSSTheming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -275,7 +275,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
|
||||
|
||||
+448
-91
@@ -127,16 +127,112 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
@protocol CPOutlineViewDataSource <CPObject>
|
||||
|
||||
@optional
|
||||
/*!
|
||||
@abstract Invoked when a drag operation concludes over the outline view.
|
||||
@discussion The data source should incorporate the data from the dragging pasteboard and update its data model.
|
||||
@param anOutlineView The outline view that is the destination of the drop.
|
||||
@param info An object that contains information about the dragging session.
|
||||
@param anItem The item that is the proposed parent for the dropped data. If anItem is nil, the data is to be dropped at the root level.
|
||||
@param anIndex The index at which to drop the data among the item's children. If you want to drop on anItem, this will be CPOutlineViewDropOnItemIndex (-1).
|
||||
@return YES if the drop was successful; otherwise, NO.
|
||||
*/
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id /*<CPDraggingInfo>*/)info item:(id)anItem childIndex:(CPInteger)anIndex;
|
||||
|
||||
/*!
|
||||
@abstract Asks the data source whether to defer displaying the children of a given item.
|
||||
@discussion This method is useful for implementing lazy loading of outline view data. Returning NO prevents the outline view from querying for children of anItem, even if it is expandable.
|
||||
@param anOutlineView The outline view that sent the message.
|
||||
@param anItem The item being considered for expansion.
|
||||
@return YES to allow the outline view to query for children of anItem; otherwise, NO. The default is YES.
|
||||
*/
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView shouldDeferDisplayingChildrenOfItem:(id)anItem;
|
||||
|
||||
/*!
|
||||
@abstract Invoked when a drag should begin.
|
||||
@discussion The data source should write the representation of the specified items to the pasteboard.
|
||||
@param anOutlineView The outline view that is the source of the drag.
|
||||
@param items An array of items to be dragged.
|
||||
@param pboard The pasteboard to which the data for the dragged items should be written.
|
||||
@return YES if the drag should begin; NO to prevent the drag.
|
||||
*/
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)items toPasteboard:(CPPasteboard)pboard;
|
||||
|
||||
/*!
|
||||
@abstract Used for promised-file dragging.
|
||||
@discussion When a promised-file drag is dropped, this method is invoked to ask the data source to create the files at the specified destination and return their names.
|
||||
@param anOutlineView The outline view that was the source of the drag.
|
||||
@param dropDestination The URL of the directory where the files should be created.
|
||||
@param items The items that were dragged, representing the promised files.
|
||||
@return An array of strings containing the names of the files that were created.
|
||||
*/
|
||||
- (CPArray)outlineView:(CPOutlineView)anOutlineView namesOfPromisedFilesDroppedAtDestination:(CPURL)dropDestination forDraggedItems:(CPArray)items;
|
||||
|
||||
/*!
|
||||
@abstract Invoked to determine if a drop is allowed at a specified location.
|
||||
@discussion This method is called repeatedly while the user drags over the outline view. It should return the drag operation that should be performed.
|
||||
@param anOutlineView The outline view that is the destination of the drag.
|
||||
@param info An object that contains information about the dragging session.
|
||||
@param anItem The item that is the proposed parent for the dropped data.
|
||||
@param anIndex The index at which to drop the data among the item's children. If you want to drop on anItem, this will be CPOutlineViewDropOnItemIndex (-1).
|
||||
@return A CPDragOperation value that indicates the type of operation to perform.
|
||||
*/
|
||||
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id /*<CPDraggingInfo>*/)info proposedItem:(id)anItem proposedChildIndex:(CPInteger)anIndex;
|
||||
|
||||
/*!
|
||||
@abstract Invoked to determine if a drop is allowed at a specified row.
|
||||
@discussion This is a legacy method from CPTableView. It is recommended to implement outlineView:validateDrop:proposedItem:proposedChildIndex: instead for more precise control in an outline view.
|
||||
@param anOutlineView The outline view that is the destination of the drag.
|
||||
@param info An object that contains information about the dragging session.
|
||||
@param theRow The proposed row for the drop.
|
||||
@param theOperation The proposed drop operation (CPTableViewDropOn or CPTableViewDropAbove).
|
||||
@return A CPDragOperation value that indicates the type of operation to perform.
|
||||
*/
|
||||
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id /*<CPDraggingInfo>*/)info proposedRow:(int)theRow proposedDropOperation:(CPTableViewDropOperation)theOperation;
|
||||
|
||||
/*!
|
||||
@abstract Used for state preservation.
|
||||
@discussion This method is called to convert a persistent, serializable object back into a model item.
|
||||
@param anOutlineView The outline view requesting the item.
|
||||
@param anObject The persistent object used to identify the model item.
|
||||
@return The model item corresponding to anObject, or nil if it cannot be found.
|
||||
*/
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView itemForPersistentObject:(id)anObject;
|
||||
|
||||
/*!
|
||||
@abstract Returns the data object to be displayed for a given item and column.
|
||||
@discussion This method is called by the outline view to get the value for each cell. It is required for cell-based outline views.
|
||||
@param anOutlineView The outline view that sent the message.
|
||||
@param aTableColumn The column for which the value is requested.
|
||||
@param anItem The item for the row being displayed.
|
||||
@return The data object (e.g., a CPString) for the specified item and column.
|
||||
*/
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView objectValueforTableColumn:(CPTableColumn)aTableColumn byItem:(id)anItem;
|
||||
|
||||
/*!
|
||||
@abstract Used for state preservation.
|
||||
@discussion This method is called to convert a model item into a persistent, serializable object (e.g., a string identifier) that can be saved.
|
||||
@param anOutlineView The outline view requesting the persistent object.
|
||||
@param anItem The item to be converted.
|
||||
@return A serializable object that persistently identifies anItem.
|
||||
*/
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView persistentObjectForItem:(id)anItem;
|
||||
|
||||
/*!
|
||||
@abstract Sets the data object for a given item and column.
|
||||
@discussion This method is called when the user edits a cell's value. The data source should update its model with the new value.
|
||||
@param anOutlineView The outline view that sent the message.
|
||||
@param anObject The new value.
|
||||
@param aTableColumn The column that was edited.
|
||||
@param anItem The item whose value was edited.
|
||||
*/
|
||||
- (void)outlineView:(CPOutlineView)anOutlineView setObjectValue:(id)anObject forTableColumn:(CPTableColumn)aTableColumn byItem:(id)anItem;
|
||||
|
||||
/*!
|
||||
@abstract Notifies the data source that the sort descriptors have changed.
|
||||
@discussion This method is called after the user clicks a column header to change the sort order. The data source should re-sort its data based on the outline view's new 'sortDescriptors' property and then call `reloadData`.
|
||||
@param anOutlineView The outline view that sent the message.
|
||||
@param oldDescriptors The previous sort descriptors.
|
||||
*/
|
||||
- (void)outlineView:(CPOutlineView)anOutlineView sortDescriptorsDidChange:(CPArray)oldDescriptors;
|
||||
|
||||
@end
|
||||
@@ -215,8 +311,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
[self setIndentationPerLevel:16.0];
|
||||
[self setIndentationMarkerFollowsDataView:YES];
|
||||
|
||||
[super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]];
|
||||
[super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]];
|
||||
[super setDataSource:self];
|
||||
[super setDelegate:self];
|
||||
|
||||
[self setDisclosureControlPrototype:[[CPDisclosureButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 10.0)]];
|
||||
}
|
||||
@@ -677,6 +773,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
[self reloadItem:anItem reloadChildren:NO];
|
||||
}
|
||||
|
||||
- (int)_numberOfRows
|
||||
{
|
||||
return _itemsForRows ? _itemsForRows.length : 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reloads the data for a given item and optionally the children.
|
||||
|
||||
@@ -685,9 +786,12 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
*/
|
||||
- (void)reloadItem:(id)anItem reloadChildren:(BOOL)shouldReloadChildren
|
||||
{
|
||||
|
||||
_pendingItemToClean = [];
|
||||
_itemAddedDuringLastLoading = [];
|
||||
|
||||
var previousRowCount = _itemsForRows.length;
|
||||
|
||||
if (!!shouldReloadChildren || !anItem)
|
||||
[self _loadItemInfoForItem:anItem intermediate:NO];
|
||||
else
|
||||
@@ -695,6 +799,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
|
||||
[self _cleanPendingItem];
|
||||
|
||||
// Safely update the table size and force a synchronous layout recalculation
|
||||
// BEFORE the views are reloaded, avoiding the clipping issue.
|
||||
if (_itemsForRows.length !== previousRowCount)
|
||||
[self noteNumberOfRowsChanged];
|
||||
|
||||
[super _reloadDataViews];
|
||||
}
|
||||
|
||||
@@ -741,9 +850,20 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
|
||||
for (var i = [previousItems count] - 1; i >= 0; i--)
|
||||
{
|
||||
var item = previousItems[i];
|
||||
var item = previousItems[i],
|
||||
found = NO;
|
||||
|
||||
if (![children containsObject:item])
|
||||
// Use strict identity (===) instead of containsObject: (which triggers isEqual:)
|
||||
for (var j = 0, count = children.length; j < count; j++)
|
||||
{
|
||||
if (children[j] === item)
|
||||
{
|
||||
found = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
[self _addPendingItem:item];
|
||||
}
|
||||
}
|
||||
@@ -757,7 +877,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
|
||||
var children = itemInfo.children;
|
||||
|
||||
for (var i = [children count]; i >= 0; i--)
|
||||
// Fixed out-of-bounds index (was previously [children count])
|
||||
for (var i = children.length - 1; i >= 0; i--)
|
||||
{
|
||||
var child = children[i];
|
||||
[self _addPendingItem:child];
|
||||
@@ -768,7 +889,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
|
||||
- (void)_cleanPendingItem
|
||||
{
|
||||
for (var i = [_pendingItemToClean count]; i >= 0; i--)
|
||||
for (var i = [_pendingItemToClean count] - 1; i >= 0; i--)
|
||||
{
|
||||
var item = _pendingItemToClean[i];
|
||||
|
||||
@@ -812,7 +933,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
var weight = itemInfo.weight,
|
||||
descendants = anItem ? [anItem] : [];
|
||||
|
||||
[_itemAddedDuringLastLoading addObject:anItem];
|
||||
if (anItem)
|
||||
[_itemAddedDuringLastLoading addObject:anItem];
|
||||
|
||||
if (itemInfo.isExpanded && [self _sendDataSourceShouldDeferDisplayingChildrenOfItem:anItem])
|
||||
{
|
||||
@@ -989,7 +1111,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
var parent = itemInfo.parent;
|
||||
|
||||
// Check if the parent is the root item because we never return the actual root item
|
||||
if (itemInfo[[parent UID]] === _rootItemInfo)
|
||||
if (parent && _itemInfosForItems[[parent UID]] === _rootItemInfo)
|
||||
parent = nil;
|
||||
|
||||
return parent;
|
||||
@@ -1390,6 +1512,21 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
return [super frameOfDataViewAtColumn:aColumn row:aRow];
|
||||
}
|
||||
|
||||
- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
|
||||
{
|
||||
if (_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_)
|
||||
{
|
||||
var item = [self itemAtRow:aRow],
|
||||
tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn item:item mouseLocation:CGPointMakeZero()];
|
||||
|
||||
[aDataView setToolTip:tooltip];
|
||||
}
|
||||
else
|
||||
{
|
||||
[super _applyToolTipToDataView:aDataView forTableColumn:aTableColumn row:aRow];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Retargets the drop item for the outlineview.
|
||||
|
||||
@@ -1879,43 +2016,28 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
return _implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectItem_;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPOutlineViewTableViewDataSource : CPObject <CPTableViewDataSource>
|
||||
{
|
||||
CPObject _outlineView;
|
||||
}
|
||||
|
||||
- (id)initWithOutlineView:(CPOutlineView)anOutlineView
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
_outlineView = anOutlineView;
|
||||
|
||||
return self;
|
||||
}
|
||||
/*** CPTableViewDataSource methods ***/
|
||||
|
||||
- (CPInteger)numberOfRowsInTableView:(CPTableView)anOutlineView
|
||||
{
|
||||
return _outlineView._itemsForRows.length;
|
||||
return _itemsForRows.length;
|
||||
}
|
||||
|
||||
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
|
||||
{
|
||||
return [_outlineView._outlineViewDataSource outlineView:_outlineView objectValueForTableColumn:aTableColumn byItem:_outlineView._itemsForRows[aRow]];
|
||||
return [_outlineViewDataSource outlineView:self objectValueForTableColumn:aTableColumn byItem:_itemsForRows[aRow]];
|
||||
}
|
||||
|
||||
- (void)tableView:(CPTableView)aTableView setObjectValue:(id)aValue forTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow
|
||||
{
|
||||
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_))
|
||||
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_))
|
||||
return;
|
||||
[_outlineView._outlineViewDataSource outlineView:_outlineView setObjectValue:aValue forTableColumn:aColumn byItem:_outlineView._itemsForRows[aRow]];
|
||||
[_outlineViewDataSource outlineView:self setObjectValue:aValue forTableColumn:aColumn byItem:_itemsForRows[aRow]];
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableColumn writeRowsWithIndexes:(CPIndexSet)theIndexes toPasteboard:(CPPasteboard)thePasteboard
|
||||
{
|
||||
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_))
|
||||
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_))
|
||||
return NO;
|
||||
|
||||
var items = [],
|
||||
@@ -1923,27 +2045,27 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
|
||||
while (index !== CPNotFound)
|
||||
{
|
||||
[items addObject:[_outlineView itemAtRow:index]];
|
||||
[items addObject:[self itemAtRow:index]];
|
||||
index = [theIndexes indexGreaterThanIndex:index];
|
||||
}
|
||||
|
||||
return [_outlineView._outlineViewDataSource outlineView:_outlineView writeItems:items toPasteboard:thePasteboard];
|
||||
return [_outlineViewDataSource outlineView:self writeItems:items toPasteboard:thePasteboard];
|
||||
}
|
||||
|
||||
- (int)_childIndexForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
|
||||
{
|
||||
if (_outlineView._shouldRetargetChildIndex)
|
||||
return _outlineView._retargedChildIndex;
|
||||
if (_shouldRetargetChildIndex)
|
||||
return _retargedChildIndex;
|
||||
|
||||
var childIndex = CPNotFound;
|
||||
|
||||
if (theDropOperation === CPTableViewDropAbove)
|
||||
{
|
||||
var parentItem = [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset],
|
||||
itemInfo = (parentItem != nil) ? _outlineView._itemInfosForItems[[parentItem UID]] : _outlineView._rootItemInfo,
|
||||
var parentItem = [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset],
|
||||
itemInfo = (parentItem != nil) ? _itemInfosForItems[[parentItem UID]] : _rootItemInfo,
|
||||
children = itemInfo.children;
|
||||
|
||||
childIndex = [children indexOfObject:[_outlineView itemAtRow:theRow]];
|
||||
childIndex = [children indexOfObject:[self itemAtRow:theRow]];
|
||||
|
||||
if (childIndex === CPNotFound)
|
||||
childIndex = children.length;
|
||||
@@ -1954,155 +2076,151 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
|
||||
return childIndex;
|
||||
}
|
||||
|
||||
- (void)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
|
||||
- (id)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
|
||||
{
|
||||
if (theDropOperation === CPTableViewDropAbove)
|
||||
return [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset];
|
||||
return [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset];
|
||||
|
||||
return [_outlineView itemAtRow:theRow];
|
||||
return [self itemAtRow:theRow];
|
||||
}
|
||||
|
||||
- (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id /*< CPDraggingInfo >*/)theInfo
|
||||
proposedRow:(CPInteger)theRow proposedDropOperation:(CPTableViewDropOperation)theOperation
|
||||
{
|
||||
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_))
|
||||
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_))
|
||||
return CPDragOperationNone;
|
||||
|
||||
// Make sure the retargeted item and index are reset
|
||||
_outlineView._retargetedItem = nil;
|
||||
_outlineView._shouldRetargetItem = NO;
|
||||
_retargetedItem = nil;
|
||||
_shouldRetargetItem = NO;
|
||||
|
||||
_outlineView._retargedChildIndex = nil;
|
||||
_outlineView._shouldRetargetChildIndex = NO;
|
||||
_retargedChildIndex = nil;
|
||||
_shouldRetargetChildIndex = NO;
|
||||
|
||||
var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil],
|
||||
var location = [self convertPoint:[theInfo draggingLocation] fromView:nil],
|
||||
parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location],
|
||||
childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location];
|
||||
|
||||
return [_outlineView._outlineViewDataSource outlineView:_outlineView validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex];
|
||||
return [_outlineViewDataSource outlineView:self validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex];
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id /*<CPDraggingInfo>*/)theInfo row:(CPInteger)theRow dropOperation:(CPTableViewDropOperation)theOperation
|
||||
{
|
||||
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_))
|
||||
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_))
|
||||
return NO;
|
||||
|
||||
var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil],
|
||||
var location = [self convertPoint:[theInfo draggingLocation] fromView:nil],
|
||||
parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location],
|
||||
childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location];
|
||||
|
||||
_outlineView._retargetedItem = nil;
|
||||
_outlineView._shouldRetargetItem = NO;
|
||||
_retargetedItem = nil;
|
||||
_shouldRetargetItem = NO;
|
||||
|
||||
_outlineView._retargedChildIndex = nil;
|
||||
_outlineView._shouldRetargetChildIndex = NO;
|
||||
_retargedChildIndex = nil;
|
||||
_shouldRetargetChildIndex = NO;
|
||||
|
||||
return [_outlineView._outlineViewDataSource outlineView:_outlineView acceptDrop:theInfo item:parentItem childIndex:childIndex];
|
||||
return [_outlineViewDataSource outlineView:self acceptDrop:theInfo item:parentItem childIndex:childIndex];
|
||||
}
|
||||
|
||||
- (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldSortDescriptors
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDataSourceMethods &
|
||||
if ((_implementedOutlineViewDataSourceMethods &
|
||||
CPOutlineViewDataSource_outlineView_sortDescriptorsDidChange_))
|
||||
{
|
||||
[[_outlineView dataSource] outlineView:_outlineView sortDescriptorsDidChange:oldSortDescriptors];
|
||||
[[self dataSource] outlineView:self sortDescriptorsDidChange:oldSortDescriptors];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
/*** CPTableViewDelegate methods ***/
|
||||
|
||||
@implementation _CPOutlineViewTableViewDelegate : CPObject <CPTableViewDelegate>
|
||||
/*!
|
||||
@ignore
|
||||
*/
|
||||
- (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn item:(id)anItem mouseLocation:(CGPoint)aPoint
|
||||
{
|
||||
CPOutlineView _outlineView;
|
||||
}
|
||||
if (!(_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_))
|
||||
return nil;
|
||||
|
||||
- (id)initWithOutlineView:(CPOutlineView)anOutlineView
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
_outlineView = anOutlineView;
|
||||
|
||||
return self;
|
||||
return [_outlineViewDelegate outlineView:self toolTipForView:aView rect:aRect tableColumn:aTableColumn item:anItem mouseLocation:aPoint];
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)theTableView shouldSelectRow:(CPInteger)theRow
|
||||
{
|
||||
return SHOULD_SELECT_ITEM(_outlineView, [_outlineView itemAtRow:theRow]);
|
||||
return SHOULD_SELECT_ITEM(self, [self itemAtRow:theRow]);
|
||||
}
|
||||
|
||||
- (BOOL)selectionShouldChangeInTableView:(CPTableView)theTableView
|
||||
{
|
||||
return SELECTION_SHOULD_CHANGE(_outlineView);
|
||||
return SELECTION_SHOULD_CHANGE(self);
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_))
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldEditTableColumn:aColumn item:[_outlineView itemAtRow:aRow]];
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_))
|
||||
return [_outlineViewDelegate outlineView:self shouldEditTableColumn:aColumn item:[self itemAtRow:aRow]];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (float)tableView:(CPTableView)theTableView heightOfRow:(CPInteger)theRow
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_))
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView heightOfRowByItem:[_outlineView itemAtRow:theRow]];
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_))
|
||||
return [_outlineViewDelegate outlineView:self heightOfRowByItem:[self itemAtRow:theRow]];
|
||||
|
||||
return [theTableView rowHeight];
|
||||
}
|
||||
|
||||
- (void)tableView:(CPTableView)aTableView willDisplayView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_))
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_))
|
||||
{
|
||||
var item = [_outlineView itemAtRow:aRowIndex];
|
||||
[_outlineView._outlineViewDelegate outlineView:_outlineView willDisplayView:aView forTableColumn:aTableColumn item:item];
|
||||
var item = [self itemAtRow:aRowIndex];
|
||||
[_outlineViewDelegate outlineView:self willDisplayView:aView forTableColumn:aTableColumn item:item];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(CPTableView)aTableView willRemoveView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_))
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_))
|
||||
{
|
||||
var item = [_outlineView itemAtRow:aRowIndex];
|
||||
[_outlineView._outlineViewDelegate outlineView:_outlineView willRemoveView:aView forTableColumn:aTableColumn item:item];
|
||||
var item = [self itemAtRow:aRowIndex];
|
||||
[_outlineViewDelegate outlineView:self willRemoveView:aView forTableColumn:aTableColumn item:item];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView isGroupRow:(CPInteger)aRow
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_))
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView isGroupItem:[_outlineView itemAtRow:aRow]];
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_))
|
||||
return [_outlineViewDelegate outlineView:self isGroupItem:[self itemAtRow:aRow]];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CPMenu)tableView:(CPTableView)aTableView menuForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_))
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_))
|
||||
{
|
||||
var item = [_outlineView itemAtRow:aRow];
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item]
|
||||
var item = [self itemAtRow:aRow];
|
||||
return [_outlineViewDelegate outlineView:self menuForTableColumn:aTableColumn item:item]
|
||||
}
|
||||
|
||||
// We reimplement CPView menuForEvent: because we can't call it directly. CPTableView implements menuForEvent:
|
||||
// to call this delegate method.
|
||||
return [_outlineView menu] || [[_outlineView class] defaultMenu];
|
||||
return [self menu] || [[self class] defaultMenu];
|
||||
}
|
||||
|
||||
- (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)anIndexSet
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_))
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView selectionIndexesForProposedSelection:anIndexSet];
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_))
|
||||
return [_outlineViewDelegate outlineView:self selectionIndexesForProposedSelection:anIndexSet];
|
||||
|
||||
return anIndexSet;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView shouldSelectTableColumn:(CPTableColumn)aTableColumn
|
||||
{
|
||||
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_))
|
||||
return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldSelectTableColumn:aTableColumn];
|
||||
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_))
|
||||
return [_outlineViewDelegate outlineView:self shouldSelectTableColumn:aTableColumn];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -2208,8 +2326,8 @@ var CPOutlineViewIndentationPerLevelKey = @"CPOutlineViewIndentationPerLevelKey"
|
||||
_outlineViewDataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
|
||||
_outlineViewDelegate = [aCoder decodeObjectForKey:CPOutlineViewDelegateKey];
|
||||
|
||||
[super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]];
|
||||
[super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]];
|
||||
[super setDataSource:self];
|
||||
[super setDelegate:self];
|
||||
|
||||
[self _updateIsViewBased];
|
||||
}
|
||||
@@ -2248,3 +2366,242 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted)
|
||||
? [CPColor colorWithCalibratedWhite:0.4 alpha: 1.0]
|
||||
: [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]);
|
||||
};
|
||||
|
||||
@implementation CPOutlineView (CPBindings)
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self !== [CPOutlineView class])
|
||||
return;
|
||||
|
||||
[self exposeBinding:@"content"];
|
||||
[self exposeBinding:@"selectionIndexPaths"];
|
||||
[self exposeBinding:@"sortDescriptors"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the currently selected index paths.
|
||||
This allows the outline view to be KVC-compliant for `selectionIndexPaths`.
|
||||
*/
|
||||
- (CPArray)selectionIndexPaths
|
||||
{
|
||||
var indexes = [self selectedRowIndexes],
|
||||
paths = [CPMutableArray array],
|
||||
index = [indexes firstIndex];
|
||||
|
||||
while (index !== CPNotFound)
|
||||
{
|
||||
var item = [self itemAtRow:index];
|
||||
|
||||
// Check if the item is a CPTreeNode proxy (which it will be when bound to CPTreeController)
|
||||
if ([item respondsToSelector:@selector(indexPath)])
|
||||
[paths addObject:[item indexPath]];
|
||||
|
||||
index = [indexes indexGreaterThanIndex:index];
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CPOutlineView (CPBinder)
|
||||
|
||||
- (id)content { return nil; }
|
||||
- (void)setContent:(id)aContent { }
|
||||
- (void)setSelectionIndexPaths:(CPArray)paths { }
|
||||
|
||||
+ (Class)_binderClassForBinding:(CPString)aBinding
|
||||
{
|
||||
if (aBinding === @"content")
|
||||
return [_CPOutlineViewContentBinder class];
|
||||
|
||||
if (aBinding === @"selectionIndexPaths")
|
||||
return [_CPOutlineViewSelectionIndexPathsBinder class];
|
||||
|
||||
return [super _binderClassForBinding:aBinding];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// --- Content Binder ---
|
||||
|
||||
/*!
|
||||
_CPOutlineViewContentBinder acts as the CPOutlineViewDataSource when the outline view
|
||||
is bound to a CPTreeController's arrangedObjects.
|
||||
*/
|
||||
@implementation _CPOutlineViewContentBinder : CPBinder
|
||||
{
|
||||
CPTreeNode _rootNode;
|
||||
}
|
||||
|
||||
- (void)setValueFor:(CPString)aBinding
|
||||
{
|
||||
var destination = [_info objectForKey:CPObservedObjectKey],
|
||||
keyPath = [_info objectForKey:CPObservedKeyPathKey],
|
||||
value = [destination valueForKeyPath:keyPath];
|
||||
|
||||
if (!value || ![value isKindOfClass:[CPTreeNode class]])
|
||||
_rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil];
|
||||
else
|
||||
_rootNode = value;
|
||||
|
||||
if ([_source dataSource] !== self)
|
||||
[_source setDataSource:self];
|
||||
else
|
||||
[_source reloadData];
|
||||
}
|
||||
|
||||
- (CPTreeNode)rootNode
|
||||
{
|
||||
return _rootNode;
|
||||
}
|
||||
|
||||
// -- CPOutlineViewDataSource implementation --
|
||||
|
||||
- (id)outlineView:(CPOutlineView)outlineView child:(CPInteger)index ofItem:(id)item
|
||||
{
|
||||
var node = item || _rootNode;
|
||||
return [[node childNodes] objectAtIndex:index];
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
|
||||
{
|
||||
var node = item || _rootNode;
|
||||
return ![node isLeaf];
|
||||
}
|
||||
|
||||
- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
|
||||
{
|
||||
var node = item || _rootNode;
|
||||
return [[node childNodes] count];
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
|
||||
{
|
||||
var rep = [item respondsToSelector:@selector(representedObject)] ? [item representedObject] : item;
|
||||
|
||||
// Dynamically fetch the value using the column's identifier (e.g., "name")
|
||||
if (rep && [tableColumn identifier] && [tableColumn identifier] !== @"")
|
||||
return [rep valueForKey:[tableColumn identifier]];
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
- (void)outlineView:(CPOutlineView)outlineView setObjectValue:(id)value forTableColumn:(CPTableColumn)tableColumn byItem:(id)item
|
||||
{
|
||||
var rep = [item respondsToSelector:@selector(representedObject)] ?[item representedObject] : item;
|
||||
|
||||
// Push the inline edit back to the model using the column's identifier
|
||||
if (rep && [tableColumn identifier] && [tableColumn identifier] !== @"")
|
||||
[rep setValue:value forKey:[tableColumn identifier]];
|
||||
}
|
||||
|
||||
- (id)content
|
||||
{
|
||||
// CPTableView internals probe the binder for its flat content to draw rows.
|
||||
if (_source && _source._itemsForRows)
|
||||
return _source._itemsForRows;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// --- Selection Index Paths Binder ---
|
||||
|
||||
/*!
|
||||
_CPOutlineViewSelectionIndexPathsBinder listens for selection changes on the CPOutlineView
|
||||
and translates the selected rows into CPIndexPaths to push to the CPTreeController.
|
||||
It also intercepts changes from the CPTreeController and auto-expands the tree to highlight them.
|
||||
*/
|
||||
@implementation _CPOutlineViewSelectionIndexPathsBinder : CPBinder
|
||||
{
|
||||
BOOL _isSyncingFromModel;
|
||||
}
|
||||
|
||||
- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
|
||||
{
|
||||
self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource];
|
||||
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(outlineViewSelectionDidChange:)
|
||||
name:CPOutlineViewSelectionDidChangeNotification
|
||||
object:aSource];
|
||||
}
|
||||
|
||||
+ (void)unbind:(CPString)aBinding forObject:(id)anObject
|
||||
{
|
||||
if (aBinding === "selectionIndexPaths")
|
||||
[[CPNotificationCenter defaultCenter]
|
||||
removeObserver:self
|
||||
name:CPOutlineViewSelectionDidChangeNotification
|
||||
object:anObject];
|
||||
|
||||
[super unbind:aBinding forObject:anObject];
|
||||
}
|
||||
|
||||
- (void)setValueFor:(CPString)aBinding
|
||||
{
|
||||
// 1. SUPPRESS KVO AT THE VERY TOP to avoid circular updates when expanding parents
|
||||
_isSyncingFromModel = YES;
|
||||
|
||||
var destination = [_info objectForKey:CPObservedObjectKey],
|
||||
keyPath = [_info objectForKey:CPObservedKeyPathKey],
|
||||
indexPaths = [destination valueForKeyPath:keyPath] || [],
|
||||
indexes = [CPMutableIndexSet indexSet];
|
||||
|
||||
// 2. Fetch the root node directly from the CPTreeController (destination)
|
||||
var rootNode = [destination respondsToSelector:@selector(arrangedObjects)] ? [destination arrangedObjects] : nil;
|
||||
|
||||
if (rootNode)
|
||||
{
|
||||
for (var i = 0, count = [indexPaths count]; i < count; i++)
|
||||
{
|
||||
var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]];
|
||||
|
||||
if (item)
|
||||
{
|
||||
var parentsToExpand = [CPMutableArray array],
|
||||
parent = [item parentNode];
|
||||
|
||||
while (parent && parent !== rootNode)
|
||||
{
|
||||
[parentsToExpand insertObject:parent atIndex:0];
|
||||
parent = [parent parentNode];
|
||||
}
|
||||
|
||||
for (var j = 0; j < [parentsToExpand count]; j++)
|
||||
[_source expandItem:parentsToExpand[j]];
|
||||
|
||||
var row = [_source rowForItem:item];
|
||||
|
||||
if (row !== CPNotFound && row >= 0)
|
||||
[indexes addIndex:row];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the CPOutlineView selection
|
||||
[_source selectRowIndexes:indexes byExtendingSelection:NO];
|
||||
|
||||
// 3. Re-enable KVO after adjustments are done
|
||||
_isSyncingFromModel = NO;
|
||||
}
|
||||
|
||||
- (void)outlineViewSelectionDidChange:(CPNotification)note
|
||||
{
|
||||
// We only want to push the change back if we aren't currently syncing down from the model
|
||||
if (_isSyncingFromModel)
|
||||
return;
|
||||
|
||||
// In CPBinder, reverseSetValueFor: takes the name of the property on _source
|
||||
// it should fetch the updated value from. Since CPOutlineView has the selectionIndexPaths method:
|
||||
[self reverseSetValueFor:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+2
-2
@@ -121,8 +121,8 @@ CPDocModalWindowMask = 1 << 6;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
|
||||
+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
|
||||
|
||||
+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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -80,6 +80,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 +156,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
BOOL _isKeyDown;
|
||||
BOOL _nestingModeDidChange;
|
||||
|
||||
_CPRuleEditorLocalizer _standardLocalizer @accessors(property=standardLocalizer);
|
||||
_CPRuleEditorLocalizer _standardLocalizer;
|
||||
CPDictionary _itemsAndValuesToAddForRowType;
|
||||
}
|
||||
|
||||
@@ -207,8 +236,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 +439,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
*/
|
||||
- (CPDictionary)formattingDictionary
|
||||
{
|
||||
return [_standardLocalizer dictionary];
|
||||
return [[self standardLocalizer] dictionary];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -396,6 +451,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
|
||||
*/
|
||||
- (void)setFormattingDictionary:(CPDictionary)dictionary
|
||||
{
|
||||
if (_standardLocalizer == nil)
|
||||
_standardLocalizer = [_CPRuleEditorLocalizer new];
|
||||
|
||||
[_standardLocalizer setDictionary:dictionary];
|
||||
_stringsFilename = nil;
|
||||
}
|
||||
@@ -440,6 +498,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 +609,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 +676,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 +845,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 +902,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 +919,7 @@ TODO: implement
|
||||
return nil;
|
||||
|
||||
var current_index = [subrowsIndexes firstIndex];
|
||||
|
||||
while (current_index !== CPNotFound)
|
||||
{
|
||||
var subpredicate = [self predicateForRow:current_index];
|
||||
@@ -1233,7 +1304,7 @@ TODO: implement
|
||||
return shouldHide;
|
||||
}
|
||||
|
||||
#pragma mark Rows management
|
||||
// MARK: Rows management
|
||||
|
||||
- (id)_rowCacheForIndex:(int)index
|
||||
{
|
||||
@@ -1278,23 +1349,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 +1432,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 +1566,7 @@ TODO: implement
|
||||
return row;
|
||||
}
|
||||
|
||||
#pragma mark Key value observing
|
||||
// MARK: Key value observing
|
||||
|
||||
- (void)_startObservingRowObjectsRecursively:(CPArray)rowObjects
|
||||
{
|
||||
@@ -1774,7 +1867,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
|
||||
@@ -1919,17 +2017,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
|
||||
@@ -2244,33 +2342,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 +2386,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
|
||||
|
||||
@@ -114,8 +114,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 +166,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 +342,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 +440,9 @@
|
||||
{
|
||||
[_addButton setHidden:[_ruleEditor _shouldHideAddButtonForSlice:self]];
|
||||
[_subtractButton setHidden:[_ruleEditor _shouldHideSubtractButtonForSlice:self]];
|
||||
|
||||
[_addButton setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]];
|
||||
[_subtractButton setToolTip:[_ruleEditor _toolTipForDeleteRowButton]];
|
||||
}
|
||||
|
||||
- (void)_configurePlusButtonByRowType:(CPRuleEditorRowType)type
|
||||
|
||||
+232
-21
@@ -29,8 +29,10 @@
|
||||
@import "CPClipView.j"
|
||||
@import "CPScroller.j"
|
||||
@import "CPView.j"
|
||||
@import "CPRulerView.j"
|
||||
|
||||
@class CPTableView
|
||||
@class CPRulerView
|
||||
|
||||
#define SHOULD_SHOW_CORNER_VIEW() (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden])
|
||||
|
||||
@@ -94,6 +96,10 @@ var TIMER_INTERVAL = 0.2,
|
||||
|
||||
CPScrollViewFadeOutTime = 1.3;
|
||||
|
||||
var CPScrollViewWillStartLiveScrollNotification = @"CPScrollViewWillStartLiveScrollNotification",
|
||||
CPScrollViewDidLiveScrollNotification = @"CPScrollViewDidLiveScrollNotification",
|
||||
CPScrollViewDidEndLiveScrollNotification = @"CPScrollViewDidEndLiveScrollNotification";
|
||||
|
||||
var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
CPScrollerStyleGlobalChangeNotification = @"CPScrollerStyleGlobalChangeNotification";
|
||||
|
||||
@@ -136,11 +142,19 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
|
||||
int _scrollerStyle;
|
||||
int _scrollerKnobStyle;
|
||||
|
||||
// Ruler Support
|
||||
BOOL _hasVerticalRuler;
|
||||
BOOL _hasHorizontalRuler;
|
||||
BOOL _rulersVisible;
|
||||
|
||||
CPRulerView _verticalRuler;
|
||||
CPRulerView _horizontalRuler;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
@@ -271,8 +285,8 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -302,6 +316,10 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
_scrollerKnobStyle = CPScrollerKnobStyleDefault;
|
||||
[self setScrollerStyle:CPScrollerStyleGlobal];
|
||||
|
||||
_hasVerticalRuler = NO;
|
||||
_hasHorizontalRuler = NO;
|
||||
_rulersVisible = NO;
|
||||
|
||||
_delegate = nil;
|
||||
_scrollTimer = nil;
|
||||
_implementedDelegateMethods = 0;
|
||||
@@ -311,8 +329,8 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
/*!
|
||||
The delegate of the scroll view
|
||||
@@ -790,8 +808,105 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Privates
|
||||
// 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)
|
||||
[self addSubview:_horizontalRuler];
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
- (CPRulerView)verticalRulerView
|
||||
{
|
||||
return _verticalRuler;
|
||||
}
|
||||
|
||||
- (void)setVerticalRulerView:(CPRulerView)aRulerView
|
||||
{
|
||||
if (_verticalRuler === aRulerView)
|
||||
return;
|
||||
|
||||
[_verticalRuler removeFromSuperview];
|
||||
_verticalRuler = aRulerView;
|
||||
|
||||
if (_verticalRuler)
|
||||
[self addSubview:_verticalRuler];
|
||||
|
||||
[self tile];
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Privates
|
||||
|
||||
/* @ignore */
|
||||
- (void)_updateScrollerStyle
|
||||
@@ -987,6 +1102,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 +1142,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 +1154,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 +1193,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 +1214,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 +1226,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 +1278,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 respondsToSelector:@selector(ruleThickness)] ? [_horizontalRuler ruleThickness] : 16.0) : 0.0,
|
||||
vertRulerThickness = showVerticalRuler ? ([_verticalRuler respondsToSelector:@selector(ruleThickness)] ? [_verticalRuler ruleThickness] : 24.0) : 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,6 +1405,7 @@ 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())
|
||||
{
|
||||
[_cornerView setFrame:[self _cornerViewFrame]];
|
||||
@@ -1263,6 +1420,37 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
[[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
|
||||
)];
|
||||
|
||||
if ([_horizontalRuler respondsToSelector:@selector(updateRuler)])
|
||||
[_horizontalRuler updateRuler];
|
||||
else
|
||||
[_horizontalRuler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
if (showVerticalRuler)
|
||||
{
|
||||
[_verticalRuler setFrame:CGRectMake(
|
||||
CGRectGetMinX(contentFrame) - vertRulerThickness,
|
||||
CGRectGetMinY(contentFrame),
|
||||
vertRulerThickness,
|
||||
CGRectGetHeight(contentFrame)
|
||||
)];
|
||||
|
||||
if ([_verticalRuler respondsToSelector:@selector(updateRuler)])
|
||||
[_verticalRuler updateRuler];
|
||||
else
|
||||
[_verticalRuler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
--_recursionCount;
|
||||
}
|
||||
|
||||
@@ -1303,8 +1491,8 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
return [_contentView documentVisibleRect];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
|
||||
- (void)_removeObservers
|
||||
@@ -1425,8 +1613,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;
|
||||
|
||||
@@ -1544,7 +1732,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPScrollView (FirstResponder)
|
||||
|
||||
@@ -1588,7 +1776,7 @@ Notifies the delegate when the scroll view has finished scrolling.
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
CPScrollViewHeaderClipViewKey = @"CPScrollViewHeaderClipViewKey",
|
||||
@@ -1605,7 +1793,14 @@ 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";
|
||||
|
||||
@implementation CPScrollView (CPCoding)
|
||||
|
||||
@@ -1640,6 +1835,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
_cornerView = [aCoder decodeObjectForKey:CPScrollViewCornerViewKey];
|
||||
_bottomCornerView = [aCoder decodeObjectForKey:CPScrollViewBottomCornerViewKey];
|
||||
|
||||
// Ruler decoding
|
||||
_hasVerticalRuler = [aCoder decodeBoolForKey:CPScrollViewHasVRulerKey];
|
||||
_hasHorizontalRuler = [aCoder decodeBoolForKey:CPScrollViewHasHRulerKey];
|
||||
_rulersVisible = [aCoder decodeBoolForKey:CPScrollViewRulersVisibleKey];
|
||||
|
||||
_verticalRuler = [aCoder decodeObjectForKey:CPScrollViewVRulerKey];
|
||||
_horizontalRuler = [aCoder decodeObjectForKey:CPScrollViewHRulerKey];
|
||||
|
||||
_delegate = nil;
|
||||
_scrollTimer = nil;
|
||||
_implementedDelegateMethods = 0;
|
||||
@@ -1693,6 +1896,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
|
||||
|
||||
[aCoder encodeInt:_scrollerStyle forKey:CPScrollViewScrollerStyleKey];
|
||||
[aCoder encodeInt:_scrollerKnobStyle forKey:CPScrollViewScrollerKnobStyleKey];
|
||||
|
||||
// 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
|
||||
|
||||
+16
-16
@@ -102,8 +102,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -167,8 +167,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -203,8 +203,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
/*!
|
||||
Returns the scroller's style
|
||||
@@ -269,8 +269,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Privates
|
||||
// MARK: -
|
||||
// MARK: Privates
|
||||
|
||||
/*! @ignore */
|
||||
- (void)_adjustScrollerSize
|
||||
@@ -295,8 +295,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Utilities
|
||||
// MARK: -
|
||||
// MARK: Utilities
|
||||
|
||||
- (CGRect)rectForPart:(CPScrollerPart)aPart
|
||||
{
|
||||
@@ -468,8 +468,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Drawing
|
||||
// MARK: -
|
||||
// MARK: Drawing
|
||||
|
||||
/*!
|
||||
Draws the specified arrow and sets the highlight.
|
||||
@@ -703,8 +703,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
- (id)currentValueForThemeAttribute:(CPString)anAttributeName
|
||||
{
|
||||
@@ -780,8 +780,8 @@ CPThemeStateScrollerKnobDark = CPThemeState("scroller-knob-dark");
|
||||
return [self currentValueForThemeAttribute:@"scroller-width"];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegates
|
||||
// MARK: -
|
||||
// MARK: Delegates
|
||||
|
||||
- (void)animationDidEnd:(CPAnimation)animation
|
||||
{
|
||||
|
||||
@@ -136,8 +136,8 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Override observers
|
||||
// MARK: -
|
||||
// MARK: Override observers
|
||||
|
||||
- (void)_removeObservers
|
||||
{
|
||||
@@ -788,7 +788,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSearchField (ThemingAdditions)
|
||||
|
||||
@@ -980,7 +980,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSearchField (CPTrackingArea)
|
||||
{
|
||||
@@ -1035,7 +1035,7 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPRecentsAutosaveNameKey = @"CPRecentsAutosaveNameKey",
|
||||
CPSendsWholeSearchStringKey = @"CPSendsWholeSearchStringKey",
|
||||
|
||||
+4
-4
@@ -586,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.
|
||||
@@ -812,7 +812,7 @@ var AFFINITY = 5;
|
||||
return [self valueForThemeAttribute:@"left-track-color" inStates:normalState];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
// MARK: - Private methods
|
||||
|
||||
- (void)_refreshCachesAndStates
|
||||
{
|
||||
@@ -1050,7 +1050,7 @@ var AFFINITY = 5;
|
||||
return value;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
// MARK: - Overrides
|
||||
|
||||
- (void)setFrameSize:(CGSize)aSize
|
||||
{
|
||||
@@ -1060,7 +1060,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
|
||||
|
||||
@@ -216,7 +216,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
[self _setVertical:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
// MARK: - Properties
|
||||
|
||||
- (CPSplitViewDividerStyle)dividerStyle
|
||||
{
|
||||
@@ -479,7 +479,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 +652,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
_subviewsManagementDisabled = NO;
|
||||
}
|
||||
|
||||
#pragma mark - Layout subviews
|
||||
// MARK: - Layout subviews
|
||||
|
||||
- (CGRect)rectOfDividerAtIndex:(int)aDivider
|
||||
{
|
||||
@@ -845,7 +845,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 +900,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.
|
||||
@@ -1487,7 +1487,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (CPTrackingArea)
|
||||
{
|
||||
@@ -1567,7 +1567,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (CPSplitViewDelegate)
|
||||
|
||||
@@ -1745,7 +1745,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey",
|
||||
CPSplitViewIsVerticalKey = @"CPSplitViewIsVerticalKey",
|
||||
@@ -1870,7 +1870,7 @@ var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPSplitView (Deprecated)
|
||||
|
||||
|
||||
@@ -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,700 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include "../Foundation/Foundation.h"
|
||||
|
||||
@import "CPView.j"
|
||||
|
||||
// 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;
|
||||
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
|
||||
_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 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
|
||||
}
|
||||
|
||||
/*!
|
||||
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];
|
||||
[_arrangedSubviews addObject:aView];
|
||||
|
||||
// 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];
|
||||
|
||||
[_arrangedSubviews addObject:aView];
|
||||
|
||||
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 arranged list and superview
|
||||
for (var i = 0; i < [container count]; i++)
|
||||
{
|
||||
var oldView = container[i];
|
||||
[oldView removeFromSuperview];
|
||||
[_arrangedSubviews removeObject:oldView];
|
||||
}
|
||||
|
||||
[container removeAllObjects];
|
||||
|
||||
for (var i = 0; i < [views count]; i++)
|
||||
{
|
||||
var newView = views[i];
|
||||
[container addObject:newView];
|
||||
[_arrangedSubviews addObject:newView];
|
||||
[self addSubview:newView];
|
||||
}
|
||||
|
||||
[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];
|
||||
[_arrangedSubviews removeObject:aView];
|
||||
|
||||
[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;
|
||||
|
||||
for (; i !== limit; i += step)
|
||||
{
|
||||
var view = views[i];
|
||||
|
||||
if (_detachesHiddenViews && [view isHidden])
|
||||
continue;
|
||||
|
||||
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 + [self _spacingAfterView:view];
|
||||
} else {
|
||||
cursor -= sizeH;
|
||||
originY = cursor;
|
||||
cursor -= [self _spacingAfterView:view];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Horizontal
|
||||
sizeW = viewSizePrimary;
|
||||
sizeH = viewOrthoSize;
|
||||
originY = orthoPos;
|
||||
|
||||
if (dir === 1) {
|
||||
originX = cursor;
|
||||
cursor += sizeW + [self _spacingAfterView:view];
|
||||
} else {
|
||||
cursor -= sizeW;
|
||||
originX = cursor;
|
||||
cursor -= [self _spacingAfterView:view];
|
||||
}
|
||||
}
|
||||
|
||||
[view setFrame:CGRectMake(originX, originY, sizeW, sizeH)];
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: CPCoding
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super initWithCoder:aCoder];
|
||||
if (self)
|
||||
{
|
||||
_orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"];
|
||||
_alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"];
|
||||
_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
|
||||
_arrangedSubviews = [[CPMutableArray alloc] init];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsLeading];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsCenter];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsTrailing];
|
||||
|
||||
_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 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
|
||||
+8
-8
@@ -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.
|
||||
@@ -230,8 +230,8 @@
|
||||
[super setDoubleValue:aValue];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Actions
|
||||
// MARK: -
|
||||
// MARK: Actions
|
||||
|
||||
/*! @ignore */
|
||||
- (IBAction)_buttonDidClick:(id)aSender
|
||||
@@ -266,8 +266,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theming
|
||||
// MARK: -
|
||||
// MARK: Theming
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
|
||||
@@ -69,13 +69,9 @@ CPCanvasStringSizingIsFunctional = NO;
|
||||
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
|
||||
}
|
||||
|
||||
+7
-7
@@ -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
|
||||
{
|
||||
|
||||
+110
-17
@@ -296,6 +296,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5;
|
||||
_CPTableDrawView _tableDrawView;
|
||||
|
||||
SEL _doubleAction;
|
||||
id _doubleClickTarget @accessors(property=doubleClickTarget);
|
||||
id _doubleClickArgument @accessors(property=doubleClickArgument);
|
||||
CPInteger _clickedRow;
|
||||
CPInteger _clickedColumn;
|
||||
unsigned _columnAutoResizingStyle;
|
||||
@@ -3486,13 +3488,73 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
[removeIndexes addIndex:columnIdx];
|
||||
}
|
||||
|
||||
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
|
||||
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
|
||||
if ([removeIndexes count] > 0)
|
||||
{
|
||||
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
|
||||
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
|
||||
|
||||
[_tableColumns removeObjectsAtIndexes:removeIndexes];
|
||||
[_tableColumns removeObjectsAtIndexes:removeIndexes];
|
||||
|
||||
_dirtyTableColumnRangeIndex = 0;
|
||||
[self _recalculateTableColumnRanges];
|
||||
_dirtyTableColumnRangeIndex = 0;
|
||||
[self _recalculateTableColumnRanges];
|
||||
|
||||
// Shift cached index sets downwards to account for the removed columns
|
||||
var shiftIndexSet = function(indexSet)
|
||||
{
|
||||
var newSet = [CPIndexSet indexSet];
|
||||
[indexSet enumerateIndexesUsingBlock:function(idx, stop)
|
||||
{
|
||||
if (![removeIndexes containsIndex:idx])
|
||||
{
|
||||
var shift = 0,
|
||||
remIdx = [removeIndexes firstIndex];
|
||||
|
||||
while (remIdx !== CPNotFound && remIdx < idx)
|
||||
{
|
||||
shift++;
|
||||
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
|
||||
}
|
||||
|
||||
[newSet addIndex:idx - shift];
|
||||
}
|
||||
}];
|
||||
return newSet;
|
||||
};
|
||||
|
||||
_exposedColumns = shiftIndexSet(_exposedColumns);
|
||||
_selectedColumnIndexes = shiftIndexSet(_selectedColumnIndexes);
|
||||
|
||||
// Shift individual index variables
|
||||
var shiftIndex = function(idx)
|
||||
{
|
||||
if (idx === CPNotFound || idx === -1)
|
||||
return idx;
|
||||
|
||||
if ([removeIndexes containsIndex:idx])
|
||||
return CPNotFound;
|
||||
|
||||
var shift = 0,
|
||||
remIdx = [removeIndexes firstIndex];
|
||||
|
||||
while (remIdx !== CPNotFound && remIdx < idx)
|
||||
{
|
||||
shift++;
|
||||
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
|
||||
}
|
||||
|
||||
return idx - shift;
|
||||
};
|
||||
|
||||
_editingColumn = shiftIndex(_editingColumn);
|
||||
|
||||
_draggedColumnIndex = shiftIndex(_draggedColumnIndex);
|
||||
if (_draggedColumnIndex === CPNotFound)
|
||||
_draggedColumnIndex = -1;
|
||||
|
||||
_clickedColumn = shiftIndex(_clickedColumn);
|
||||
if (_clickedColumn === CPNotFound)
|
||||
_clickedColumn = -1;
|
||||
}
|
||||
|
||||
[_differedColumnDataToRemove removeAllObjects];
|
||||
_needsDifferedTableColumnRemove = NO;
|
||||
@@ -3616,10 +3678,21 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
[self _setEditingState:NO forView:dataView];
|
||||
|
||||
[self _sendDelegateWillDisplayView:dataView forTableColumn:tableColumn row:row];
|
||||
[self _applyToolTipToDataView:dataView forTableColumn:tableColumn row:row];
|
||||
|
||||
return dataView;
|
||||
}
|
||||
|
||||
- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
|
||||
{
|
||||
var tooltip = nil;
|
||||
|
||||
if (_implementedDelegateMethods & CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_)
|
||||
tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn row:aRow mouseLocation:CGPointMakeZero()];
|
||||
|
||||
[aDataView setToolTip:tooltip];
|
||||
}
|
||||
|
||||
- (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView
|
||||
{
|
||||
[self _setObjectValueForTableColumn:aTableColumn row:aRow forView:aDataView useCache:!_invalidateObjectValuesCache];
|
||||
@@ -3627,17 +3700,28 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
|
||||
- (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView useCache:(BOOL)useCache
|
||||
{
|
||||
var providedByDataSource = NO;
|
||||
|
||||
if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_)
|
||||
[aDataView setObjectValue:[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]];
|
||||
{
|
||||
var objectValue = [self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache];
|
||||
[aDataView setObjectValue:objectValue];
|
||||
providedByDataSource = YES;
|
||||
}
|
||||
|
||||
// This gives the table column an opportunity to apply its bindings.
|
||||
// It will override the value set above if there is a binding.
|
||||
// It will override the value set above if there is an explicit column binding.
|
||||
var columnHasBindings = [[[CPBinder allBindingsForObject:aTableColumn] allKeys] count] > 0;
|
||||
|
||||
if (_contentBindingExplicitlySet)
|
||||
[self _prepareContentBindedDataView:aDataView forRow:aRow];
|
||||
else
|
||||
// For both cell-based and view-based
|
||||
if (columnHasBindings)
|
||||
{
|
||||
[aTableColumn _prepareDataView:aDataView forRow:aRow];
|
||||
}
|
||||
// Only forcefully bind the raw content object if the data source didn't already provide a formatted value
|
||||
else if (_contentBindingExplicitlySet && !providedByDataSource)
|
||||
{
|
||||
[self _prepareContentBindedDataView:aDataView forRow:aRow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_prepareContentBindedDataView:(CPView)dataView forRow:(CPInteger)aRow
|
||||
@@ -4712,7 +4796,12 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
|
||||
//double click actions
|
||||
if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction)
|
||||
[self sendAction:_doubleAction to:_target];
|
||||
{
|
||||
var target = _doubleClickTarget || _target,
|
||||
argument = [self infoForBinding:@"doubleClickArgument"] ? _doubleClickArgument : self;
|
||||
|
||||
[CPApp sendAction:_doubleAction to:target from:argument];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -5672,8 +5761,8 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark DataSource methods to implement
|
||||
// MARK: -
|
||||
// MARK: DataSource methods to implement
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -5941,8 +6030,8 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods to implement
|
||||
// MARK: -
|
||||
// MARK: Delegate methods to implement
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -5958,7 +6047,6 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
Not yet implemented
|
||||
*/
|
||||
- (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint
|
||||
{
|
||||
@@ -6077,6 +6165,11 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
|
||||
{
|
||||
if (aBinding == @"content")
|
||||
_contentBindingExplicitlySet = YES;
|
||||
else if (aBinding == @"doubleClickTarget")
|
||||
{
|
||||
if ([options objectForKey:CPSelectorNameBindingOption])
|
||||
[self setDoubleAction:CPSelectorFromString([options objectForKey:CPSelectorNameBindingOption])];
|
||||
}
|
||||
|
||||
[super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
|
||||
}
|
||||
|
||||
+80
-13
@@ -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";
|
||||
|
||||
@@ -241,8 +245,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Control Size
|
||||
// MARK: -
|
||||
// MARK: Control Size
|
||||
|
||||
- (void)setControlSize:(CPControlSize)aControlSize
|
||||
{
|
||||
@@ -253,7 +257,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
- (DOMElement)_inputElement
|
||||
@@ -332,7 +336,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
characters:nil
|
||||
charactersIgnoringModifiers:nil
|
||||
isARepeat:NO
|
||||
keyCode:nil];
|
||||
keyCode:nil
|
||||
isActionKey:NO];
|
||||
|
||||
[CPTextFieldInputOwner keyUp:cappEvent];
|
||||
|
||||
@@ -376,7 +381,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
|
||||
@@ -996,7 +1001,26 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
var acceptInvalidValue = NO;
|
||||
|
||||
if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
|
||||
acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
|
||||
{
|
||||
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;
|
||||
@@ -1130,6 +1154,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
|
||||
@@ -1472,7 +1502,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
|
||||
{
|
||||
@@ -1814,7 +1844,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
[self _didEdit];
|
||||
}
|
||||
|
||||
#pragma mark Setting the Delegate
|
||||
// MARK: Setting the Delegate
|
||||
|
||||
- (void)setDelegate:(id <CPTextFieldDelegate>)aDelegate
|
||||
{
|
||||
@@ -1974,6 +2004,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 +2057,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
Sets the text color of the receiver.
|
||||
@@ -2059,7 +2126,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
// MARK: Private
|
||||
|
||||
- (BOOL)_isWithinUsablePlatformRect
|
||||
{
|
||||
@@ -2358,7 +2425,7 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPTextField (TableDataView)
|
||||
|
||||
@@ -2375,7 +2442,7 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPTextField (Deprecated)
|
||||
|
||||
|
||||
+262
-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,7 +32,6 @@
|
||||
@import "CPText.j"
|
||||
@import "CPFontManager.j"
|
||||
|
||||
|
||||
@class CPTextStorage
|
||||
@class CPLayoutManager
|
||||
@class CPTextContainer
|
||||
@@ -46,27 +40,30 @@
|
||||
/*
|
||||
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 +74,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 +86,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 +108,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 +156,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 +206,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 +260,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 +283,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 +322,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 +341,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 +355,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 +365,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 +390,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 +441,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if ([self currentTrait] != typefaceIndex)
|
||||
[self setCurrentTrait:typefaceIndex ];
|
||||
|
||||
[_previewView setPreviewFont:font];
|
||||
|
||||
_fontChanges = kNothingChanged;
|
||||
}
|
||||
|
||||
@@ -382,23 +455,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 +494,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 +520,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]];
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
@import "CPFont.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,11 +289,12 @@ _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];
|
||||
|
||||
@@ -303,13 +307,17 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
- (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 +340,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 +372,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 +453,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 +546,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 +574,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 +730,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 +746,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 +874,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 +894,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 +950,6 @@ _oncontextmenuhandler = function () { return false; };
|
||||
inTextContainer:(CPTextContainer)container
|
||||
rectCount:(CGRectPointer)rectCount
|
||||
{
|
||||
|
||||
var rectArray = [],
|
||||
lineFragments = _objectsInRange(_lineFragments, selectedCharRange);
|
||||
|
||||
@@ -921,21 +968,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 +996,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;
|
||||
@@ -1082,8 +1132,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,6 +1142,10 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)fgColor andBackgroundColor:(CPColor)bgColor andUnderline:(CPUnderlineStyle)aUnderline
|
||||
{
|
||||
|
||||
if (!aString || aString.length === 0)
|
||||
return nil;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var style,
|
||||
span = document.createElement("span");
|
||||
@@ -1172,18 +1226,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 +1243,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 +1357,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 +1408,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 +1425,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 +1442,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 +1464,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);
|
||||
}
|
||||
|
||||
@@ -1353,9 +1505,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 +1531,14 @@ var _objectsInRange = function(aList, aRange)
|
||||
{
|
||||
_runs[i]._range.location += rangeOffset;
|
||||
|
||||
if (verticalOffset && _runs[i].elem)
|
||||
if (verticalOffset)
|
||||
{
|
||||
if (_runs[i].view)
|
||||
_runs[i].view._frame.origin.y += verticalOffset;
|
||||
|
||||
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
|
||||
if (_runs[i].elem)
|
||||
_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,365 @@
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPArray.j>
|
||||
|
||||
@import "CPText.j"
|
||||
|
||||
CPLeftTabStopType = 0;
|
||||
@import <Foundation/CPDictionary.j>
|
||||
|
||||
CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName";
|
||||
|
||||
var _sharedDefaultParagraphStyle,
|
||||
_defaultTabStopArray;
|
||||
// Define missing global tab stop type constants
|
||||
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
|
||||
|
||||
var _sharedDefaultParagraphStyle = nil;
|
||||
|
||||
@implementation CPParagraphStyle : CPObject
|
||||
{
|
||||
int _type @accessors(property = tabStopType);
|
||||
double _location @accessors(property = location);
|
||||
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);
|
||||
}
|
||||
|
||||
- (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]];
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
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"];
|
||||
}
|
||||
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"];
|
||||
}
|
||||
|
||||
// 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]];
|
||||
}
|
||||
|
||||
@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];
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(CPZone)aZone
|
||||
{
|
||||
// Return an immutable copy
|
||||
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* CPRulerView.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 "CPView.j"
|
||||
@import "CPTextField.j"
|
||||
@import "CPColor.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPMenu.j"
|
||||
@import "CPMenuItem.j"
|
||||
|
||||
// Orientations matching AppKit standards
|
||||
// typedef enum CPRulerOrientation
|
||||
CPHorizontalRuler = 0,
|
||||
CPVerticalRuler = 1,
|
||||
CPRulerOrientationHorizontal = 0,
|
||||
CPRulerOrientationVertical = 1
|
||||
|
||||
@class CPRulerView;
|
||||
|
||||
|
||||
// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons)
|
||||
|
||||
@implementation CPRulerMarker : CPView
|
||||
{
|
||||
CPRulerView _rulerView @accessors(property=rulerView);
|
||||
float _imageValue @accessors(property=imageValue);
|
||||
id _representedObject @accessors(property=representedObject);
|
||||
CPTextField _label;
|
||||
CPView _customHandleView;
|
||||
}
|
||||
|
||||
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject
|
||||
{
|
||||
if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)])
|
||||
{
|
||||
_rulerView = aRulerView;
|
||||
_imageValue = anImageValue;
|
||||
_representedObject = anObject;
|
||||
|
||||
_label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
|
||||
[_label setFont:[CPFont systemFontOfSize:10.0]];
|
||||
[_label setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
[_label setAlignment:CPCenterTextAlignment];
|
||||
[self addSubview:_label];
|
||||
|
||||
_customHandleView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
[self addSubview:_customHandleView];
|
||||
|
||||
[self updateMarkerIcon];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPTextField)label
|
||||
{
|
||||
return _label;
|
||||
}
|
||||
|
||||
- (void)setRepresentedObject:(id)anObject
|
||||
{
|
||||
_representedObject = anObject;
|
||||
[self updateMarkerIcon];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)aFrame
|
||||
{
|
||||
[super setFrame:aFrame];
|
||||
[self updateMarkerIcon];
|
||||
}
|
||||
|
||||
// Dynamically sets the Unicode triangle direction based on the alignment or indent type,
|
||||
// or draws custom split-height grab handles for indentation controls.
|
||||
- (void)updateMarkerIcon
|
||||
{
|
||||
var isIndentMarker = (_representedObject === @"CPFirstLineIndent" || _representedObject === @"CPHeadIndent");
|
||||
|
||||
if (isIndentMarker)
|
||||
{
|
||||
[_label setHidden:YES];
|
||||
[_customHandleView setHidden:NO];
|
||||
|
||||
var frame = [self bounds];
|
||||
[_customHandleView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
|
||||
|
||||
// Remove old internal rendering to update cleanly
|
||||
[[_customHandleView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
|
||||
var isFirstLine = (_representedObject === @"CPFirstLineIndent");
|
||||
|
||||
// Dark outline/border representation
|
||||
[_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]];
|
||||
|
||||
// Inner fill (top handle is lighter, bottom is slightly darker)
|
||||
var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, frame.size.width - 2.0, frame.size.height - 2.0)];
|
||||
|
||||
if (isFirstLine)
|
||||
[innerView setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]];
|
||||
else
|
||||
[innerView setBackgroundColor:[CPColor colorWithWhite:0.80 alpha:1.0]];
|
||||
|
||||
[_customHandleView addSubview:innerView];
|
||||
|
||||
// Horizontal indicator line to visually guide drag interactions
|
||||
var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 2.0, 1.0, frame.size.height - 4.0)];
|
||||
[gripLine setBackgroundColor:[CPColor colorWithWhite:0.6 alpha:1.0]];
|
||||
[innerView addSubview:gripLine];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_label setHidden:NO];
|
||||
[_customHandleView setHidden:YES];
|
||||
[_label setFrame:[self bounds]];
|
||||
|
||||
if ([_representedObject isKindOfClass:[CPTextTab class]])
|
||||
{
|
||||
var align = [_representedObject alignment];
|
||||
if (align === CPLeftTextAlignment)
|
||||
[_label setStringValue:@"▶"]; // Left-aligned points Right
|
||||
else if (align === CPCenterTextAlignment)
|
||||
[_label setStringValue:@"▼"]; // Center-aligned points Down
|
||||
else if (align === CPRightTextAlignment)
|
||||
[_label setStringValue:@"◀"]; // Right-aligned points Left
|
||||
}
|
||||
else if ([_representedObject isKindOfClass:[CPString class]])
|
||||
{
|
||||
if (_representedObject === @"CPTailIndent")
|
||||
[_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent
|
||||
else
|
||||
[_label setStringValue:@"⇡"]; // Fallback standard up marker
|
||||
}
|
||||
else
|
||||
{
|
||||
[_label setStringValue:@"⇡"]; // Fallback standard up marker
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: -
|
||||
// MARK: Context Menu Support
|
||||
|
||||
- (CPMenu)menuForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var menu = [[CPMenu alloc] initWithTitle:@"Marker Context Menu"];
|
||||
|
||||
// If the marker represents a standard tab stop, allow changing its type
|
||||
if ([_representedObject isKindOfClass:[CPTextTab class]])
|
||||
{
|
||||
var itemLeft = [menu addItemWithTitle:@"Left Tab Stop" action:@selector(changeTypeToLeft:) keyEquivalent:@""],
|
||||
itemCenter = [menu addItemWithTitle:@"Center Tab Stop" action:@selector(changeTypeToCenter:) keyEquivalent:@""],
|
||||
itemRight = [menu addItemWithTitle:@"Right Tab Stop" action:@selector(changeTypeToRight:) keyEquivalent:@""];
|
||||
|
||||
[itemLeft setTarget:self];
|
||||
[itemCenter setTarget:self];
|
||||
[itemRight setTarget:self];
|
||||
|
||||
var align = [_representedObject alignment];
|
||||
if (align === CPLeftTextAlignment) [itemLeft setState:CPOnState];
|
||||
else if (align === CPCenterTextAlignment) [itemCenter setState:CPOnState];
|
||||
else if (align === CPRightTextAlignment) [itemRight setState:CPOnState];
|
||||
|
||||
[menu addItem:[CPMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
// Determine the context-specific delete title
|
||||
var deleteTitle = @"Delete Tab Stop";
|
||||
if ([_representedObject isKindOfClass:[CPString class]])
|
||||
{
|
||||
if (_representedObject === @"CPFirstLineIndent")
|
||||
deleteTitle = @"Delete 1st line indentation marker";
|
||||
else if (_representedObject === @"CPHeadIndent")
|
||||
deleteTitle = @"Delete head indentation marker";
|
||||
else if (_representedObject === @"CPTailIndent")
|
||||
deleteTitle = @"Delete tail indentation marker";
|
||||
}
|
||||
|
||||
var itemDelete = [menu addItemWithTitle:deleteTitle action:@selector(deleteMarker:) keyEquivalent:@""];
|
||||
[itemDelete setTarget:self];
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (void)changeTypeToLeft:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPLeftTextAlignment];
|
||||
}
|
||||
|
||||
- (void)changeTypeToCenter:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPCenterTextAlignment];
|
||||
}
|
||||
|
||||
- (void)changeTypeToRight:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPRightTextAlignment];
|
||||
}
|
||||
|
||||
- (void)_changeAlignment:(CPTextAlignment)alignment
|
||||
{
|
||||
if (![_representedObject isKindOfClass:[CPTextTab class]])
|
||||
return;
|
||||
|
||||
var oldTab = _representedObject;
|
||||
var newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue];
|
||||
|
||||
// Using setRepresentedObject: automatically updates the marker triangle direction
|
||||
[self setRepresentedObject:newTab];
|
||||
|
||||
var client = [_rulerView clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didUpdateMarker:oldTab:)])
|
||||
{
|
||||
[client rulerView:_rulerView didUpdateMarker:self oldTab:oldTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteMarker:(id)sender
|
||||
{
|
||||
var client = [_rulerView clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
|
||||
{
|
||||
[client rulerView:_rulerView didRemoveMarker:self];
|
||||
}
|
||||
[_rulerView removeMarker:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// MARK: - CPRulerView (Pure DOM + Interactive Engine)
|
||||
|
||||
@implementation CPRulerView : CPView
|
||||
{
|
||||
CPScrollView _scrollView @accessors(property=scrollView);
|
||||
CPRulerOrientation _orientation @accessors(property=orientation);
|
||||
CPView _clientView @accessors(property=clientView);
|
||||
|
||||
float _ruleThickness @accessors(property=ruleThickness);
|
||||
float _reservedThicknessForMarkers;
|
||||
CPArray _markers;
|
||||
|
||||
// Dragger variables
|
||||
CPRulerMarker _draggingMarker;
|
||||
CGPoint _dragStartPoint;
|
||||
float _dragStartLocation;
|
||||
}
|
||||
|
||||
- (id)initWithScrollView:(CPScrollView)aScrollView orientation:(CPRulerOrientation)anOrientation
|
||||
{
|
||||
if (self = [super initWithFrame:CGRectMakeZero()])
|
||||
{
|
||||
_scrollView = aScrollView;
|
||||
_orientation = anOrientation;
|
||||
_clientView = [aScrollView documentView];
|
||||
|
||||
_ruleThickness = (anOrientation === CPHorizontalRuler) ? 16.0 : 24.0;
|
||||
_reservedThicknessForMarkers = 0.0;
|
||||
_markers = [];
|
||||
|
||||
[self setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)aFrame
|
||||
{
|
||||
[super setFrame:aFrame];
|
||||
[self updateRuler];
|
||||
}
|
||||
|
||||
// Markers registration
|
||||
- (void)addMarker:(CPRulerMarker)aMarker
|
||||
{
|
||||
if ([_markers containsObject:aMarker])
|
||||
return;
|
||||
|
||||
[_markers addObject:aMarker];
|
||||
[self addSubview:aMarker];
|
||||
[self _positionMarker:aMarker];
|
||||
}
|
||||
|
||||
- (void)removeMarker:(CPRulerMarker)aMarker
|
||||
{
|
||||
[_markers removeObject:aMarker];
|
||||
[aMarker removeFromSuperview];
|
||||
}
|
||||
|
||||
- (void)setMarkers:(CPArray)newMarkers
|
||||
{
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
[[_markers objectAtIndex:i] removeFromSuperview];
|
||||
|
||||
_markers = [newMarkers mutableCopy];
|
||||
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
{
|
||||
var marker = [_markers objectAtIndex:i];
|
||||
[self addSubview:marker];
|
||||
[self _positionMarker:marker];
|
||||
}
|
||||
}
|
||||
|
||||
- (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
{
|
||||
var marker = [_markers objectAtIndex:i];
|
||||
if (CGRectContainsPoint([marker frame], aPoint))
|
||||
return marker;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)_positionMarker:(CPRulerMarker)aMarker
|
||||
{
|
||||
if (!_scrollView)
|
||||
return;
|
||||
|
||||
var clipView = [_scrollView contentView],
|
||||
scrollPoint = [clipView bounds].origin,
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
|
||||
rulerHeight = CGRectGetHeight([self bounds]),
|
||||
rulerWidth = CGRectGetWidth([self bounds]),
|
||||
markerLocation = [aMarker imageValue];
|
||||
|
||||
if (isHorizontal)
|
||||
{
|
||||
var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker
|
||||
y = rulerHeight - 11.0,
|
||||
w = 12.0,
|
||||
h = 12.0;
|
||||
|
||||
// Align the First Line Indent (upper half) and Head Indent (lower half) controls
|
||||
if ([aMarker representedObject] === @"CPFirstLineIndent")
|
||||
{
|
||||
y = 0.0;
|
||||
h = Math.floor(rulerHeight / 2.0);
|
||||
}
|
||||
else if ([aMarker representedObject] === @"CPHeadIndent")
|
||||
{
|
||||
y = Math.floor(rulerHeight / 2.0);
|
||||
h = rulerHeight - y - 1.0; // Subtract 1px to stay cleanly above bottom border
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep normal horizontal markers within the bounds of the ruler to prevent clipping
|
||||
if (x < 0.0)
|
||||
x = 0.0;
|
||||
else if (x + 12.0 > rulerWidth)
|
||||
x = rulerWidth - 12.0;
|
||||
}
|
||||
|
||||
[aMarker setFrame:CGRectMake(x, y, w, h)];
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = rulerWidth - 11.0,
|
||||
y = markerLocation - scrollPoint.y - 6.0;
|
||||
|
||||
// Keep vertical marker within the bounds of the ruler to prevent clipping
|
||||
if (y < 0.0)
|
||||
y = 0.0;
|
||||
else if (y + 12.0 > rulerHeight)
|
||||
y = rulerHeight - 12.0;
|
||||
|
||||
[aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Interaction Handlers
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
var locationInWindow = [anEvent locationInWindow],
|
||||
localPoint = [self convertPoint:locationInWindow fromView:nil],
|
||||
clipView = [_scrollView contentView],
|
||||
scrollPoint = [clipView bounds].origin,
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
|
||||
|
||||
var rulerLocation = isHorizontal ? (localPoint.x + scrollPoint.x) : (localPoint.y + scrollPoint.y);
|
||||
|
||||
// 1. Check if clicked an existing marker
|
||||
var clickedMarker = [self _markerAtPoint:localPoint];
|
||||
if (clickedMarker)
|
||||
{
|
||||
_draggingMarker = clickedMarker;
|
||||
_dragStartPoint = localPoint;
|
||||
_dragStartLocation = [_draggingMarker imageValue];
|
||||
}
|
||||
// 2. Otherwise, create a new marker dynamically where the user clicked
|
||||
else
|
||||
{
|
||||
var newMarker = [[CPRulerMarker alloc] initWithRulerView:self
|
||||
markerLocation:rulerLocation
|
||||
imageValue:rulerLocation
|
||||
representedObject:nil];
|
||||
[self addMarker:newMarker];
|
||||
|
||||
// Notify the client view (e.g., CPTextView) that a new marker was added
|
||||
var client = [self clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)])
|
||||
[client rulerView:self didAddMarker:newMarker];
|
||||
|
||||
_draggingMarker = newMarker;
|
||||
_dragStartPoint = localPoint;
|
||||
_dragStartLocation = rulerLocation;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(CPEvent)anEvent
|
||||
{
|
||||
if (!_draggingMarker)
|
||||
return;
|
||||
|
||||
var locationInWindow = [anEvent locationInWindow],
|
||||
localPoint = [self convertPoint:locationInWindow fromView:nil],
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
|
||||
|
||||
var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y),
|
||||
newLocation = _dragStartLocation + delta;
|
||||
|
||||
if (newLocation < 0) newLocation = 0;
|
||||
|
||||
[_draggingMarker setImageValue:newLocation];
|
||||
|
||||
// Smoothly redraw ruler and margin bounds on every drag step
|
||||
[self updateRuler];
|
||||
|
||||
// Check if dragged off the ruler (more than 15px off the boundary)
|
||||
var draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
|
||||
|
||||
if (draggedOff)
|
||||
{
|
||||
// Visual feedback: Dim the handle to 40% and turn the triangle icon gray
|
||||
[_draggingMarker setAlphaValue:0.4];
|
||||
[[_draggingMarker label] setTextColor:[CPColor grayColor]];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore standard styling when dragged back into the active strip
|
||||
[_draggingMarker setAlphaValue:1.0];
|
||||
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
}
|
||||
|
||||
// Notify the CPTextView that the marker coordinates shifted
|
||||
var client = [self clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)])
|
||||
[client rulerView:self didMoveMarker:_draggingMarker];
|
||||
}
|
||||
|
||||
- (void)mouseUp:(CPEvent)anEvent
|
||||
{
|
||||
if (!_draggingMarker)
|
||||
return;
|
||||
|
||||
var localPoint = [self convertPoint:[anEvent locationInWindow] fromView:nil],
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
|
||||
|
||||
// If dragged more than 15 pixels off the ruler, delete the marker
|
||||
draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
|
||||
|
||||
if (draggedOff)
|
||||
{
|
||||
var client = [self clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
|
||||
[client rulerView:self didRemoveMarker:_draggingMarker];
|
||||
|
||||
[self removeMarker:_draggingMarker];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure marker style is fully restored if not deleted
|
||||
[_draggingMarker setAlphaValue:1.0];
|
||||
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
}
|
||||
|
||||
_draggingMarker = nil;
|
||||
[self updateRuler];
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: DOM Layout Builder
|
||||
|
||||
- (void)updateRuler
|
||||
{
|
||||
// Wipe subviews to redraw the dynamic visible tick lines/numbers
|
||||
[self setSubviews:@[]];
|
||||
|
||||
if (!_scrollView)
|
||||
return;
|
||||
|
||||
var clipView = [_scrollView contentView],
|
||||
scrollBounds = [clipView bounds],
|
||||
scrollPoint = scrollBounds.origin,
|
||||
visibleSize = scrollBounds.size,
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
|
||||
|
||||
if (isHorizontal)
|
||||
{
|
||||
var start = Math.floor(scrollPoint.x / 10) * 10,
|
||||
end = scrollPoint.x + visibleSize.width,
|
||||
rulerHeight = CGRectGetHeight([self bounds]),
|
||||
rulerWidth = CGRectGetWidth([self bounds]);
|
||||
|
||||
// Draw solid horizontal bottom border (pure, razor-sharp CSS DOM view)
|
||||
var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, rulerWidth, 1)];
|
||||
[bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
|
||||
[self addSubview:bottomBorder];
|
||||
|
||||
// Find indent markers to determine background highlight boundaries
|
||||
var firstLineMarker = nil,
|
||||
headMarker = nil;
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
{
|
||||
var m = [_markers objectAtIndex:i];
|
||||
if ([m representedObject] === @"CPFirstLineIndent")
|
||||
firstLineMarker = m;
|
||||
else if ([m representedObject] === @"CPHeadIndent")
|
||||
headMarker = m;
|
||||
}
|
||||
|
||||
var halfHeight = Math.floor(rulerHeight / 2.0);
|
||||
|
||||
// Draw First Line Indent background - top half (lighter gray)
|
||||
if (firstLineMarker)
|
||||
{
|
||||
var firstLineX = [firstLineMarker imageValue] - scrollPoint.x;
|
||||
if (firstLineX > 0)
|
||||
{
|
||||
var firstLineBg = [[CPView alloc] initWithFrame:CGRectMake(0, 0, firstLineX, halfHeight)];
|
||||
[firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.93 alpha:1.0]];
|
||||
[self addSubview:firstLineBg];
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Head Indent background - bottom half (slightly darker gray)
|
||||
if (headMarker)
|
||||
{
|
||||
var headX = [headMarker imageValue] - scrollPoint.x;
|
||||
if (headX > 0)
|
||||
{
|
||||
var headBg = [[CPView alloc] initWithFrame:CGRectMake(0, halfHeight, headX, rulerHeight - halfHeight - 1.0)];
|
||||
[headBg setBackgroundColor:[CPColor colorWithWhite:0.86 alpha:1.0]];
|
||||
[self addSubview:headBg];
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruler tick lines and labels on top of shaded areas
|
||||
for (var val = start; val <= end; val += 10)
|
||||
{
|
||||
if (val < 0) continue;
|
||||
|
||||
var screenX = val - scrollPoint.x,
|
||||
isMajor = (val % 50 === 0),
|
||||
tickHeight = isMajor ? 8.0 : 4.0,
|
||||
tickY = rulerHeight - tickHeight - 1.0;
|
||||
|
||||
// Tick mark CSS line view
|
||||
var tick = [[CPView alloc] initWithFrame:CGRectMake(screenX, tickY, 1.0, tickHeight)];
|
||||
[tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]];
|
||||
[self addSubview:tick];
|
||||
|
||||
// Unit label
|
||||
if (isMajor)
|
||||
{
|
||||
var labelX = screenX - 20.0,
|
||||
alignment = CPCenterTextAlignment;
|
||||
|
||||
// Adjust label frame and alignment if it lands near left/right bounds
|
||||
if (labelX < 0.0)
|
||||
{
|
||||
labelX = Math.max(0.0, screenX);
|
||||
alignment = CPLeftTextAlignment;
|
||||
}
|
||||
else if (labelX + 40.0 > rulerWidth)
|
||||
{
|
||||
labelX = rulerWidth - 40.0;
|
||||
alignment = CPRightTextAlignment;
|
||||
}
|
||||
|
||||
var label = [[CPTextField alloc] initWithFrame:CGRectMake(labelX, 1.0, 40.0, 12.0)];
|
||||
[label setStringValue:[CPString stringWithFormat:@"%d", val]];
|
||||
[label setFont:[CPFont systemFontOfSize:8.0]];
|
||||
[label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];
|
||||
[label setAlignment:alignment];
|
||||
[self addSubview:label];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Vertical Ruler
|
||||
var start = Math.floor(scrollPoint.y / 10) * 10,
|
||||
end = scrollPoint.y + visibleSize.height,
|
||||
rulerHeight = CGRectGetHeight([self bounds]),
|
||||
rulerWidth = CGRectGetWidth([self bounds]);
|
||||
|
||||
// Draw solid vertical right border (pure DOM)
|
||||
var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, rulerHeight)];
|
||||
[rightBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
|
||||
[self addSubview:rightBorder];
|
||||
|
||||
for (var val = start; val <= end; val += 10)
|
||||
{
|
||||
if (val < 0) continue;
|
||||
|
||||
var screenY = val - scrollPoint.y,
|
||||
isMajor = (val % 50 === 0),
|
||||
tickWidth = isMajor ? 8.0 : 4.0,
|
||||
tickX = rulerWidth - tickWidth - 1.0;
|
||||
|
||||
// Tick mark CSS line view
|
||||
var tick = [[CPView alloc] initWithFrame:CGRectMake(tickX, screenY, tickWidth, 1.0)];
|
||||
[tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]];
|
||||
[self addSubview:tick];
|
||||
|
||||
// Unit label
|
||||
if (isMajor)
|
||||
{
|
||||
var labelY = screenY - 6.0;
|
||||
|
||||
// Adjust label frame if it lands near top/bottom bounds
|
||||
if (labelY < 0.0)
|
||||
labelY = 0.0;
|
||||
else if (labelY + 12.0 > rulerHeight)
|
||||
labelY = rulerHeight - 12.0;
|
||||
|
||||
var label = [[CPTextField alloc] initWithFrame:CGRectMake(1.0, labelY, rulerWidth - 12.0, 12.0)];
|
||||
[label setStringValue:[CPString stringWithFormat:@"%d", val]];
|
||||
[label setFont:[CPFont systemFontOfSize:8.0]];
|
||||
[label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];
|
||||
[label setAlignment:CPRightTextAlignment];
|
||||
[self addSubview:label];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reposition and display active markers
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
{
|
||||
var marker = [_markers objectAtIndex:i];
|
||||
if ([marker superview] !== self)
|
||||
[self addSubview:marker];
|
||||
[self _positionMarker:marker];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -89,8 +89,8 @@ CPLineMovesUp = 4;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
// MARK: -
|
||||
// MARK: Init methods
|
||||
|
||||
- (id)initWithContainerSize:(CGSize)aSize
|
||||
{
|
||||
@@ -118,8 +118,8 @@ CPLineMovesUp = 4;
|
||||
[_layoutManager addTextContainer:self];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setter methods
|
||||
// MARK: -
|
||||
// MARK: Setter methods
|
||||
|
||||
- (void)setContainerSize:(CGSize)someSize
|
||||
{
|
||||
@@ -148,27 +148,58 @@ 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);
|
||||
var newSize = CGSizeMake(_size.width, _size.height);
|
||||
|
||||
if (_widthTracksTextView)
|
||||
newSize.width = [_textView frame].size.width;
|
||||
|
||||
if (_heightTracksTextView)
|
||||
newSize.height = [_textView frame].size.height;
|
||||
|
||||
[self setContainerSize:newSize];
|
||||
}
|
||||
@@ -177,7 +208,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 +218,7 @@ CPLineMovesUp = 4;
|
||||
|
||||
if (_textView)
|
||||
{
|
||||
[self setWidthTracksTextView:_widthTracksTextView]; // We only support width
|
||||
[self _updateFrameObserver];
|
||||
[_textView setTextContainer:self];
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,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 +99,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
- (void)setDelegate:(id <CPTextStorageDelegate>)aDelegate
|
||||
{
|
||||
@@ -121,8 +121,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Layout manager methods
|
||||
// MARK: -
|
||||
// MARK: Layout manager methods
|
||||
|
||||
- (void)addLayoutManager:(CPLayoutManager)aManager
|
||||
{
|
||||
@@ -148,8 +148,8 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Editing methods
|
||||
// MARK: -
|
||||
// MARK: Editing methods
|
||||
|
||||
- (void)processEditing
|
||||
{
|
||||
|
||||
+737
-314
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
|
||||
@@ -51,8 +54,8 @@ var CPSystemTypesetterFactory,
|
||||
@implementation CPTypesetter : CPObject
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
@@ -119,8 +122,8 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (id)sharedInstance
|
||||
{
|
||||
@@ -135,31 +138,34 @@ var CPSystemTypesetterFactory,
|
||||
return [_layoutManager textContainers];
|
||||
}
|
||||
|
||||
// Retrieves correct CPTextTab stop accounting for CPArray properties
|
||||
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
|
||||
{
|
||||
var tabStops = [_currentParagraph tabStops];
|
||||
|
||||
if (!tabStops)
|
||||
tabStops = [CPParagraphStyle _defaultTabStops];
|
||||
tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops];
|
||||
|
||||
var l = tabStops.length;
|
||||
var l = [tabStops count];
|
||||
|
||||
if (aWidth > tabStops[l - 1]._location)
|
||||
if (l === 0)
|
||||
return nil;
|
||||
|
||||
for (var i = l - 1; i >= 0; i--)
|
||||
// Find the first tab stop that is strictly greater than the current width
|
||||
for (var i = 0; i < l; i++)
|
||||
{
|
||||
if (aWidth > tabStops[i]._location)
|
||||
{
|
||||
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];
|
||||
// If aWidth exceeds the last tab stop, dynamically calculate the next
|
||||
// tab location using the default tab interval.
|
||||
var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
|
||||
var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval;
|
||||
|
||||
return nil;
|
||||
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
|
||||
}
|
||||
|
||||
- (BOOL)_flushRange:(CPRange)lineRange
|
||||
@@ -263,6 +269,14 @@ 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)
|
||||
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
|
||||
else if ([_layoutManager extraLineFragmentTextContainer])
|
||||
@@ -289,12 +303,94 @@ 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;
|
||||
}
|
||||
|
||||
// Calculate the right wrapping margin based on tail indent
|
||||
var tailIndent = [_currentParagraph tailIndent];
|
||||
if (tailIndent > 0.0)
|
||||
rightMargin = tailIndent;
|
||||
else if (tailIndent < 0.0)
|
||||
rightMargin = containerSizeWidth + tailIndent;
|
||||
else
|
||||
rightMargin = containerSizeWidth;
|
||||
|
||||
// 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,15 +403,22 @@ 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++;
|
||||
|
||||
// We are processing characters, so we are no longer at the start of a physical line
|
||||
isStartOfPhysicalLine = NO;
|
||||
|
||||
var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
|
||||
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
|
||||
|
||||
@@ -356,15 +459,68 @@ var CPSystemTypesetterFactory,
|
||||
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 (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 += 28.0; // standard fallback spacer
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 32: // ' '
|
||||
wrapRange = CPMakeRangeCopy(lineRange);
|
||||
wrapWidth = rangeWidth;
|
||||
wrapRange._height = _lineHeight;
|
||||
wrapRange._base = _lineBase;
|
||||
|
||||
// Optimization: Start measuring from the next character to avoid O(n^2)
|
||||
// string width calculation within a line since spaces do not carry ligatures or kerning.
|
||||
// Only reset the measuring range if the next character is NOT another space.
|
||||
// This prevents compounded subpixel rounding errors with contiguous spaces.
|
||||
if (theString.charCodeAt(glyphIndex + 1) !== 32)
|
||||
{
|
||||
currentAnchor = rangeWidth;
|
||||
measuringRange = CPMakeRange(glyphIndex + 1, 0);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 10:
|
||||
@@ -375,7 +531,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)
|
||||
{
|
||||
@@ -419,12 +576,17 @@ var CPSystemTypesetterFactory,
|
||||
containerSizeHeight = containerSize.height;
|
||||
}
|
||||
|
||||
lineOrigin.x = 0;
|
||||
// If this is a soft wrap (isWordWrapped), next line gets headIndent.
|
||||
// If it was a paragraph return, it gets firstLineHeadIndent.
|
||||
isFirstLineOfParagraph = !isWordWrapped;
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
|
||||
numLines++;
|
||||
isNewline = NO;
|
||||
_lineFragments = [];
|
||||
_lineHeight = 0;
|
||||
_lineBase = ascent;
|
||||
_lineBase = 0;
|
||||
isStartOfPhysicalLine = YES;
|
||||
}
|
||||
|
||||
isTabStop = NO;
|
||||
|
||||
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,7 @@
|
||||
@import "CPColor.j"
|
||||
@import "CPGraphics.j"
|
||||
@import "CPFontManager.j"
|
||||
@import "_CPTableTextAttachment.j"
|
||||
|
||||
@global CPForegroundColorAttributeName
|
||||
@global CPBackgroundColorAttributeName
|
||||
@@ -43,6 +44,11 @@
|
||||
@global CPJustifiedTextAlignment
|
||||
@global CPNaturalTextAlignment
|
||||
|
||||
@global CPLeftTabStopType
|
||||
@global CPRightTabStopType
|
||||
@global CPCenterTabStopType
|
||||
@global CPDecimalTabStopType
|
||||
|
||||
var PAPERSIZE = @"PaperSize",
|
||||
LEFTMARGIN = @"LeftMargin",
|
||||
RIGHTMARGIN = @"RightMargin",
|
||||
@@ -64,8 +70,8 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
CPColor ulColor;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
|
||||
+ (CPString)produceRTF:(CPAttributedString)aText documentAttributes:(CPDictionary)dict
|
||||
@@ -76,19 +82,14 @@ 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];
|
||||
@@ -98,7 +99,6 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
return self;
|
||||
}
|
||||
|
||||
// private stuff follows
|
||||
- (CPString)fontTable
|
||||
{
|
||||
if (![fontDict count])
|
||||
@@ -313,7 +313,6 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
break;
|
||||
}
|
||||
|
||||
// write first line indent and left indent
|
||||
var twips = _points2twips([paraStyle firstLineHeadIndent]);
|
||||
|
||||
if (twips != 0.0)
|
||||
@@ -351,26 +350,29 @@ 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])];
|
||||
}
|
||||
|
||||
return headerString;
|
||||
@@ -380,6 +382,205 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
attributes:(CPDictionary) attributes
|
||||
paragraphStart:(BOOL) first
|
||||
{
|
||||
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 +593,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 +607,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 +618,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 +657,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 +688,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";
|
||||
}
|
||||
}
|
||||
@@ -522,11 +706,9 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
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
|
||||
|
||||
if (!first)
|
||||
{
|
||||
@@ -562,10 +744,9 @@ 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,
|
||||
@@ -577,10 +758,14 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
substring = [string substringWithRange:currRange];
|
||||
runString = [self runStringForString:substring
|
||||
attributes:attributes
|
||||
paragraphStart:YES];
|
||||
paragraphStart:paragraphStart];
|
||||
|
||||
result += runString;
|
||||
first = NO;
|
||||
|
||||
if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n')
|
||||
paragraphStart = YES;
|
||||
else
|
||||
paragraphStart = NO;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -598,9 +783,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,557 @@
|
||||
/* _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"
|
||||
@import "CPTextView.j"
|
||||
@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
|
||||
+3
-3
@@ -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
|
||||
|
||||
+23
-5
@@ -169,8 +169,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
[self addSubview:_tokenScrollView];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Delegate methods
|
||||
// MARK: -
|
||||
// MARK: Delegate methods
|
||||
|
||||
/*!
|
||||
Set the delegate of the receiver
|
||||
@@ -349,6 +349,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;
|
||||
@@ -453,8 +459,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
{
|
||||
[_tokenScrollView documentView]._DOMElement.appendChild(element);
|
||||
|
||||
//post CPControlTextDidBeginEditingNotification
|
||||
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
|
||||
// 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]];
|
||||
|
||||
[[CPRunLoop mainRunLoop] performBlock:function()
|
||||
{
|
||||
@@ -498,6 +504,8 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
|
||||
[self _resignFirstKeyResponder];
|
||||
|
||||
_isEditing = NO;
|
||||
|
||||
if (_shouldNotifyTarget)
|
||||
{
|
||||
_shouldNotifyTarget = NO;
|
||||
@@ -551,7 +559,7 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
CPTokenFieldCachedDragFunction = nil;
|
||||
|
||||
document.body.ondrag = CPTokenFieldCachedDragFunction;
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1032,6 +1040,16 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
CPTokenFieldTextDidChangeValue = [self stringValue];
|
||||
#endif
|
||||
|
||||
// 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.
|
||||
// If it was NO we want to leave it at NO however and only enable it in insertText:. This is what
|
||||
|
||||
@@ -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,697 @@
|
||||
/*
|
||||
* 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"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingContentArray
|
||||
{
|
||||
return [CPSet setWithObjects:@"content"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsert
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
- (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];
|
||||
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
_contentObject = value;
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self preservesSelection])
|
||||
[self __setSelectedObjects:oldSelectedObjects];
|
||||
else
|
||||
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
}
|
||||
|
||||
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
|
||||
- (id)contentArray { return _contentObject; }
|
||||
- (id)arrangedObjects { return _arrangedObjects; }
|
||||
|
||||
- (void)rearrangeObjects
|
||||
{
|
||||
[self willChangeValueForKey:@"arrangedObjects"];
|
||||
[self _rearrangeObjects];
|
||||
[self didChangeValueForKey:@"arrangedObjects"];
|
||||
}
|
||||
|
||||
- (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
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (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 willChangeValueForKey:@"selectionIndexPaths"];
|
||||
|
||||
_selectionIndexPaths = [newPaths copy];
|
||||
|
||||
var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"];
|
||||
|
||||
if (binderClass)
|
||||
{
|
||||
var binding = [binderClass getBinding:@"selectionIndexPaths" forObject:self];
|
||||
|
||||
if (binding)
|
||||
[binding reverseSetValueFor:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
[self didChangeValueForKey:@"selectionIndexPaths"];
|
||||
|
||||
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++)
|
||||
[objects addObject:[[nodes objectAtIndex:i] representedObject]];
|
||||
|
||||
return 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 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"];
|
||||
}
|
||||
|
||||
- (void)remove:(id)sender
|
||||
{
|
||||
[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths];
|
||||
}
|
||||
|
||||
- (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
|
||||
}
|
||||
|
||||
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[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"];
|
||||
}
|
||||
|
||||
- (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
|
||||
+83
-39
@@ -22,13 +22,12 @@
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
|
||||
@implementation CPTreeNode : CPObject
|
||||
{
|
||||
id _representedObject @accessors(readonly, property=representedObject);
|
||||
|
||||
CPTreeNode _parentNode @accessors(readonly, property=parentNode);
|
||||
id _representedObject @accessors(property=representedObject);
|
||||
CPTreeNode _parentNode @accessors(property=parentNode);
|
||||
CPMutableArray _childNodes;
|
||||
}
|
||||
|
||||
@@ -52,36 +51,35 @@
|
||||
|
||||
- (CPIndexPath)indexPath
|
||||
{
|
||||
if (_parentNode != nil)
|
||||
// If we have a parent, calculate path based on parent's path + our index
|
||||
if (_parentNode)
|
||||
{
|
||||
var path;
|
||||
var index;
|
||||
|
||||
index = [[_parentNode childNodes] indexOfObject:self];
|
||||
path = [_parentNode indexPath];
|
||||
|
||||
if (path != nil)
|
||||
{
|
||||
return [path indexPathByAddingIndex:index];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [CPIndexPath indexPathWithIndex:index];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
// Search the parent's child nodes, not our own!
|
||||
var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
|
||||
|
||||
// If the parent is the root (and technically has no path itself in some implementations),
|
||||
// we might get nil. Handle that gracefully.
|
||||
var parentPath = [_parentNode indexPath];
|
||||
|
||||
if (parentPath)
|
||||
return [parentPath indexPathByAddingIndex:index];
|
||||
|
||||
return [CPIndexPath indexPathWithIndex:index];
|
||||
}
|
||||
|
||||
// If we are the root, we don't have an index path in the context of a tree controller usually,
|
||||
// or we are [] (empty path). Returning nil is acceptable for the absolute root.
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isLeaf
|
||||
{
|
||||
return [_childNodes count] <= 0;
|
||||
return [_childNodes count] == 0;
|
||||
}
|
||||
|
||||
- (CPArray)childNodes
|
||||
{
|
||||
// Return a copy to prevent external modification without KVC
|
||||
return [_childNodes copy];
|
||||
}
|
||||
|
||||
@@ -90,18 +88,29 @@
|
||||
return [self mutableArrayValueForKey:@"childNodes"];
|
||||
}
|
||||
|
||||
- (void)insertObject:(id)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
|
||||
// MARK: - KVC Compliance Methods
|
||||
|
||||
aTreeNode._parentNode = self;
|
||||
- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
// Optional: Auto-detach from old parent if strictly moving nodes
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
|
||||
{
|
||||
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
|
||||
}
|
||||
|
||||
// Direct ivar access is allowed here since we are inside the class implementation
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
|
||||
[_childNodes insertObject:aTreeNode atIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
[_childNodes objectAtIndex:anIndex]._parentNode = nil;
|
||||
var node = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
if ([node isKindOfClass:[CPTreeNode class]])
|
||||
node._parentNode = nil;
|
||||
|
||||
[_childNodes removeObjectAtIndex:anIndex];
|
||||
}
|
||||
@@ -110,17 +119,34 @@
|
||||
{
|
||||
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
oldTreeNode._parentNode = nil;
|
||||
aTreeNode._parentNode = self;
|
||||
if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
oldTreeNode._parentNode = nil;
|
||||
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
|
||||
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return _childNodes[anIndex];
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (CPInteger)count
|
||||
{
|
||||
return [_childNodes count];
|
||||
}
|
||||
|
||||
- (id)objectAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
// MARK: - Utility
|
||||
|
||||
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
|
||||
{
|
||||
[_childNodes sortUsingDescriptors:sortDescriptors];
|
||||
@@ -129,25 +155,39 @@
|
||||
return;
|
||||
|
||||
var count = [_childNodes count];
|
||||
|
||||
while (count--)
|
||||
[_childNodes[count] sortWithSortDescriptors:sortDescriptors recursively:YES];
|
||||
{
|
||||
var child = [_childNodes objectAtIndex:count];
|
||||
if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
|
||||
[child sortWithSortDescriptors:sortDescriptors recursively:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (CPTreeNode)descendantNodeAtIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
var index = 0,
|
||||
count = [indexPath length],
|
||||
node = self;
|
||||
if (!indexPath || [indexPath length] == 0)
|
||||
return self;
|
||||
|
||||
for (; index < count; ++index)
|
||||
node = [node objectInChildNodesAtIndex:[indexPath indexAtPosition:index]];
|
||||
var node = self,
|
||||
length = [indexPath length];
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var index = [indexPath indexAtPosition:i],
|
||||
count = [node count];
|
||||
|
||||
if (index >= count || index < 0)
|
||||
return nil;
|
||||
|
||||
node = [node objectAtIndex:index];
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// Coding implementation remains correct
|
||||
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
|
||||
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
|
||||
@@ -163,6 +203,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
|
||||
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
|
||||
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
|
||||
|
||||
// Safety check to ensure decoding gave us a CPArray
|
||||
if (!_childNodes)
|
||||
_childNodes = [[CPMutableArray alloc] init];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
+93
-17
@@ -602,7 +602,8 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
|
||||
// We will have to adjust the z-index of all views starting at this index.
|
||||
var count = _subviews.length,
|
||||
lastWindow;
|
||||
lastWindow,
|
||||
isNewAddOrMove = aSubview._superview !== self;
|
||||
|
||||
// Dirty the key view loop, in case the window wants to auto recalculate it
|
||||
[[self window] _dirtyKeyViewLoop];
|
||||
@@ -661,6 +662,11 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var origin = aSubview._frame.origin;
|
||||
CPDOMDisplayServerSetStyleLeftTop(aSubview._DOMElement, _boundsTransform, origin.x, origin.y);
|
||||
#endif
|
||||
|
||||
[aSubview setNextResponder:self];
|
||||
[aSubview _scaleSizeUnitSquareToSize:[self _hierarchyScaleSize]];
|
||||
|
||||
@@ -673,6 +679,9 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
if (!_window && lastWindow)
|
||||
[aSubview _setWindow:nil];
|
||||
|
||||
if (isNewAddOrMove)
|
||||
[aSubview _postViewDidAppearNotification];
|
||||
|
||||
// This method might be called before we are fully unarchived, in which case the theme state isn't set up yet
|
||||
// and none of the below matters anyhow.
|
||||
if (_themeState)
|
||||
@@ -735,6 +744,7 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
// If the view is not hidden and one of its ancestors is hidden,
|
||||
// notify the view that it is now unhidden.
|
||||
[self _setSuperview:nil];
|
||||
[self _postViewDidDisappearNotification];
|
||||
|
||||
[self _notifyWindowDidResignKey];
|
||||
[self _notifyViewDidResignFirstResponder];
|
||||
@@ -1405,6 +1415,9 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
_inverseBoundsTransform = nil;
|
||||
}
|
||||
|
||||
if (_layer)
|
||||
[_layer _owningViewBoundsChanged];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
var index = _subviews.length;
|
||||
|
||||
@@ -1461,6 +1474,9 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
origin.y *= size.height / frameSize.height;
|
||||
}
|
||||
|
||||
if (_layer)
|
||||
[_layer _owningViewBoundsChanged];
|
||||
|
||||
if (_postsBoundsChangedNotifications && !_inhibitFrameAndBoundsChangedNotifications)
|
||||
[CachedNotificationCenter postNotificationName:CPViewBoundsDidChangeNotification object:self];
|
||||
|
||||
@@ -1719,11 +1735,7 @@ var CPViewHighDPIDrawingEnabled = YES;
|
||||
|
||||
_superview = aSuperview;
|
||||
|
||||
if (hasOldSuperview)
|
||||
[self _postViewDidDisappearNotification];
|
||||
|
||||
if (hasNewSuperview)
|
||||
[self _postViewDidAppearNotification];
|
||||
// Notifications are now posted manually from _insertSubview and _removeFromSuperview
|
||||
}
|
||||
|
||||
- (void)_recursiveLostHiddenAncestor
|
||||
@@ -3214,7 +3226,8 @@ setBoundsOrigin:
|
||||
{
|
||||
_layer._owningView = nil;
|
||||
#if PLATFORM(DOM)
|
||||
_DOMElement.removeChild(_layer._DOMElement);
|
||||
if (_layer._DOMElement && _layer._DOMElement.parentNode === _DOMElement)
|
||||
_DOMElement.removeChild(_layer._DOMElement);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -3222,33 +3235,56 @@ setBoundsOrigin:
|
||||
|
||||
if (_layer)
|
||||
{
|
||||
var bounds = CGRectMakeCopy([self bounds]);
|
||||
|
||||
[_layer _setOwningView:self];
|
||||
[_layer setFrame:[self bounds]]; // Sync layer frame with view bounds
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_layer._DOMElement.style.zIndex = 100;
|
||||
|
||||
_DOMElement.appendChild(_layer._DOMElement);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the core animation layer used by the receiver.
|
||||
Returns the core animation layer used by the receiver and creates one if necessary.
|
||||
*/
|
||||
- (CALayer)layer
|
||||
{
|
||||
if (_wantsLayer && !_layer)
|
||||
{
|
||||
var layer = [[CALayer alloc] init];
|
||||
[self setLayer:layer];
|
||||
[self setNeedsLayout:YES];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
return _layer;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the receiver wants a core animation layer.
|
||||
@param \c YES means the receiver wants a layer.
|
||||
@param aFlag \c YES means the receiver wants a layer.
|
||||
*/
|
||||
- (void)setWantsLayer:(BOOL)aFlag
|
||||
{
|
||||
_wantsLayer = !!aFlag;
|
||||
aFlag = !!aFlag;
|
||||
|
||||
if (_wantsLayer === aFlag)
|
||||
return;
|
||||
|
||||
_wantsLayer = aFlag;
|
||||
|
||||
if (_wantsLayer)
|
||||
{
|
||||
// Accessing the layer will create it if it doesn't exist.
|
||||
[self layer];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the layer if we no longer want it.
|
||||
if (_layer)
|
||||
[self setLayer:nil];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -3260,6 +3296,38 @@ setBoundsOrigin:
|
||||
return _wantsLayer;
|
||||
}
|
||||
|
||||
/*!
|
||||
Rotates the view's visual representation by a given angle (in degrees) around its center point.
|
||||
|
||||
This method achieves the rotation by applying a transform directly to the view's backing CALayer.
|
||||
Because this is a direct layer manipulation, the view's own `frame` property is not updated to
|
||||
reflect the new visual bounding box. Consequently, a `CPViewBoundsDidChangeNotification` is
|
||||
**not** posted by this method. Note that this is a deviation from Cocoa's behavior.
|
||||
|
||||
This method requires the view to be layer-backed. If the view is not
|
||||
already layer-backed, this method will automatically set wantsLayer to YES.
|
||||
@param angle The angle in degrees to rotate the view.
|
||||
*/
|
||||
- (void)rotateByAngle:(CGFloat)angle
|
||||
{
|
||||
// Ensure the view is layer-backed
|
||||
[self setWantsLayer:YES];
|
||||
|
||||
var layer = [self layer];
|
||||
|
||||
if (!layer)
|
||||
return;
|
||||
|
||||
// Convert degrees to radians for the transform
|
||||
var radians = angle * Math.PI / 180.0;
|
||||
|
||||
var rotationTransform = CGAffineTransformMakeRotation(radians);
|
||||
var currentTransform = [layer affineTransform];
|
||||
var newTransform = CGAffineTransformConcat(currentTransform, rotationTransform);
|
||||
|
||||
[layer setAffineTransform:newTransform];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -3299,7 +3367,7 @@ setBoundsOrigin:
|
||||
|
||||
@implementation CPView (Theming)
|
||||
|
||||
#pragma mark Override
|
||||
// MARK: Override
|
||||
|
||||
- (BOOL)setThemeState:(ThemeState)aState
|
||||
{
|
||||
@@ -3336,7 +3404,7 @@ setBoundsOrigin:
|
||||
}
|
||||
|
||||
|
||||
#pragma mark First responder
|
||||
// MARK: First responder
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
@@ -3398,7 +3466,7 @@ setBoundsOrigin:
|
||||
[_subviews[count] _notifyWindowDidResignKey];
|
||||
}
|
||||
|
||||
#pragma mark Theme Attributes
|
||||
// MARK: Theme Attributes
|
||||
|
||||
- (void)_setThemeIncludingDescendants:(CPTheme)aTheme
|
||||
{
|
||||
@@ -3765,7 +3833,8 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask",
|
||||
CPViewSizeScaleKey = @"CPViewSizeScaleKey",
|
||||
CPViewIsScaledKey = @"CPViewIsScaledKey",
|
||||
CPViewAppearanceKey = @"CPViewAppearanceKey",
|
||||
CPViewTrackingAreasKey = @"CPViewTrackingAreasKey";
|
||||
CPViewTrackingAreasKey = @"CPViewTrackingAreasKey",
|
||||
CPViewWantsLayerKey = @"CPViewWantsLayerKey";
|
||||
|
||||
@implementation CPView (CPCoding)
|
||||
|
||||
@@ -3868,6 +3937,10 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask",
|
||||
_opacity = 1.0;
|
||||
|
||||
[self setBackgroundColor:[aCoder decodeObjectForKey:CPViewBackgroundColorKey]];
|
||||
|
||||
if ([aCoder containsValueForKey:CPViewWantsLayerKey])
|
||||
[self setWantsLayer:[aCoder decodeBoolForKey:CPViewWantsLayerKey]];
|
||||
|
||||
[self _setupViewFlags];
|
||||
|
||||
[self setAppearance:[aCoder decodeObjectForKey:CPViewAppearanceKey]];
|
||||
@@ -3955,6 +4028,9 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask",
|
||||
if (_identifier)
|
||||
[aCoder encodeObject:_identifier forKey:CPReuseIdentifierKey];
|
||||
|
||||
if (_wantsLayer)
|
||||
[aCoder encodeBool:_wantsLayer forKey:CPViewWantsLayerKey];
|
||||
|
||||
[aCoder encodeSize:[self scaleSize] forKey:CPViewScaleKey];
|
||||
[aCoder encodeSize:[self _hierarchyScaleSize] forKey:CPViewSizeScaleKey];
|
||||
[aCoder encodeBool:_isScaled forKey:CPViewIsScaledKey];
|
||||
|
||||
@@ -166,7 +166,7 @@ var CPViewControllerCachedCibs;
|
||||
|
||||
if (!cib)
|
||||
{
|
||||
// if the cib isn't cached yet : fetch it and cache it
|
||||
// if the cib isn't cached yet: fetch it and cache it
|
||||
cib = [[CPCib alloc] initWithCibNamed:_cibName bundle:_cibBundle];
|
||||
[CPViewControllerCachedCibs setObject:cib forKey:_cibName];
|
||||
}
|
||||
@@ -261,6 +261,11 @@ var CPViewControllerCachedCibs;
|
||||
if (_view == nil && [cibOwner isKindOfClass:[CPDocument class]])
|
||||
[self setView:[cibOwner valueForKey:@"view"]];
|
||||
|
||||
// If the view was just loaded, we must set its next responder.
|
||||
// This is the first half of inserting the controller into the responder chain.
|
||||
if (_view)
|
||||
[_view setNextResponder:self];
|
||||
|
||||
if (!_view)
|
||||
{
|
||||
var reason = [CPString stringWithFormat:@"View for %@ could not be loaded from Cib or no view specified. Override loadView to load the view manually.", self];
|
||||
@@ -408,12 +413,28 @@ var CPViewControllerCachedCibs;
|
||||
[self willChangeValueForKey:"isViewLoaded"];
|
||||
|
||||
_view = aView;
|
||||
|
||||
// When the view is set manually, we must set its next responder.
|
||||
if (_view)
|
||||
[_view setNextResponder:self];
|
||||
|
||||
_isViewLoaded = aView != nil;
|
||||
|
||||
if (willChangeIsViewLoaded)
|
||||
[self didChangeValueForKey:"isViewLoaded"];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method nextResponder
|
||||
@discussion The CPViewController implementation of this method returns the superview
|
||||
of the view controller's view. This is the second half of the insertion,
|
||||
completing the chain: view -> viewController -> superview.
|
||||
*/
|
||||
- (id)nextResponder
|
||||
{
|
||||
return [_view superview];
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyNotifiesObserversOfIsViewLoaded
|
||||
{
|
||||
return NO;
|
||||
@@ -465,6 +486,10 @@ var CPViewControllerViewKey = @"CPViewControllerViewKey",
|
||||
if (self)
|
||||
{
|
||||
_view = [aCoder decodeObjectForKey:CPViewControllerViewKey];
|
||||
// When the view is unarchived, we must also set its next responder.
|
||||
if (_view)
|
||||
[_view setNextResponder:self];
|
||||
|
||||
_title = [aCoder decodeObjectForKey:CPViewControllerTitleKey];
|
||||
_cibName = [aCoder decodeObjectForKey:CPViewControllerCibNameKey];
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ CPVisualEffectStateInactive = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
@@ -74,8 +74,8 @@ CPVisualEffectStateInactive = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CPVisualEffectView API
|
||||
// MARK: -
|
||||
// MARK: CPVisualEffectView API
|
||||
|
||||
/*! Sets the appearance of the CPVisualEffectView.
|
||||
|
||||
@@ -114,8 +114,8 @@ CPVisualEffectStateInactive = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Utilities
|
||||
// MARK: -
|
||||
// MARK: Utilities
|
||||
|
||||
- (void)_setEffectEnabled:(BOOL)shouldEnable
|
||||
{
|
||||
@@ -156,8 +156,8 @@ CPVisualEffectStateInactive = 2;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CPCoding
|
||||
// MARK: -
|
||||
// MARK: CPCoding
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
@@ -183,4 +183,4 @@ CPVisualEffectStateInactive = 2;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@end
|
||||
|
||||
+5
-1
@@ -278,7 +278,11 @@ CPWebViewAppKitScrollMaxPollCount = 3;
|
||||
|
||||
if (_effectiveScrollMode === CPWebViewScrollAppKit)
|
||||
{
|
||||
var visibleRect = [_frameView visibleRect];
|
||||
// Use `[_scrollView documentVisibleRect]` instead of `[_frameView visibleRect]`.
|
||||
// `visibleRect` accounts for window clipping, which collapses to 0x0 if the view is
|
||||
// animated off-screen, resulting in an incorrectly shrunken web layout.
|
||||
var visibleRect = [_scrollView documentVisibleRect];
|
||||
|
||||
[_frameView setFrameSize:CGSizeMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect))];
|
||||
|
||||
// try to get the document size so we can correctly set the frame
|
||||
|
||||
@@ -1266,6 +1266,10 @@ CPTexturedBackgroundWindowMask
|
||||
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_windowView addSubview:_contentView];
|
||||
|
||||
// The window view manages the exact layout of the content view (e.g. offsetting for the toolbar).
|
||||
if ([_windowView respondsToSelector:@selector(tile)])
|
||||
[_windowView tile];
|
||||
|
||||
/*
|
||||
If the initial first responder has been set to something other than
|
||||
the window, set it to the window because it will no longer be valid.
|
||||
@@ -1947,7 +1951,8 @@ CPTexturedBackgroundWindowMask
|
||||
return [[self firstResponder] keyUp:anEvent];
|
||||
|
||||
case CPKeyDown:
|
||||
if ([anEvent charactersIgnoringModifiers] === CPTabCharacter)
|
||||
if ([anEvent charactersIgnoringModifiers] === CPTabCharacter &&
|
||||
!([anEvent modifierFlags] & (CPAlternateKeyMask | CPCommandKeyMask)))
|
||||
{
|
||||
if ([anEvent modifierFlags] & CPShiftKeyMask)
|
||||
[self selectPreviousKeyView:self];
|
||||
@@ -4450,7 +4455,7 @@ var interpolate = function(fromValue, toValue, progress)
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
@implementation CPWindow (CSSTheming)
|
||||
|
||||
@@ -4461,7 +4466,7 @@ var interpolate = function(fromValue, toValue, progress)
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
// MARK: -
|
||||
|
||||
function _CPWindowFullPlatformWindowSessionMake(aWindowView, aContentRect, hasShadow, aLevel)
|
||||
{
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -80,8 +80,8 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark drawing
|
||||
// MARK: -
|
||||
// MARK: drawing
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* CAAnimationGroup.j
|
||||
* AppKit
|
||||
* Created by Daniel Boehringer.
|
||||
* Copyright 2025.
|
||||
*
|
||||
* Implements grouping for Core Animation.
|
||||
*/
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
@import "CAAnimation.j"
|
||||
|
||||
@implementation CAAnimationGroup : CAAnimation
|
||||
{
|
||||
CPArray _animations;
|
||||
}
|
||||
|
||||
+ (id)group
|
||||
{
|
||||
return [[self alloc] init];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_animations = [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setAnimations:(CPArray)anArray
|
||||
{
|
||||
if (_animations === anArray)
|
||||
return;
|
||||
|
||||
_animations = anArray;
|
||||
}
|
||||
|
||||
- (CPArray)animations
|
||||
{
|
||||
return _animations;
|
||||
}
|
||||
|
||||
/*
|
||||
Iterates through children and executes them recursively.
|
||||
This effectively runs all grouped animations concurrently.
|
||||
*/
|
||||
- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments
|
||||
{
|
||||
var count = [_animations count],
|
||||
i = 0;
|
||||
|
||||
for (; i < count; i++)
|
||||
{
|
||||
var animation = [_animations objectAtIndex:i];
|
||||
|
||||
// Recursively call runActionForKey on the child.
|
||||
// If the child is a CABasicAnimation, it will call [anObject addAnimation:...]
|
||||
// If the child is another Group, it will recurse here.
|
||||
if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)])
|
||||
{
|
||||
[animation runActionForKey:aKey object:anObject arguments:arguments];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,12 +1,28 @@
|
||||
@import <Foundation/CPObject.j>
|
||||
|
||||
@import "CAPropertyAnimation.j"
|
||||
|
||||
// Value calculation modes
|
||||
kCAAnimationLinear = @"linear";
|
||||
kCAAnimationDiscrete = @"discrete";
|
||||
kCAAnimationPaced = @"paced";
|
||||
kCAAnimationCubic = @"cubic";
|
||||
kCAAnimationCubicPaced = @"cubicPaced";
|
||||
|
||||
// Rotation Mode Values
|
||||
kCAAnimationRotateAuto = @"auto";
|
||||
kCAAnimationRotateAutoReverse = @"autoReverse";
|
||||
|
||||
@implementation CAKeyframeAnimation : CAPropertyAnimation
|
||||
{
|
||||
CPArray _values @accessors(property=values);
|
||||
CPArray _keyTimes @accessors(property=keyTimes);
|
||||
CPArray _timingFunctions @accessors(property=timingFunctions);
|
||||
id _path @accessors(property=path);
|
||||
CPString _calculationMode @accessors(property=calculationMode);
|
||||
CPString _rotationMode @accessors(property=rotationMode);
|
||||
CPArray _tensionValues @accessors(property=tensionValues);
|
||||
CPArray _continuityValues @accessors(property=continuityValues);
|
||||
CPArray _biasValues @accessors(property=biasValues);
|
||||
}
|
||||
|
||||
- (id)init
|
||||
@@ -16,8 +32,11 @@
|
||||
_values = [CPArray array];
|
||||
_keyTimes = [CPArray array];
|
||||
_timingFunctions = [CPArray array];
|
||||
_tensionValues = [CPArray array];
|
||||
_continuityValues = [CPArray array];
|
||||
_biasValues = [CPArray array];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
@import "CGGeometry.j"
|
||||
@import "CPColor.j"
|
||||
@import "CPView.j"
|
||||
@import "CAMediaTimingFunction.j"
|
||||
|
||||
|
||||
#define DOM(aLayer) aLayer._DOMElement
|
||||
|
||||
@@ -118,6 +120,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
|
||||
|
||||
CGAffineTransform _transformToLayer;
|
||||
CGAffineTransform _transformFromLayer;
|
||||
|
||||
CPMutableDictionary _activeAnimations;
|
||||
}
|
||||
|
||||
@global document
|
||||
@@ -160,6 +164,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
|
||||
|
||||
_sublayers = [];
|
||||
|
||||
_activeAnimations = [CPMutableDictionary dictionary];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_DOMElement = document.createElement("div");
|
||||
|
||||
@@ -977,6 +983,244 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex)
|
||||
return _delegate;
|
||||
}
|
||||
|
||||
/*
|
||||
Adds an animation to the layer.
|
||||
Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint).
|
||||
The animation is exerted by means of periodically applying the keypath on the delegate
|
||||
Only works if the delegate is set!
|
||||
*/
|
||||
- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key
|
||||
{
|
||||
if (!anim) return;
|
||||
|
||||
// --- 1. Handle Animation Groups ---
|
||||
// If it's a group, we simply schedule its children individually.
|
||||
if ([anim respondsToSelector:@selector(animations)] && [anim animations])
|
||||
{
|
||||
var animations = [anim animations],
|
||||
count = [animations count],
|
||||
i = 0;
|
||||
|
||||
for (; i < count; i++)
|
||||
{
|
||||
var child = [animations objectAtIndex:i];
|
||||
|
||||
// Recurse: Add the child animation.
|
||||
// We pass 'nil' for the key so the child's own 'keyPath'
|
||||
// is used as the storage identifier in the dictionary.
|
||||
[self addAnimation:child forKey:nil];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. Determine KeyPath ---
|
||||
var keyPath = key;
|
||||
|
||||
// If the animation object has an explicit keyPath (like CABasicAnimation), use it.
|
||||
if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath])
|
||||
keyPath = [anim keyPath];
|
||||
|
||||
// If we can't determine a property to animate, we must abort.
|
||||
if (!keyPath) return;
|
||||
|
||||
// --- 3. Determine Values ---
|
||||
var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil;
|
||||
|
||||
// If startValue is missing, try to read it from the layer.
|
||||
// We wrap this in a try-catch to prevent crashes if 'keyPath' is invalid.
|
||||
if (startValue == nil)
|
||||
{
|
||||
try {
|
||||
startValue = [[self delegate] valueForKey:keyPath];
|
||||
}
|
||||
catch (e) {
|
||||
// The keyPath was likely invalid (not KVC compliant), abort.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil;
|
||||
|
||||
if (endValue == nil)
|
||||
return;
|
||||
|
||||
var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25;
|
||||
|
||||
// Default to EaseInEaseOut if not specified
|
||||
var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
|
||||
// --- 4. Prepare Context ---
|
||||
var context = {
|
||||
"animation": anim,
|
||||
"keyPath": keyPath,
|
||||
"startValue": startValue,
|
||||
"endValue": endValue,
|
||||
"duration": duration * 1000.0, // ms
|
||||
"timingFunction": timingFunction,
|
||||
"startTime": null,
|
||||
"requestId": null
|
||||
};
|
||||
|
||||
// --- 5. Render Loop ---
|
||||
var _self = self;
|
||||
|
||||
var renderLoop = function(timestamp) {
|
||||
if ([_self _renderAnimationStep:context timestamp:timestamp])
|
||||
context.requestId = window.requestAnimationFrame(renderLoop);
|
||||
else
|
||||
context.requestId = null;
|
||||
};
|
||||
|
||||
// --- 6. Storage & Kickoff ---
|
||||
// Use the keyPath as the identifier if no specific key was provided
|
||||
var storageKey = (key && key.length > 0) ? key : keyPath;
|
||||
|
||||
// Remove any conflicting animation on this specific property/key
|
||||
[self removeAnimationForKey:storageKey];
|
||||
|
||||
context.requestId = window.requestAnimationFrame(renderLoop);
|
||||
[_activeAnimations setObject:context forKey:storageKey];
|
||||
}
|
||||
|
||||
- (void)removeAnimationForKey:(CPString)key
|
||||
{
|
||||
var context = [_activeAnimations objectForKey:key];
|
||||
if (context)
|
||||
{
|
||||
if (context.requestId !== null)
|
||||
window.cancelAnimationFrame(context.requestId);
|
||||
[_activeAnimations removeObjectForKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllAnimations
|
||||
{
|
||||
var keys = [_activeAnimations allKeys],
|
||||
count = [keys count];
|
||||
while (count--)
|
||||
[self removeAnimationForKey:[keys objectAtIndex:count]];
|
||||
}
|
||||
|
||||
/*
|
||||
Solves Cubic Bezier for t.
|
||||
p1, p2 are the control points (x,y). p0 is 0,0, p3 is 1,1.
|
||||
This is a simplified solver for standard Core Animation timing functions.
|
||||
*/
|
||||
- (float)_solveBezier:(float)t forTimingFunction:(CAMediaTimingFunction)tf
|
||||
{
|
||||
if (!tf) return t;
|
||||
|
||||
// Linear optimization
|
||||
if (tf === [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear])
|
||||
return t;
|
||||
|
||||
var points = [tf controlPoints]; // [c1x, c1y, c2x, c2y]
|
||||
var p1x = points[0], p1y = points[1],
|
||||
p2x = points[2], p2y = points[3];
|
||||
|
||||
// Simple polynomial evaluation (De Casteljau's algorithm/Cubic formula subset)
|
||||
// Since we are usually dealing with standard easing, we can approximate 1D easing on the Y axis
|
||||
// based on linear time X, or do a full solve.
|
||||
// For brevity/speed in JS, we often approximate basic easing:
|
||||
|
||||
// 3t^2 * (1-t) + t^3 ... standard bezier blending functions
|
||||
var cx = 3.0 * p1x;
|
||||
var bx = 3.0 * (p2x - p1x) - cx;
|
||||
var ax = 1.0 - cx - bx;
|
||||
|
||||
var cy = 3.0 * p1y;
|
||||
var by = 3.0 * (p2y - p1y) - cy;
|
||||
var ay = 1.0 - cy - by;
|
||||
|
||||
// Solve for X given t (time) using Newton-Raphson
|
||||
var sampleT = t;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var x = ((ax * sampleT + bx) * sampleT + cx) * sampleT - t;
|
||||
if (Math.abs(x) < 1e-3) break;
|
||||
var d = (3.0 * ax * sampleT + 2.0 * bx) * sampleT + cx;
|
||||
if (Math.abs(d) < 1e-6) break;
|
||||
sampleT = sampleT - x / d;
|
||||
}
|
||||
|
||||
// Solve for Y given derived T
|
||||
return ((ay * sampleT + by) * sampleT + cy) * sampleT;
|
||||
}
|
||||
|
||||
- (BOOL)_renderAnimationStep:(JSObject)context timestamp:(double)timestamp
|
||||
{
|
||||
if (context.startTime === null)
|
||||
context.startTime = timestamp;
|
||||
|
||||
var elapsed = timestamp - context.startTime,
|
||||
linearProgress = elapsed / context.duration;
|
||||
|
||||
if (linearProgress > 1.0) linearProgress = 1.0;
|
||||
|
||||
// Apply Timing Function
|
||||
var progress = [self _solveBezier:linearProgress forTimingFunction:context.timingFunction];
|
||||
|
||||
var start = context.startValue,
|
||||
end = context.endValue,
|
||||
current = nil;
|
||||
|
||||
// Number
|
||||
if (typeof start === "number")
|
||||
{
|
||||
current = start + (end - start) * progress;
|
||||
}
|
||||
// Point / Size / Rect
|
||||
else if (start && start.x !== undefined && start.y !== undefined) // CGPoint
|
||||
{
|
||||
current = CGPointMake(start.x + (end.x - start.x) * progress,
|
||||
start.y + (end.y - start.y) * progress);
|
||||
}
|
||||
else if (start && start.width !== undefined && start.height !== undefined) // CGSize
|
||||
{
|
||||
current = CGSizeMake(start.width + (end.width - start.width) * progress,
|
||||
start.height + (end.height - start.height) * progress);
|
||||
}
|
||||
else if (start && start.origin !== undefined && start.size !== undefined) // CGRect
|
||||
{
|
||||
current = CGRectMake(
|
||||
start.origin.x + (end.origin.x - start.origin.x) * progress,
|
||||
start.origin.y + (end.origin.y - start.origin.y) * progress,
|
||||
start.size.width + (end.size.width - start.size.width) * progress,
|
||||
start.size.height + (end.size.height - start.size.height) * progress
|
||||
);
|
||||
}
|
||||
|
||||
if (current !== nil)
|
||||
[[self delegate] setValue:current forKey:context.keyPath];
|
||||
|
||||
if (linearProgress >= 1.0)
|
||||
{
|
||||
var anim = context.animation;
|
||||
|
||||
// Cleanup
|
||||
var shouldRemove = [anim respondsToSelector:@selector(isRemovedOnCompletion)] ? [anim isRemovedOnCompletion] : YES;
|
||||
|
||||
if (shouldRemove) {
|
||||
// Find key by context identity to handle groups correctly
|
||||
var keys = [_activeAnimations allKeys];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if ([_activeAnimations objectForKey:keys[i]] === context) {
|
||||
[_activeAnimations removeObjectForKey:keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate
|
||||
var delegate = [anim delegate];
|
||||
if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)])
|
||||
[delegate animationDidStop:anim finished:YES];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)_setOwningView:(CPView)anOwningView
|
||||
{
|
||||
|
||||
@@ -164,9 +164,36 @@ var _CPAnimationContextStack = nil,
|
||||
|
||||
if ([animation isKindOfClass:[CAKeyframeAnimation class]])
|
||||
{
|
||||
|
||||
values = [animation values];
|
||||
keyTimes = [animation keyTimes];
|
||||
timingFunctions = [animation timingFunctionsControlPoints];
|
||||
|
||||
var path = [animation path];
|
||||
var calculationMode = [animation calculationMode] || kCAAnimationLinear; // Default to linear
|
||||
var rotationMode = [animation rotationMode];
|
||||
var tensionValues = [animation tensionValues];
|
||||
var continuityValues = [animation continuityValues];
|
||||
var biasValues = [animation biasValues];
|
||||
|
||||
return {
|
||||
object: anObject,
|
||||
root: anObject,
|
||||
keypath: animatedKeyPath,
|
||||
duration: duration,
|
||||
completion: completionFunction,
|
||||
|
||||
// Keyframe-specific properties
|
||||
values: values,
|
||||
keytimes: keyTimes,
|
||||
timingfunctions: timingFunctions,
|
||||
path: path,
|
||||
calculationMode: calculationMode,
|
||||
rotationMode: rotationMode,
|
||||
tensionValues: tensionValues,
|
||||
continuityValues: continuityValues,
|
||||
biasValues: biasValues
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -183,18 +210,19 @@ var _CPAnimationContextStack = nil,
|
||||
values = [fromValue, toValue];
|
||||
keyTimes = [0, 1];
|
||||
timingFunctions = isBasicAnimation ? [animation timingFunctionControlPoints] : [_timingFunction controlPoints];
|
||||
}
|
||||
|
||||
return {
|
||||
object:anObject,
|
||||
root:anObject,
|
||||
keypath:animatedKeyPath,
|
||||
values:values,
|
||||
keytimes:keyTimes,
|
||||
duration:duration,
|
||||
timingfunctions:timingFunctions,
|
||||
completion:completionFunction
|
||||
};
|
||||
// Return a basic animation action
|
||||
return {
|
||||
object:anObject,
|
||||
root:anObject,
|
||||
keypath:animatedKeyPath,
|
||||
values:values,
|
||||
keytimes:keyTimes,
|
||||
duration:duration,
|
||||
timingfunctions:timingFunctions,
|
||||
completion:completionFunction
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_flushAnimations
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@import "_CPObjectAnimator.j"
|
||||
@import "CPView.j"
|
||||
@import "CPCompatibility.j"
|
||||
@@ -116,6 +115,13 @@ var DEFAULT_CSS_PROPERTIES = nil,
|
||||
{
|
||||
var target = anAction.object;
|
||||
|
||||
if (anAction.path)
|
||||
{
|
||||
// Path animations are handled differently. They don't map to standard properties like width/height.
|
||||
// They use CSS offset-path.
|
||||
return [self _addPathAnimation:animations forAction:anAction domElement:[target _DOMElement] identifier:[target UID]];
|
||||
}
|
||||
|
||||
return [self _addAnimations:animations forAction:anAction domElement:[target _DOMElement] identifier:[target UID]];
|
||||
}
|
||||
|
||||
@@ -132,6 +138,20 @@ var DEFAULT_CSS_PROPERTIES = nil,
|
||||
[animations addObject:animation];
|
||||
}
|
||||
|
||||
var calculationMode = anAction.calculationMode;
|
||||
var timingFunctions = anAction.timingfunctions;
|
||||
|
||||
if (calculationMode === kCAAnimationDiscrete)
|
||||
{
|
||||
// For discrete animations, we use the steps() timing function.
|
||||
// This makes the property jump between values.
|
||||
timingFunctions = "steps(1, end)";
|
||||
}
|
||||
// For other calculation modes like 'paced' or 'cubic', more complex logic would be needed here.
|
||||
// 'paced' would require pre-calculating keyTimes based on distance.
|
||||
// 'cubic' would require generating many keyframes to simulate a spline.
|
||||
// For now, we let them fall through to the default behavior.
|
||||
|
||||
var css_mapping = [self _cssPropertiesForKeyPath:anAction.keypath];
|
||||
|
||||
[css_mapping enumerateObjectsUsingBlock:function(aDict, anIndex, stop)
|
||||
@@ -140,10 +160,93 @@ var DEFAULT_CSS_PROPERTIES = nil,
|
||||
property = [aDict objectForKey:@"property"],
|
||||
getter = [aDict objectForKey:@"value"];
|
||||
|
||||
animation.addPropertyAnimation(property, getter, anAction.duration, anAction.keytimes, anAction.values, anAction.timingfunctions, completionFunction);
|
||||
animation.addPropertyAnimation(property, getter, anAction.duration, anAction.keytimes, anAction.values, timingFunctions, completionFunction);
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)_addPathAnimation:(CPArray)animations forAction:(id)anAction domElement:(Object)aDomElement identifier:(CPString)anIdentifier
|
||||
{
|
||||
var animation = [animations objectPassingTest:function(anim, idx, stop)
|
||||
{
|
||||
return anim.identifier == anIdentifier;
|
||||
}];
|
||||
|
||||
if (animation == nil)
|
||||
{
|
||||
animation = new CSSAnimation(aDomElement, anIdentifier, [anAction.object debug_description]);
|
||||
[animations addObject:animation];
|
||||
}
|
||||
|
||||
// 1. Get the SVG path string.
|
||||
var svgPath = [anAction.path SVGString];
|
||||
|
||||
// 2. Set the offset-path property.
|
||||
aDomElement.style.offsetPath = "path('" + svgPath + "')";
|
||||
|
||||
// 3. Set the anchor point and neutralize the static position.
|
||||
if (anAction.keypath === @"frameOrigin")
|
||||
{
|
||||
aDomElement.style.offsetAnchor = "0% 0%";
|
||||
aDomElement.style.left = "0px";
|
||||
aDomElement.style.top = "0px";
|
||||
}
|
||||
else
|
||||
{
|
||||
aDomElement.style.offsetAnchor = "50% 50%";
|
||||
}
|
||||
|
||||
// 4. Set the rotation mode.
|
||||
var rotationMode = anAction.rotationMode;
|
||||
if (rotationMode === kCAAnimationRotateAuto)
|
||||
{
|
||||
aDomElement.style.offsetRotate = "auto";
|
||||
}
|
||||
else if (rotationMode === kCAAnimationRotateAutoReverse)
|
||||
{
|
||||
aDomElement.style.offsetRotate = "auto reverse";
|
||||
}
|
||||
else
|
||||
{
|
||||
aDomElement.style.offsetRotate = "0deg";
|
||||
}
|
||||
|
||||
// Create a new completion handler that cleans up after the animation.
|
||||
var originalCompletion = anAction.completion;
|
||||
var cleanupCompletion = function()
|
||||
{
|
||||
// STEP 1: Run the original completion handler first. This is critical.
|
||||
// It sets the final frameOrigin, which updates the static 'left' and 'top'
|
||||
// styles to their final values, locking the view in the correct place.
|
||||
if (originalCompletion)
|
||||
originalCompletion();
|
||||
|
||||
// STEP 2: Now that the view's static position is correct, we can safely
|
||||
// remove the animation-specific properties. This prevents state leakage.
|
||||
aDomElement.style.offsetPath = null;
|
||||
aDomElement.style.offsetAnchor = null;
|
||||
aDomElement.style.offsetRotate = null;
|
||||
};
|
||||
|
||||
// 5. Create the @keyframes rule, passing in our NEW cleanup handler.
|
||||
var getter = function(start, current) { return current; };
|
||||
var values = ["0%", "100%"];
|
||||
var keytimes = [0, 1];
|
||||
var timingfunctions;
|
||||
|
||||
if (anAction.calculationMode === kCAAnimationPaced) {
|
||||
timingfunctions = "linear";
|
||||
} else {
|
||||
timingfunctions = [[CPAnimationContext currentContext] timingFunction];
|
||||
}
|
||||
|
||||
animation.addPropertyAnimation("offset-distance", getter, anAction.duration, keytimes, values, timingfunctions, cleanupCompletion);
|
||||
|
||||
// 6. Keep fill-mode as 'forwards'. This is essential to prevent the view
|
||||
// from jumping to (0,0) in the tiny gap between the animation ending
|
||||
// and our completion handler running.
|
||||
animation.setFillMode("forwards");
|
||||
}
|
||||
|
||||
+ (CPArray)_cssPropertiesForKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
return [[self _defaultCSSProperties] objectForKey:aKeyPath];
|
||||
|
||||
@@ -306,3 +306,8 @@ CSSAnimation.prototype.start = function()
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
CSSAnimation.prototype.setFillMode = function(aProperty)
|
||||
{
|
||||
this.target.style.setProperty(ANIMATION_FILL_MODE_PROPERTY, aProperty);
|
||||
};
|
||||
|
||||
@@ -82,6 +82,15 @@ var PrimaryPlatformWindow = NULL;
|
||||
|
||||
CPWindow _currentMainWindow;
|
||||
CPWindow _previousMainWindow;
|
||||
|
||||
// state of touch momentum scrolling
|
||||
CPTimer _momentumScrollTimer;
|
||||
float _touchVelocityX;
|
||||
float _touchVelocityY;
|
||||
float _lastTouchMoveTimestamp;
|
||||
float _lastMomentumTimestamp;
|
||||
BOOL _isTwoFingerScrolling;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent)
|
||||
location = [[CPApp currentEvent] locationInWindow],
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode];
|
||||
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
|
||||
anEvent._data1 = @{ "simulated": YES };
|
||||
anEvent._DOMEvent = aDOMEvent;
|
||||
|
||||
@@ -192,6 +192,33 @@ KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "[";
|
||||
KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\";
|
||||
KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]";
|
||||
|
||||
var KeyNameToUnicodeMap = {};
|
||||
KeyNameToUnicodeMap["Backspace"] = CPDeleteCharacter;
|
||||
KeyNameToUnicodeMap["Delete"] = CPDeleteFunctionKey;
|
||||
KeyNameToUnicodeMap["Tab"] = CPTabCharacter;
|
||||
KeyNameToUnicodeMap["Enter"] = CPCarriageReturnCharacter;
|
||||
KeyNameToUnicodeMap["Escape"] = CPEscapeFunctionKey;
|
||||
KeyNameToUnicodeMap["PageUp"] = CPPageUpFunctionKey;
|
||||
KeyNameToUnicodeMap["PageDown"] = CPPageDownFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowLeft"] = CPLeftArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowUp"] = CPUpArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey;
|
||||
KeyNameToUnicodeMap["End"] = CPEndFunctionKey;
|
||||
// Add safeguards for punctuation
|
||||
KeyNameToUnicodeMap[";"] = ";";
|
||||
KeyNameToUnicodeMap["-"] = "-";
|
||||
KeyNameToUnicodeMap["="] = "=";
|
||||
KeyNameToUnicodeMap[","] = ",";
|
||||
KeyNameToUnicodeMap["."] = ".";
|
||||
KeyNameToUnicodeMap["/"] = "/";
|
||||
KeyNameToUnicodeMap["`"] = "`";
|
||||
KeyNameToUnicodeMap["'"] = "'";
|
||||
KeyNameToUnicodeMap["["] = "[";
|
||||
KeyNameToUnicodeMap["\\"] = "\\";
|
||||
KeyNameToUnicodeMap["]"] = "]";
|
||||
|
||||
var ModifierKeyCodes = [
|
||||
CPKeyCodes.META,
|
||||
CPKeyCodes.WEBKIT_RIGHT_META,
|
||||
@@ -208,6 +235,10 @@ var resizeTimer = nil;
|
||||
var PreventScroll = true;
|
||||
var blurTimer = nil;
|
||||
|
||||
var MOMENTUM_DAMPING = 0.95, // The friction factor. Higher is less friction (0.95 is a good start).
|
||||
MIN_MOMENTUM_VELOCITY = 0.05, // The velocity (pixels/ms) at which the scroll animation will stop.
|
||||
MIN_MOMENTUM_START_VELOCITY = 0.1; // The minimum velocity required from a flick to initiate momentum scrolling.
|
||||
|
||||
var touchStartingPointX,
|
||||
touchStartingPointY;
|
||||
|
||||
@@ -239,6 +270,13 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
[self updateFromNativeContentRect];
|
||||
|
||||
_charCodes = {};
|
||||
|
||||
_momentumScrollTimer = nil;
|
||||
_touchVelocityX = 0;
|
||||
_touchVelocityY = 0;
|
||||
_lastTouchMoveTimestamp = 0;
|
||||
_lastMomentumTimestamp = 0;
|
||||
_isTwoFingerScrolling = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -427,16 +465,16 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.addEventListener("keyup", keyEventCallback, NO);
|
||||
theDocument.addEventListener("keydown", keyEventCallback, NO);
|
||||
theDocument.addEventListener("keypress", keyEventCallback, NO);
|
||||
// "keypress" listener removed as it's deprecated and no longer used in the new logic.
|
||||
|
||||
theDocument.addEventListener("touchstart", touchEventCallback, {passive: false});
|
||||
theDocument.addEventListener("touchend", touchEventCallback, {passive: false});
|
||||
theDocument.addEventListener("touchmove", touchEventCallback, {passive: false});
|
||||
theDocument.addEventListener("touchcancel", touchEventCallback, {passive: false});
|
||||
|
||||
_DOMWindow.addEventListener("DOMMouseScroll", scrollEventCallback, NO);
|
||||
_DOMWindow.addEventListener("wheel", scrollEventCallback, NO);
|
||||
_DOMWindow.addEventListener("mousewheel", scrollEventCallback, NO);
|
||||
_DOMWindow.addEventListener("DOMMouseScroll", scrollEventCallback, { passive: false });
|
||||
_DOMWindow.addEventListener("wheel", scrollEventCallback, { passive: false });
|
||||
_DOMWindow.addEventListener("mousewheel", scrollEventCallback, { passive: false });
|
||||
|
||||
_DOMWindow.addEventListener("resize", resizeEventCallback, NO);
|
||||
|
||||
@@ -459,7 +497,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.removeEventListener("keyup", keyEventCallback, NO);
|
||||
theDocument.removeEventListener("keydown", keyEventCallback, NO);
|
||||
theDocument.removeEventListener("keypress", keyEventCallback, NO);
|
||||
|
||||
theDocument.removeEventListener("touchstart", touchEventCallback, NO);
|
||||
theDocument.removeEventListener("touchend", touchEventCallback, NO);
|
||||
@@ -471,9 +508,9 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
_DOMWindow.removeEventListener("focus", onFocusEventCallback, NO);
|
||||
|
||||
//FIXME: does firefox really need a different value?
|
||||
_DOMWindow.removeEventListener("DOMMouseScroll", scrollEventCallback, NO);
|
||||
_DOMWindow.removeEventListener("wheel", scrollEventCallback, NO);
|
||||
_DOMWindow.removeEventListener("mousewheel", scrollEventCallback, NO);
|
||||
_DOMWindow.removeEventListener("DOMMouseScroll", scrollEventCallback, { passive: false });
|
||||
_DOMWindow.removeEventListener("wheel", scrollEventCallback, { passive: false });
|
||||
_DOMWindow.removeEventListener("mousewheel", scrollEventCallback, { passive: false });
|
||||
|
||||
[PlatformWindows removeObject:self];
|
||||
|
||||
@@ -492,7 +529,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.attachEvent("onkeyup", keyEventCallback);
|
||||
theDocument.attachEvent("onkeydown", keyEventCallback);
|
||||
theDocument.attachEvent("onkeypress", keyEventCallback);
|
||||
// "onkeypress" listener removed.
|
||||
|
||||
_DOMWindow.attachEvent("onresize", resizeEventCallback);
|
||||
|
||||
@@ -522,7 +559,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.detachEvent("onkeyup", keyEventCallback);
|
||||
theDocument.detachEvent("onkeydown", keyEventCallback);
|
||||
theDocument.detachEvent("onkeypress", keyEventCallback);
|
||||
|
||||
_DOMWindow.detachEvent("onresize", resizeEventCallback);
|
||||
|
||||
@@ -698,157 +734,138 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
(_capsLockActive ? CPAlphaShiftKeyMask : 0);
|
||||
|
||||
// With a few exceptions, all key events are blocked from propagating to
|
||||
// the browser. Here the following exceptions are being allowed:
|
||||
//
|
||||
// - All keys pressed along with a ctrl or cmd key _unless_ they are in
|
||||
// one of the two blacklists.
|
||||
// - Any key listed in the whitelist.
|
||||
//
|
||||
// The ctrl/cmd keys are used for browser hotkeys as are the keys listed in
|
||||
// the whitelist (F1-F12 at the time of writing).
|
||||
//
|
||||
// If a key is listed in both the blacklist and whitelist, the blacklist is
|
||||
// checked first. The key will be blocked from propagating in that case.
|
||||
// the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys)
|
||||
// and function keys (F1-F12) to pass through, unless explicitly blacklisted.
|
||||
|
||||
StopDOMEventPropagation = YES;
|
||||
var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0;
|
||||
var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase();
|
||||
|
||||
// Make sure it is not in the blacklists.
|
||||
if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode]))
|
||||
if (!(CharacterKeysToPrevent[charForPropagationCheck] || KeyCodesToPrevent[keyCodeForPropagationCheck]))
|
||||
{
|
||||
// It is not in the blacklist, let it through if the ctrl/cmd key is
|
||||
// also down or it's in the whitelist.
|
||||
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[aDOMEvent.keyCode])
|
||||
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck])
|
||||
StopDOMEventPropagation = NO;
|
||||
}
|
||||
|
||||
var overrideCharacters = nil,
|
||||
var characters = @"",
|
||||
charactersIgnoringModifiers = @"";
|
||||
|
||||
var keyCode = aDOMEvent.keyCode;
|
||||
if (keyCode in MozKeyCodeToKeyCodeMap)
|
||||
keyCode = MozKeyCodeToKeyCodeMap[keyCode];
|
||||
|
||||
var isActionKey;
|
||||
var key = aDOMEvent.key;
|
||||
|
||||
if (key) {
|
||||
isActionKey =
|
||||
key === 'Enter' ||
|
||||
key === 'Backspace' ||
|
||||
key === 'Tab' ||
|
||||
key === 'Escape' ||
|
||||
key === 'Delete' ||
|
||||
key.startsWith('Arrow') ||
|
||||
key === 'Home' ||
|
||||
key === 'End' ||
|
||||
key === 'PageUp' ||
|
||||
key === 'PageDown';
|
||||
}
|
||||
else
|
||||
{
|
||||
isActionKey =
|
||||
(keyCode === 13) || (keyCode === 8) || (keyCode === 9) ||
|
||||
(keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40);
|
||||
}
|
||||
|
||||
switch (aDOMEvent.type)
|
||||
{
|
||||
case "keydown":
|
||||
// Grab and store the keycode now since it is correct and consistent at this point.
|
||||
if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap)
|
||||
_keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode];
|
||||
else
|
||||
_keyCode = aDOMEvent.keyCode;
|
||||
|
||||
var characters;
|
||||
|
||||
// Handle key codes for which String.fromCharCode won't work.
|
||||
// Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys.
|
||||
if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined))
|
||||
characters = KeyCodesToUnicodeMap[_keyCode];
|
||||
|
||||
// The problem with keyCode is that this property refers to keys on the keyboard and not to characters
|
||||
// This is why String.fromCharCode does not always work in more recent versions of Firefox
|
||||
// E.g. pressing a '#' on a German keyboard gives you a charCode of 163, which refers to '£' and not '#'
|
||||
// The property key works fine, though. From there we can get the actual character more robustly.
|
||||
// Therefore we prefer key over keyCode whenever possible
|
||||
|
||||
if (!characters)
|
||||
characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase();
|
||||
|
||||
overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters;
|
||||
|
||||
// check for caps lock state
|
||||
if (_keyCode === CPKeyCodes.CAPS_LOCK)
|
||||
if ([ModifierKeyCodes containsObject:keyCode])
|
||||
{
|
||||
_capsLockActive = YES;
|
||||
|
||||
// Make sure the caps lock flag is set in modifierFlags
|
||||
modifierFlags |= CPAlphaShiftKeyMask;
|
||||
}
|
||||
|
||||
if ([ModifierKeyCodes containsObject:_keyCode])
|
||||
{
|
||||
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
|
||||
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))
|
||||
var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil);
|
||||
_charCodes[keyCode] = YES;
|
||||
|
||||
if (aDOMEvent.key)
|
||||
{
|
||||
//we are simply going to skip all keypress events that use cmd/ctrl key
|
||||
//this lets us be consistent in all browsers and send on the keydown
|
||||
//which means we can cancel the event early enough, but only if sendEvent needs to
|
||||
}
|
||||
else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey))
|
||||
{
|
||||
// this branch is taken by events which fire keydown, keypress, and keyup.
|
||||
// this is the only time we'll ALLOW character keys to propagate (needed for text fields)
|
||||
StopDOMEventPropagation = NO;
|
||||
break;
|
||||
if (aDOMEvent.key.length === 1)
|
||||
{
|
||||
characters = aDOMEvent.key;
|
||||
}
|
||||
// Correctly handle dead keys to prevent inserting "Dead"
|
||||
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
|
||||
characters = @"";
|
||||
}
|
||||
// For other named keys, map them or fall back to an empty string.
|
||||
else
|
||||
{
|
||||
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//this branch is taken by "remedial" key events
|
||||
// In this state we continue to keypress and send the CPEvent
|
||||
characters = KeyCodesToUnicodeMap[keyCode];
|
||||
|
||||
if (!characters)
|
||||
{
|
||||
characters = String.fromCharCode(keyCode);
|
||||
if (modifierFlags & CPShiftKeyMask || _capsLockActive)
|
||||
characters = characters.toUpperCase();
|
||||
else
|
||||
characters = characters.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
case "keypress":
|
||||
// we unconditionally break on keypress events with modifiers,
|
||||
// because we forced the event to be sent on the keydown
|
||||
if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)))
|
||||
break;
|
||||
|
||||
var keyCode = _keyCode,
|
||||
charCode = aDOMEvent.keyCode || aDOMEvent.charCode,
|
||||
isARepeat = (_charCodes[keyCode] != nil);
|
||||
|
||||
_lastKey = keyCode;
|
||||
_charCodes[keyCode] = charCode;
|
||||
|
||||
var characters = overrideCharacters;
|
||||
// Is this a special key?
|
||||
if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0))
|
||||
characters = KeyCodesToUnicodeMap[charCode];
|
||||
|
||||
if (!characters)
|
||||
characters = String.fromCharCode(charCode);
|
||||
|
||||
charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift.
|
||||
|
||||
// Safari won't send proper capitalization during cmd-key events
|
||||
if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive))
|
||||
characters = characters.toUpperCase();
|
||||
charactersIgnoringModifiers = characters.toLowerCase();
|
||||
|
||||
event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode];
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey];
|
||||
|
||||
break;
|
||||
|
||||
case "keyup":
|
||||
var keyCode = aDOMEvent.keyCode,
|
||||
charCode = _charCodes[keyCode];
|
||||
|
||||
_keyCode = -1;
|
||||
_lastKey = -1;
|
||||
_charCodes[keyCode] = nil;
|
||||
|
||||
// check for caps lock state
|
||||
if (keyCode === CPKeyCodes.CAPS_LOCK)
|
||||
{
|
||||
_capsLockActive = NO;
|
||||
|
||||
// Make sure the caps lock flag is cleared in modifierFlags
|
||||
modifierFlags &= ~CPAlphaShiftKeyMask;
|
||||
_capsLockActive = !_capsLockActive;
|
||||
if (_capsLockActive)
|
||||
modifierFlags |= CPAlphaShiftKeyMask;
|
||||
else
|
||||
modifierFlags &= ~CPAlphaShiftKeyMask;
|
||||
}
|
||||
|
||||
if ([ModifierKeyCodes containsObject:keyCode])
|
||||
{
|
||||
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
|
||||
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
|
||||
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode);
|
||||
if (aDOMEvent.key)
|
||||
{
|
||||
if (aDOMEvent.key.length === 1) {
|
||||
characters = aDOMEvent.key;
|
||||
}
|
||||
// Ensure keyup events also don't produce "Dead"
|
||||
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
|
||||
characters = @"";
|
||||
}
|
||||
else
|
||||
{
|
||||
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
|
||||
}
|
||||
}
|
||||
else
|
||||
characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode);
|
||||
|
||||
charactersIgnoringModifiers = characters.toLowerCase();
|
||||
|
||||
if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive)
|
||||
@@ -856,7 +873,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags
|
||||
timestamp: timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode];
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey];
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -869,12 +886,11 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
if (event && ![_platformPasteboard windowShouldSuppressKeyEvent])
|
||||
{
|
||||
[CPApp sendEvent:event];
|
||||
|
||||
[_platformPasteboard windowDidSendKeyEvent:event];
|
||||
}
|
||||
|
||||
var didStop = NO;
|
||||
// Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion.
|
||||
|
||||
if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation]))
|
||||
{
|
||||
didStop = YES;
|
||||
@@ -1157,49 +1173,117 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
[aWindow becomeMainWindow];
|
||||
}
|
||||
|
||||
- (void)_momentumScrollStep
|
||||
{
|
||||
// Use a high-resolution timer for smooth animation
|
||||
var now = performance.now();
|
||||
var interval = now - _lastMomentumTimestamp;
|
||||
_lastMomentumTimestamp = now;
|
||||
|
||||
// If the interval is too large (e.g., tab was backgrounded), stop the animation.
|
||||
if (interval > 100)
|
||||
{
|
||||
[_momentumScrollTimer invalidate];
|
||||
_momentumScrollTimer = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
var location = _lastMouseEventLocation || CGPointMakeZero();
|
||||
|
||||
// Create a fake event to pass to the existing scrollEvent handler
|
||||
var newEvent = {
|
||||
shiftKey: NO, ctrlKey: NO, altKey: NO, metaKey: NO,
|
||||
_overrideLocation: location,
|
||||
_hasPreciseScrollingDeltas: YES,
|
||||
// The scroll delta is velocity (pixels/ms) * time (ms)
|
||||
deltaX: _touchVelocityX * interval,
|
||||
deltaY: _touchVelocityY * interval,
|
||||
type: CPDOMEventScrollWheel,
|
||||
preventDefault: function() {},
|
||||
stopPropagation: function() {}
|
||||
};
|
||||
|
||||
[self scrollEvent:newEvent];
|
||||
|
||||
// Apply time-corrected damping for consistent friction feel across different frame rates
|
||||
var dampingFactor = Math.pow(MOMENTUM_DAMPING, interval / (1000 / 60));
|
||||
_touchVelocityX *= dampingFactor;
|
||||
_touchVelocityY *= dampingFactor;
|
||||
|
||||
// Stop the animation when velocity is negligible
|
||||
if (Math.abs(_touchVelocityX) < MIN_MOMENTUM_VELOCITY && Math.abs(_touchVelocityY) < MIN_MOMENTUM_VELOCITY)
|
||||
{
|
||||
[_momentumScrollTimer invalidate];
|
||||
_momentumScrollTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchEvent:(DOMEvent)aDOMEvent
|
||||
{
|
||||
// Handle gesture state and momentum start/stop.
|
||||
if (aDOMEvent.type === CPDOMEventTouchStart)
|
||||
{
|
||||
_isTwoFingerScrolling = aDOMEvent.touches.length === 2;
|
||||
// A new touch always stops any existing momentum scroll.
|
||||
if (_momentumScrollTimer) {
|
||||
[_momentumScrollTimer invalidate];
|
||||
_momentumScrollTimer = nil;
|
||||
}
|
||||
}
|
||||
else if (aDOMEvent.type === CPDOMEventTouchEnd || aDOMEvent.type === CPDOMEventTouchCancel)
|
||||
{
|
||||
if (_isTwoFingerScrolling)
|
||||
{
|
||||
// If the gesture ends with enough velocity, start the momentum animation.
|
||||
if (Math.abs(_touchVelocityX) > MIN_MOMENTUM_START_VELOCITY || Math.abs(_touchVelocityY) > MIN_MOMENTUM_START_VELOCITY) {
|
||||
_lastMomentumTimestamp = aDOMEvent.timeStamp;
|
||||
_momentumScrollTimer = [CPTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:@selector(_momentumScrollStep) userInfo:nil repeats:YES];
|
||||
}
|
||||
_isTwoFingerScrolling = NO;
|
||||
|
||||
// Prevent default browser actions (like pinch-zoom) for our handled scroll gesture.
|
||||
if (aDOMEvent.preventDefault) aDOMEvent.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var newEvent = {},
|
||||
touch = aDOMEvent.touches.length ? aDOMEvent.touches[0] : aDOMEvent.changedTouches[0];
|
||||
|
||||
newEvent.timestamp = [CPEvent currentTimestamp];
|
||||
newEvent.target = aDOMEvent.target;
|
||||
newEvent.shiftKey = newEvent.ctrlKey = newEvent.altKey = newEvent.metaKey = false;
|
||||
|
||||
newEvent.clientX = touch.clientX;
|
||||
|
||||
/*
|
||||
Normally the document can't scroll in Cappuccino: our body element has top:0 and bottom:0 with absolute positioning. So it should always be exactly the height of the viewport. The below handles a special case. iOS scrolls the document when the virtual keyboard is present and it needs to move a text input upwards visually to avoid covering the input with the keyboard. For most purposes we can ignore this, except here. In theory I think we could always apply this (scrollTop should always be 0 on every other device and situation) but let's be defensive and only apply it for touch events to minimise the risk of surprises.
|
||||
*/
|
||||
newEvent.clientY = _DOMWindow.document.body.scrollTop + touch.clientY;
|
||||
|
||||
newEvent.preventDefault = function() { if (aDOMEvent.preventDefault) aDOMEvent.preventDefault() };
|
||||
newEvent.stopPropagation = function() { if (aDOMEvent.stopPropagation) aDOMEvent.stopPropagation() };
|
||||
|
||||
// single finger event-> simulate a simple mouse-click
|
||||
// Single-finger event -> simulate a simple mouse-click
|
||||
if (aDOMEvent.touches && (aDOMEvent.touches.length == 1 || (aDOMEvent.touches.length == 0 && aDOMEvent.changedTouches.length == 1)))
|
||||
{
|
||||
switch (aDOMEvent.type)
|
||||
{
|
||||
case CPDOMEventTouchStart: newEvent.type = CPDOMEventMouseDown;
|
||||
break;
|
||||
case CPDOMEventTouchEnd: newEvent.type = CPDOMEventMouseUp;
|
||||
break;
|
||||
case CPDOMEventTouchMove: newEvent.type = CPDOMEventMouseMoved;
|
||||
break;
|
||||
case CPDOMEventTouchCancel: newEvent.type = CPDOMEventMouseUp;
|
||||
break;
|
||||
case CPDOMEventTouchStart:
|
||||
newEvent.type = CPDOMEventMouseDown;
|
||||
break;
|
||||
case CPDOMEventTouchEnd:
|
||||
newEvent.type = CPDOMEventMouseUp;
|
||||
break;
|
||||
case CPDOMEventTouchMove:
|
||||
newEvent.type = CPDOMEventMouseMoved;
|
||||
break;
|
||||
case CPDOMEventTouchCancel:
|
||||
newEvent.type = CPDOMEventMouseUp;
|
||||
break;
|
||||
}
|
||||
|
||||
newEvent._isFromTouch = true; // Identify the event as touch-originated for tolerant click counting
|
||||
[self mouseEvent:newEvent];
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// two fingers->simulate scrolling events
|
||||
if (aDOMEvent.touches && aDOMEvent.touches.length == 2)
|
||||
// Two-fingers -> simulate scrolling events
|
||||
if (_isTwoFingerScrolling && aDOMEvent.touches && aDOMEvent.touches.length == 2)
|
||||
{
|
||||
if (aDOMEvent.preventDefault)
|
||||
aDOMEvent.preventDefault();
|
||||
@@ -1212,23 +1296,41 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
case CPDOMEventTouchStart:
|
||||
touchStartingPointX = touch.pageX;
|
||||
touchStartingPointY = touch.pageY;
|
||||
_lastTouchMoveTimestamp = aDOMEvent.timeStamp;
|
||||
_touchVelocityX = 0;
|
||||
_touchVelocityY = 0;
|
||||
break;
|
||||
case CPDOMEventTouchMove:
|
||||
newEvent._hasPreciseScrollingDeltas = YES;
|
||||
newEvent.deltaX = touchStartingPointX - touch.pageX;
|
||||
newEvent.deltaY = touchStartingPointY - touch.pageY;
|
||||
newEvent.type = CPDOMEventScrollWheel;
|
||||
var now = aDOMEvent.timeStamp;
|
||||
var deltaTime = now - _lastTouchMoveTimestamp;
|
||||
|
||||
[self scrollEvent:newEvent];
|
||||
if (deltaTime > 0)
|
||||
{
|
||||
var deltaX = touchStartingPointX - touch.pageX;
|
||||
var deltaY = touchStartingPointY - touch.pageY;
|
||||
|
||||
// Calculate velocity for momentum, smoothing it with an exponential moving average.
|
||||
var currentVelocityX = deltaX / deltaTime;
|
||||
var currentVelocityY = deltaY / deltaTime;
|
||||
_touchVelocityX = 0.8 * currentVelocityX + 0.2 * _touchVelocityX;
|
||||
_touchVelocityY = 0.8 * currentVelocityY + 0.2 * _touchVelocityY;
|
||||
|
||||
// Send the raw delta for immediate, direct-manipulation feedback.
|
||||
newEvent._hasPreciseScrollingDeltas = YES;
|
||||
newEvent.deltaX = deltaX;
|
||||
newEvent.deltaY = deltaY;
|
||||
newEvent.type = CPDOMEventScrollWheel;
|
||||
[self scrollEvent:newEvent];
|
||||
}
|
||||
|
||||
touchStartingPointX = touch.pageX;
|
||||
touchStartingPointY = touch.pageY;
|
||||
_lastTouchMoveTimestamp = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// cancel other touch cases preventively
|
||||
|
||||
// Preventively cancel other touch cases (e.g., 3+ fingers)
|
||||
if (aDOMEvent.preventDefault)
|
||||
aDOMEvent.preventDefault();
|
||||
|
||||
@@ -1289,7 +1391,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
if (aDOMEvent.button !== _firstMouseDownButton)
|
||||
return;
|
||||
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseUp, timestamp, location), 0, nil);
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseUp, timestamp, location, aDOMEvent._isFromTouch);
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
|
||||
|
||||
_mouseIsDown = NO;
|
||||
_lastMouseUp = event;
|
||||
@@ -1335,15 +1438,17 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
_DOMEventMode = YES;
|
||||
_mouseIsDown = YES;
|
||||
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
|
||||
|
||||
// Fake a down and up event so that event tracking mode will work correctly
|
||||
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
|
||||
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
|
||||
clickCount:clickCount pressure:0]];
|
||||
|
||||
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
|
||||
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
|
||||
clickCount:clickCount pressure:0]];
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1355,7 +1460,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
StopContextMenuDOMEventPropagation = YES;
|
||||
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseDown, timestamp, location), 0, nil);
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
|
||||
|
||||
_mouseIsDown = YES;
|
||||
_lastMouseDown = event;
|
||||
@@ -1855,19 +1961,25 @@ var _CPEventFromNativeMouseEvent = function(aNativeEvent, anEventType, aPoint, m
|
||||
return aNativeEvent;
|
||||
};
|
||||
|
||||
var CLICK_SPACE_DELTA = 5.0,
|
||||
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0;
|
||||
var CLICK_SPACE_DELTA = 5.0,
|
||||
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0,
|
||||
// Define a more generous time delta for touch events to make double-tapping easier.
|
||||
TOUCH_CLICK_TIME_DELTA = 0.80; // Increased from 0.55s to 0.80s
|
||||
|
||||
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation)
|
||||
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation, isFromTouch)
|
||||
{
|
||||
if (!aComparisonEvent)
|
||||
return 1;
|
||||
|
||||
// For touch events, allow a larger pixel delta to accommodate "fat fingers"
|
||||
// and a longer time delta to accommodate less precise tapping.
|
||||
var spaceDelta = isFromTouch ? 25.0 : CLICK_SPACE_DELTA;
|
||||
var timeDelta = isFromTouch ? TOUCH_CLICK_TIME_DELTA : CLICK_TIME_DELTA;
|
||||
var comparisonLocation = [aComparisonEvent locationInWindow];
|
||||
|
||||
return (aTimestamp - [aComparisonEvent timestamp] < CLICK_TIME_DELTA &&
|
||||
ABS(comparisonLocation.x - aLocation.x) < CLICK_SPACE_DELTA &&
|
||||
ABS(comparisonLocation.y - aLocation.y) < CLICK_SPACE_DELTA) ? [aComparisonEvent clickCount] + 1 : 1;
|
||||
return (aTimestamp - [aComparisonEvent timestamp] < timeDelta &&
|
||||
ABS(comparisonLocation.x - aLocation.x) < spaceDelta &&
|
||||
ABS(comparisonLocation.y - aLocation.y) < spaceDelta) ? [aComparisonEvent clickCount] + 1 : 1;
|
||||
};
|
||||
|
||||
// Global.
|
||||
|
||||
@@ -132,38 +132,36 @@ CPKeyCodes = {
|
||||
|
||||
/*!
|
||||
* Returns true if the key fires a keypress event in the current browser.
|
||||
* The keypress event is deprecated, but this function helps manage legacy
|
||||
* event handling by predicting its behavior.
|
||||
*
|
||||
* Accoridng to MSDN [1] IE only fires keypress events for the following keys:
|
||||
* - Letters: A - Z (uppercase and lowercase)
|
||||
* - Numerals: 0 - 9
|
||||
* - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
|
||||
* - System: ESC, SPACEBAR, ENTER
|
||||
*
|
||||
* That's not entirely correct though, for instance there's no distinction
|
||||
* between upper and lower case letters.
|
||||
*
|
||||
* [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
|
||||
*
|
||||
* Safari is similar to IE, but does not fire keypress for ESC.
|
||||
*
|
||||
* Additionally, IE6 does not fire keydown or keypress events for letters when
|
||||
* the control or alt keys are held down and the shift key is not. IE7 does
|
||||
* fire keydown in these cases, though, but not keypress.
|
||||
*
|
||||
* @param keyCode A key code.
|
||||
* @param opt_heldKeyCode Key code of a currently-held key.
|
||||
* @param opt_shiftKey Whether the shift key is held down.
|
||||
* @param opt_ctrlKey Whether the control key is held down.
|
||||
* @param opt_altKey Whether the alt key is held down.
|
||||
* @return Returns YES if it's a key that fires a keypress event.
|
||||
* @param {number} keyCode A key code.
|
||||
* @param {string} key The `key` property from the keyboard event.
|
||||
* @param {number} opt_heldKeyCode Key code of a currently-held key.
|
||||
* @param {boolean} opt_shiftKey Whether the shift key is held down.
|
||||
* @param {boolean} opt_ctrlKey Whether the control key is held down.
|
||||
* @param {boolean} opt_altKey Whether the alt key is held down.
|
||||
* @return {boolean} Returns YES if it's a key that fires a keypress event.
|
||||
*/
|
||||
CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey)
|
||||
{
|
||||
// The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys)
|
||||
// Regular keys all fire the keypress event
|
||||
// Modern approach: Use event.key if available, as it is the most reliable standard.
|
||||
if (key)
|
||||
{
|
||||
// Any key that produces a single, printable character fires a keypress event.
|
||||
if (key.length === 1)
|
||||
return true;
|
||||
|
||||
if (key && key.length == 1)
|
||||
return true;
|
||||
// "Enter" is a special non-printable key that historically fires keypress for compatibility.
|
||||
if (key === "Enter")
|
||||
return true;
|
||||
|
||||
// For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"),
|
||||
// modern browsers do not fire a keypress event.
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Legacy Fallback Logic (for browsers that don't support event.key) ---
|
||||
|
||||
if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport))
|
||||
return true;
|
||||
@@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif
|
||||
|
||||
/*!
|
||||
* Test for whether or not a given keyCode represents a character key.
|
||||
* NOTE: This is a legacy function for browsers that don't support `event.key`.
|
||||
* It is unreliable because `keyCode` represents a physical key, not the character produced.
|
||||
*
|
||||
* @param keyCode A key code.
|
||||
* @return Returns YES if the keyCode is a character key.
|
||||
* @param {number} keyCode A key code.
|
||||
* @return {boolean} Returns YES if the keyCode is a character key.
|
||||
*/
|
||||
CPKeyCodes.isCharacterKey = function(keyCode)
|
||||
{
|
||||
|
||||
@@ -66,8 +66,8 @@ var CPViewThemeClassKey = @"CPViewThemeClassKey",
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Theme State
|
||||
// MARK: -
|
||||
// MARK: Theme State
|
||||
|
||||
- (unsigned)themeState
|
||||
{
|
||||
@@ -155,7 +155,7 @@ var CPViewThemeClassKey = @"CPViewThemeClassKey",
|
||||
return [self unsetThemeState:aState];
|
||||
}
|
||||
|
||||
#pragma mark Theme Attributes
|
||||
// MARK: Theme Attributes
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
@@ -494,8 +494,8 @@ var NULL_THEME = {};
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark CSS styling additions
|
||||
// MARK: -
|
||||
// MARK: CSS styling additions
|
||||
|
||||
- (CPTheme)actualTheme
|
||||
{
|
||||
|
||||
@@ -210,7 +210,7 @@ var ListColumnIdentifier = @"1";
|
||||
CPTableColumn _tableColumn;
|
||||
}
|
||||
|
||||
#pragma mark Creating and Displaying a List
|
||||
// MARK: Creating and Displaying a List
|
||||
|
||||
/*!
|
||||
Creates a pop up list of choices that will display in a scrollable CPTableView.
|
||||
@@ -353,7 +353,7 @@ var ListColumnIdentifier = @"1";
|
||||
[self scrollItemAtIndexToTop:[_tableView selectedRow]];
|
||||
}
|
||||
|
||||
#pragma mark Setting Display Attributes
|
||||
// MARK: Setting Display Attributes
|
||||
|
||||
/*!
|
||||
Returns the desired width of the list.
|
||||
@@ -448,7 +448,7 @@ var ListColumnIdentifier = @"1";
|
||||
return _panel;
|
||||
}
|
||||
|
||||
#pragma mark Setting a Data Source
|
||||
// MARK: Setting a Data Source
|
||||
|
||||
- (void)setDataSource:(_CPPopUpListDataSource)aDataSource
|
||||
{
|
||||
@@ -468,7 +468,7 @@ var ListColumnIdentifier = @"1";
|
||||
return _dataSource;
|
||||
}
|
||||
|
||||
#pragma mark Manipulating the Selection
|
||||
// MARK: Manipulating the Selection
|
||||
|
||||
/*!
|
||||
Select the next item in the list if there one. If there is currently no selected item,
|
||||
@@ -556,7 +556,7 @@ var ListColumnIdentifier = @"1";
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark Manipulating the Displayed List
|
||||
// MARK: Manipulating the Displayed List
|
||||
|
||||
/*!
|
||||
Scroll the list down one page.
|
||||
@@ -637,7 +637,7 @@ var ListColumnIdentifier = @"1";
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPPopUpListItemWasClickedNotification object:self];
|
||||
}
|
||||
|
||||
#pragma mark Handling Events
|
||||
// MARK: Handling Events
|
||||
|
||||
/*!
|
||||
Handles standard key equivalents for moving the selection
|
||||
@@ -785,7 +785,7 @@ var ListColumnIdentifier = @"1";
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Internal Helpers
|
||||
// MARK: Internal Helpers
|
||||
|
||||
/*! @ignore */
|
||||
- (void)listWillPopUp
|
||||
|
||||
+16
-16
@@ -74,8 +74,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
// MARK: -
|
||||
// MARK: Class methods
|
||||
|
||||
/*!
|
||||
Overrides the default windowView class loader.
|
||||
@@ -89,8 +89,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*!
|
||||
Create and init a _CPPopoverWindow with given the given frame.
|
||||
@@ -138,8 +138,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getters / Setters
|
||||
// MARK: -
|
||||
// MARK: Getters / Setters
|
||||
|
||||
- (void)setAppearance:(int)anAppearance
|
||||
{
|
||||
@@ -177,8 +177,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
_implementedDelegateMethods |= _CPPopoverWindow_didShow_;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Observer
|
||||
// MARK: -
|
||||
// MARK: Observer
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -236,8 +236,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Utilities
|
||||
// MARK: -
|
||||
// MARK: Utilities
|
||||
|
||||
- (CGPoint)computeOriginFromRect:(CGRect)aRect ofView:(CPView)positioningView preferredEdge:(int)anEdge
|
||||
{
|
||||
@@ -447,8 +447,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Actions
|
||||
// MARK: -
|
||||
// MARK: Actions
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -462,8 +462,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Overrides
|
||||
// MARK: -
|
||||
// MARK: Overrides
|
||||
|
||||
/*!
|
||||
@ignore
|
||||
@@ -697,8 +697,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Private
|
||||
// MARK: -
|
||||
// MARK: Private
|
||||
|
||||
- (CPString)_currentTransformMatrix
|
||||
{
|
||||
|
||||
+6
-6
@@ -45,8 +45,8 @@ var _CPToolTipHeight = 24.0,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class Methods
|
||||
// MARK: -
|
||||
// MARK: Class Methods
|
||||
|
||||
/*! @ignore
|
||||
Invalidate any scheduled tooltips, or hide any visible one
|
||||
@@ -160,8 +160,8 @@ var _CPToolTipHeight = 24.0,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Initialization
|
||||
// MARK: -
|
||||
// MARK: Initialization
|
||||
|
||||
/*!
|
||||
Returns an initialized _CPToolTip with string.
|
||||
@@ -208,8 +208,8 @@ var _CPToolTipHeight = 24.0,
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Controls
|
||||
// MARK: -
|
||||
// MARK: Controls
|
||||
|
||||
/*!
|
||||
Show the tooltip after computing the position.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user