From de27ed87287e8e3d86ac26928bbf11cb690e6642 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 27 Feb 2019 10:29:30 -0500 Subject: [PATCH] Add color palette to image editor. --- Signal.xcodeproj/project.pbxproj | 4 + .../Contents.json | 21 ++ .../Screen Shot 2019-02-26 at 1.57.23 PM.png | Bin 0 -> 15805 bytes .../ImageEditor/ImageEditorPaletteView.swift | 258 ++++++++++++++++++ .../Views/ImageEditor/ImageEditorView.swift | 47 ++-- SignalMessaging/categories/UIView+OWS.swift | 16 ++ 6 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 Signal/Images.xcassets/image_editor_palette.imageset/Contents.json create mode 100644 Signal/Images.xcassets/image_editor_palette.imageset/Screen Shot 2019-02-26 at 1.57.23 PM.png create mode 100644 SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index aadd52a2a..69f3722b8 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; }; 34074F61203D0CBE004596AE /* OWSSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 34074F5F203D0CBD004596AE /* OWSSounds.m */; }; 34074F62203D0CBE004596AE /* OWSSounds.h in Headers */ = {isa = PBXBuildFile; fileRef = 34074F60203D0CBE004596AE /* OWSSounds.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34080EFE2225F96D0087E99F /* ImageEditorPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */; }; 340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */; }; 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */; }; 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; }; @@ -635,6 +636,7 @@ 3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareButtonsView.h; sourceTree = ""; }; 34074F5F203D0CBD004596AE /* OWSSounds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSounds.m; sourceTree = ""; }; 34074F60203D0CBE004596AE /* OWSSounds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSounds.h; sourceTree = ""; }; + 34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorPaletteView.swift; sourceTree = ""; }; 340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = ""; }; 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItemTest.m; sourceTree = ""; }; 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = ""; }; @@ -1913,6 +1915,7 @@ 34BBC84E220B8A0100857249 /* ImageEditorCropViewController.swift */, 34BBC852220C7AD900857249 /* ImageEditorItem.swift */, 34BEDB0D21C405B0007B0EAE /* ImageEditorModel.swift */, + 34080EFD2225F96D0087E99F /* ImageEditorPaletteView.swift */, 34BBC85C220D19D600857249 /* ImageEditorPanGestureRecognizer.swift */, 34BBC84C220B2D0800857249 /* ImageEditorPinchGestureRecognizer.swift */, 34BBC854220C7ADA00857249 /* ImageEditorStrokeItem.swift */, @@ -3333,6 +3336,7 @@ 34AC09DF211B39B100997B47 /* OWSNavigationController.m in Sources */, 34074F61203D0CBE004596AE /* OWSSounds.m in Sources */, 34BEDB1721C80BCA007B0EAE /* OWSAnyTouchGestureRecognizer.m in Sources */, + 34080EFE2225F96D0087E99F /* ImageEditorPaletteView.swift in Sources */, 34B6A909218B8824007C4606 /* OWS112TypingIndicatorsMigration.swift in Sources */, 4C3E245D21F2B395000AE092 /* DirectionalPanGestureRecognizer.swift in Sources */, 346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */, diff --git a/Signal/Images.xcassets/image_editor_palette.imageset/Contents.json b/Signal/Images.xcassets/image_editor_palette.imageset/Contents.json new file mode 100644 index 000000000..bf932ac9b --- /dev/null +++ b/Signal/Images.xcassets/image_editor_palette.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Screen Shot 2019-02-26 at 1.57.23 PM.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/image_editor_palette.imageset/Screen Shot 2019-02-26 at 1.57.23 PM.png b/Signal/Images.xcassets/image_editor_palette.imageset/Screen Shot 2019-02-26 at 1.57.23 PM.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8e2e2bccdb87b8a80d807098152ff6afec8d40 GIT binary patch literal 15805 zcmZv@bwE_z_dQH^N_R+i4&5SBg7hHLDIH3el=MjV&`5VmDK$upbazR2*E>G(`TF>K z`G-6A-gD2n`<%1)+H0>HuKrR18-pAJ4h{}m@x?O@*ned>I7DeQ6xflO38)SZ4&KK` zPEK7>PL5XH#nIBn&H@gO<&&AQF`FU>3&h04*cdXz&WhpUuJQ3>l!kFfSLaY?7j3t3 z4{cV8fx$8<-f~v}fzOxTPPAO^-4BCGr+1!Xh4nrU#DL0F7ac~?rt?5}ccr(^e29oo zQ9+p*8W|aw=38(xqW#G7jM5iaw67am^WnNB;mson;i=Hfm*A_%==N}F-#PFBZ zc(RUVt_(MWporIi@-Y*~mx;Hz*v|3*ZEG3sZRl46j7Q*3V6h% zb9&;_NXW5VQ1J$MJb0RTSa?$SNqO}6e)0(?vctn$T0 z4C-bLPWdj5g5qb35^BGN@g(xe3XauXuk1&o3`;vClT%?{o~p&$$H!W7M8sPLl+X@> z$H&LEpO23nk%1S8YkSf-a18D?%TcyZaAEV1VWXwzrl+bRYUXIqWn%7VYQg1U?*yAC zI5=?+QP`oqg_{YjhrOMHtEh(r{a<&8!j6BP=BB6p>lQa#33@$Mby_(`7YkZJE&(nc zdPxjgT3T@zb4yW;XV3p>4*MlRZ|&yhB+AY0?(WXz&d=rOV#UoXA|k@g!^h3X#|gWG z)78_#&BTM#!S(6i5Bc{w&n#TcTx^`&Y#beEe?8a4)bX901U>z)j{f)e_c$#)Z2qq& z2iJdI3-*HCzpilea`ABg@7b`X;=j&{s@r&2*y%m9vA1w=h4mpRBq;vZ?f=h(|LgF# zhQR-8D9q3Id&}Rh{L@mL``7q?d!N6J_19ULwIngbx&LQ*Neq6Yudm_YM70#3$!K}N z@B7-+Stz_S%=F%Q>9{j>x~o(AdZ9c=Fz;&}9ytduIR(P|RY-8XT}_;EA4808l1-C7P|8o#*(O9IDu^D8T;#&U~KEqY5a~8w~Q(lao>j(=!TSQWV^=HY3OOdEklN zNFJ~bqQIZ6dP{U)_c;O}3O4#F&MirFhy0(B-wevWiH?dh8hJ*cZj|xCXN-9ymfR@J zw=|k!GQjU$K2np8JcV|-sc*GxdcIuUC(jziy)f!`-Pw|AT)b>rABmBMY!i7#JKXWX? z@We=HFRzzI55kS2|Ju|d?sdoM5ES0;iv5wmso;aJ6je42N#Atnr2UtQcpA-|7yq%5 zX!3L(Pgs^pOmDo~M?Z}hE)A~;NQ~gmtD;AXh6C^<{Q-jilK8V4o*yKXGFSzA>0=s9NAsiZlyCCLrlY`(O7vz#vSb0;jJv+ zjw{N=DueZdP&<(IkUv*KpqiX}n5LFe`jt+GPW$DJvW0@l2gu^iU32g>A|HI$rsn6V z@{*H~1)2jv8=I*4vcx}DP(;Clse-o6Z3o(G~qctwMF>!u5OcYvNCRmpBp~sq$0+lIwSFEKa#Ww8Q z%&9zpC|8WerF(LZjN?~P%*s*b3S2Pbx%_TCt9@#08cvQ?@wCErlNW#zdERWDL}oR8 zMNp?7(XAp!*dRLOv;XO?Q`G%xB;ZxJu%nGv!)VP%CKAFLgFTQ6gkIJ_JvJu#YA^aa}+#`=7qZEtrNj)l}e>;}H z7Q2_ZPo{nf7(P+7r<(iIlAw67Z&6CSCrcT2VYQEPmSA8s%N&oIr)@-&C507bTwj5d zTj9tAC~1QZz)4&)YUgnf!htJeQ~mJufvo85sh5a&w0uU<8>(2V_g=dR-^vAeh*`>RlROFD>#18WVqM zCPINlvnTT39^$VB<+avQQ!nl*od+Cjw-yM&yd7U#Eik9%1i zl$x_?_0OToW+@idn9Bo)K4Qzn`)fygBvSAvLJLREoos4(^LG)XW)1~T-t0-1z2b%; z#O!1pyJPO>5|*2&o{Uy_HGHja=-3f~UJE~i*&Le)YxY{#w&1yScyvr25&hv$4D2zg zkmr>v_SMl{zCf@A=(2PKgP62<%A`_Fq^0!@_kFmg7<8YU;o?>*K(i&MeiP!tZ!{Vw$r8ur?tX*&=P`OEu^ygtlv`4#mJ8xkx6}u z`?$19dX`CB+xzt6Xbnh#`AJR2S++VvRx;xSx2j|@y}p#N=aWnQN#0(gH;q=Pge}g9 z(JMg$@T368SfgsmISv8VhhvV(q7$>Ui8L@tIcxAdK9lh3B!3UH&%q8q38oSa5^W`SbZG%3LaUS=1gu!&)z zQ-*x+lt$5RbLeWCl?S}_!MbFP_EeJ=4Pzc&0h;ODOIes5aYm5Y{}89A0)KXC6I?tY z(|2s}{{Wf*Qz+A$0~5~Feh4l4G}o)Kf?tu3iHK6RYb{pQ`eWNG>ZEV%Qnc8fDs7Y6 z%J-?1GWGO?#V_q9uyPJ4?*ozPb~#8E#pni&@a!!rTSkg)C({(t9({kM1YNs<+=VlXmW=D?v3Kuc?1aN`8 zx^5;=lTF&(!xbe3BK#N4w0z2_&Y(1h-uZE)*4>!r|*%WZQ zAf)C)A+U;eUr{xksgU?b+-d=3RBO%#+vZOlwj#Sw-E8^>*z$W!-VMK&Fgs%8qqnQg zK0^YDM(Rce5va-Gd%@rLW(Rn4qqZqx*Z`%|DbRope$s5_@Kg@jeSib4xx*`>AEH-x z`~kQUVQbpS2Hs*2?v5u=R3sCMWfys6JI1P^lNsSsICojMpEBwnpJtF)bns85rz9jH z*Zm$(z1*hu-)1$ei(Rx;%2eo=H@;{}Zm|)Isdh~7Jm!Y&&z}u?t7X*~@8M@Ol47KA z$Yf`W=+TZIcNVDwU?vCrZt~;?!;%aCeQudjNq?-PmU3OV_EmDq!|$(e(>{1z)Qj!u zUf*$nGpH5nS*8P{-qwbfdb_mAPmI>2yg((cp z>Cj5BPVia?v6p}|q2~E|?F~@<2BXs|Pw%CC!gt{!ycFaRi8f-?o68Hoq$K%gE!m(A z(=c;cV*FKDr>TygdBPB1emw#}Tzk%i5QkHvalDGdYyuZ&5!w(rsK4JR22dhD)P|3W zM5);V7$n(Gy>ppBB9F+4GLY{_E$={ft)t^lK^D6Qw(y6up?(vD=L?0&kZO~T2;Sn? zYe4l|jDXY1>{S)|r-^y|?A9LaGncuysQDHmK=~X{h>!3m&_+ILh*vdvvEb`Q>79D1 zT9Ts^6JXnT-a7$(W#PLZ&pEWNj=O3sz1DK5OT~!UnBL&+*FJcMEKSm#tVAZx$nf{Z zbOSOtk-;36-GTL7F`reFbB*fl*@qELg9|iVh06=AZArd}HUJxXcQmHaJni&ZV*xgS z7mpZ`Ye(X``@XLK5B6)@s41OL^)|xPr;FC5%BfwptuyfgWf-j8IWK_{XL_r!zn4Ha zUOil63{zArMF4K&(hou@opa{glVIzo={G#)HFhDflgs&2_qg9kv(DK{1@OH)99$j| z^-@P_z|6Hw*mJUI2P&z1>A?D}E&Fo*1fUM2r?|DQvGTiR{sZ@%3FC>x#oqzTHz{atlMaL*cO${J@fcHy#Ve z#kku$*701`PA4rk@Id0wIGc})l#!0Tt8}s`e)+TK%_gQ>l8&_53I2Pk`1?k2!CZ6* z^_OK<$x`7oDKwxGqd>_7BW0e@?FtN{&;Ab4B^F{T8Pc_w8qwV))FWbIbn%Ct>OT*+ zBMxz6JMgkhI`z-rvU0XT6D#$p0Og{hnMVs~{_Hy;yZOH;)gZx!^3ys*fV=6%78$_0 z<4OtpHp#B`ZK)5V-G}-58HI+peywpL9ZFOh4dZqbt3>~Co#U+7Jy{btBYml=a)xDV$ZKn>Dc86 zU_bD$8I@dJ;0HC zC9|+;HCP0Q2Iu~ddzw456qRrj z;plZDoO`0hZ_HAhj`h3M^$GLL8hN1Sw3)x=vA@7(2fq#C+ZQ zaL`_8P_A9YkYOPW5giD*d&Laq4!%(XX{AfT3o|Ly3eUgM|53~UXjg2W*LKaESkZrY`d(X0wwRA_+>22gsVT|(beVJb1I^jdl0G&u9)t&T) zbEquY<;%quBKwO`LX-?Di97QmH1iN;A}kobfMuJ1Me`CUrBv<*gbEAR=U1xSgLFlz z`muV3{^*3?vl|l!Tx$CAId)SiN)I9ER~|Y>qX}jeyFzbF9mI51tqO*?P4{}S8~65q zz7kXqg)^1@E9HZ@RLk+TH90*O#LXcJPivY{!OK;pFzg_U81X`@nhmnbssZ=-3ISyF zQud!$XPmBenAjHp)UuZ#06ULU9mv_R(BY0T3~xmKjyI$(i~RGs+72eN>+QET0_0A* za5~2K*~tnF0sx$TV@rVC;dkij2tOEH^x8s1#Gw3qmI~|#J=N(hS)S_yn+>jy~Ey0P!0A9oA9Mj$dcrL|Xuvigle$V{|LB)&cdk zcR*|%4&1}U(w%(g)ZRC}7&+yi$(b2L?q@J+%l{a_PIbpuT?Cx(0@OFgwTZ^#3f0 zB;4@+3;b~`6@yr?3{^+@1M>3a=S#$gQRg}n=Ggr`NHrmZK7b6bCI$a*G{-U{#XeJ% zaPg#lcfT<(3;O&X_53#baSUbJ@+Rw2g21{h@HP~0-J;;Gn>Ngi`Nf1mz%i{yFN?F;!1i~-{3g2fY-hz`;UgOn{b^bHn#5hbKE1yrJY*K7af`J;9 zL#+S$qLrC$l7-@gf?@6NjO`OmRfxumIm->1fFRw>V@4|%sM z6WAY@=CqP=ud(=BP5yT?)LSZ>`pVV-@qW1N@B_UE=b+h+38-Tf?r~}8t?d@~ci6~O z{~Z|$*sqWrEZRa-(|T{^ELFCBhYm8b<}Fw|SIvCn2Ga>3{*LguhlI49?GWmyMVye1 zxQ*Up8XHz_y~adL^~ZpJq%oAcKa^Zrn0v0T^P6dcc#2M1uz;4t&i=)yh0a4q-jl0d z;*gAh0StNc=44DLO*^uz zMM}u)c1R0cf*LH+uh!O@Dy^K0kxnM77cINn#@#oa9?KhJ+HlSs*;dHN9kHBgUa7zA zG$-hrYuDeeFadah!Ck&&583B3Pj?KMX&!Kv7uJg4jU)mA$8ni8m56u(Xq`-gFJdtD zU=+#G$@$dbeVy0bmnC%IWMUs&GOJ!Y=99w9in0j{oJt7@AexrdFFq6 z6*h4uenaMeRlO8qu*P^+OD{n7mg!k~J%DUV?a?&4nt+!YKL zYtB+x9I%w(w|0~IkGSKxLH^(6XK)w2(A$yh5G4084Y+>6?;DzjUEESDuor%f_#>s? zSn_J$rqXFfkGHwX4e=ZW3yCW;>ZpL)Lh7oyQcjq+Rl= zNXrHY81vN(E^8m3MxYzm%y*1#x z#6E*feNQ3lKh;bBdB5==K+))anP=<1pn(_Y7sJ%k<1AtXghR1<(B^yBe( z>an_H@MWNm^N{fJ{_90dkNX35cuidlb9+4Y9g5K_!q=QN0pWmI!i)4K*>THa4Q?ti zlyHl*plO~6KfUVdbAmtIdZ1xOmeR2K{)KOn_LiYuyZw^SPEZH()ui{)eC9&qvUoN< z#e|lyB^1!tps4-^;YUQhI3Q|ZZ4^g!wbiou;|6rlbjiv0a1dw%9Lz_=1md{pF_`l{ z#i6`A4&tv)6T@KZqrMVeMubYhc{rnY99L862Hkb|top-KYx@%%U89_0fVV1i?ZXsE z$)2Z+YRR_R_)Zp$?K|nPbykM&6Ox@U6WG4NK=t!arOKFHw?MgFAjW|+4kJrWa z?f9Nz6g#)$SJJ@~yxEK%`3&92G=Wbk6*;blG4HDE@kjyy;RWygz>8^|M7DAzLJ{h++JT z`xPD67bO459O6lthvTX0V%NUvph{Q%sbP2PxUB3DvX44*-mIA>5Qc|j{d7c@`OQ|A zQPO$wg4!`gG6s;$unLqI$nmCha{SK6-EeRnbDLs#Yh<%(sla-07KTBqSC2XB@VDj; zG0KkFyr{{S_{S1PYX&K({+rT-$Jep}5CLsi=O{O?M7xjKFG?ZUuXFSdC@QiM6!&zG z#Vkb3FFw#mFO4kePNr>trl{;uTPgo3tMiM)_Wku9En;p#86oTMp9y63c-JHK;%{p@ zm(2qrPx}PgH{&mh8yxvA@AU@=1`&invi|yr#{zj>T>vm zPl4e;`!lJk^v};C??p(~Dn(@tAF8(SCZT?miV(`<+ zrcYzWWN8SDG^W_vrI@Y8vD5ScK)eu?mhTKCi{ppZ9tkvzeSV}{B_#ET|6V+4SvVXR zNpX#dQa4BVswz`=4J+gAMbFN#-vTAmT>x9(-R<)92Hr?we|6nNGQ0lO`6Pzcx$={m z8{S&k7EnC`>wTPCf?L9>==-F)UOiOm_=eSHh}zosJ=g@Gm-$ziy=eBqVAXyV-1gcq z*19DEhHeypM>nz2Sfz}!ML8|Or@?k{(}j$PrZH>@Z%gpwY9E>#Ob$+D`Sis%hpX{q zpty-i@|7dVAsi%P3k zmeV{R3*J{m^FUhU=SlLETehO51+Pd8yi0^3IrDc>1mOF`qEjqo#(9e7n&4ghQf2G0 zvKyON>9oSy@gYC$0R#b7MLuQ^JIc^*{WW(wFu{0{unlm)vHQO?`};_-xJ>y;R2eG@2_Quo@pdCp$B&#&=p{TEfM&1{@MSV9Y3TY?1aY;sOl|o6?Zq5%N_A0 zmGZhf@Z5FjhekFMH3Kt6*RiIK5h%h8vZfo&G;3F3c=65{1+{bjBMQW2r3ALT7uy_e zwUDE?23gWbqFASM6ou_Pg{{&pqSam@+^SB}m_i{MPRu|2jznga!={*0{jDkvzRAy; zNJGB=!22~p+ZQ*>2t6Pa0VXJKfu&F1xY;Bgh25L-6|M%6V05taki&_v*09jtB8SP<>)ZCw~X~%=Kh^YOP-UTKvX>bLTbioI4JGwLNQ!=@>Nt zYI}}dEb5nq(gO~ulhW(7P4v3eKhkt+{-6m7IfM4Wnb1B>wjYb1iEMBpx z`2i_&u@N@_%k4P@%e;I#;z`=lg~5@4wVS>?W~cq(Z%>BVW6FU`tVa4eC}|-aFn@ta zrX@zh{6$3k`H(k4uRyFG+v>xS&b_krY1xpmxI?@kF2pZQJLfhCsM)#2>bL;Mn31KB zu0tY)c}0fSWb{>9-auFhwp`rH=WeT^uP!Mk^=t)l03;i9p(gS0)@l8 zg(9N1k(YzDI~yq)%7FxYvjW2hWRJP~-^THxr);8aKW%P)d9JIUB=7c!L{};$l=KWz z!?F$jK?YrQZ0Z3-O_DTRMPa5{)s>XSVXwjq40rkw?s~5^{=`+kzI*g=+|Yo(uezH( z@W`y=pdbCs7wp-|b@^$0uBz<01wCI66NH2UiCmU`)F>tdsbmlhbTWmX+fXMc>2gM- zMnjlf%EEwP7S702x8GRncl@aYW8_j^i(v`8hijbxXbEmuYkB+UiA1yXZsgUqn}NK$ zf+x|{V^`Yq-$X(TrtcA9IhtU%`=xRX3Ez3<^z&4~u0lIo zZi=ToQ)PgG^0xooj8WX|qts!ROB#PvUO;=-$(pMArQf<#eGXo}Sd(gn z25B?SaRB1&f|}qatwgpie+qfsnxvZME{cqxYzg*$O&5WcZ$fk6D`Y3(7|4j|26%Wo zwQ+H=i`mXY;uh)6EuoMH&9jl!5Rzi5`~WXsl~X2Sqz z&+h>A+}z%Tb>@=$-j;E9_RZw+JR8sN0IB<5U`q;OODE)udIB~><9sb zjq8${nni1`Fr6dMeAEY&H@er>m0{KWn>W5kao8l;q*V?yWh2T|)5|9v&FvNJQP-oW z8mb{=`MRb1$xBDCbfuN{u#3F$2!3`qeO3yVrt)z9n=uEOXDX3?5pz^_1L$RcSCsr-J1)QIr&_ zB@*V>*J9!pMLwRKX4LH~G059DT(^3DeBsM zYf$OQd#XtGXklp0?Qen9Uuf<8YR>g3PKaJBqvR60o$ECg zCLge0Dez!X>{mk1`oA1~58_vEPt_~Rp#U|S=s5lYLUyMjY1M*x7o@+u3qgF}M0Z*_h;h(lsj4No8p|g%WLr#z&Zg71 zs|*l%UcH-CQS1%9$sfrbrUkPTsvGG|9^eHANnc~3niLm3{l)C74RL%%?0YGfWXrvq zHB*(Lr|`7Xx0E!wg~N(ph%rsbJU*y^xtS&K_ANuh*^C+=)TBa0G$_xo$d}TZo7$`^ z-+~MUwFv`=2r}0S=IH8%)0JrO%bWE-BmCU%BuuuZbYC$Kh?7;CU=0;WHe*NHDUtRg zh7MG#ewzt7>t6rjCIrlJ%1rc8+yt?ldPX z`_Nc!u~EN=sz!K|;}hx^lOz~iX2EtR23gF=o?v{#K!_-W9h$Y=6{L9+OAhyTMinAK%_z=p~%{930dVUUR}+3P#HfIdC6~y zR8QaM*L-i5#f|ywXl?pjwE=Vy7VG7WpftdTh3!OCgF@T?84Va34qtJDs>c0!pz@nu zD7&KqMT6ECs)&+CCqsLGd^1#O5qSBNTD=4QSCavWLWolVO?uzjo$4c8(Ow}=!LNIJ zpS~E7rOtc-aTyJgrNg{-qL=Vp&iBGkKKT4bHyhiwJ6VQqY3Y)7c{L5a6RXwi$ULa# z;=DhcjCtm=IDcwe26PXB<8}g7!3M+CixfotlYuZ zTiQ|9h@V)+!na>?KO#BhMfi5DLQp{b5OU4$*HA&jlrE8A`u#8?w%j2;-p?mwmL3GM zNwC~15Ua%NptE-8Huf-x!{}!9KEPHOsWP;$F+hB(vn^F71+Ee-A47Z#WN6r=Do@ei z3ZVYjP^&AbA4w>G^)=PCh&K^~=pzh3LBa^ofiJz(KZAX)=5~6ztZ3_XzMhBnth@p> z?(hec;>VZ-@xV`d5VPYo^wz#uoEwhNMD5-9Wb&h&)8q~qbRiIxTVe1=%JY-N$V*X` zJC|9LWi{J}(&X;RVR8k+9=l7TUF|eeH8@zUtH85sz6}F^e-4j5${RXjC{SLwcWu>y ztQO%DO8sHfl#YEUbjwiJHrkxq1dHDO)gYsLuk4Nia-djB4&?dA>Mc57Q!cJ$B(~*c z^fOF%`>!G&Lx53IkzN$vln3bP->Cmp#`sH#DpCDjk4h@YH}5UMJzEU2bzC$?zvBvq z^Rx7V(mrvY4PM~R-0g*2!Ny5JfKWLQ(c`^k$^VZUG)}i1?LNv(^AuD%BKxvwzIaL@A0P&e7u(e zU*eC}a4_96qXF-ms~KdIX)sy5JM$-c_db&GuEWa9udt4TruLsbz;1p`s?`7P_H;vSI%CV~EL6$*9RP%# zE*|8(!31v3owC&MKV$#(jP|&hZ*S93_pP)GEUr^j`$NhI$=YAj4fLriRpg;mlD@Pf zy{6jdtvqw|Aze##tC6bR?ep!Ln5C>>xAmNtHJx2ke`Pe20KL8vK`J7}tI;sO`e!pb z)Ti~g(p(_BPO8v)@lg`!b7YHa`)wull-b*T=x@|-W-VGughuO_*XrLrdgC&1meS<0 z=VJodL8IJ?ke$9kjO~`TN~iJ2Q-F(DA)$x`Mcy@q5b}A&bS=XQ9B9`p9UzVWlIVb4 z-@jscD82^C5bfAsRwa5XCqUo>e|WO zv>CGJv08;P-pG38rYuY}_AjfEu}3wL8{V~e((=#)deuLc<(V;JRjUW))wY@up1d@@ z=@3^VM>5a%7E~HI;q%s!`Ms!1qJ)g2rXkRp*e#oF^aMon1aQ zU`jR$IHx5L6lGw9A!z$nX@ESy;2&1{??~+?8O7NV0%frOv^H_JcWaCxG=(rD9=~|0 z1n3$a(5I&Q@YV0NHzVBL&A6IAZ1wZfn&Z>`BX{I_n|Y=Elli#Sq}{z8++Vsq81{K% zCi3};mwY92PfTL5$Mts|E8veUl(pITVV<9inng!z?KSwY&;SE$zlR3JGC3}?Acf)S z*wywYU?Q5-VNEc`DQ|_YxnP5|^((zwg|F7N2~6^x?SzO4r_KhSWKZ|%fy%(psg_1VF z`^6)SZP`hT{Ih>37OyAZ9wVO`mynwX`PnZaw=eO_->rCI@E*i>3f3T-i6n20!4Q3i z2jYRF|682P(296$%vFcY;yFGPx6bJLhF^YJbNwxKX-+kPMS0ki%%N zK#n2T(EV2PT*AYm9@{rMY)|j8W`<^o)L7>EWL4C==K!7YA^&RA$aC0+(_f@6nCiqzM8pUsHQlEMjot|C_`Rz|Q%37$TVGqFO9iXBoeg6nCeu?a& z_cq^x_tigP3H0lW5H5z(2XT})MeQpZME4Atd&r+2u6vgg1yR7m3+>RiLDQkmo(=|i zo>2N8$zZp_u+*u9q>W9YzjgJ0DdCxH^UbtXF7A~dLqnZ72TVu(RUS(ODwCjj*vh-_ z4@Wj<0Boh$FkPX$=Q&%iVI505hr*T;3C0`0e2W>!2FD1{L^AnnTJxEVGu7>Bj>X0t zGBXv6Tf3~YSZGA$gzxDG*aihKzR|tRve%lt)1!|ys;B3Wa>!QYucrD#t;+)ZGNx5d z?pvD?qJnfNR3I0Hy}b8*HnMk87|eQ(5>t6>DBfRadL(?VKNV`i_8H9>M&-ib3 zA{;w=*9vp**)G1sVXx59aDKAn{rNr+io3>P&d|2@X6B5Q!)W5Qbr(k6=3La0;=tim`oZc2+#G4yQj+U~-3_JY_gj5| z8OzHOm|VYA>^Hf-KI7KnDfzZ}q{mgY{0QM(r>!@wD?-VZ50+$l^tXlm_{_H~Niv_n z{f|X6V!ALN)B*gRE^ag{M~DS?10t)TxS}~c!;6iMNK)Lpwe#IRX>OBC4oGe6a}4~w z4&$?1UitnlBxY=!>_ZV-hL97y1Y%5}4M6;|e>O8?wVuOMKh|mniBI3hl6?yHiQi&) z&E@_T=p=p7iVr1Mk}&}w^r(b z+i6K~Ys!i7ez~1-im#xiSJVl8i|xiARFb>_0CES%^sD_jTh$FY6arBAQXLO>iY8g@}v(tWEp?ZCl%*iYblSAkoM-tf4fMfOQ%@|V4 zB{f5KlSl2Pz{Bq8{Wy2+4U84wR)NXob*zlSKo=YNJWi#;usU2r(B%vVweQKb;v}Ml z%8xy(S7i$GV!MlkUz)~!Wnt9Rr~gt@jbSb>>w5Lj`WKHL2OaXq%5GR{qPAOpp{7(i zwt2!*x<6|@J!@Xg>qhlk(`04sYtg}&zTfq!@#mrYu6fQX`l#l$);`SP8DaFNZcIpM z1UxV@nD_CTdgS{jqf`##4+=35%ghYJJqT-G2^ zkJ679#FOOEC+vy5F|8Bd?w4`fuLQ*V%*&{RLal{~u*c-(O{f)jVG5$m;oGE-^V5y?55rfRD)IehLebtcpKcpm#8Vr)oY zm{TAhsa?0SiKPo$gn7RSwm`z9fvZ<+s#aFVl7(r#>of44E1qYy)OROsM})DyhrFGS zJ4SwRMSgNu1W?x?(j+xzj(O|0C{GDM)qcWFhCTuYDEeUp}jl IHU9Ab0UT6sFaQ7m literal 0 HcmV?d00001 diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift new file mode 100644 index 000000000..c648fc2c7 --- /dev/null +++ b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift @@ -0,0 +1,258 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit + +public protocol ImageEditorPaletteViewDelegate: class { + func selectedColorDidChange() +} + +// MARK: - + +public class ImageEditorPaletteView: UIView { + + public weak var delegate: ImageEditorPaletteViewDelegate? + + public required init() { + super.init(frame: .zero) + + createContents() + } + + @available(*, unavailable, message: "use other init() instead.") + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: - Views + + // The actual default is selected later. + public var selectedColor = UIColor.white + + private let imageView = UIImageView() + private let selectionView = UIView() + private let selectionWrapper = OWSLayerView() + private var selectionConstraint: NSLayoutConstraint? + + private func createContents() { + self.backgroundColor = .clear + self.isOpaque = false + + if let image = UIImage(named: "image_editor_palette") { + imageView.image = image + } else { + owsFailDebug("Missing image.") + } + addSubview(imageView) + // We use an invisible margin to expand the hot area of + // this control. + let margin: CGFloat = 8 + // TODO: Review sizing when there's an asset. + imageView.autoSetDimensions(to: CGSize(width: 8, height: 200)) + imageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)) + + selectionWrapper.layoutCallback = { [weak self] (view) in + guard let strongSelf = self else { + return + } + strongSelf.updateState(fireEvent: false) + } + imageView.addSubview(selectionWrapper) + selectionWrapper.autoPinEdgesToSuperviewEdges() + + selectionView.addBorder(with: .white) + selectionView.layer.cornerRadius = selectionSize / 2 + selectionView.autoSetDimensions(to: CGSize(width: selectionSize, height: selectionSize)) + selectionWrapper.addSubview(selectionView) + selectionView.autoHCenterInSuperview() + + isUserInteractionEnabled = true + addGestureRecognizer(PaletteGestureRecognizer(target: self, action: #selector(didTouch))) + + updateState(fireEvent: false) + } + + // 0 = the color at the top of the image is selected. + // 1 = the color at the bottom of the image is selected. + private let selectionSize: CGFloat = 20 + private var selectionAlpha: CGFloat = 0 + + private func selectColor(atLocationY y: CGFloat) { + selectionAlpha = y.inverseLerp(0, imageView.height(), shouldClamp: true) + + updateState(fireEvent: true) + } + + private func updateState(fireEvent: Bool) { + var selectedColor = UIColor.white + if let image = imageView.image, + let cgImage = image.cgImage { + if let imageColor = image.color(atLocation: CGPoint(x: CGFloat(cgImage.width) * 0.5, y: CGFloat(cgImage.height) * selectionAlpha)) { + selectedColor = imageColor + } else { + owsFailDebug("Couldn't determine image color.") + } + } else { + owsFailDebug("Missing image.") + } + self.selectedColor = selectedColor + + selectionView.backgroundColor = selectedColor + + // There must be a better way to pin the selection view's location, + // but I can't find it. + self.selectionConstraint?.autoRemove() + let selectionY = selectionWrapper.height() * selectionAlpha + let selectionConstraint = NSLayoutConstraint(item: selectionView, + attribute: .centerY, relatedBy: .equal, toItem: selectionWrapper, attribute: .top, multiplier: 1, constant: selectionY) + selectionConstraint.autoInstall() + self.selectionConstraint = selectionConstraint + + if fireEvent { + self.delegate?.selectedColorDidChange() + } + } + + // MARK: Events + + @objc + func didTouch(gesture: UIGestureRecognizer) { + Logger.verbose("gesture: \(NSStringForUIGestureRecognizerState(gesture.state))") + switch gesture.state { + case .began, .changed, .ended: + break + default: + return + } + + let location = gesture.location(in: imageView) + selectColor(atLocationY: location.y) + } +} + +// MARK: - + +extension UIImage { + func color(atLocation locationPoints: CGPoint) -> UIColor? { + guard let cgImage = cgImage else { + owsFailDebug("Missing cgImage.") + return nil + } + guard let dataProvider = cgImage.dataProvider else { + owsFailDebug("Could not create dataProvider.") + return nil + } + guard let pixelData = dataProvider.data else { + owsFailDebug("dataProvider has no data.") + return nil + } + let bytesPerPixel: Int = cgImage.bitsPerPixel / 8 + guard bytesPerPixel == 4 else { + owsFailDebug("Invalid bytesPerPixel: \(bytesPerPixel).") + return nil + } + let imageWidth: Int = cgImage.width + let imageHeight: Int = cgImage.height + guard imageWidth > 0, + imageHeight > 0 else { + owsFailDebug("Invalid image size.") + return nil + } + // Convert the location from points to pixels and clamp to the image bounds. + let xPixels: Int = Int(round(locationPoints.x * self.scale)).clamp(0, imageWidth - 1) + let yPixels: Int = Int(round(locationPoints.y * self.scale)).clamp(0, imageHeight - 1) + let dataLength = (pixelData as Data).count + let data: UnsafePointer = CFDataGetBytePtr(pixelData) + let index: Int = (imageWidth * yPixels + xPixels) * bytesPerPixel + guard index >= 0, index < dataLength else { + owsFailDebug("Invalid index.") + return nil + } + + let red = CGFloat(data[index]) / CGFloat(255.0) + let green = CGFloat(data[index+1]) / CGFloat(255.0) + let blue = CGFloat(data[index+2]) / CGFloat(255.0) + let alpha = CGFloat(data[index+3]) / CGFloat(255.0) + + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } +} + +// MARK: - + +// The most permissive GR possible. Accepts any number of touches in any locations. +private class PaletteGestureRecognizer: UIGestureRecognizer { + + @objc + public override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + @objc + public override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + @objc + public override func shouldRequireFailure(of otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + @objc + public override func shouldBeRequiredToFail(by otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + @objc + public override func touchesBegan(_ touches: Set, with event: UIEvent) { + handle(event: event) + } + + @objc + public override func touchesMoved(_ touches: Set, with event: UIEvent) { + handle(event: event) + } + + @objc + public override func touchesEnded(_ touches: Set, with event: UIEvent) { + handle(event: event) + } + + @objc + public override func touchesCancelled(_ touches: Set, with event: UIEvent) { + handle(event: event) + } + + private func handle(event: UIEvent) { + var hasValidTouch = false + if let allTouches = event.allTouches { + for touch in allTouches { + switch touch.phase { + case .began, .moved, .stationary: + hasValidTouch = true + default: + break + } + } + } + + if hasValidTouch { + switch self.state { + case .possible: + self.state = .began + case .began, .changed: + self.state = .changed + default: + self.state = .failed + } + } else { + switch self.state { + case .began, .changed: + self.state = .ended + default: + self.state = .failed + } + } + } +} diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift index b0c858dff..d75b5ddb2 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift @@ -23,6 +23,8 @@ public class ImageEditorView: UIView { private let canvasView: ImageEditorCanvasView + private let paletteView = ImageEditorPaletteView() + enum EditorMode: String { // This is the default mode. It is used for interacting with text items. case none @@ -37,8 +39,11 @@ public class ImageEditorView: UIView { } } - private static let defaultColor = UIColor.white - private var currentColor = ImageEditorView.defaultColor + private var currentColor: UIColor { + get { + return paletteView.selectedColor + } + } @objc public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) { @@ -71,6 +76,8 @@ public class ImageEditorView: UIView { self.addSubview(canvasView) canvasView.autoPinEdgesToSuperviewEdges() + paletteView.delegate = self + self.isUserInteractionEnabled = true let moveTextGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleMoveTextGesture(_:))) @@ -129,6 +136,7 @@ public class ImageEditorView: UIView { private let newTextButton = UIButton(type: .custom) private var allButtons = [UIButton]() + // TODO: Should this method be private? @objc public func addControls(to containerView: UIView) { configure(button: undoButton, @@ -151,11 +159,7 @@ public class ImageEditorView: UIView { label: "Text", selector: #selector(didTapNewText(sender:))) - let redButton = colorButton(color: UIColor.red) - let whiteButton = colorButton(color: UIColor.white) - let blackButton = colorButton(color: UIColor.black) - - allButtons = [brushButton, cropButton, undoButton, redoButton, newTextButton, redButton, whiteButton, blackButton] + allButtons = [brushButton, cropButton, undoButton, redoButton, newTextButton] let stackView = UIStackView(arrangedSubviews: allButtons) stackView.axis = .vertical @@ -166,6 +170,10 @@ public class ImageEditorView: UIView { stackView.autoAlignAxis(toSuperviewAxis: .horizontal) stackView.autoPinTrailingToSuperviewMargin(withInset: 10) + containerView.addSubview(paletteView) + paletteView.autoVCenterInSuperview() + paletteView.autoPinLeadingToSuperviewMargin(withInset: 10) + updateButtons() } @@ -180,17 +188,6 @@ public class ImageEditorView: UIView { button.addTarget(self, action: selector, for: .touchUpInside) } - private func colorButton(color: UIColor) -> UIButton { - let button = OWSButton { [weak self] in - self?.didSelectColor(color) - } - let size: CGFloat = 20 - let swatch = UIImage(color: color, size: CGSize(width: size, height: size)) - button.setImage(swatch, for: .normal) - button.addBorder(with: UIColor.white) - return button - } - private func updateButtons() { undoButton.isEnabled = model.canUndo() redoButton.isEnabled = model.canRedo() @@ -262,12 +259,6 @@ public class ImageEditorView: UIView { updateButtons() } - @objc func didSelectColor(_ color: UIColor) { - Logger.verbose("") - - currentColor = color - } - // MARK: - Gestures private func updateGestureState() { @@ -679,3 +670,11 @@ extension ImageEditorView: ImageEditorCropViewControllerDelegate { // TODO: } } + +// MARK: - + +extension ImageEditorView: ImageEditorPaletteViewDelegate { + public func selectedColorDidChange() { + // TODO: + } +} diff --git a/SignalMessaging/categories/UIView+OWS.swift b/SignalMessaging/categories/UIView+OWS.swift index bb774a851..b29b58f16 100644 --- a/SignalMessaging/categories/UIView+OWS.swift +++ b/SignalMessaging/categories/UIView+OWS.swift @@ -125,6 +125,14 @@ extension UIView { } public extension CGFloat { + public func clamp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { + return CGFloatClamp(self, minValue, maxValue) + } + + public func clamp01(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { + return CGFloatClamp01(self) + } + // Linear interpolation public func lerp(_ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat { return CGFloatLerp(minValue, maxValue, self) @@ -139,6 +147,14 @@ public extension CGFloat { public static let halfPi: CGFloat = CGFloat.pi * 0.5 } +public extension Int { + public func clamp(_ minValue: Int, _ maxValue: Int) -> Int { + assert(minValue <= maxValue) + + return Swift.max(minValue, Swift.min(maxValue, self)) + } +} + public extension CGPoint { public func toUnitCoordinates(viewBounds: CGRect, shouldClamp: Bool) -> CGPoint { return CGPoint(x: (x - viewBounds.origin.x).inverseLerp(0, viewBounds.width, shouldClamp: shouldClamp),