From 8871cec197abd7f98c758982f222a0ecfc118319 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Sun, 6 Sep 2020 19:31:13 -0600 Subject: [PATCH 1/9] Initial project setup --- .gitignore | 6 +++++ .gitmodules | 3 +++ BuildSystem | 1 + CONTRIBUTING.md | 10 ++++++++ Dependencies/prereqs-licenses.json | 8 ++++++ Makefile | 22 ++++++++++++++++ Package.swift | 28 +++++++++++++++++++++ README.md | 27 ++++++++++++++++++++ Sources/KSSCoreUI/KSSCoreUI.swift | 3 +++ Tests/KSSCoreUITests/KSSCoreUITests.swift | 15 +++++++++++ Tests/KSSCoreUITests/XCTestManifests.swift | 9 +++++++ Tests/LinuxMain.swift | 7 ++++++ logo.png | Bin 0 -> 8819 bytes 13 files changed, 139 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 BuildSystem create mode 100644 CONTRIBUTING.md create mode 100644 Dependencies/prereqs-licenses.json create mode 100644 Makefile create mode 100644 Package.swift create mode 100644 Sources/KSSCoreUI/KSSCoreUI.swift create mode 100644 Tests/KSSCoreUITests/KSSCoreUITests.swift create mode 100644 Tests/KSSCoreUITests/XCTestManifests.swift create mode 100644 Tests/LinuxMain.swift create mode 100644 logo.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44ec6d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.build +.swiftpm +docs +docs.zip +REVISION + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a7e9edb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "BuildSystem"] + path = BuildSystem + url = https://github.com/klassen-software-solutions/BuildSystem.git diff --git a/BuildSystem b/BuildSystem new file mode 160000 index 0000000..4fd2ed4 --- /dev/null +++ b/BuildSystem @@ -0,0 +1 @@ +Subproject commit 4fd2ed4845b43c4562da753c7886592e6af7f621 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8030ea7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing to KSSCoreUI + +If you are going to contribute to this project, please make yourself familiar with our standards and +procedures: + +* [Git Procedures](https://www.kss.cc/standards-git.html) +* [Swift Coding Standards](https://www.kss.cc/standards-swift.html) + +Note that the iOS version is not currently included in the CI. That may change as we work on +more iOS projects, but for now the iOS version of the tests must be run manually within Xcode. diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json new file mode 100644 index 0000000..48a3edc --- /dev/null +++ b/Dependencies/prereqs-licenses.json @@ -0,0 +1,8 @@ +{ + "dependencies": [], + "generated": { + "process": "license-scanner", + "project": "KSSCoreUI", + "time": "2020-09-06T19:24:30.657777-06:00" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..013b885 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +AUTHOR := Klassen Software Solutions +AUTHOR_URL := https://www.kss.cc/ + +include BuildSystem/swift/common.mk + +check: Tests/LinuxMain.swift + +TEST_SOURCES := $(wildcard Tests/KSSFoundationTests/*.swift Tests/KSSTestTests/*.swift) + +Tests/LinuxMain.swift: $(TEST_SOURCES) + swift test --generate-linuxmain + echo "// auto-generated by the build system. do not edit" > $@ + echo "" >> $@ + echo "import XCTest" >> $@ + echo "import KSSFoundationTests" >> $@ + echo "import KSSTestTests" >> $@ + echo "" >> $@ + echo "var tests = [XCTestCaseEntry]()" >> $@ + echo "tests += KSSFoundationTests.__allTests()" >> $@ + echo "tests += KSSTestTests.__allTests()" >> $@ + echo "" >> $@ + echo "XCTMain(tests)" >> $@ diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..cc537ce --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "KSSCoreUI", + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "KSSCoreUI", + targets: ["KSSCoreUI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "KSSCoreUI", + dependencies: []), + .testTarget( + name: "KSSCoreUITests", + dependencies: ["KSSCoreUI"]), + ] +) diff --git a/README.md b/README.md index 37140ba..badbc2b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # KSSCoreUI Miscellaneous Swift UI utilities + +## Description + +This package is divided into a number of Swift Modules providing utility methods related to UI +classes. A key feature of this package is that it has no dependencies other than the KSSCore +libraries and a standard Apple development environment. + +The modules provided by this package are the following: + +* _KSSCocoa_ - items that depend on Cocoa +* _KSSMap_ - items that depend on MapKit +* _KSSUI_ - items that depend on SwiftUI +* _KSSWeb_ - items that depend on WebKit + + [API Documentation](https://www.kss.cc/apis/KSSCoreUI/docs/index.html) + + ## Module Availability + + Note that not all modules are available on all architectures. In addition, within a module there will + be things that are only available on some architectures. For example, anything that depends on + Cocoa will be available on _macOS_ but not on _iOS_. + + Presently we support the following: + + * _macOS_ - All modules are available + * _iOS_ - All modules are available, except for `KSSCocoa` and `KSSWeb`. + diff --git a/Sources/KSSCoreUI/KSSCoreUI.swift b/Sources/KSSCoreUI/KSSCoreUI.swift new file mode 100644 index 0000000..7001ff7 --- /dev/null +++ b/Sources/KSSCoreUI/KSSCoreUI.swift @@ -0,0 +1,3 @@ +struct KSSCoreUI { + var text = "Hello, World!" +} diff --git a/Tests/KSSCoreUITests/KSSCoreUITests.swift b/Tests/KSSCoreUITests/KSSCoreUITests.swift new file mode 100644 index 0000000..9c48561 --- /dev/null +++ b/Tests/KSSCoreUITests/KSSCoreUITests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import KSSCoreUI + +final class KSSCoreUITests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(KSSCoreUI().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/KSSCoreUITests/XCTestManifests.swift b/Tests/KSSCoreUITests/XCTestManifests.swift new file mode 100644 index 0000000..dbdad07 --- /dev/null +++ b/Tests/KSSCoreUITests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(KSSCoreUITests.allTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..c265b45 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import KSSCoreUITests + +var tests = [XCTestCaseEntry]() +tests += KSSCoreUITests.allTests() +XCTMain(tests) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..875d480a1feed01d925d5e6aaf58093cbc68a2bb GIT binary patch literal 8819 zcmbt)byQT{8aE0eATb~z4MQj(-7tgFjC6x^OLv130z-#%N;7oBPzur=14wrxA>AMP zzV}|gcisEPx4yI1iT!(?-+p56XP>jqIl&5Yl2{ld7)VG+SP&^OrTeq-uN@8b{%E%x zr*eOK;HV@if>b(4wsqe?x0BLzL_&J<^w<6XDLM5S5)ulnxr&C9hOCT$k*zh0fw8Tj z35%Px-Muyv639*9{?gjS$$-ku+RDaJzzq!its!uK{R?IVQvFtOvIGM)WEH4HZ5>Rg zxLLSZ*nmP9R8&+T2V+wKB{7M=>Gv%#@ST&Bod7GVtE(%ED<_MsgBdG3KR-V!8wV=~ z2lKrKv!lC>lYtwvjU&xpo&2jGF%w542Xi|ob6Xp#U;P>w+B!Rdfxuse{=NO>r<1wq ze=ONJ{vFnRfULhHtn4gotpAN>;%5FoXul-?lV)u6@2KpY9jt!GWNgH0Vr61&V&mj^ z&tw0u=I(R*Z{q*uq?>`=U&ekX@vqoGzdRREGI6xEa{iS9RU30BAr8>*fc^>pCnJBc zL~X6@984S??`cBZe^dU%{;99=A2UL{|FZEX_)mhogZX`=2EY6gV*lH~pV&Y3HUFD| zKfyl;zfv!tVD4sOr6Fd1pU~e?u(Pp&SpTc!pG;9(D_aL;I|Cz=U!MJ@`~m$_`?np< z|7qtB$sbH(BLPQeLkkllr$5H)e$;-?eh~vR)<0kn>wnb--OmL9SzBXsQ+F`~CletK zHg-N{Hg;xq4i$D@0XB944t@~pKXm@^;Fmgune{f*vNtq{gN^It1~5QdK(8YL1E`6fh6SjFwZk5sKNvhk!YdHI2x zy^1cEq{=6xMpi~-uYF|Wb^JcjVFdf3;B84ow;{7CwjsaC@rK6MHHp6srj>^`(OLLH zvrw`?wCsSA)@S910^iwJq^Eu`%y6cMZcKnVxtqV-C6#7wjQ${Qe7s+}Ed?G|)^Q$G zU$}AUO*E!`CdC$|Pu2R`&ld$59TgAr{|De^k9x~DHKKP@Sbi{j=X>Wbl6i3M&)Nh) zo51;SA^j@uO>>9(-3OZ8n(%Pb6=a-e5q@Wf+%vVB^^Yuni1;XTb9d$_<<+*#Q>-jx z_dt{rlh{u+UL?l8Uu;O8md_uCKRoltR8~%X-ttOl`!Y#{nsL>ET8e9IY|N%m!R{r| z!Cj311zE{Adw!#s3sPm+OFKSTr$k+#5$0J~y6C|ZAqlUhktWWuF_3nv%Ezi%+OIkx ztP_!mOshY4W$?vw>%2n-mZvT!)Rbq2^(W>$%fQ=CT-+vsLus5BJCg&sG?Fs0Ln#gG zrpn6dW^Z||*ciH!np1*`H=2;yQyLpLaHWY5b~;CwCqOkmw>|BKGk=`^)dM?J5l`in zmH5S~rvrCI&J$L17LzG=eWQ0jb~e#)aB%YHJogUatWRa-J^em7I3cuex?Y7rmAV`; z_5$Mxky7XeN2`P%+wr)Z6ec~uiCcW>`UR?c?>9~mtIiu_t>GH)Ml?=dLfnrj$ke5^ zupR{UTM}--YbjKAY(rCin+WC4LQ1&&oSf7fKxP(nd#b{926B<&_0?xCHZz3bA2Y{X zeJ23IDdG9M-4I5l@mkVdda)kVx_CCP6pdZA3NLSbL3PjVk=EU|1o-s3Z zN&|4X+S+4yMi~=WSnAN*V~9VKUJ5HCXaW{`?`4@r2M%8=zk7)A(OE#F9DNudQu_+c z?}xllQuqcmG8C~eC%vdsP82Y>a6E?OPV(*p4@fRE$}EC85yu7Nb_=WGmC_+ zTiEH8IS(XbPP!w5!KT!yhaNH2>&`nesL&ar_=L`z@0DD(YY=X(e>zC`TMfAvL1 zW_?t)t|b;VHzY*b{)8FSW*^NbH>+UDW5_ILTY3@S2tukcgX&iS&s0_?3V!BX&>C3FWA0bnXscJgp|$Xq z=ei2Wh7{#Cw7hS8`HF z7w3Up^@Odmvgv3X;jL|GEEhhG%89IXWrSI-TwtB;lStT*97Yq;FFcC+otG%eb<}mYjQ2l}^dBpHM%T;p8LbKrai{r66 z%>58FyjPaRDP3*Dwv8(hn5g}hZyVKsWZ0+2Ii8UeC1@Hm4bHmCuCAzM9UiYLnO#0V z+{Sf&3`AB+q@S!9u93c(d^c^Br zj0dL4L7Gsvo!lN#hUnCgPjl^&?AIt!*h<7pSOR{HSP9yvZQJ(xQTaj4--_&u!-uO3 z1x+X`vC`6fzqnY7cQ&FA6Ox|9$&79`RU`M?ocXbAu;V6CDLw zqMBRN9KTS9Trel~Ey!V?!)cY^|AJ3V?7|=%j5RH)_pgNcg?!^zDolFo{4b=pi z2ao&w$fM1{2W+W!;9{gd``G~MGsq=kKDPlE;_X|s4i(+nF2$**rtjIfZuS$~{@m5f z_xZSLukf`R>sCQH51#!E!NV&lB?>)$+^*W1ZtF#3*>cvb8n7RaURgL?GP(*bDY4PX zjM|zlZ>1FQo*tl?Yt_4BwC$;u+E$cKp?I%>f*xElx*{stN`7@(5+e4}tGy6_k_Ji< z5{|vl<@p&-BikVae|Ud5|$nLL95lDlaw zTlmG2L5g*hXyw`yJ-~yu;LOb9^UZg)?y(tlgM0}I5FJKM3*xDwCt`S-BnGvzHjCu2eV|Q;!0&|{ z1~vs(h!;90C+!-ehvv%7O%?8|!a-Cs-X}L4`=N9+8GRRE+a~s%IeS|sr5`lJD8bb3 zg47?XVgeX0g{lAB;pSomlh38u_S4gPn^@9gG2*}BWj~Lqtm^y@2 z(SYQ;uMR(|Oq7UkGX>nDPIk>B_;4?hCiPs`UgPh8s9pM}L&hVRY><|-GVWs&pLtVn zK5LQFg4RZm3bhtz%y5;<5;^*gk=WkV;;s~4sPZsBoJm2w6?y2@mu>qFjIVVGY5`On z>xBgdpKUF3C}u(+j8Rx_kc(Df$rHj^4actR$Z0bC{!i&)S^8I-n`U_GvlM=d48bqL zX7FIr98!c0@K)iB>rb!3OTMOXJhRtwh~v%mB8BupWvnMY9JKDf!BySzC|Pj4uHO-h z=4@{u(PB^;h`QPab}03ysU{ol)fGy0&sx=Og+hh>*hB|r?>6*qm)8q!&Z6^`YziYl zTHur52Nv6_IWlvM?O)xZn<_i4hSc;Z8#v1ZtvbKII7dT zoA&bBjH+|Id!KO`MHZw`F@Aos1B`Jbz}c@!(Br|~0rP6@ly%pRQ+Hd3AaF#(=z;r| zZi{bQ?#iqU5B@`QL3Plm;@xe%~g6@oBgWsD>` zIkf#?N$=^|jMmdlO-$qOPak<}1GSuE3!c^XQ%S*k7Nk)Cc-*)=Vyxd#Yjs!!fIz;j zZM#8M4NQWAg1l8SUr7XhcS%tAZ1}ODmU+ZF@iTD6HDcriEa+$#9~ z-fp%dzQ`2)MxDM(0}D5DH3Vn#H&>hmH0^y6z;kh2D~vXPemp zYiZ{<`vWo`?wXYBUykG`&z3yKHjY)%j>sYVII}ii^!PPfwNm@^-Rh@2e>vhPn|_@4 z`ig>3OhFKPRe?b{%3Wy7eI=H!6pC$E5CxF#tWZxuwXBPskdx7OJ5VIS3&($40SIz=|5)~;%90#q3;*D#`JmPqNa zFrz2Qlzv(G;`TFVK)eKgWg}|QuFdDtAND!hT9%9;z?)s=!5oW+d(U)X07Is*zKY;>><3_4C=DPg~6ycV1N4x_d%tW!S<fbi#%}4nZSz8{#rljxQ z(!L}2rZ*8i|9&(}qa*uhc|8Hl&X)!S)6%hGa9cd>i1fw>8o?Sa6-EZqa)qa zoD!$#9Q_nBt0<30FUgV@u=f&VV`#Z72C|3buQBobta|CMy&@V2@^nW<){>n$UB}mlk$Dsl_?4wzpF;Ip zC8gz)Wtu@>{Tak#6eY1GS#% z*i!3g{6yT@JEX8B`O%;Lvi8f~K&foz7>2orab)8%Zt+mOQkih_@M2lfI^0oa>d=ii z8qyA*zPxUz?OaXYl0aD?n6+1*U`UjM+Znm)KviVG$RpOzF;mHjuBhRp$wzU!O z0Ra8RF^IqMxZ#`Jk?aH><;SA}BAY17k_tpIK9;l3&sQ;!x1m~c&HV5Gh+2k&2gS7Rdw@D}FiNc&fIrMiai%J`0 z(4QCi>6Meht5JKm$H8awPk7P=>t6OiH?Eyd&-(|+mMFksKo@GKRQX(LUK$>cCuN^D z9+FB(z3x7HjWv0vw0r1%M)U;=Wq$}`%N5?v&m$vtvxh^o>hfFNuvFM?x6!uV*epGR?J; zjJp=9x}LCVLwRQ1^G`-ycIQYzmk8y)_^@Y*g#fY6s{MBKEkmNc*gB=D8qt$s(w(<8 z)FOmIN~_a)4@6=ErIaVj=k^cxMIu}zPNGfs=_L`^`8Z=L)Q0b5^f_;DdztdEc#m-} z3#mdvaReUYkh+qcntgW`Zx0aVv49q{-DvJjThDM92O^i$?MDcx#UxCMPFW zNpxKsr7J3IM+*NQA*)qu%%WEx)iL1K(|EP_lE0V$B7rHbiiRie(kEFzK_WSQZqHPO z^W~rogdOTCakJb(w8s1>9w4^L<};U%w{sku^N1d%jH&Q2)(X8rsEH|fRe9|HqX$}U(6yg==;5iS=XScO7<5OIiai$8iz1csAWAmH_~{s5t=V#W z@PXsi*^qxD%16@oB^bgvM~(MOYcGbPrn!omxX-X8Mfd=5RGzHh=PhLhICZ=Bu>FMt zAKcl}W3!ilQZU9hyBP#k@h}ENgj0!q({W9j7#w7iPw+s@2;!ulHtwI+zTc82&XpH( zzj1DKRq{1_$z73CuucSrci?nV98+;nJ&I?K(9V_6F$hUtg%eoi@MX=&P?&yRXKpYxb?+=ayN zM8%l$3_4lQ^4VrdIMZcKa8=JhU;u6nEv+`VhSGPvcQo#4&z~%celB>uQnO4>3omBo zKcBW#q?})J*rxQ$bW#bRnhb{YpCF5Lb+sL?~tRu3inpZRjl^=K5 zS+Gm0e8Hb=g{eme(yQbx19D|(-R-rripwiYN;cn%U$0F7uB{%qe)n6P04zY!fU&f4 zo$29O(~?e~s@UkOQY~h8Hn49?Xy4rSRGuPgLUQ zZNq|qV<@9}jM;P;L=%fIFmKm+T_{&Q988r9=A5}Am}-F4C&Q;s*8PhB-u(S2pTmNP9`o~k=J8Y0gVJ%AH-DZlG_I^_@)2=9dgJPjCH?DY0kLsJk|c~mOSphDLVdI_E21lJ;DcRojGRF=CPxVt288>fl(cZ zx8LK?Y;ja*`NDx`R&puq=tQBCLcjFaB?Ih!rS!u^U20CpYrCvmFDN*$|`+)U7D`0W>?RgU z*LOUBFvrTbZ+jhM`JAbdi!a6&`KBD^p4Z6$+<_!@Brv8E>NI9=mOWJr9mXdWI1pbH z3!AG>X;zbg$PZ+%&Su#M?Qig~+jkTx`hjxYpNMkc?NbgObmE-L?}U`1wk}i^kYkP}t_vYvWa>7KARG+>2?8n!sI7%%75(yg#E^#>5DRq2)7+jc`eT>1> z$vEKNL#{^NQ7GBcCF)X4if&FVx14-@zNjCsE|N4C30fZc7>(N+pM&~z7GHSrbt7H) zT=6{DeiPnCce!%GTbCT&OF}C9YHca+m#)a=muVzD%B^t68@WT|1i|%l-)C*8E*vw< zVo`=~7o9q=Lx>Qjh1B1Q)AgpXy?6QFS8Zbj$Am|bLx(K;s?k6LsmX%z_>edufqZIM zHQvJ!#RNrP0SxEp*I#rzx9Cg=cU=(+G3+CEUpnF6h;cdXQ)G08)2(y0xtd8E27S>q>!hwk|#|sB+3q@Wo7CCJrlAY_1PnZ za{G~$XhvV3^2cH`k~1pP=wR!dGTQOK0ljI%|2X|xg^g1z%A=IvQ)ToBaf8>?RawS$ z`(};ohFt)`&F2=N{~KM4Vkp(25GHqN+J__A$rjJ(pz)pOh?9Xa(}cIiOK_HX+Wcug z;dtKoo@YPFC9c|3ExSe0NGBDJ(z7buyGnITu5y>%wO}egZ3;0}1@AuLymDHrnS)y< zSSh+)Twgc|IxaIzxeB_BUNN~kBQv<4I5$TU1(?(+SM)r;?t&?AdEG4_l#*g;?4s!(L!e~1@XnOzPE*7<-Moe?0a5Dl2UI?wsw9E7(jdZl${{T#6fH}= zxD$tjvZmZ%sb!_B`LsmKo8vQuaFT2%tdvUb(+549_A^WMr{MBh!pw%?dWBiWNBP_7 zm<^aYO_aIJFIokxR?pglk$0+qo?`Ytq(1)dF((#XaNPd=xJf zEnm@MDN+BdH^wEH6c z^HxVDY zng+z9Z?;jQ!ao}`xf4f4nZBqgRb`k}7ppvC5zLRpR$rZc$nLV8MklrzUQK~RAvwtaQ_v+!B$kL6QlBwb+Vl5W}XbE#>uC=zI~J0iE> zn()cnoVpNmeK9B{kS;n=8n-1dhjQ^HrUO?z=lw?|2!)fO{YNRQ3Am8;+S&lAFq!)dMMZAB)v!6*9JWLjeHJc11=BVqMtB{FWPQ zSo*oAxnkd^FUZ~}B4aq|&6n3>1_v3rr(2PVKi2tR0W5&lO`zBgz^+amy z=w#*Rp{W5aRxX4SS9_d%n4S5{P(gB3$%vex+aI=?;L!EvDR$}Jl;+0XGpx~haupc&&2%DN0V b%xj$dLB6trxjm6ze}X{7<-|%w^!@%1;}cD8 literal 0 HcmV?d00001 From 470fd98ba866c4a206e07f3a108b7db4a5935585 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Sun, 6 Sep 2020 20:38:34 -0600 Subject: [PATCH 2/9] Moved KSSCocoa --- .gitignore | 1 + Dependencies/prereqs-licenses.json | 16 +++- Makefile | 2 +- Package.swift | 22 ++--- Sources/KSSCocoa/NSApplicationExtension.swift | 84 ++++++++++++++++ Sources/KSSCocoa/NSColorExtension.swift | 23 +++++ Sources/KSSCocoa/NSFontExtension.swift | 41 ++++++++ Sources/KSSCocoa/NSImageExtension.swift | 96 +++++++++++++++++++ Sources/KSSCocoa/NSMenuExtension.swift | 40 ++++++++ .../KSSCocoa/NSViewControllerExtension.swift | 50 ++++++++++ Sources/KSSCocoa/NSViewExtension.swift | 51 ++++++++++ Sources/KSSCocoa/NSWindowExtension.swift | 47 +++++++++ Sources/KSSCoreUI/KSSCoreUI.swift | 3 - .../NSApplicationExtensionTests.swift | 21 ++++ .../KSSCocoaTests/NSImageExtensionTests.swift | 52 ++++++++++ Tests/KSSCocoaTests/XCTestManifests.swift | 31 ++++++ Tests/KSSCoreUITests/KSSCoreUITests.swift | 15 --- Tests/KSSCoreUITests/XCTestManifests.swift | 9 -- Tests/LinuxMain.swift | 10 +- 19 files changed, 567 insertions(+), 47 deletions(-) create mode 100644 Sources/KSSCocoa/NSApplicationExtension.swift create mode 100644 Sources/KSSCocoa/NSColorExtension.swift create mode 100644 Sources/KSSCocoa/NSFontExtension.swift create mode 100644 Sources/KSSCocoa/NSImageExtension.swift create mode 100644 Sources/KSSCocoa/NSMenuExtension.swift create mode 100644 Sources/KSSCocoa/NSViewControllerExtension.swift create mode 100644 Sources/KSSCocoa/NSViewExtension.swift create mode 100644 Sources/KSSCocoa/NSWindowExtension.swift delete mode 100644 Sources/KSSCoreUI/KSSCoreUI.swift create mode 100644 Tests/KSSCocoaTests/NSApplicationExtensionTests.swift create mode 100644 Tests/KSSCocoaTests/NSImageExtensionTests.swift create mode 100644 Tests/KSSCocoaTests/XCTestManifests.swift delete mode 100644 Tests/KSSCoreUITests/KSSCoreUITests.swift delete mode 100644 Tests/KSSCoreUITests/XCTestManifests.swift diff --git a/.gitignore b/.gitignore index 44ec6d8..87d168c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ .swiftpm docs docs.zip +Package.resolved REVISION diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index 48a3edc..6a4d0bb 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -1,8 +1,20 @@ { - "dependencies": [], + "dependencies": [ + { + "moduleLicense": "MIT License", + "moduleName": "KSSCore", + "moduleUrl": "https://github.com/klassen-software-solutions/KSSCore.git", + "x-isOsiApproved": true, + "x-licenseTextEncoded": "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAxOSBLbGFzc2VuIFNvZnR3YXJlIFNvbHV0aW9ucwoKUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBvZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weQpvZiB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZSAiU29mdHdhcmUiKSwgdG8gZGVhbAppbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzCnRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwKY29waWVzIG9mIHRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzCmZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgpUaGUgYWJvdmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBwZXJtaXNzaW9uIG5vdGljZSBzaGFsbCBiZSBpbmNsdWRlZCBpbiBhbGwKY29waWVzIG9yIHN1YnN0YW50aWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS4KClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTIE9SCklNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZLApGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBTkQgTk9OSU5GUklOR0VNRU5ULiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUKQVVUSE9SUyBPUiBDT1BZUklHSFQgSE9MREVSUyBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSwgREFNQUdFUyBPUiBPVEhFUgpMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SIE9USEVSV0lTRSwgQVJJU0lORyBGUk9NLApPVVQgT0YgT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBTT0ZUV0FSRSBPUiBUSEUgVVNFIE9SIE9USEVSIERFQUxJTkdTIElOIFRIRQpTT0ZUV0FSRS4K", + "x-spdxId": "MIT", + "x-usedBy": [ + "KSSCoreUI" + ] + } + ], "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-06T19:24:30.657777-06:00" + "time": "2020-09-06T20:40:48.470505-06:00" } } \ No newline at end of file diff --git a/Makefile b/Makefile index 013b885..c55ac7c 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ include BuildSystem/swift/common.mk check: Tests/LinuxMain.swift -TEST_SOURCES := $(wildcard Tests/KSSFoundationTests/*.swift Tests/KSSTestTests/*.swift) +TEST_SOURCES := $(wildcard Tests/KSSCocoaTests/*.swift) Tests/LinuxMain.swift: $(TEST_SOURCES) swift test --generate-linuxmain diff --git a/Package.swift b/Package.swift index cc537ce..c160746 100644 --- a/Package.swift +++ b/Package.swift @@ -5,24 +5,18 @@ import PackageDescription let package = Package( name: "KSSCoreUI", + platforms: [ + .macOS(.v10_11), + .iOS(.v13), + ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "KSSCoreUI", - targets: ["KSSCoreUI"]), + .library(name: "KSSCocoa", targets: ["KSSCocoa"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", .branch("development/v4") /*from: "3.2.1"*/), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. - .target( - name: "KSSCoreUI", - dependencies: []), - .testTarget( - name: "KSSCoreUITests", - dependencies: ["KSSCoreUI"]), + .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), + .testTarget(name: "KSSCocoaTests", dependencies: ["KSSCocoa", .product(name: "KSSTest", package: "KSSCore")]), ] ) diff --git a/Sources/KSSCocoa/NSApplicationExtension.swift b/Sources/KSSCocoa/NSApplicationExtension.swift new file mode 100644 index 0000000..cda7a86 --- /dev/null +++ b/Sources/KSSCocoa/NSApplicationExtension.swift @@ -0,0 +1,84 @@ +// +// NSApplicationExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-02-25. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa +import KSSFoundation + + +public extension NSApplication { + + /** + Return the name of the application as read from the bundle. Note that as the name is a required + key in the bundle, if it does not exist this will cause a fatal error. + */ + var name: String { Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String } + + /** + Return the version of the application as read from the bundle. Note that as the verion is an optional + key in the bundle, this may return nil. + */ + var version: String? { Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String } + + /** + Return the build number of the application as read from the bundle. Note that as the build number is a required + key in the bundle, if it does not exist, or if it cannot be converted to an integer, this will cause a fatal error. + + - warning: If the bundle version includes a decimal point (which may be true during library unit testing) it will + be rounded down to the nearest integer. + */ + var buildNumber: Int { Int(Double(Bundle.main.infoDictionary![kCFBundleVersionKey as String] as! String)!) } + + + /** + Search for and create if necessary a common directory for the containing application. + + - parameters: + - directory: The directory we are searching for. + - domain: The search domain. + + - throws: + Any error that FileManager.findOrCreateDirectory may throw. + + - returns: + The URL of the found directory. + */ + func findOrCreateApplicationDirectory(for directory: FileManager.SearchPathDirectory, + in domain: FileManager.SearchPathDomainMask) throws -> URL + { + // Determine the required directory path. + let fileManager = FileManager.default + let commonURL = try fileManager.url(for: directory, + in: domain, + appropriateFor: nil, + create: true) + let applicationDirectoryURL = commonURL.appendingPathComponent(self.name) + + // Find or create it. + try fileManager.findOrCreateDirectory(at: applicationDirectoryURL, + withIntermediateDirectories: true) + return applicationDirectoryURL + } + + /** + Returns true if the current appearance is a dark mode appearance. + */ + var isDarkMode: Bool { + if #available(OSX 10.15, *) { + let appearanceDescription = effectiveAppearance.debugDescription.lowercased() + return appearanceDescription.contains("dark") + } else { + if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle") as? String { + return appleInterfaceStyle.lowercased().contains("dark") + } + return false + } + } +} +#endif diff --git a/Sources/KSSCocoa/NSColorExtension.swift b/Sources/KSSCocoa/NSColorExtension.swift new file mode 100644 index 0000000..a750ea8 --- /dev/null +++ b/Sources/KSSCocoa/NSColorExtension.swift @@ -0,0 +1,23 @@ +// +// NSColorExtension.swift +// HTTPMonitor +// +// Created by Steven W. Klassen on 2020-08-13. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +#if canImport(Cocoa) + +import Cocoa +import Foundation + + +public extension NSColor { + /// Specifies the color to be used for highlighting errors. Typically this would be used as a background, + /// but it can also be used as a foreground color if you add `.withAlphaComponent(1)` to make + /// it stand out better. + class var errorHighlightColor: NSColor { NSColor.systemYellow.withAlphaComponent(0.50) } +} + +#endif diff --git a/Sources/KSSCocoa/NSFontExtension.swift b/Sources/KSSCocoa/NSFontExtension.swift new file mode 100644 index 0000000..41a873f --- /dev/null +++ b/Sources/KSSCocoa/NSFontExtension.swift @@ -0,0 +1,41 @@ +// +// NSFontExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-02-11. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa + +public extension NSFont { + + /** + Returns a font like the existing one but with the specified traits. + + - returns: + The new font or nil if the conversion could not be made. + */ + func withTraits(traits: NSFontDescriptor.SymbolicTraits) -> NSFont? { + let descriptor = fontDescriptor.withSymbolicTraits(traits) + return NSFont(descriptor: descriptor, size: 0) + } + + /** + Returns a new font like the existing one but converted to bold. + */ + func bold() -> NSFont { + return withTraits(traits: .bold)! + } + + /** + Returns a new font like the existing one but converted to italic. + */ + func italic() -> NSFont { + return withTraits(traits: .italic)! + } +} + +#endif diff --git a/Sources/KSSCocoa/NSImageExtension.swift b/Sources/KSSCocoa/NSImageExtension.swift new file mode 100644 index 0000000..f0bdaf9 --- /dev/null +++ b/Sources/KSSCocoa/NSImageExtension.swift @@ -0,0 +1,96 @@ +// +// NSImageExtension.swift +// +// Created by Steven W. Klassen on 2020-03-04. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import os +import AppKit +import Cocoa +import Foundation + +public extension NSImage { + + /** + Reads an image from an input stream. Will be nil if there is an error reading the stream or if + the stream does not contain a supported image type. + */ + convenience init?(fromInputStream inStream: InputStream) { + inStream.open() + defer { inStream.close() } + if let imageData = try? Data(fromInputStream: inStream) { + self.init(data: imageData) + return + } + return nil + } + + /** + Performs a color invert of the image and returns the new one. If an error occurs, a message is logged + and the original image is returned. + */ + @available(OSX 10.14, *) + func inverted() -> NSImage { + guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + os_log(.error, "Could not create CGImage from NSImage") + return self + } + + let ciImage = CIImage(cgImage: cgImage) + guard let filter = CIFilter(name: "CIColorInvert") else { + os_log(.error, "Could not create CIColorInvert filter") + return self + } + + filter.setValue(ciImage, forKey: kCIInputImageKey) + guard let outputImage = filter.outputImage else { + os_log(.error, "Could not obtain output CIImage from filter") + return self + } + + guard let outputCgImage = outputImage.toCGImage() else { + os_log(.error, "Could not create CGImage from CIImage") + return self + } + + return NSImage(cgImage: outputCgImage, size: self.size) + } + + /** + Resize an image to fit within the given size. + - note: This is based on code found at https://stackoverflow.com/questions/11949250/how-to-resize-nsimage/42915296#42915296 + */ + func resized(to newSize: NSSize) -> NSImage? { + if let bitmapRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height), + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, + colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0) + { + bitmapRep.size = newSize + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmapRep) + draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: .zero, operation: .copy, fraction: 1.0) + NSGraphicsContext.restoreGraphicsState() + + let resizedImage = NSImage(size: newSize) + resizedImage.addRepresentation(bitmapRep) + return resizedImage + } + + return nil + } +} + +fileprivate extension CIImage { + func toCGImage() -> CGImage? { + let context = CIContext(options: nil) + if let cgImage = context.createCGImage(self, from: self.extent) { + return cgImage + } + return nil + } +} + +#endif diff --git a/Sources/KSSCocoa/NSMenuExtension.swift b/Sources/KSSCocoa/NSMenuExtension.swift new file mode 100644 index 0000000..e88add9 --- /dev/null +++ b/Sources/KSSCocoa/NSMenuExtension.swift @@ -0,0 +1,40 @@ +// +// NSMenuExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-02-14. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +#if canImport(Cocoa) + +import Cocoa + +public extension NSMenu { + + /** + Performs a deep search of the menu for a menu item with the given tag. + + - returns: + The first item that has the requested tag or nil if no such item could be found. + */ + @available(*, deprecated, message: "Use item(withTag:) instead") + func findMenuItem(withTag tag: Int) -> NSMenuItem? { + for item in self.items { + if !item.isSeparatorItem { + if item.tag == tag { + return item + } + if let submenu = item.submenu { + if let subitem = submenu.findMenuItem(withTag: tag) { + return subitem + } + } + } + } + return nil + } +} + +#endif diff --git a/Sources/KSSCocoa/NSViewControllerExtension.swift b/Sources/KSSCocoa/NSViewControllerExtension.swift new file mode 100644 index 0000000..3df2101 --- /dev/null +++ b/Sources/KSSCocoa/NSViewControllerExtension.swift @@ -0,0 +1,50 @@ +// +// NSViewControllerExtension.swift +// KSSCore +// +// Created by Steven W. Klassen on 2017-04-12. +// Copyright © 2017 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Foundation +import Cocoa +import KSSFoundation + + +public extension NSViewController { + + /** + Search for and create if necessary a common directory for the containing application. + + - parameters: + - directory: The directory we are searching for. + - domain: The search domain. + + - throws: + Any error that FileManager.findOrCreateDirectory may throw. + + - returns: + The URL of the found directory. + */ + func findOrCreateApplicationDirectory(for directory: FileManager.SearchPathDirectory, + in domain: FileManager.SearchPathDomainMask) throws -> URL + { + // Determine the required directory path. + let fileManager = FileManager.default + let commonURL = try fileManager.url(for: directory, + in: domain, + appropriateFor: nil, + create: true) + let applicationName = Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String + let applicationDirectoryURL = commonURL.appendingPathComponent(applicationName) + + // Find or create it. + try fileManager.findOrCreateDirectory(at: applicationDirectoryURL, + withIntermediateDirectories: true) + return applicationDirectoryURL + } +} + +#endif diff --git a/Sources/KSSCocoa/NSViewExtension.swift b/Sources/KSSCocoa/NSViewExtension.swift new file mode 100644 index 0000000..31e0da4 --- /dev/null +++ b/Sources/KSSCocoa/NSViewExtension.swift @@ -0,0 +1,51 @@ +// +// NSViewExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-02-24. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa +import SwiftUI + + +public extension NSView { + /** + Returns the SwiftUI original root content hosted by this view. Note that this is a copy of the view + before any modifiers have been applied. + + - warning: + SwiftUI views are structs hence what is returned is a copy of the view. The intended purpose of this + is for examining the state and using it to update things that cannot be done using just SwiftUI controls. + You cannot use this to modify the view itself, although depending on the view's `@State` and other + data, you may be able to modify that portion of the object. But that will be highly dependant on the + details of the view itself. + + - warning: + Due to the current limitations of Swift Generics, this method must rely on `Mirror` based reflection + in order to find the original root view. + + - returns: + A copy of the original `View` or `nil` if this is not an `NSHostingView` that contains a view + of the type specified by `RootView`. + */ + @available(OSX 10.15, *) + func originalRootView() -> RootView? { + if let hostingView = self as? NSHostingView { + return hostingView.rootView + } + let mirror = Mirror(reflecting: self) + if let rootView = mirror.descendant("_rootView") { + let mirror2 = Mirror(reflecting: rootView) + if let content = mirror2.descendant("content") as? RootView { + return content + } + } + return nil + } +} + +#endif diff --git a/Sources/KSSCocoa/NSWindowExtension.swift b/Sources/KSSCocoa/NSWindowExtension.swift new file mode 100644 index 0000000..5223e74 --- /dev/null +++ b/Sources/KSSCocoa/NSWindowExtension.swift @@ -0,0 +1,47 @@ +// +// NSWindowExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-02-24. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import os +import Cocoa + +public extension NSWindow { + /** + Force the tab bar to become visible if possible. + */ + @available(OSX 10.13, *) + func turnOnTabBar(_ sender: Any? = nil) { + if let group = self.tabGroup { + if !group.isTabBarVisible { + self.toggleTabBar(sender) + } + } + } + + /** + Force the tab bar to become invisible if possible. + */ + @available(OSX 10.14, *) + func turnOffTabBar(_ sender: Any?) { + if let group = self.tabGroup { + if group.isTabBarVisible { + self.toggleTabBar(sender) + + // If we are the only item in the tab group, then toggling the bar is + // not sufficient, we also need to remove ourselves from the group. + if self.tabbedWindows?.count == 1 { + os_log(.debug, "Forcing off TabBar by removing us from the group") + group.removeWindow(self) + } + } + } + } +} + +#endif diff --git a/Sources/KSSCoreUI/KSSCoreUI.swift b/Sources/KSSCoreUI/KSSCoreUI.swift deleted file mode 100644 index 7001ff7..0000000 --- a/Sources/KSSCoreUI/KSSCoreUI.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct KSSCoreUI { - var text = "Hello, World!" -} diff --git a/Tests/KSSCocoaTests/NSApplicationExtensionTests.swift b/Tests/KSSCocoaTests/NSApplicationExtensionTests.swift new file mode 100644 index 0000000..a4ec978 --- /dev/null +++ b/Tests/KSSCocoaTests/NSApplicationExtensionTests.swift @@ -0,0 +1,21 @@ +#if canImport(Cocoa) + +import XCTest +import KSSCocoa +import KSSTest + +class NSApplicationExtensionTests: XCTestCase { + func testMetadata() { + assertEqual(to: "xctest") { NSApplication.shared.name } + assertNil { NSApplication.shared.version } + assertTrue { NSApplication.shared.buildNumber > 0 } + } + + func testIsDarkMode() { + // There is no way for us to test this since we don't know what the developers + // settings will be. So the only test is that it doesn't crash or anything. + _ = NSApplication.shared.isDarkMode + } +} + +#endif diff --git a/Tests/KSSCocoaTests/NSImageExtensionTests.swift b/Tests/KSSCocoaTests/NSImageExtensionTests.swift new file mode 100644 index 0000000..cc96b01 --- /dev/null +++ b/Tests/KSSCocoaTests/NSImageExtensionTests.swift @@ -0,0 +1,52 @@ +#if canImport(Cocoa) + +import XCTest +import KSSCocoa +import KSSTest + +class NSImageExtensionTests: XCTestCase { + func testInitFromInputStream() { + var image = NSImage(fromInputStream: streamFromEncodedString(plusSymbolEncodedString)) + assertNotNil { image } + assertEqual(to: 56) { image?.size.width } + assertEqual(to: 56) { image?.size.height } + + image = NSImage(fromInputStream: streamFromEncodedString(notAnImageEncodedString)) + assertNil { image } + } + + @available(OSX 10.14, *) + func testInverted() { + let inputImage = NSImage(fromInputStream: streamFromEncodedString(plusSymbolEncodedString))! + let outputImage = inputImage.inverted() + assertEqual(to: inputImage.size.width) { outputImage.size.width } + assertEqual(to: inputImage.size.height) { outputImage.size.height } + assertNotEqual(to: inputImage) { outputImage } + } + + func testResized() { + let image = NSImage(fromInputStream: streamFromEncodedString(plusSymbolEncodedString))? + .resized(to: NSSize(width: 16, height: 18)) + assertNotNil { image } + assertEqual(to: 16) { image?.size.width } + assertEqual(to: 18) { image?.size.height } + } +} + +fileprivate let plusSymbolEncodedString = """ +iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAMAAACfWMssAAAAYFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAD////68c6fAAAAHnRSTlMAAQgPERYXGSIkJSYnKDg7PJWWmJ+goaLa29zd/P0v +2OJcAAAAAWJLR0QfBQ0QvQAAAHhJREFUSMft1skKgCAQgGG1xTbb93Te/zHToxQYU6eY/zj4HQTBYYz6 +edUyFRjHD4CNI2AKtgQBpYOSIEGCNhF7KQeVPxN3rtcQTHdX18Kjmu8gG0yYmeHuklHmVbuTtT+L6MkR +JPgGor9yvgOsmOWBlfOY09L2904sZSVqkhks3wAAAABJRU5ErkJggg== +""" + +fileprivate let notAnImageEncodedString = "dGhpcyBzaG91bGQgbm90IGJlIGFuIGltYWdlCg==" + +fileprivate func streamFromEncodedString(_ encodedString: String) -> InputStream { + let decodedData = Data(base64Encoded: encodedString, options: .ignoreUnknownCharacters)! + return InputStream(data: decodedData) +} + +#endif diff --git a/Tests/KSSCocoaTests/XCTestManifests.swift b/Tests/KSSCocoaTests/XCTestManifests.swift new file mode 100644 index 0000000..e58d224 --- /dev/null +++ b/Tests/KSSCocoaTests/XCTestManifests.swift @@ -0,0 +1,31 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension NSApplicationExtensionTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__NSApplicationExtensionTests = [ + ("testIsDarkMode", testIsDarkMode), + ("testMetadata", testMetadata), + ] +} + +extension NSImageExtensionTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__NSImageExtensionTests = [ + ("testInitFromInputStream", testInitFromInputStream), + ("testInverted", testInverted), + ("testResized", testResized), + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(NSApplicationExtensionTests.__allTests__NSApplicationExtensionTests), + testCase(NSImageExtensionTests.__allTests__NSImageExtensionTests), + ] +} +#endif diff --git a/Tests/KSSCoreUITests/KSSCoreUITests.swift b/Tests/KSSCoreUITests/KSSCoreUITests.swift deleted file mode 100644 index 9c48561..0000000 --- a/Tests/KSSCoreUITests/KSSCoreUITests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import KSSCoreUI - -final class KSSCoreUITests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(KSSCoreUI().text, "Hello, World!") - } - - static var allTests = [ - ("testExample", testExample), - ] -} diff --git a/Tests/KSSCoreUITests/XCTestManifests.swift b/Tests/KSSCoreUITests/XCTestManifests.swift deleted file mode 100644 index dbdad07..0000000 --- a/Tests/KSSCoreUITests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(KSSCoreUITests.allTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index c265b45..2e254eb 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1,11 @@ -import XCTest +// auto-generated by the build system. do not edit -import KSSCoreUITests +import XCTest +import KSSFoundationTests +import KSSTestTests var tests = [XCTestCaseEntry]() -tests += KSSCoreUITests.allTests() +tests += KSSFoundationTests.__allTests() +tests += KSSTestTests.__allTests() + XCTMain(tests) From 4f8d211c171cc78c59acfb287ebf986986d9fa52 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Sun, 6 Sep 2020 20:57:13 -0600 Subject: [PATCH 3/9] Moved KSSMap --- Dependencies/prereqs-licenses.json | 2 +- Package.swift | 2 + Sources/KSSMap/MKMapViewExtension.swift | 68 +++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 Sources/KSSMap/MKMapViewExtension.swift diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index 6a4d0bb..bd048f3 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -15,6 +15,6 @@ "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-06T20:40:48.470505-06:00" + "time": "2020-09-06T20:56:32.608635-06:00" } } \ No newline at end of file diff --git a/Package.swift b/Package.swift index c160746..672db6c 100644 --- a/Package.swift +++ b/Package.swift @@ -11,12 +11,14 @@ let package = Package( ], products: [ .library(name: "KSSCocoa", targets: ["KSSCocoa"]), + .library(name: "KSSMap", targets: ["KSSMap"]), ], dependencies: [ .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", .branch("development/v4") /*from: "3.2.1"*/), ], targets: [ .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), + .target(name: "KSSMap", dependencies: ["KSSCocoa"]), .testTarget(name: "KSSCocoaTests", dependencies: ["KSSCocoa", .product(name: "KSSTest", package: "KSSCore")]), ] ) diff --git a/Sources/KSSMap/MKMapViewExtension.swift b/Sources/KSSMap/MKMapViewExtension.swift new file mode 100644 index 0000000..7e1d99d --- /dev/null +++ b/Sources/KSSMap/MKMapViewExtension.swift @@ -0,0 +1,68 @@ +// +// MKMapViewExtension.swift +// KSSCore +// +// Created by Steven W. Klassen on 2019-03-08. +// Copyright © 2019 Klassen Software Solutions. All rights reserved. +// + +import MapKit + +public extension MKMapView { + /** + scrollToCurrentLocation will obtain the user's current location and attempt to + scroll that map to that position. + */ + func scrollToCurrentLocation() { + if isScrollEnabled { + if let coord = userLocation.location?.coordinate{ + setCenter(coord, animated: true) + } + } + } + + /** + zoomIn zooms the map in approximately the same amount as a single click on + the zoom controls. If zooming is not enabled, then this does nothing. + */ + func zoomIn() { + doZoom(0.5) + } + + /** + zoomOut zooms the map out approximately the same amount as a single click on + the zoom controls. If zooming is not enabled, then this does nothing. + */ + func zoomOut() { + doZoom(2) + } + + /** + snapToNorth rotates the map, if necessary, in order to point it north. If already + pointing north or if rotations are not enabled, then this does nothing. + */ + func snapToNorth() { + if isRotateEnabled && camera.heading != 0 { + let c = MKMapCamera(lookingAtCenter: camera.centerCoordinate, + fromDistance: camera.altitude, + pitch: camera.pitch, + heading: 0) + setCamera(c, animated: true) + } + } + + private func doZoom(_ factor: Double) { + if isZoomEnabled { + let r = region + let s = MKCoordinateSpan(latitudeDelta: min(r.span.latitudeDelta * factor, 180), + longitudeDelta: min(r.span.longitudeDelta * factor, 180)) + setRegion(MKCoordinateRegion(center: r.center, span: s), animated: true) + + // Most of the time snapToNorth will do nothing, since setRegion will have + // already done this, but when rotated near the poles, and already zoomed out, + // setRegion will do nothing. Hence we call this so that our result is + // reasonably close to that of using the zoom controls. + snapToNorth() + } + } +} From 82e7e504dbaf6d422660644a2ddf2d871eb787a1 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Sun, 6 Sep 2020 21:07:00 -0600 Subject: [PATCH 4/9] Moved KSSWeb --- Dependencies/prereqs-licenses.json | 2 +- Makefile | 10 +- Package.swift | 3 + Sources/KSSWeb/KSSWebView.swift | 92 +++++++++++++++++++ .../KSSWebTests/KSSWebConstructorTests.swift | 32 +++++++ Tests/KSSWebTests/XCTestManifests.swift | 18 ++++ Tests/LinuxMain.swift | 8 +- 7 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 Sources/KSSWeb/KSSWebView.swift create mode 100644 Tests/KSSWebTests/KSSWebConstructorTests.swift create mode 100644 Tests/KSSWebTests/XCTestManifests.swift diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index bd048f3..10f3b8d 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -15,6 +15,6 @@ "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-06T20:56:32.608635-06:00" + "time": "2020-09-06T21:02:41.105178-06:00" } } \ No newline at end of file diff --git a/Makefile b/Makefile index c55ac7c..4b8bb49 100644 --- a/Makefile +++ b/Makefile @@ -5,18 +5,18 @@ include BuildSystem/swift/common.mk check: Tests/LinuxMain.swift -TEST_SOURCES := $(wildcard Tests/KSSCocoaTests/*.swift) +TEST_SOURCES := $(wildcard Tests/KSSCocoaTests/*.swift Tests/KSSWebTests/*.swift) Tests/LinuxMain.swift: $(TEST_SOURCES) swift test --generate-linuxmain echo "// auto-generated by the build system. do not edit" > $@ echo "" >> $@ echo "import XCTest" >> $@ - echo "import KSSFoundationTests" >> $@ - echo "import KSSTestTests" >> $@ + echo "import KSSCocoaTests" >> $@ + echo "import KSSWebTests" >> $@ echo "" >> $@ echo "var tests = [XCTestCaseEntry]()" >> $@ - echo "tests += KSSFoundationTests.__allTests()" >> $@ - echo "tests += KSSTestTests.__allTests()" >> $@ + echo "tests += KSSCocoaTests.__allTests()" >> $@ + echo "tests += KSSWebTests.__allTests()" >> $@ echo "" >> $@ echo "XCTMain(tests)" >> $@ diff --git a/Package.swift b/Package.swift index 672db6c..e248abb 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let package = Package( products: [ .library(name: "KSSCocoa", targets: ["KSSCocoa"]), .library(name: "KSSMap", targets: ["KSSMap"]), + .library(name: "KSSWeb", targets: ["KSSWeb"]), ], dependencies: [ .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", .branch("development/v4") /*from: "3.2.1"*/), @@ -19,6 +20,8 @@ let package = Package( targets: [ .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), .target(name: "KSSMap", dependencies: ["KSSCocoa"]), + .target(name: "KSSWeb", dependencies: ["KSSCocoa"]), .testTarget(name: "KSSCocoaTests", dependencies: ["KSSCocoa", .product(name: "KSSTest", package: "KSSCore")]), + .testTarget(name: "KSSWebTests", dependencies: ["KSSWeb"]), ] ) diff --git a/Sources/KSSWeb/KSSWebView.swift b/Sources/KSSWeb/KSSWebView.swift new file mode 100644 index 0000000..e822a03 --- /dev/null +++ b/Sources/KSSWeb/KSSWebView.swift @@ -0,0 +1,92 @@ +// +// KSSWebView.swift +// +// Created by Steven W. Klassen on 2020-02-20. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa +import SwiftUI +import WebKit +import os + +/** + SwiftUI view for displaying HTML. This is essentially a SwiftUI wrapper around a WKWebView. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSWebView: NSViewRepresentable { + /** + A binding to the URL whose content is to be displayed. + */ + @Binding public var url: URL + + /** + Construct a web view bound to the given url. + */ + public init(url: Binding) { + self._url = url + } + + /// :nodoc: + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + /// :nodoc: + public func makeNSView(context: Context) -> WKWebView { + let webView = WKWebView() + if url.scheme == "http" || url.scheme == "https" { + let request = URLRequest(url: url) + webView.load(request) + } else if url.scheme == "file" { + webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) + } else { + os_log("Not sure how to handle url: %s", url.absoluteString) + } + webView.navigationDelegate = context.coordinator + return webView + } + + /// :nodoc: + public func updateNSView(_ nsView: WKWebView, context: Context) { + // Intentionally empty + } +} + +/// :nodoc: +@available(OSX 10.15, *) +extension KSSWebView { + public class Coordinator: NSObject, WKNavigationDelegate { + var parent: KSSWebView + + init(_ parent: KSSWebView) { + self.parent = parent + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url { + if url.scheme == "file" { + decisionHandler(.allow) + return + } else if url.scheme == "http" || url.scheme == "https" || url.scheme == "mailto" { + decisionHandler(.cancel) + NSWorkspace.shared.open(url) + return + } + } + os_log("Unsupported request: %s", navigationAction.request.url?.absoluteString ?? "No URL") + decisionHandler(.cancel) + } + } +} + +#else + +// Force the compiler to give us a more descriptive message. +@available(iOS, unavailable) +public struct KSSWebView {} + +#endif diff --git a/Tests/KSSWebTests/KSSWebConstructorTests.swift b/Tests/KSSWebTests/KSSWebConstructorTests.swift new file mode 100644 index 0000000..57145c0 --- /dev/null +++ b/Tests/KSSWebTests/KSSWebConstructorTests.swift @@ -0,0 +1,32 @@ +// +// Created by Steven W. Klassen on 2020-06-18. +// + +#if canImport(Cocoa) + +import SwiftUI +import XCTest + +import KSSWeb + +@available(OSX 10.15.0, *) +fileprivate struct MyView: View { + @State private var url: URL? = nil + @State private var url2 = URL(string: "http://hello.not.there/")! + @State private var testBool: Bool = true + + var body: some View { + VStack { + KSSWebView(url: $url2) + } + } +} + +class KSSWebConstructorTests: XCTestCase { + func testObjectWillCompile() { + // Intentionally empty. This file tests that the UI controls can be included + // in a view. It is a compile, not a runtime, test. + } +} + +#endif diff --git a/Tests/KSSWebTests/XCTestManifests.swift b/Tests/KSSWebTests/XCTestManifests.swift new file mode 100644 index 0000000..9dacbe7 --- /dev/null +++ b/Tests/KSSWebTests/XCTestManifests.swift @@ -0,0 +1,18 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension KSSWebConstructorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__KSSWebConstructorTests = [ + ("testObjectWillCompile", testObjectWillCompile), + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(KSSWebConstructorTests.__allTests__KSSWebConstructorTests), + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 2e254eb..854f02c 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,11 +1,11 @@ // auto-generated by the build system. do not edit import XCTest -import KSSFoundationTests -import KSSTestTests +import KSSCocoaTests +import KSSWebTests var tests = [XCTestCaseEntry]() -tests += KSSFoundationTests.__allTests() -tests += KSSTestTests.__allTests() +tests += KSSCocoaTests.__allTests() +tests += KSSWebTests.__allTests() XCTMain(tests) From 29365c26597f1b5c7c72aa698d14858eeb1576b3 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Mon, 7 Sep 2020 10:59:25 -0600 Subject: [PATCH 5/9] Moved KSSUI as KSSSwiftUI --- Dependencies/prereqs-licenses.json | 2 +- Makefile | 18 - Package.swift | 7 +- README.md | 2 +- Sources/KSSSwiftUI/KSSCommandTextField.swift | 220 +++++++++++ .../KSSSwiftUI/KSSNSButtonViewSettable.swift | 174 +++++++++ .../KSSSwiftUI/KSSNSControlViewSettable.swift | 116 ++++++ Sources/KSSSwiftUI/KSSNativeButton.swift | 346 +++++++++++++++++ Sources/KSSSwiftUI/KSSSearchField.swift | 206 ++++++++++ Sources/KSSSwiftUI/KSSTextView.swift | 266 +++++++++++++ Sources/KSSSwiftUI/KSSToggle.swift | 157 ++++++++ Sources/KSSSwiftUI/KSSURLTextField.swift | 107 ++++++ Sources/KSSSwiftUI/KSSValidatingView.swift | 82 ++++ Sources/KSSSwiftUI/ViewExtension.swift | 94 +++++ Sources/KSSSwiftUI/filterIconWithMenu.swift | 362 ++++++++++++++++++ .../KSSSwiftUI/filterIconWithoutMenu.swift | 290 ++++++++++++++ Tests/KSSCocoaTests/XCTestManifests.swift | 31 -- .../KSSCommandTextFieldTests.swift | 74 ++++ .../KSSNativeButtonTests.swift | 169 ++++++++ .../KSSSwiftUITests/KSSSearchFieldTests.swift | 66 ++++ Tests/KSSSwiftUITests/KSSTextViewTests.swift | 37 ++ Tests/KSSSwiftUITests/KSSToggleTests.swift | 134 +++++++ .../KSSURLTextFieldTests.swift | 58 +++ Tests/KSSWebTests/XCTestManifests.swift | 18 - Tests/LinuxMain.swift | 11 - 25 files changed, 2965 insertions(+), 82 deletions(-) create mode 100644 Sources/KSSSwiftUI/KSSCommandTextField.swift create mode 100644 Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift create mode 100644 Sources/KSSSwiftUI/KSSNSControlViewSettable.swift create mode 100644 Sources/KSSSwiftUI/KSSNativeButton.swift create mode 100644 Sources/KSSSwiftUI/KSSSearchField.swift create mode 100644 Sources/KSSSwiftUI/KSSTextView.swift create mode 100644 Sources/KSSSwiftUI/KSSToggle.swift create mode 100644 Sources/KSSSwiftUI/KSSURLTextField.swift create mode 100644 Sources/KSSSwiftUI/KSSValidatingView.swift create mode 100644 Sources/KSSSwiftUI/ViewExtension.swift create mode 100644 Sources/KSSSwiftUI/filterIconWithMenu.swift create mode 100644 Sources/KSSSwiftUI/filterIconWithoutMenu.swift delete mode 100644 Tests/KSSCocoaTests/XCTestManifests.swift create mode 100644 Tests/KSSSwiftUITests/KSSCommandTextFieldTests.swift create mode 100644 Tests/KSSSwiftUITests/KSSNativeButtonTests.swift create mode 100644 Tests/KSSSwiftUITests/KSSSearchFieldTests.swift create mode 100644 Tests/KSSSwiftUITests/KSSTextViewTests.swift create mode 100644 Tests/KSSSwiftUITests/KSSToggleTests.swift create mode 100644 Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift delete mode 100644 Tests/KSSWebTests/XCTestManifests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index 10f3b8d..0ac3a38 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -15,6 +15,6 @@ "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-06T21:02:41.105178-06:00" + "time": "2020-09-07T10:56:32.344954-06:00" } } \ No newline at end of file diff --git a/Makefile b/Makefile index 4b8bb49..dce45d1 100644 --- a/Makefile +++ b/Makefile @@ -2,21 +2,3 @@ AUTHOR := Klassen Software Solutions AUTHOR_URL := https://www.kss.cc/ include BuildSystem/swift/common.mk - -check: Tests/LinuxMain.swift - -TEST_SOURCES := $(wildcard Tests/KSSCocoaTests/*.swift Tests/KSSWebTests/*.swift) - -Tests/LinuxMain.swift: $(TEST_SOURCES) - swift test --generate-linuxmain - echo "// auto-generated by the build system. do not edit" > $@ - echo "" >> $@ - echo "import XCTest" >> $@ - echo "import KSSCocoaTests" >> $@ - echo "import KSSWebTests" >> $@ - echo "" >> $@ - echo "var tests = [XCTestCaseEntry]()" >> $@ - echo "tests += KSSCocoaTests.__allTests()" >> $@ - echo "tests += KSSWebTests.__allTests()" >> $@ - echo "" >> $@ - echo "XCTMain(tests)" >> $@ diff --git a/Package.swift b/Package.swift index e248abb..f7bc4be 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let package = Package( products: [ .library(name: "KSSCocoa", targets: ["KSSCocoa"]), .library(name: "KSSMap", targets: ["KSSMap"]), + .library(name: "KSSSwiftUI", targets: ["KSSSwiftUI"]), .library(name: "KSSWeb", targets: ["KSSWeb"]), ], dependencies: [ @@ -19,9 +20,11 @@ let package = Package( ], targets: [ .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), - .target(name: "KSSMap", dependencies: ["KSSCocoa"]), - .target(name: "KSSWeb", dependencies: ["KSSCocoa"]), + .target(name: "KSSMap", dependencies: []), + .target(name: "KSSSwiftUI", dependencies: ["KSSCocoa"]), + .target(name: "KSSWeb", dependencies: []), .testTarget(name: "KSSCocoaTests", dependencies: ["KSSCocoa", .product(name: "KSSTest", package: "KSSCore")]), + .testTarget(name: "KSSSwiftUITests", dependencies: ["KSSSwiftUI", .product(name: "KSSTest", package: "KSSCore")]), .testTarget(name: "KSSWebTests", dependencies: ["KSSWeb"]), ] ) diff --git a/README.md b/README.md index badbc2b..097012b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The modules provided by this package are the following: * _KSSCocoa_ - items that depend on Cocoa * _KSSMap_ - items that depend on MapKit -* _KSSUI_ - items that depend on SwiftUI +* _KSSSwiftUI_ - items that depend on SwiftUI * _KSSWeb_ - items that depend on WebKit [API Documentation](https://www.kss.cc/apis/KSSCoreUI/docs/index.html) diff --git a/Sources/KSSSwiftUI/KSSCommandTextField.swift b/Sources/KSSSwiftUI/KSSCommandTextField.swift new file mode 100644 index 0000000..a32a311 --- /dev/null +++ b/Sources/KSSSwiftUI/KSSCommandTextField.swift @@ -0,0 +1,220 @@ +// +// KSSCommandTextField.swift +// +// Created by Steven W. Klassen on 2020-01-24. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +#if canImport(Cocoa) + +import Cocoa +import KSSCocoa +import SwiftUI + + +/** + TextField control suitable for entering command line type items. + + This control provides a SwiftUI View that can be used for command-like entries. It is implemented as a + SwiftUI wrapper around a NSTextField control and allows for the following features. + + - Single line text entry, + - Command line submission on pressing the `Return` or `Enter` keys, + - Command line history accessed via the up and down arrow keys, + - Optional validation of the input before submission, + - Automatic highlighting of errors. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSCommandTextField: NSViewRepresentable, KSSNSControlViewSettable, KSSValidatingView { + /// Settings applicable to all KSS `NSControl` based Views. + public var nsControlViewSettings = KSSNSControlViewSettings() + + /** + Binding to the text of the current command. This will be updated when the user presses the `Return` + or `Enter` keys. + */ + @Binding public var command: String + + /** + Help text to be displayed in the text field when it is empty. + */ + public let helpText: String + + @State private var hasFocus = false + @State private var history = CommandHistory() + + + /** + Construct a new text field with the given binding and help text. + */ + public init(command: Binding, helpText: String = "command") { + self._command = command + self.helpText = helpText + } + + // MARK: Items for NSViewRepresentable + + /// :nodoc: + public func makeNSView(context: Context) -> NSTextField { + let textField = NSTextField() + textField.placeholderString = helpText + textField.delegate = context.coordinator + textField.stringValue = command + _ = applyNSControlViewSettings(textField, context: context) + return textField + } + + /// :nodoc: + public func updateNSView(_ textField: NSTextField, context: Context) { + DispatchQueue.main.async { + _ = self.applyNSControlViewSettings(textField, context: context) + } + } + + // MARK: Items for KSSValidatingView + + + /// :nodoc: + public var validatorFn: ((String) -> Bool)? = nil + + /// :nodoc: + public var errorHighlightColor: NSColor = NSColor.errorHighlightColor +} + + +@available(OSX 10.15, *) +extension KSSCommandTextField { + /// :nodoc: + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + /// :nodoc: + public class Coordinator: NSObject, NSTextFieldDelegate { + let parent: KSSCommandTextField + + init(_ parent: KSSCommandTextField) { + self.parent = parent + } + + public func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if commandSelector == #selector(NSTextView.insertNewline(_:)) { + submitCommand(control, textView: textView) + } + else if commandSelector == #selector(NSTextView.moveUp(_:)) { + previousInHistory(textView: textView) + } + else if commandSelector == #selector(NSTextView.moveDown(_:)) { + nextInHistory(textView: textView) + } + + return false + } + + private func submitCommand(_ control: NSControl, textView: NSTextView) { + let value = textView.string + if let fn = parent.validatorFn { + if !fn(value) { + parent.ensureBackgroundColorIs(parent.errorHighlightColor, for: control) + return + } + } + parent.ensureBackgroundColorIs(nil, for: control) + parent.command = value + parent.history.addCommand(value) + } + + private func previousInHistory(textView: NSTextView) { + if let prev = parent.history.previous() { + textView.string = prev + } + } + + private func nextInHistory(textView: NSTextView) { + if let next = parent.history.next() { + textView.string = next + } + } + } + + // I don't like this. I would prefer having an "errorState" variable like I do with + // the URLTextField class, and have updateNSView change the background color. However, + // I cannot for the life of me get the NSTextField to redraw itself at the correct + // time. If I could figure out how to get a SwiftUI TextField to respond to the + // keypresses that I need, then I would drop NSTextField altogether. But I've spent + // almost a week now trying to figure that out without success. + private func ensureBackgroundColorIs(_ color: NSColor?, for control: NSControl) { + if let textField = control as? NSTextField { + textField.backgroundColor = color + } + } +} + + +private final class CommandHistory { + let maximumHistoryLength: Int + private var commands: [String] = [] + private var currentCommandPosition = -1 + + init(maximumHistoryLength: Int = 1000) { + precondition(maximumHistoryLength > 1, "A history must allow more than 1 item.") + self.maximumHistoryLength = maximumHistoryLength + } + + func addCommand(_ command: String) { + precondition(isConsistent(), inconsistentStateMessage()) + let last = commands.last + if !command.isEmpty && command != last { + commands.append(command) + while commands.count > maximumHistoryLength { + _ = commands.remove(at: 0) + } + currentCommandPosition = commands.count - 1 + } + } + + func next() -> String? { + precondition(isConsistent(), inconsistentStateMessage()) + guard currentCommandPosition < commands.count - 1 else { + return nil + } + currentCommandPosition += 1 + return commands[currentCommandPosition] + } + + func previous() -> String? { + precondition(isConsistent(), inconsistentStateMessage()) + guard currentCommandPosition > 0 else { + return nil + } + currentCommandPosition -= 1 + return commands[currentCommandPosition] + } + + private func isConsistent() -> Bool { + if commands.count > maximumHistoryLength { + return false + } + if commands.isEmpty { + return currentCommandPosition == -1 + } + return currentCommandPosition >= 0 && currentCommandPosition < commands.count + } + + private func inconsistentStateMessage() -> String { + return "inconsistent state: " + + "maximumHistoryLength: \(maximumHistoryLength)" + + ", currentCommandPosition: \(currentCommandPosition)" + + ", commands: \(commands)" + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public struct KSSCommandTextField {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift new file mode 100644 index 0000000..33d1bfc --- /dev/null +++ b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift @@ -0,0 +1,174 @@ +// +// KSSNSButtonViewSettable.swift +// +// +// Created by Steven W. Klassen on 2020-08-06. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import os +import Cocoa +import Foundation +import SwiftUI + + + +/** + This protocol is used by a number of KSS Views that use an NSButton as their basis. It allows a number of + their settings, common to most buttons, to be set via modifiers. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public protocol KSSNSButtonViewSettable : KSSNSControlViewSettable { + /// :nodoc: + var nsButtonViewSettings: KSSNSButtonViewSettings { get set } +} + + +/** + This object controls the settings that can be set on an `NSButton` based view. + */ +@available(OSX 10.15, *) +public class KSSNSButtonViewSettings: NSObject { + /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance + /// of the image may be modified if `autoInvertImage` is specified. + public var alternateImage: NSImage? = nil + + /// If set to true, and if `image` or `alternateImage` exist, they will have their colors automatically + /// inverted when we are displaying in "dark mode". This is most useful if they are monochrome images. + public var autoInvertImage: Bool = true + + /// Allows the border to be turned on/off. + public var isBordered: Bool? = nil + + /// If true then the button's border is only displayed when the pointer is over the button and the + /// button is active. + public var showsBorderOnlyWhileMouseInside: Bool? = nil + + /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. + public var toolTip: String? = nil +} + + +@available(OSX 10.15, *) +public extension KSSNSButtonViewSettable { + /** + Apply the `NSButton` based settings, including the `NSControl` settings, to the given control. This will + return true if any of the settings result in a change from the existing ones. + - note: Typically this will be called in both the `makeNSView` and `updateNSView` methods of the view. + - note: This method also calls `applyNSControlViewSettings` so you don't need to call it again. + */ + func applyNSButtonViewSettings(_ control: View.NSViewType, + context: View.Context) -> Bool + where View.NSViewType : NSButton + { + var somethingChanged = applyNSControlViewSettings(control, context: context) + if let image = nsButtonViewSettings.alternateImage { + if control.alternateImage != image { + let colorScheme = context.environment.colorScheme + let shouldInvert = nsButtonViewSettings.autoInvertImage && (colorScheme == .dark) + control.alternateImage = shouldInvert ? image.inverted() : image + somethingChanged = true + } + } + if let isBordered = nsButtonViewSettings.isBordered { + if control.isBordered != isBordered { + control.isBordered = isBordered + somethingChanged = true + } + } + if let showBorder = nsButtonViewSettings.showsBorderOnlyWhileMouseInside { + if control.showsBorderOnlyWhileMouseInside != showBorder { + control.showsBorderOnlyWhileMouseInside = showBorder + somethingChanged = true + } + } + if let toolTip = nsButtonViewSettings.toolTip { + if control.toolTip != toolTip { + control.toolTip = toolTip + somethingChanged = true + } + } + return somethingChanged + } +} + +// MARK: NSButton View Modifiers + +@available(OSX 10.15, *) +public extension NSViewRepresentable { + /** + Set the alternate image in an `NSViewRepresentable` view. + - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. + */ + func nsAlternateImage(_ image: NSImage) -> Self { + if let buttonView = self as? KSSNSButtonViewSettable { + buttonView.nsButtonViewSettings.alternateImage = image + } else { + os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring alternate image change") + } + return self + } + + /** + Automatically invert the image and alternate image when in dark mode. + - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. + */ + func nsAutoInvertImage(_ autoInvert: Bool) -> Self { + if let buttonView = self as? KSSNSButtonViewSettable { + buttonView.nsButtonViewSettings.autoInvertImage = autoInvert + } else { + os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring auto invert image change") + } + return self + } + + /** + Turn the border on/off. + - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. + */ + func nsIsBordered(_ isBordered: Bool) -> Self { + if let buttonView = self as? KSSNSButtonViewSettable { + buttonView.nsButtonViewSettings.isBordered = isBordered + } else { + os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring is bordered change") + } + return self + } + + /** + Show the border only when the mouse is inside. + - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. + */ + func nsShowsBorderOnlyWhileMouseInside(_ showBorder: Bool) -> Self { + if let buttonView = self as? KSSNSButtonViewSettable { + buttonView.nsButtonViewSettings.showsBorderOnlyWhileMouseInside = showBorder + } else { + os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring show border change") + } + return self + } + + /** + Show a tooltip when the mouse hovers over the button. + - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. + */ + func nsToolTip(_ toolTip: String) -> Self { + if let buttonView = self as? KSSNSButtonViewSettable { + buttonView.nsButtonViewSettings.toolTip = toolTip + } else { + os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring tool tip change") + } + return self + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public protocol KSSNSButtonViewSettable {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift b/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift new file mode 100644 index 0000000..438fd71 --- /dev/null +++ b/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift @@ -0,0 +1,116 @@ +// +// KSSNSControlViewSettable.swift +// +// Created by Steven W. Klassen on 2020-08-02. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import os +import Cocoa +import Foundation +import SwiftUI + + + +/** + This protocol is used by a number of the KSS Views that use an NSControl as their basis. It allows the + font and font size to be set. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public protocol KSSNSControlViewSettable { + /// :nodoc: + var nsControlViewSettings: KSSNSControlViewSettings { get set } +} + +/** + This object controls the settings that can be set on an `NSControl` based view. + */ +@available(OSX 10.15, *) +public class KSSNSControlViewSettings: NSObject { + /// Specify the font. If `nil` then the controls default font will be used. + public var font: NSFont? = nil + + /// Specify the font size. This allows the size to be changed without changing the other characteristics + /// of the font. If `nil` then the default font size will be used. + /// - note: This assumes that the font has already been set or has a default. If the controls current + /// font cannot be determined, then setting this will log a warning message and make no change. + public var fontSize: CGFloat? = nil +} + + +@available(OSX 10.15, *) +public extension KSSNSControlViewSettable { + /** + Apply the `NSControl` based settings to the given control. This will return true if any of the settings + result in a change from the existing ones. + - note: Typically this will be called in both the `makeNSView` and `updateNSView` methods of the view. + */ + func applyNSControlViewSettings(_ control: View.NSViewType, + context: View.Context) -> Bool + where View.NSViewType : NSControl + { + var somethingChanged = false + if let font = nsControlViewSettings.font { + control.font = font + somethingChanged = true + } + if let fontSize = nsControlViewSettings.fontSize { + guard fontSize > 0 else { + os_log("Warning: Invalid font size: %f, ignoring size change", fontSize) + return somethingChanged + } + if let currentFont = control.font { + let fontDescriptor = currentFont.fontDescriptor + control.font = NSFont(descriptor: fontDescriptor, size: fontSize) + somethingChanged = true + } else { + os_log("Warning: No current font is set, ignoring size change") + } + } + return somethingChanged + } +} + +// MARK: NSControl View Modifiers + +@available(OSX 10.15, *) +public extension NSViewRepresentable { + /** + Set the font in an `NSViewRepresentable` view. + - note: If the view does not conform to the `KSSNSControlViewSettable` protocol, a warning message + is logged and no change is make to the view. + */ + func nsFont(_ font: NSFont) -> Self { + if let controlViewSettable = self as? KSSNSControlViewSettable { + controlViewSettable.nsControlViewSettings.font = font + } else { + os_log("Warning: View is not a KSSNSControlViewSettable, ignoring font change") + } + return self + } + + /** + Set the font size in an `NSViewRepresentable` view, without changing the other font characteristics. + - note: If the view does not conform to the `KSSNSControlViewSettable` protocol, a warning message + is logged and no change is make to the view. + */ + func nsFontSize(_ fontSize: CGFloat) -> Self { + if let controlViewSettable = self as? KSSNSControlViewSettable { + controlViewSettable.nsControlViewSettings.fontSize = fontSize + } else { + os_log("Warning: View is not a KSSNSControlViewSettable, ignoring font size change") + } + return self + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public protocol KSSNSControlViewSettable {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSNativeButton.swift b/Sources/KSSSwiftUI/KSSNativeButton.swift new file mode 100644 index 0000000..c5b849e --- /dev/null +++ b/Sources/KSSSwiftUI/KSSNativeButton.swift @@ -0,0 +1,346 @@ +// +// NativeButton.swift +// +// Created by Steven W. Klassen on 2020-02-17. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import os +import Cocoa +import KSSCocoa +import SwiftUI + + +/** + SwiftUI wrapper around an NSButton. This is intended to be used when a SwiftUI Button is not sufficient. + For example, when you wish to use a multi-font string, or alllow a default action. + + This wrapper allows many of the configuration items available for NSButton to be used. + + - note: This is based on example code found at + https://stackoverflow.com/questions/57283931/swiftui-on-mac-how-do-i-designate-a-button-as-being-the-primary + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSNativeButton: NSViewRepresentable, KSSNativeButtonCommonHelper { + + /// Settings applicable to all KSS `NSControl` based Views. + public var nsControlViewSettings = KSSNSControlViewSettings() + + /// Settings applicable to all KSS `NSButton` based Views. + public var nsButtonViewSettings = KSSNSButtonViewSettings() + + /** + Used to specify a keyboard equivalent to the button action. This can either refer to the return key or + the escape key. + */ + public enum KeyEquivalent: String { + /// Represents the escape key. + case escape = "\u{1b}" + + /// Represents the return key. + case `return` = "\r" + } + + /// Specifies a simple string as the content of the button. + public private(set) var title: String? = nil + + /// Specifies an attributed string as the content of the button. + public private(set) var attributedTitle: NSAttributedString? = nil + + /// Specifies an image as the content of the button. Note that the appearance of the image may be + /// modified if `autoInvertImage` is specified. + public private(set) var image: NSImage? = nil + + /// Specifies a keyboard equivalent to pressing the button. + public var keyEquivalent: KeyEquivalent? = nil + + /// Specifies the type of the button. + public var buttonType: NSButton.ButtonType? = nil + + /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance + /// of the image may be modified if `autoInvertImage` is specified. + @available(*, deprecated, message: "Use nsButtonViewSettings.alternateImage") + public var alternateImage: NSImage? { nsButtonViewSettings.alternateImage } + + /// Specifies type type of border. + public var bezelStyle: NSButton.BezelStyle? = nil + + /// Allows the border to be turned on/off. + @available(*, deprecated, message: "Use nsButtonViewSettings.isBordered") + public var isBordered: Bool? { nsButtonViewSettings.isBordered } + + /// If set to true, and if `image` or `alternateImage` exist, they will have their colors automatically + /// inverted when we are displaying in "dark mode". This is most useful if they are monochrome images. + @available(*, deprecated, message: "Use nsButtonViewSettings.autoInvertImage") + public var autoInvertImage: Bool { nsButtonViewSettings.autoInvertImage } + + /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. + @available(*, deprecated, message: "Use nsButtonViewSettings.toolTip") + public var toolTip: String? { nsButtonViewSettings.toolTip } + + private let action: () -> Void + + /** + Construct a button with a simple string. + */ + public init(_ title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } + + /** + Construct a button with a simple string. + */ + @available(*, deprecated, message: "Use init(_, action:) plus modifiers") + public init(_ title: String, + keyEquivalent: KeyEquivalent? = nil, + buttonType: NSButton.ButtonType? = nil, + bezelStyle: NSButton.BezelStyle? = nil, + isBordered: Bool? = nil, + toolTip: String? = nil, + action: @escaping () -> Void) + { + self.init(title, action: action) + self.keyEquivalent = keyEquivalent + self.buttonType = buttonType + self.bezelStyle = bezelStyle + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /** + Construct a button with an attributed string. + */ + public init(withAttributedTitle attributedTitle: NSAttributedString, action: @escaping () -> Void) { + self.attributedTitle = attributedTitle + self.action = action + } + + /** + Construct a button with an attributed string. + */ + @available(*, deprecated, message: "Use init(withAttributedTitle:, action:) plus modifiers") + public init(withAttributedTitle attributedTitle: NSAttributedString, + keyEquivalent: KeyEquivalent? = nil, + buttonType: NSButton.ButtonType? = nil, + bezelStyle: NSButton.BezelStyle? = nil, + isBordered: Bool? = nil, + toolTip: String? = nil, + action: @escaping () -> Void) + { + self.init(withAttributedTitle: attributedTitle, action: action) + self.keyEquivalent = keyEquivalent + self.buttonType = buttonType + self.bezelStyle = bezelStyle + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /** + Construct a button with an image. + */ + public init(withImage image: NSImage, action: @escaping () -> Void) { + self.image = image + self.action = action + } + + /** + Construct a button with an image. + */ + @available(*, deprecated, message: "Use init(withImage:, action:) plus modifiers") + public init(withImage image: NSImage, + alternateImage: NSImage? = nil, + autoInvertImage: Bool = true, + keyEquivalent: KeyEquivalent? = nil, + buttonType: NSButton.ButtonType? = nil, + bezelStyle: NSButton.BezelStyle? = nil, + isBordered: Bool? = nil, + toolTip: String? = nil, + action: @escaping () -> Void) + { + self.init(withImage: image, action: action) + self.nsButtonViewSettings.alternateImage = alternateImage + self.nsButtonViewSettings.autoInvertImage = autoInvertImage + self.keyEquivalent = keyEquivalent + self.buttonType = buttonType + self.bezelStyle = bezelStyle + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /// :nodoc: + public func makeNSView(context: NSViewRepresentableContext) -> NSButton { + let button = commonMakeButton(context: context) + button.onAction { _ in self.action() } + if let keyEquivalent = keyEquivalent { + button.keyEquivalent = keyEquivalent.rawValue + } + return button + } + + /// :nodoc: + public func updateNSView(_ button: NSButton, context: NSViewRepresentableContext) { + DispatchQueue.main.async { + self.commonUpdateButton(button, context: context) + } + } +} + +// MARK: KSSNativeButton View Modifiers + +@available(OSX 10.15, *) +public extension NSViewRepresentable { + /** + Sets a key equivalent trigger for the button. + - note: If the view is not a `KSSNativeButton` view, then a warning will be logged and no change made. + */ + func nsKeyEquivalent(_ keyEquivalent: KSSNativeButton.KeyEquivalent) -> Self { + if var buttonView = self as? KSSNativeButton { + buttonView.keyEquivalent = keyEquivalent + return buttonView as! Self + } else { + os_log("Warning: View is not a KSSNativeButton, ignoring key equivalent change") + } + return self + } + + /** + Sets the button type. + - note: If the view is not a `KSSNativeButton` view, then a warning will be logged and no change made. + */ + func nsButtonType(_ buttonType: NSButton.ButtonType) -> Self { + if var buttonView = self as? KSSNativeButton { + buttonView.buttonType = buttonType + return buttonView as! Self + } else { + os_log("Warning: View is not a KSSNativeButton, ignoring button type change") + } + return self + } + + /** + Sets the bezel style. + - note: If the view is not a `KSSNativeButton` view, then a warning will be logged and no change made. + */ + func nsBezelStyle(_ bezelStyle: NSButton.BezelStyle) -> Self { + if var buttonView = self as? KSSNativeButton { + buttonView.bezelStyle = bezelStyle + return buttonView as! Self + } else { + os_log("Warning: View is not a KSSNativeButton, ignoring bezel style change") + } + return self + } +} + + +// The following are helper items used to reduce the amount of repeated code between +// the various KSS "native" buttons. +@available(OSX 10.15, *) +protocol KSSNativeButtonCommonHelper : KSSNSButtonViewSettable { + var title: String? { get } + var attributedTitle: NSAttributedString? { get } + var image: NSImage? { get } + + var buttonType: NSButton.ButtonType? { get } + var bezelStyle: NSButton.BezelStyle? { get } +} + +/// :nodoc: +@available(OSX 10.15, *) +extension KSSNativeButtonCommonHelper { + func commonMakeButton(context: NSViewRepresentableContext) -> NSButton + where View.NSViewType : NSButton + { + let button = NSButton(title: "", target: nil, action: nil) + button.translatesAutoresizingMaskIntoConstraints = false + button.setContentHuggingPriority(.defaultHigh, for: .vertical) + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + if let attributedTitle = attributedTitle { + button.attributedTitle = attributedTitle + } else if let title = title { + button.title = title + } else if image != nil { + // Intentionally empty. Since the choise of image is dependant on the + // colorScheme, which may change, we delay the setting of the image + // until the commonUpdateButton call. + } else { + fatalError("One of 'title', 'attributedTitle' or 'image' is required") + } + + if let buttonType = buttonType { + button.setButtonType(buttonType) + } + + if let bezelStyle = bezelStyle { + button.bezelStyle = bezelStyle + } + + _ = applyNSButtonViewSettings(button as! View.NSViewType, context: context) + return button + } + + + func commonUpdateButton(_ button: NSButton, + context: NSViewRepresentableContext) + where View.NSViewType : NSButton + { + _ = applyNSButtonViewSettings(button as! View.NSViewType, context: context) + let colorScheme = context.environment.colorScheme + let shouldInvert = nsButtonViewSettings.autoInvertImage && (colorScheme == .dark) + if let image = image { + button.image = shouldInvert ? image.inverted() : image + } + if let alternateImage = nsButtonViewSettings.alternateImage { + button.alternateImage = shouldInvert ? alternateImage.inverted() : alternateImage + } + } +} + +// The following is the "glue" needed to allow the button action to be set (since +// the NSButton selector needs to be an Objective-C object). +fileprivate var controlActionClosureProtocolAssociatedObjectKey: UInt8 = 0 + +fileprivate final class ActionTrampoline: NSObject { + public let action: (T) -> Void + + public init(action: @escaping (T) -> Void) { + self.action = action + } + + @objc + public func action(sender: AnyObject) { + action(sender as! T) + } +} + +/// :nodoc: +protocol KSSNativeButtonControlActionClosureProtocol: NSObjectProtocol { + var target: AnyObject? { get set } + var action: Selector? { get set } +} + +/// :nodoc: +extension KSSNativeButtonControlActionClosureProtocol { + func onAction(_ action: @escaping (Self) -> Void) { + let trampoline = ActionTrampoline(action: action) + self.target = trampoline + self.action = #selector(ActionTrampoline.action(sender:)) + objc_setAssociatedObject(self, &controlActionClosureProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN) + } +} + +/// :nodoc: +extension NSControl: KSSNativeButtonControlActionClosureProtocol {} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public struct KSSNativeButton {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSSearchField.swift b/Sources/KSSSwiftUI/KSSSearchField.swift new file mode 100644 index 0000000..a1cd672 --- /dev/null +++ b/Sources/KSSSwiftUI/KSSSearchField.swift @@ -0,0 +1,206 @@ +// +// KSSSearchField.swift +// +// Created by Steven W. Klassen on 2020-07-31. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa +import SwiftUI + +/** + Provides a SwiftUI view based on an NSSearchField. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSSearchField: NSViewRepresentable, KSSNSControlViewSettable { + /// Settings applicable to all KSS `NSControl` based Views. + public var nsControlViewSettings = KSSNSControlViewSettings() + + /// Type used for the search callback. + public typealias Callback = (String?)->Void + + /// :nodoc: + public let helpText: String + + /// :nodoc: + public let recentSearchesKey: String + + /// :nodoc: + public let isFilterField: Bool + + /// :nodoc: + public let searchCallback: Callback? + + /** + Create a search field. The field is essentially a text field where the user can type a search, an optional + menu that provides a list of the most recent searches, and a cancel button that is used to stop the + search. + + - parameters: + - helpText: A short text that will be displayed in the search field when it is empty. + - recentSearchesKey: A user defaults key for storing the recent searches. + - isFilterField: If true then a filter icon instead of the search icon will be used. + - searchCallback: A lambda that will be called when it is time to search. + */ + public init(helpText: String = "", + recentSearchesKey: String = "", + isFilterField: Bool = false, + _ searchCallback: Callback? = nil) + { + self.helpText = helpText + self.recentSearchesKey = recentSearchesKey + self.isFilterField = isFilterField + self.searchCallback = searchCallback + } + + // MARK: NSViewRepresentable Items + + /// :nodoc: Required part of the `NSViewRepresentable` protocol. + public typealias NSViewType = NSSearchField + + /// :nodoc: Required part of the `NSViewRepresentable` protocol. + public func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + /// :nodoc: Required part of the `NSViewRepresentable` protocol. + public func makeNSView(context: Context) -> NSSearchField { + let searchField = NSSearchField() + searchField.sendsSearchStringImmediately = false + if !helpText.isEmpty { + searchField.placeholderString = helpText + } + if !recentSearchesKey.isEmpty { + searchField.recentsAutosaveName = recentSearchesKey + searchField.maximumRecents = 5 + searchField.addRecentsMenu() + } + if isFilterField { + searchField.setupAsFilterField(includeMenu: !recentSearchesKey.isEmpty) + } + searchField.delegate = context.coordinator + _ = applyNSControlViewSettings(searchField, context: context) + return searchField + } + + /// :nodoc: Required part of the `NSViewRepresentable` protocol. + public func updateNSView(_ searchField: NSSearchField, context: Context) { + DispatchQueue.main.async { + if let window = searchField.window { + searchField.cell?.isEnabled = window.isKeyWindow + } + _ = self.applyNSControlViewSettings(searchField, context: context) + } + } +} + + +@available(OSX 10.14, *) +fileprivate extension NSSearchField { + func setupAsFilterField(includeMenu: Bool) { + if let searchFieldCell = self.cell as? NSSearchFieldCell { + if let searchButtonCell = searchFieldCell.searchButtonCell { + let size = searchButtonCell.image!.size + searchButtonCell.image = getFilterImage(ofSize: size, includeMenu: includeMenu) + + DistributedNotificationCenter.default().addObserver( + self, + selector: #selector(self.onThemeChanged(notification:)), + name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), + object: nil) + } + } + } + + func getFilterImage(ofSize size: NSSize, includeMenu: Bool) -> NSImage { + // The constants found here result in an image size and spacing that is similar + // to that of the magnifying glass icon. + let height = min(size.width, size.height) - (includeMenu ? 10 : 8) + let width = height + (includeMenu ? 5 : 0) + let newSize = CGSize(width: width, height: height) + let imageStream = includeMenu + ? filterIconWithMenuInputStream() + : filterIconWithoutMenuInputStream() + var image = NSImage(fromInputStream: imageStream)! + image = image.resized(to: newSize)! + if NSApplication.shared.isDarkMode { + image = image.inverted() + } + return image + } + + @available(OSX 10.14, *) + @objc func onThemeChanged(notification: NSNotification) { + if let searchFieldCell = self.cell as? NSSearchFieldCell { + if let searchButtonCell = searchFieldCell.searchButtonCell { + searchButtonCell.image = searchButtonCell.image?.inverted() + } + } + } + + func addRecentsMenu() { + let cellMenu = NSMenu(title: "Search Menu") + cellMenu.addItem(withTitle: "Recents", andTag: NSSearchField.recentsMenuItemTag) + cellMenu.addItem(NSMenuItem.separator()) + cellMenu.addItem(withTitle: "Clear History", andTag: NSSearchField.clearRecentsMenuItemTag) + self.searchMenuTemplate = cellMenu + } +} + +fileprivate extension NSMenu { + func addItem(withTitle title: String, andTag tag: Int) { + let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") + item.tag = tag + self.addItem(item) + } +} + +/// :nodoc: Required part of the `NSViewRepresentable` protocol. +@available(OSX 10.15, *) +extension KSSSearchField { + public class Coordinator: NSObject, NSSearchFieldDelegate, NSTextFieldDelegate { + let owner: KSSSearchField + var isSearching = false + + init(_ owner: KSSSearchField) { + self.owner = owner + } + + public func searchFieldDidStartSearching(_ sender: NSSearchField) { + isSearching = true + performSearch(basedOn: sender) + } + + public func searchFieldDidEndSearching(_ sender: NSSearchField) { + isSearching = false + owner.searchCallback?(nil) + } + + public func controlTextDidChange(_ obj: Notification) { + if isSearching { + if let sender = obj.object as? NSSearchField { + performSearch(basedOn: sender) + } + } + } + + private func performSearch(basedOn searchField: NSSearchField) { + if let lambda = owner.searchCallback { + let searchText = searchField.stringValue + let haveSearchText = isSearching && !searchText.isEmpty + lambda(haveSearchText ? searchText : nil) + } + } + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public struct KSSSearchField {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSTextView.swift b/Sources/KSSSwiftUI/KSSTextView.swift new file mode 100644 index 0000000..0ffdcdc --- /dev/null +++ b/Sources/KSSSwiftUI/KSSTextView.swift @@ -0,0 +1,266 @@ + +#if canImport(Cocoa) + +import Cocoa +import Combine +import SwiftUI + +/** + SwiftUI text view control allowing multi-font content. The content is controlled via a binding to an + `NSMutableAttributedString`. + + - note: This is based on code provided by Thiago Holanda, called the `MacEditorTextView`. + The original code is available from https://twitter.com/tholanda and is subject to the MIT License. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSTextView: NSViewRepresentable { + /** + The binding used to control the text view contents. + */ + @Binding public var text: NSMutableAttributedString + + /** + Used to determine if the control is editable or not. This is set using the `editable` modifier. + */ + public private(set) var isEditable = true + + /** + Used to determine if the control will support search and replace. (The replace is based on whether or not + `isEditabe` is also true.) + + - note: Turning this on is required for supporting search and replace, but it is not enough. The application + must still tie the related menu items to the `performFindPanelAction` of the first responder. + */ + public private(set) var isSearchable = true + + /** + Used to determine if the control should automatically scroll to the bottom when the text is changed. This + is intended to be used if your view is one that always appends and saves the developer from having to + do this manually. This is set using the `autoScrollToBottom` modifier. + */ + public private(set) var isAutoScrollToBottom = false + + /** + Construct a new text view with the given binding. + */ + public init(text: Binding) { + self._text = text + } + + /** + This is a modifier that returns a View with the `isEditable` field changed. + */ + public func editable(_ isEditable: Bool) -> KSSTextView { + var newView = self + newView.isEditable = isEditable + return newView + } + + /** + This is a modifier that returns a View with the `isSearchable` field changed. + */ + public func searchable(_ isSearchable: Bool) -> KSSTextView { + var newView = self + newView.isSearchable = isSearchable + return newView + } + + /** + This is a modifier that returns a View with the `isAutoScrollToBottom` field changed. + */ + public func autoScrollToBottom() -> KSSTextView { + var newView = self + newView.isAutoScrollToBottom = true + return newView + } + + /// :nodoc: + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + /// :nodoc: + public func makeNSView(context: Context) -> CustomTextView { + let textView = CustomTextView(text: self.text, + isEditable: self.isEditable, + isSearchable: self.isSearchable, + isAutoScrollToBottom: self.isAutoScrollToBottom) + textView.delegate = context.coordinator + + return textView + } + + /// :nodoc: + public func updateNSView(_ view: CustomTextView, context: Context) { + view.text = text + view.selectedRanges = context.coordinator.selectedRanges + } + +} + + +/// :nodoc: +@available(OSX 10.15, *) +extension KSSTextView { + public class Coordinator: NSObject, NSTextViewDelegate { + var parent: KSSTextView + var selectedRanges: [NSValue] = [] + + init(_ parent: KSSTextView) { + self.parent = parent + } + + public func textDidBeginEditing(_ notification: Notification) { + guard let textView = notification.object as? NSTextView else { + return + } + + self.parent.text.setAttributedString(textView.textStorage ?? NSAttributedString()) + } + + public func textDidChange(_ notification: Notification) { + guard let textView = notification.object as? NSTextView else { + return + } + + self.parent.text.setAttributedString(textView.textStorage ?? NSAttributedString()) + self.selectedRanges = textView.selectedRanges + } + + public func textDidEndEditing(_ notification: Notification) { + guard let textView = notification.object as? NSTextView else { + return + } + + self.parent.text.setAttributedString(textView.textStorage ?? NSAttributedString()) + } + } +} + + +/// :nodoc: +@available(OSX 10.15, *) +public final class CustomTextView: NSView { + private let isEditable: Bool + private let isSearchable: Bool + private let isAutoScrollToBottom: Bool + + weak var delegate: NSTextViewDelegate? + + var text: NSMutableAttributedString { + didSet { + textView.textStorage?.setAttributedString(text) + + if isAutoScrollToBottom { + if let documentView = scrollView.documentView { + documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height)) + } + } + } + } + + var selectedRanges: [NSValue] = [] { + didSet { + guard selectedRanges.count > 0 else { + return + } + + textView.selectedRanges = selectedRanges + } + } + + private lazy var scrollView: NSScrollView = { + let scrollView = NSScrollView() + scrollView.drawsBackground = true + scrollView.borderType = .noBorder + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalRuler = false + scrollView.autoresizingMask = [.width, .height] + scrollView.translatesAutoresizingMaskIntoConstraints = false + return scrollView + }() + + private lazy var textView: NSTextView = { + let contentSize = scrollView.contentSize + let textStorage = NSTextStorage() + + + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + + + let textContainer = NSTextContainer(containerSize: scrollView.frame.size) + textContainer.widthTracksTextView = true + textContainer.containerSize = NSSize( + width: contentSize.width, + height: CGFloat.greatestFiniteMagnitude + ) + + layoutManager.addTextContainer(textContainer) + + + let textView = NSTextView(frame: .zero, textContainer: textContainer) + textView.autoresizingMask = .width + textView.backgroundColor = NSColor.textBackgroundColor + textView.delegate = self.delegate + textView.drawsBackground = true + textView.isEditable = self.isEditable + textView.isHorizontallyResizable = false + textView.isVerticallyResizable = true + textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + textView.minSize = NSSize(width: 0, height: contentSize.height) + textView.textColor = NSColor.labelColor + textView.usesFindBar = self.isSearchable + return textView + }() + + init(text: NSMutableAttributedString, + isEditable: Bool, + isSearchable: Bool, + isAutoScrollToBottom: Bool) + { + self.isEditable = isEditable + self.isSearchable = isSearchable + self.text = text + self.isAutoScrollToBottom = isAutoScrollToBottom + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewWillDraw() { + super.viewWillDraw() + + setupScrollViewConstraints() + setupTextView() + } + + func setupScrollViewConstraints() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + + addSubview(scrollView) + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: topAnchor), + scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: leadingAnchor) + ]) + } + + func setupTextView() { + scrollView.documentView = textView + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public struct KSSTextField {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSToggle.swift b/Sources/KSSSwiftUI/KSSToggle.swift new file mode 100644 index 0000000..77922eb --- /dev/null +++ b/Sources/KSSSwiftUI/KSSToggle.swift @@ -0,0 +1,157 @@ +// +// NativeButton.swift +// +// Created by Steven W. Klassen on 2020-02-17. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// + +#if canImport(Cocoa) + +import Cocoa +import SwiftUI + + +/** + SwiftUI wrapper around an NSButton configured to act as a toggle. This is intended to be used when + the SwiftUI `Toggle` is not sufficient, for example, when you wish to use a multi-font string or + a tool tip. + */ +@available(OSX 10.15, *) +@available(iOS, unavailable) +public struct KSSToggle: NSViewRepresentable, KSSNativeButtonCommonHelper { + /// Settings applicable to all KSS `NSControl` based Views. + public var nsControlViewSettings = KSSNSControlViewSettings() + + /// Settings applicable to all KSS `NSButton` based Views. + public var nsButtonViewSettings = KSSNSButtonViewSettings() + + /// Specifies a simple string as the content of the button. + public private(set) var title: String? = nil + + /// Specifies an attributed string as the content of the button. + public private(set) var attributedTitle: NSAttributedString? = nil + + /// Specifies an image as the content of the button. Note that the appearance of the image may be + /// modified if `autoInvertImage` is specified. + public private(set) var image: NSImage? = nil + + /// Binding to the item that will reflect the current state of the toggle. + @Binding public var isOn: Bool + + /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance + /// of the image may be modified if `autoInvertImage` is specified. + @available(*, deprecated, message: "Use nsButtonViewSettings.alternateImage") + public var alternateImage: NSImage? { nsButtonViewSettings.alternateImage } + + /// If set to true, and if `image` or `alternateImage` exist, they will have their colors automatically + /// inverted when we are displaying in "dark mode". This is most useful if they are monochrome images. + @available(*, deprecated, message: "Use nsButtonViewSettings.autoInvertImage") + public var autoInvertImage: Bool { nsButtonViewSettings.autoInvertImage } + + /// Allows the border to be turned on/off. + @available(*, deprecated, message: "Use nsButtonViewSettings.isBordered") + public var isBordered: Bool? { nsButtonViewSettings.isBordered } + + /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. + @available(*, deprecated, message: "Use nsButtonViewSettings.toolTip") + public var toolTip: String? { nsButtonViewSettings.toolTip } + + let buttonType: NSButton.ButtonType? = .pushOnPushOff + let bezelStyle: NSButton.BezelStyle? = .regularSquare + + /** + Construct a button with a simple string. + */ + public init(_ title: String, isOn: Binding) { + self.title = title + self._isOn = isOn + } + + /** + Construct a button with a simple string. + */ + @available(*, deprecated, message: "Use init(_, isOn:) plus modifiers") + public init(_ title: String, + isOn: Binding, + isBordered: Bool? = nil, + toolTip: String? = nil) + { + self.init(title, isOn: isOn) + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /** + Construct a button with an attributed string. + */ + public init(withAttributedTitle attributedTitle: NSAttributedString, isOn: Binding) { + self.attributedTitle = attributedTitle + self._isOn = isOn + } + + /** + Construct a button with an attributed string. + */ + @available(*, deprecated, message: "Use init(withAttributedTitle:, isOn:) plus modifiers") + public init(withAttributedTitle attributedTitle: NSAttributedString, + isOn: Binding, + isBordered: Bool? = nil, + toolTip: String? = nil) + { + self.init(withAttributedTitle: attributedTitle, isOn: isOn) + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /** + Construct a button with an image. + */ + public init(withImage image: NSImage, isOn: Binding) { + self.image = image + self._isOn = isOn + } + + /** + Construct a button with an image. + */ + @available(*, deprecated, message: "Use init(withImage:, isOn:) plus modifiers") + public init(withImage image: NSImage, + isOn: Binding, + alternateImage: NSImage? = nil, + autoInvertImage: Bool = true, + isBordered: Bool? = nil, + toolTip: String? = nil) + { + self.init(withImage: image, isOn: isOn) + self.nsButtonViewSettings.alternateImage = alternateImage + self.nsButtonViewSettings.autoInvertImage = autoInvertImage + self.nsButtonViewSettings.isBordered = isBordered + self.nsButtonViewSettings.toolTip = toolTip + } + + /// :nodoc: + public func makeNSView(context: NSViewRepresentableContext) -> NSButton { + let button = commonMakeButton(context: context) + button.onAction { _ in self.isOn = !self.isOn } + return button + } + + /// :nodoc: + public func updateNSView(_ button: NSButton, context: NSViewRepresentableContext) { + DispatchQueue.main.async { + self.commonUpdateButton(button, context: context) + button.state = self.isOn ? .on : .off + if button.alternateImage == nil { + button.alphaValue = self.isOn ? 1.0 : 0.8 + } + } + } +} + +#else + +// Force the compiler to give us a more descriptive error message. +@available(iOS, unavailable) +public struct KSSToggle {} + +#endif diff --git a/Sources/KSSSwiftUI/KSSURLTextField.swift b/Sources/KSSSwiftUI/KSSURLTextField.swift new file mode 100644 index 0000000..43bf919 --- /dev/null +++ b/Sources/KSSSwiftUI/KSSURLTextField.swift @@ -0,0 +1,107 @@ +// +// KSSURLTextField.swift +// +// Created by Steven W. Klassen on 2020-01-16. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +#if canImport(Cocoa) +import Cocoa +#endif + +import Combine +import SwiftUI + +/** + Provides a text field used to enter a URL. + */ +@available(OSX 10.15, *) +public struct KSSURLTextField: View, KSSValidatingView { + /** + A binding to the URL to be updated. Note that setting this only sets the initial value of the field when it + is created. To change the URL from an outside source you must provide it with a publisher using the + `urlPublisher` modifier. + */ + @Binding public var url: URL? + + /** + The help text to be displayed in the field when it is empty. + */ + public let helpText: String + + static private var nilUrlPublisher = PassthroughSubject().eraseToAnyPublisher() + private var _urlPublisher: AnyPublisher = KSSURLTextField.nilUrlPublisher + + @State private var errorState: Bool = false + @State private var text: String = "" + + /** + Construct a text field with the given url binding and help text. + */ + public init(url: Binding, helpText: String = "url") { + self._url = url + self.helpText = helpText + } + + /// :nodoc: + public var body: some View { + TextField(helpText, text: $text, onCommit: { self.updateUrl() }) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .background(errorState ? self.errorHighlightColor : nil) + .onAppear { self.text = self.url?.absoluteString ?? "" } + .onReceive(_urlPublisher, perform: { url in + self.text = url?.absoluteString ?? "" + self.updateUrl() + }) + } + + /** + This modifier returns a view that will listen to the given publisher for URL changes. When the publisher + sends a message, that url will be set into the field. The main use case for this is to allow a recent history + menu to populate the control, and is necessary since setting the url binding does not actually set the + url into the underlying control. Hence another means had to be provided. + */ + public func urlPublisher(_ publisher: AnyPublisher) -> Self { + var newView = self + newView._urlPublisher = publisher + return newView + } + + private func validatedURL() -> URL? { + if let u = URL(string: text) { + if let fn = validatorFn { + if fn(u) { + return u + } + } else { + return u + } + } + return nil + } + + private func updateUrl() { + if let u = validatedURL() { + url = u + errorState = false + } else if text.isEmpty { + url = nil + errorState = false + } else { + errorState = true + } + } + + // MARK: KSSValidatingView Items + + /// :nodoc: +#if canImport(Cocoa) + public var errorHighlightColor: Color = Color(NSColor.errorHighlightColor) +#else + public var errorHighlightColor: Color = Color.yellow.opacity(0.5) +#endif + + /// :nodoc: + public var validatorFn: ((URL) -> Bool)? = nil +} diff --git a/Sources/KSSSwiftUI/KSSValidatingView.swift b/Sources/KSSSwiftUI/KSSValidatingView.swift new file mode 100644 index 0000000..ab44e4a --- /dev/null +++ b/Sources/KSSSwiftUI/KSSValidatingView.swift @@ -0,0 +1,82 @@ +// +// KSSValidatingView.swift +// +// +// Created by Steven W. Klassen on 2020-08-14. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +import os +import Foundation +import KSSCocoa +import SwiftUI + + +/** + This protocol specifies the API required in order to support the `errorHighlight` and `validator` + view modifiers. + */ +public protocol KSSValidatingView { + /// The type of the object to be validated. + associatedtype ValidatedType + + /// The type of the color to be used. This must be either `NSColor` (for + /// `NSViewRepresentable` objects) or `Color` (for all other View objects). + associatedtype ColorType + + /// The color used to highlight things if the validation fails. + var errorHighlightColor: ColorType { get set } + + /// The callback used to validate an object. + var validatorFn: ((ValidatedType) -> Bool)? { get set } +} + + +// MARK: View Modifiers + +@available(OSX 10.15, *) +public extension KSSValidatingView { + /** + Returns a modified View with the validation function set. + */ + func validator(perform: @escaping (ValidatedType) -> Bool) -> Self { + var newView = self + newView.validatorFn = perform + return newView + } +} + +#if canImport(Cocoa) +// TODO: deprecate this? In theory the more general one below should be sufficient +public extension KSSValidatingView where ColorType == NSColor { + /** + Returns a modified View with the color used for the error highlights set. + */ + func errorHighlight(_ color: NSColor? = nil) -> Self { + var newView = self + newView.errorHighlightColor = color ?? NSColor.errorHighlightColor + return newView + } +} +#endif + +@available(OSX 10.15, *) +public extension KSSValidatingView where ColorType == Color { + /** + Returns a modified View with the color used for the error highlights set. + */ + func errorHighlight(_ color: Color? = nil) -> Self { + var newView = self +#if canImport(Cocoa) + newView.errorHighlightColor = color ?? Color(NSColor.errorHighlightColor) +#else + // TODO: Add a UIColor.errorHighlightColor. This may require adding a UIKit + // package, or perhaps renaming KSSCocoa to to something more general + // (perhaps KSSNativeUI?) to allow it to contain lower level Cocoa and UIKit + // items. + newView.errorHighlightColor = color ?? Color.yellow.opacity(0.5) +#endif + return newView + } +} diff --git a/Sources/KSSSwiftUI/ViewExtension.swift b/Sources/KSSSwiftUI/ViewExtension.swift new file mode 100644 index 0000000..ceb6dd5 --- /dev/null +++ b/Sources/KSSSwiftUI/ViewExtension.swift @@ -0,0 +1,94 @@ +// +// ViewExtension.swift +// WSTerminal +// +// Created by Steven W. Klassen on 2020-01-23. +// Copyright © 2020 Klassen Software Solutions. All rights reserved. +// Released under the MIT license. +// + +import Foundation +import SwiftUI + +#if canImport(Cocoa) +import Cocoa +#endif + +@available(OSX 10.15, *) +public extension View { + /** + Returns a view that is visible or not visible based on `isVisible`. + */ + func visible(_ isVisible: Bool) -> some View { + modifier(VisibleModifier(isVisible: isVisible)) + } + + /** + Returns a view that inverts its color if `shouldInvert` is true. + */ + func invertColorIf(_ shouldInvert: Bool) -> some View { + modifier(InvertColorIfModifier(shouldInvert: shouldInvert)) + } + + /** + Returns a view that species an error state if `isInErrorState` is true. Note that presently + the error state is indicated by changing the background color to be a yellow. + */ + func errorStateIf(_ isInErrorState: Bool) -> some View { + modifier(ErrorStateIfModifier(isInErrorState: isInErrorState)) + } +} + + +@available(OSX 10.15, *) +fileprivate struct VisibleModifier: ViewModifier { + let isVisible: Bool + + func body(content: Content) -> some View { + Group { + if isVisible { + content + } else { + EmptyView() + } + } + } +} + +@available(OSX 10.15, *) +fileprivate struct InvertColorIfModifier: ViewModifier { + let shouldInvert: Bool + + func body(content: Content) -> some View { + Group { + if shouldInvert { + content.colorInvert() + } else { + content + } + } + } +} + +#if canImport(Cocoa) +@available(OSX 10.15, *) +fileprivate let errorHighlightColor = Color(NSColor.errorHighlightColor) +#else +// TODO: replace this with a more general solution +fileprivate let errorHighlightColor = Color.yellow.opacity(0.5) +#endif + +@available(OSX 10.15, *) +fileprivate struct ErrorStateIfModifier: ViewModifier { + let isInErrorState: Bool + + func body(content: Content) -> some View { + Group { + if isInErrorState { + content.background(errorHighlightColor) + } else { + content + } + } + } +} diff --git a/Sources/KSSSwiftUI/filterIconWithMenu.swift b/Sources/KSSSwiftUI/filterIconWithMenu.swift new file mode 100644 index 0000000..1834fa5 --- /dev/null +++ b/Sources/KSSSwiftUI/filterIconWithMenu.swift @@ -0,0 +1,362 @@ +// This file is automatically generated by generate_resource_file.sh. DO NOT EDIT! + +import Foundation + +fileprivate let encodedString = """ +iVBORw0KGgoAAAANSUhEUgAAAmQAAAGgCAYAAAAEgHuDAAAAAXNSR0IArs4c6QAAAJBlWElmTU0AKgAA +AAgABgEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAAB +AAIAAIdpAAQAAAABAAAAZgAAAAABj/+bAAFVVQGP/5sAAVVVAAOgAQADAAAAAQABAACgAgAEAAAAAQAA +AmSgAwAEAAAAAQAAAaAAAAAAlKt6xwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAgtpVFh0WE1MOmNvbS5h +ZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhN +UCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIK +ICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAg +ICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpQ +aG90b21ldHJpY0ludGVycHJldGF0aW9uPjI8L3RpZmY6UGhvdG9tZXRyaWNJbnRlcnByZXRhdGlvbj4K +ICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAg +ICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgPC9yZGY6RGVzY3Jp +cHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cs+OiooAAEAASURBVHgB7Z0HuCVFtbYHhzTA +kHPOQUBJkpMBEAEDiF6uoqCICfXqFVEQ0YsR9feaMCHRLAISJAiSRHKSKGnIccgMQ5jh/t8HZ8s5hxP2 +7t3dtar7Xc/zzZ6zd3fVWm9VV1dXV1fPMgGDQHsIzK5QF5UWkxYf+P+C+pxfWmDQ5zz6/1zDNIf+njhI +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 +""" + +func filterIconWithMenuInputStream() -> InputStream { + let decodedData = Data(base64Encoded: encodedString, options: .ignoreUnknownCharacters)! + return InputStream(data: decodedData) +} diff --git a/Sources/KSSSwiftUI/filterIconWithoutMenu.swift b/Sources/KSSSwiftUI/filterIconWithoutMenu.swift new file mode 100644 index 0000000..8735304 --- /dev/null +++ b/Sources/KSSSwiftUI/filterIconWithoutMenu.swift @@ -0,0 +1,290 @@ +// This file is automatically generated by generate_resource_file.sh. DO NOT EDIT! + +import Foundation + +fileprivate let encodedString = """ +iVBORw0KGgoAAAANSUhEUgAAAaAAAAGkCAYAAAB3p/FPAAAAAXNSR0IArs4c6QAAAJBlWElmTU0AKgAA +AAgABgEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAAB +AAIAAIdpAAQAAAABAAAAZgAAAAABj/+bAAFVVQGP/5sAAVVVAAOgAQADAAAAAQABAACgAgAEAAAAAQAA +AaCgAwAEAAAAAQAAAaQAAAAAH2+ItgAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAgtpVFh0WE1MOmNvbS5h +ZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhN +UCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIK +ICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAg +ICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpQ +aG90b21ldHJpY0ludGVycHJldGF0aW9uPjI8L3RpZmY6UGhvdG9tZXRyaWNJbnRlcnByZXRhdGlvbj4K +ICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAg +ICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgPC9yZGY6RGVzY3Jp +cHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cs+OiooAAD5MSURBVHgB7Z0HvCRVmfYRhjAg +OQ1xBoaccxBHR5HomnU/lFXUNS2GFVld190VMK5r+tTPZU2ACV3EgIgiknNQco7DkEEyDAMM7Pc819vX +np7u29XdFd5T9X9/v+d23+6qc973f07V21V16tRii2EQgAAEIAABCEAAAhCAAAQgAAEIQAACEIBAgQRe +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 +""" + +func filterIconWithoutMenuInputStream() -> InputStream { + let decodedData = Data(base64Encoded: encodedString, options: .ignoreUnknownCharacters)! + return InputStream(data: decodedData) +} diff --git a/Tests/KSSCocoaTests/XCTestManifests.swift b/Tests/KSSCocoaTests/XCTestManifests.swift deleted file mode 100644 index e58d224..0000000 --- a/Tests/KSSCocoaTests/XCTestManifests.swift +++ /dev/null @@ -1,31 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension NSApplicationExtensionTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__NSApplicationExtensionTests = [ - ("testIsDarkMode", testIsDarkMode), - ("testMetadata", testMetadata), - ] -} - -extension NSImageExtensionTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__NSImageExtensionTests = [ - ("testInitFromInputStream", testInitFromInputStream), - ("testInverted", testInverted), - ("testResized", testResized), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(NSApplicationExtensionTests.__allTests__NSApplicationExtensionTests), - testCase(NSImageExtensionTests.__allTests__NSImageExtensionTests), - ] -} -#endif diff --git a/Tests/KSSSwiftUITests/KSSCommandTextFieldTests.swift b/Tests/KSSSwiftUITests/KSSCommandTextFieldTests.swift new file mode 100644 index 0000000..e370131 --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSCommandTextFieldTests.swift @@ -0,0 +1,74 @@ +// +// KSSCommandTextFieldTests.swift +// +// +// Created by Steven W. Klassen on 2020-08-05. +// Released under the MIT license. +// + +#if canImport(Cocoa) + +import Foundation +import KSSTest +import SwiftUI +import XCTest + +@testable import KSSSwiftUI + + +@available(OSX 10.15, *) +class KSSCommandTextFieldTests: XCTestCase { + struct TestThatItCompilesInAView : View { + @State private var command: String = "" + + var body: some View { + VStack { + KSSCommandTextField(command: $command) + KSSCommandTextField(command: $command, helpText: "message to send") + .errorHighlight(NSColor.green) + .validator { _ in return false } + .nsFont(NSFont.systemFont(ofSize: 10)) + .nsFontSize(12) + } + } + } + + + func testConstruction() { + var commandField = KSSCommandTextField(command: .constant("hi")) + assertEqual(to: "hi") { commandField.command } + assertEqual(to: "command") { commandField.helpText } + assertNil { commandField.validatorFn } + assertEqual(to: NSColor.errorHighlightColor) { commandField.errorHighlightColor } + + commandField = KSSCommandTextField(command: .constant("hi"), helpText: "help") + assertEqual(to: "hi") { commandField.command } + assertEqual(to: "help") { commandField.helpText } + assertNil { commandField.validatorFn } + assertEqual(to: NSColor.errorHighlightColor) { commandField.errorHighlightColor } + } + + func testModifiers() { + let commandField = KSSCommandTextField(command: .constant("hi")) + .errorHighlight(NSColor.red) + .validator { _ in return true } + assertEqual(to: NSColor.red) { commandField.errorHighlightColor } + assertNotNil { commandField.validatorFn } + } + + func testNSCommandModifiers() { + var commandField = KSSCommandTextField(command: .constant("hi")) + assertNil { commandField.nsControlViewSettings.font } + assertNil { commandField.nsControlViewSettings.fontSize } + + commandField = commandField.nsFont(NSFont.boldSystemFont(ofSize: 10)) + assertNotNil { commandField.nsControlViewSettings.font } + assertNil { commandField.nsControlViewSettings.fontSize } + + commandField = commandField.nsFontSize(10) + assertNotNil { commandField.nsControlViewSettings.font } + assertEqual(to: 10) { commandField.nsControlViewSettings.fontSize } + } +} + +#endif diff --git a/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift b/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift new file mode 100644 index 0000000..6459022 --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift @@ -0,0 +1,169 @@ +// +// KSSNativeButtonTests.swift +// +// +// Created by Steven W. Klassen on 2020-08-05. +// + +#if canImport(Cocoa) + +import Foundation +import KSSTest +import SwiftUI +import XCTest + +import KSSSwiftUI + +@available(OSX 10.15, *) +class KSSNativeButtonTests: XCTestCase { + struct TestThatItCompilesInAView : View { + var body: some View { + VStack { + KSSNativeButton("button 1") { print("button 1") } + KSSNativeButton("button 2", + keyEquivalent: .escape, + buttonType: .momentaryLight, + bezelStyle: .circular, + isBordered: true, + toolTip: "this is a tooltip") { print("button 2") } + .nsFont(NSFont.systemFont(ofSize: 10)) + .nsFontSize(12) + KSSNativeButton("button 3") { print("button 3") } + .nsFont(NSFont.systemFont(ofSize: 10)) + .nsFontSize(12) + .nsIsBordered(true) + .nsToolTip("this is a tooltip") + } + } + } + + + func testConstruction() { + var button = KSSNativeButton("button") { print("hi") } + assertEqual(to: "button") { button.title } + assertNil { button.attributedTitle } + assertNil { button.image } + assertNil { button.keyEquivalent } + assertNil { button.buttonType } + assertNil { button.bezelStyle } + + button = KSSNativeButton("button", + keyEquivalent: .return, + buttonType: .momentaryPushIn, + bezelStyle: .inline, + isBordered: false, + toolTip: "this is a tooltip") { print("hi") } + XCTAssertEqual(button.title, "button") + XCTAssertNil(button.attributedTitle) + XCTAssertNil(button.image) + XCTAssertNil(button.alternateImage) + XCTAssertEqual(button.keyEquivalent, .return) + XCTAssertEqual(button.buttonType, .momentaryPushIn) + XCTAssertEqual(button.bezelStyle, .inline) + XCTAssertFalse(button.isBordered!) + XCTAssertEqual(button.toolTip, "this is a tooltip") + XCTAssertTrue(button.autoInvertImage) + + button = KSSNativeButton(withAttributedTitle: NSAttributedString(string: "button")) { print("hi") } + assertNil { button.title } + assertEqual(to: NSAttributedString(string: "button")) { button.attributedTitle } + assertNil { button.image } + assertNil { button.keyEquivalent } + assertNil { button.buttonType } + assertNil { button.bezelStyle } + + button = KSSNativeButton(withAttributedTitle: NSAttributedString(string: "button"), + keyEquivalent: .return, + buttonType: .momentaryPushIn, + bezelStyle: .inline, + isBordered: false, + toolTip: "this is a tooltip") { print("hi") } + XCTAssertNil(button.title) + XCTAssertEqual(button.attributedTitle, NSAttributedString(string: "button")) + XCTAssertNil(button.image) + XCTAssertNil(button.alternateImage) + XCTAssertEqual(button.keyEquivalent, .return) + XCTAssertEqual(button.buttonType, .momentaryPushIn) + XCTAssertEqual(button.bezelStyle, .inline) + XCTAssertFalse(button.isBordered!) + XCTAssertEqual(button.toolTip, "this is a tooltip") + XCTAssertTrue(button.autoInvertImage) + + button = KSSNativeButton(withImage: NSImage()) { print("hi") } + assertNil { button.title } + assertNil { button.attributedTitle } + assertNotNil { button.image } + assertNil { button.keyEquivalent } + assertNil { button.buttonType } + assertNil { button.bezelStyle } + + button = KSSNativeButton(withImage: NSImage(), + alternateImage: NSImage(), + autoInvertImage: false, + keyEquivalent: .return, + buttonType: .momentaryPushIn, + bezelStyle: .inline, + isBordered: false, + toolTip: "this is a tooltip") { print("hi") } + XCTAssertNil(button.title) + XCTAssertNil(button.attributedTitle) + XCTAssertNotNil(button.image) + XCTAssertNotNil(button.alternateImage) + XCTAssertEqual(button.keyEquivalent, .return) + XCTAssertEqual(button.buttonType, .momentaryPushIn) + XCTAssertEqual(button.bezelStyle, .inline) + XCTAssertFalse(button.isBordered!) + XCTAssertEqual(button.toolTip, "this is a tooltip") + XCTAssertFalse(button.autoInvertImage) + } + + func testNSCommandModifiers() { + var button = KSSNativeButton("button") { print("hi") } + assertNil { button.nsControlViewSettings.font } + assertNil { button.nsControlViewSettings.fontSize } + + button = button.nsFont(NSFont.boldSystemFont(ofSize: 10)) + assertNotNil { button.nsControlViewSettings.font } + assertNil { button.nsControlViewSettings.fontSize } + + button = button.nsFontSize(10) + assertNotNil { button.nsControlViewSettings.font } + assertEqual(to: 10) { button.nsControlViewSettings.fontSize } + } + + func testNSButtonModifiers() { + var button = KSSNativeButton("button") { print("hi") } + assertNil { button.nsButtonViewSettings.alternateImage } + assertTrue { button.nsButtonViewSettings.autoInvertImage } + assertNil { button.nsButtonViewSettings.isBordered } + assertNil { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside } + assertNil { button.nsButtonViewSettings.toolTip } + + button = button.nsAlternateImage(NSImage()) + .nsAutoInvertImage(false) + .nsIsBordered(true) + .nsShowsBorderOnlyWhileMouseInside(true) + .nsToolTip("tooltip") + assertNotNil { button.nsButtonViewSettings.alternateImage } + assertFalse { button.nsButtonViewSettings.autoInvertImage } + assertTrue { button.nsButtonViewSettings.isBordered! } + assertTrue { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside! } + assertEqual(to: "tooltip") { button.nsButtonViewSettings.toolTip } + } + + func testKSSNativeButtonModifiers() { + var button = KSSNativeButton("button") { print("hi") } + assertNil { button.keyEquivalent } + assertNil { button.buttonType } + assertNil { button.bezelStyle } + + button = button.nsKeyEquivalent(.escape) + .nsButtonType(.momentaryLight) + .nsBezelStyle(.disclosure) + assertEqual(to: .escape) { button.keyEquivalent } + assertEqual(to: .momentaryLight) { button.buttonType } + assertEqual(to: .disclosure) { button.bezelStyle } + } +} + +#endif diff --git a/Tests/KSSSwiftUITests/KSSSearchFieldTests.swift b/Tests/KSSSwiftUITests/KSSSearchFieldTests.swift new file mode 100644 index 0000000..7289e4b --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSSearchFieldTests.swift @@ -0,0 +1,66 @@ +// +// KSSSearchFieldTests.swift +// +// +// Created by Steven W. Klassen on 2020-08-05. +// + +#if canImport(Cocoa) + +import Foundation +import KSSTest +import SwiftUI +import XCTest + +@testable import KSSSwiftUI + + +@available(OSX 10.15, *) +class KSSSearchFieldTests: XCTestCase { + struct TestThatItCompilesInAView : View { + var body: some View { + VStack { + KSSSearchField() + KSSSearchField(helpText: "help", recentSearchesKey: "recentKey") { _ in print("hi") } + .nsFont(NSFont.systemFont(ofSize: 10)) + .nsFontSize(12) + } + } + } + + func testConstruction() { + var control = KSSSearchField() + assertEqual(to: "") { control.helpText } + assertEqual(to: "") { control.recentSearchesKey } + assertFalse { control.isFilterField } + assertNil { control.searchCallback } + + control = KSSSearchField(helpText: "help", recentSearchesKey: "recents") { _ in } + assertEqual(to: "help") { control.helpText } + assertEqual(to: "recents") { control.recentSearchesKey } + assertFalse { control.isFilterField } + assertNotNil { control.searchCallback } + + control = KSSSearchField(isFilterField: true) + assertEqual(to: "") { control.helpText } + assertEqual(to: "") { control.recentSearchesKey } + assertTrue { control.isFilterField } + assertNil { control.searchCallback } + } + + func testNSCommandModifiers() { + var control = KSSSearchField() + assertNil { control.nsControlViewSettings.font } + assertNil { control.nsControlViewSettings.fontSize } + + control = control.nsFont(NSFont.boldSystemFont(ofSize: 10)) + assertNotNil { control.nsControlViewSettings.font } + assertNil { control.nsControlViewSettings.fontSize } + + control = control.nsFontSize(10) + assertNotNil { control.nsControlViewSettings.font } + assertEqual(to: 10) { control.nsControlViewSettings.fontSize } + } +} + +#endif diff --git a/Tests/KSSSwiftUITests/KSSTextViewTests.swift b/Tests/KSSSwiftUITests/KSSTextViewTests.swift new file mode 100644 index 0000000..51a9b8a --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSTextViewTests.swift @@ -0,0 +1,37 @@ +// +// KSSTextViewTests.swift +// +// +// Created by Steven W. Klassen on 2020-03-16. +// + +#if canImport(Cocoa) + +import SwiftUI +import KSSTest +import XCTest + +import KSSSwiftUI + + +@available(OSX 10.15, *) +class KSSTextViewTests: XCTestCase { + @State private var testMutableAttributedString = NSMutableAttributedString() + + func testConstruction() { + var view = KSSTextView(text: $testMutableAttributedString) + assertTrue { view.isEditable } + assertTrue { view.isSearchable } + assertFalse { view.isAutoScrollToBottom } + + view = KSSTextView(text: $testMutableAttributedString) + .editable(false) + .searchable(false) + .autoScrollToBottom() + assertFalse { view.isEditable } + assertFalse { view.isSearchable } + assertTrue { view.isAutoScrollToBottom } + } +} + +#endif diff --git a/Tests/KSSSwiftUITests/KSSToggleTests.swift b/Tests/KSSSwiftUITests/KSSToggleTests.swift new file mode 100644 index 0000000..63b53e7 --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSToggleTests.swift @@ -0,0 +1,134 @@ +// +// KSSToggleTests.swift +// +// +// Created by Steven W. Klassen on 2020-08-05. +// + +#if canImport(Cocoa) + +import Foundation +import KSSTest +import SwiftUI +import XCTest + +import KSSSwiftUI + + +@available(OSX 10.15, *) +class KSSToggleTests : XCTestCase { + struct TestThatItCompilesInAView : View { + @State var isOn: Bool = true + + var body: some View { + VStack { + KSSToggle("button", isOn: $isOn) + KSSToggle("button", isOn: $isOn, isBordered: true, toolTip: "hi") + KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), + isOn: $isOn, + isBordered: false, + toolTip: "hi") + KSSToggle(withImage: NSImage(), + isOn: $isOn, + alternateImage: NSImage(), + autoInvertImage: false, + isBordered: false, + toolTip: "hi") + .nsFont(NSFont.systemFont(ofSize: 10)) + .nsFontSize(12) + } + } + } + + + func testConstruction() { + var button = KSSToggle("button", isOn: .constant(true)) + assertEqual(to: "button") { button.title } + XCTAssertNil(button.attributedTitle) + XCTAssertNil(button.image) + + button = KSSToggle("button", + isOn: .constant(true), + isBordered: false, + toolTip: "this is a tooltip") + assertEqual(to: "button") { button.title } + XCTAssertNil(button.attributedTitle) + XCTAssertNil(button.image) + XCTAssertNil(button.alternateImage) + XCTAssertFalse(button.isBordered!) + assertEqual(to: "this is a tooltip") { button.toolTip } + XCTAssertTrue(button.autoInvertImage) + + button = KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), isOn: .constant(true)) + XCTAssertNil(button.title) + assertEqual(to: NSAttributedString(string: "button")) { button.attributedTitle } + XCTAssertNil(button.image) + + button = KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), + isOn: .constant(true), + isBordered: false, + toolTip: "this is a tooltip") + XCTAssertNil(button.title) + XCTAssertEqual(button.attributedTitle, NSAttributedString(string: "button")) + XCTAssertNil(button.image) + XCTAssertNil(button.alternateImage) + XCTAssertFalse(button.isBordered!) + XCTAssertEqual(button.toolTip, "this is a tooltip") + XCTAssertTrue(button.autoInvertImage) + + button = KSSToggle(withImage: NSImage(), isOn: .constant(true)) + assertNil { button.title } + assertNil { button.attributedTitle } + assertNotNil { button.image } + + button = KSSToggle(withImage: NSImage(), + isOn: .constant(true), + alternateImage: NSImage(), + autoInvertImage: false, + isBordered: false, + toolTip: "this is a tooltip") + XCTAssertNil(button.title) + XCTAssertNil(button.attributedTitle) + XCTAssertNotNil(button.image) + XCTAssertNotNil(button.alternateImage) + XCTAssertFalse(button.isBordered!) + XCTAssertEqual(button.toolTip, "this is a tooltip") + XCTAssertFalse(button.autoInvertImage) + } + + func testNSCommandModifiers() { + var button = KSSToggle("button", isOn: .constant(true)) + assertNil { button.nsControlViewSettings.font } + assertNil { button.nsControlViewSettings.fontSize } + + button = button.nsFont(NSFont.boldSystemFont(ofSize: 10)) + assertNotNil { button.nsControlViewSettings.font } + assertNil { button.nsControlViewSettings.fontSize } + + button = button.nsFontSize(10) + assertNotNil { button.nsControlViewSettings.font } + assertEqual(to: 10) { button.nsControlViewSettings.fontSize } + } + + func testNSButtonModifiers() { + var button = KSSToggle("button", isOn: .constant(true)) + assertNil { button.nsButtonViewSettings.alternateImage } + assertTrue { button.nsButtonViewSettings.autoInvertImage } + assertNil { button.nsButtonViewSettings.isBordered } + assertNil { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside } + assertNil { button.nsButtonViewSettings.toolTip } + + button = button.nsAlternateImage(NSImage()) + .nsAutoInvertImage(false) + .nsIsBordered(true) + .nsShowsBorderOnlyWhileMouseInside(true) + .nsToolTip("tooltip") + assertNotNil { button.nsButtonViewSettings.alternateImage } + assertFalse { button.nsButtonViewSettings.autoInvertImage } + assertTrue { button.nsButtonViewSettings.isBordered! } + assertTrue { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside! } + assertEqual(to: "tooltip") { button.nsButtonViewSettings.toolTip } + } +} + +#endif diff --git a/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift new file mode 100644 index 0000000..5de7465 --- /dev/null +++ b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift @@ -0,0 +1,58 @@ +// +// KSSURLTextFieldTests.swift +// +// +// Created by Steven W. Klassen on 2020-08-05. +// Released under the MIT license. +// + +import Foundation +import SwiftUI +import XCTest + +@testable import KSSSwiftUI + + +@available(OSX 10.15, *) +class KSSURLTextFieldTests: XCTestCase { + struct TestThatItCompilesInAView : View { + @State var url: URL? = nil + + var body: some View { + VStack { + KSSURLTextField(url: $url) + KSSURLTextField(url: $url, helpText: "help") + .errorHighlight(Color.green) + .validator { _ in return false } + } + } + } + + + func testConstruction() { + // TODO: make this more general, just check against Color.errorHighligthColor if possible +#if canImport(Cocoa) + let correctColor = Color(NSColor.errorHighlightColor) +#else + let correctColor = Color.yellow.opacity(0.5) +#endif + + var control = KSSURLTextField(url: .constant(nil)) + assertEqual(to: "url") { control.helpText } + assertNil { control.validatorFn } + assertEqual(to: correctColor) { control.errorHighlightColor } + + control = KSSURLTextField(url: .constant(nil), helpText: "help") + assertEqual(to: "help") { control.helpText } + assertNil { control.validatorFn } + assertEqual(to: correctColor) { control.errorHighlightColor } + } + + func testModifiers() { + let control = KSSURLTextField(url: .constant(nil)) + .errorHighlight(Color.red) + .validator { _ in return true } + assertEqual(to: Color.red) { control.errorHighlightColor } + assertNotNil { control.validatorFn } + } +} diff --git a/Tests/KSSWebTests/XCTestManifests.swift b/Tests/KSSWebTests/XCTestManifests.swift deleted file mode 100644 index 9dacbe7..0000000 --- a/Tests/KSSWebTests/XCTestManifests.swift +++ /dev/null @@ -1,18 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension KSSWebConstructorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__KSSWebConstructorTests = [ - ("testObjectWillCompile", testObjectWillCompile), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(KSSWebConstructorTests.__allTests__KSSWebConstructorTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 854f02c..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,11 +0,0 @@ -// auto-generated by the build system. do not edit - -import XCTest -import KSSCocoaTests -import KSSWebTests - -var tests = [XCTestCaseEntry]() -tests += KSSCocoaTests.__allTests() -tests += KSSWebTests.__allTests() - -XCTMain(tests) From 01bc671bcaab5ca7c978c4ec43e4d3f2569fd6e6 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Mon, 7 Sep 2020 16:26:19 -0600 Subject: [PATCH 6/9] Removed all the deprecated items Also updated the prerequisites and moved in the more generic tooltip. Closes #1 --- Dependencies/prereqs-licenses.json | 3 +- Package.swift | 2 +- Sources/KSSCocoa/NSMenuExtension.swift | 40 --------- .../KSSSwiftUI/KSSNSButtonViewSettable.swift | 22 ----- Sources/KSSSwiftUI/KSSNativeButton.swift | 82 ------------------- Sources/KSSSwiftUI/KSSToggle.swift | 64 --------------- Sources/KSSSwiftUI/ViewExtension.swift | 26 ++++++ .../KSSNativeButtonTests.swift | 66 +-------------- Tests/KSSSwiftUITests/KSSToggleTests.swift | 55 +------------ .../KSSURLTextFieldTests.swift | 1 + 10 files changed, 32 insertions(+), 329 deletions(-) delete mode 100644 Sources/KSSCocoa/NSMenuExtension.swift diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index 0ac3a38..2c26e60 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -4,6 +4,7 @@ "moduleLicense": "MIT License", "moduleName": "KSSCore", "moduleUrl": "https://github.com/klassen-software-solutions/KSSCore.git", + "moduleVersion": "4.0.0", "x-isOsiApproved": true, "x-licenseTextEncoded": "TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAxOSBLbGFzc2VuIFNvZnR3YXJlIFNvbHV0aW9ucwoKUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBvZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weQpvZiB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZSAiU29mdHdhcmUiKSwgdG8gZGVhbAppbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzCnRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCwgZGlzdHJpYnV0ZSwgc3VibGljZW5zZSwgYW5kL29yIHNlbGwKY29waWVzIG9mIHRoZSBTb2Z0d2FyZSwgYW5kIHRvIHBlcm1pdCBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzCmZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGUgZm9sbG93aW5nIGNvbmRpdGlvbnM6CgpUaGUgYWJvdmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBwZXJtaXNzaW9uIG5vdGljZSBzaGFsbCBiZSBpbmNsdWRlZCBpbiBhbGwKY29waWVzIG9yIHN1YnN0YW50aWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS4KClRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTIE9SCklNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZLApGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBTkQgTk9OSU5GUklOR0VNRU5ULiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUKQVVUSE9SUyBPUiBDT1BZUklHSFQgSE9MREVSUyBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSwgREFNQUdFUyBPUiBPVEhFUgpMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SIE9USEVSV0lTRSwgQVJJU0lORyBGUk9NLApPVVQgT0YgT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBTT0ZUV0FSRSBPUiBUSEUgVVNFIE9SIE9USEVSIERFQUxJTkdTIElOIFRIRQpTT0ZUV0FSRS4K", "x-spdxId": "MIT", @@ -15,6 +16,6 @@ "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-07T10:56:32.344954-06:00" + "time": "2020-09-07T15:59:11.440315-06:00" } } \ No newline at end of file diff --git a/Package.swift b/Package.swift index f7bc4be..600fb1a 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( .library(name: "KSSWeb", targets: ["KSSWeb"]), ], dependencies: [ - .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", .branch("development/v4") /*from: "3.2.1"*/), + .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", from: "4.0.0"), ], targets: [ .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), diff --git a/Sources/KSSCocoa/NSMenuExtension.swift b/Sources/KSSCocoa/NSMenuExtension.swift deleted file mode 100644 index e88add9..0000000 --- a/Sources/KSSCocoa/NSMenuExtension.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// NSMenuExtension.swift -// WSTerminal -// -// Created by Steven W. Klassen on 2020-02-14. -// Copyright © 2020 Klassen Software Solutions. All rights reserved. -// Released under the MIT license. -// - -#if canImport(Cocoa) - -import Cocoa - -public extension NSMenu { - - /** - Performs a deep search of the menu for a menu item with the given tag. - - - returns: - The first item that has the requested tag or nil if no such item could be found. - */ - @available(*, deprecated, message: "Use item(withTag:) instead") - func findMenuItem(withTag tag: Int) -> NSMenuItem? { - for item in self.items { - if !item.isSeparatorItem { - if item.tag == tag { - return item - } - if let submenu = item.submenu { - if let subitem = submenu.findMenuItem(withTag: tag) { - return subitem - } - } - } - } - return nil - } -} - -#endif diff --git a/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift index 33d1bfc..246ea32 100644 --- a/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift +++ b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift @@ -46,9 +46,6 @@ public class KSSNSButtonViewSettings: NSObject { /// If true then the button's border is only displayed when the pointer is over the button and the /// button is active. public var showsBorderOnlyWhileMouseInside: Bool? = nil - - /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. - public var toolTip: String? = nil } @@ -85,12 +82,6 @@ public extension KSSNSButtonViewSettable { somethingChanged = true } } - if let toolTip = nsButtonViewSettings.toolTip { - if control.toolTip != toolTip { - control.toolTip = toolTip - somethingChanged = true - } - } return somethingChanged } } @@ -150,19 +141,6 @@ public extension NSViewRepresentable { } return self } - - /** - Show a tooltip when the mouse hovers over the button. - - note: If the view is not a `KSSNSButtonViewSettable` view, then a warning will be logged and no change made. - */ - func nsToolTip(_ toolTip: String) -> Self { - if let buttonView = self as? KSSNSButtonViewSettable { - buttonView.nsButtonViewSettings.toolTip = toolTip - } else { - os_log("Warning: View is not a KSSNSButtonViewSettable, ignoring tool tip change") - } - return self - } } #else diff --git a/Sources/KSSSwiftUI/KSSNativeButton.swift b/Sources/KSSSwiftUI/KSSNativeButton.swift index c5b849e..f456055 100644 --- a/Sources/KSSSwiftUI/KSSNativeButton.swift +++ b/Sources/KSSSwiftUI/KSSNativeButton.swift @@ -60,27 +60,9 @@ public struct KSSNativeButton: NSViewRepresentable, KSSNativeButtonCommonHelper /// Specifies the type of the button. public var buttonType: NSButton.ButtonType? = nil - /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance - /// of the image may be modified if `autoInvertImage` is specified. - @available(*, deprecated, message: "Use nsButtonViewSettings.alternateImage") - public var alternateImage: NSImage? { nsButtonViewSettings.alternateImage } - /// Specifies type type of border. public var bezelStyle: NSButton.BezelStyle? = nil - /// Allows the border to be turned on/off. - @available(*, deprecated, message: "Use nsButtonViewSettings.isBordered") - public var isBordered: Bool? { nsButtonViewSettings.isBordered } - - /// If set to true, and if `image` or `alternateImage` exist, they will have their colors automatically - /// inverted when we are displaying in "dark mode". This is most useful if they are monochrome images. - @available(*, deprecated, message: "Use nsButtonViewSettings.autoInvertImage") - public var autoInvertImage: Bool { nsButtonViewSettings.autoInvertImage } - - /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. - @available(*, deprecated, message: "Use nsButtonViewSettings.toolTip") - public var toolTip: String? { nsButtonViewSettings.toolTip } - private let action: () -> Void /** @@ -91,26 +73,6 @@ public struct KSSNativeButton: NSViewRepresentable, KSSNativeButtonCommonHelper self.action = action } - /** - Construct a button with a simple string. - */ - @available(*, deprecated, message: "Use init(_, action:) plus modifiers") - public init(_ title: String, - keyEquivalent: KeyEquivalent? = nil, - buttonType: NSButton.ButtonType? = nil, - bezelStyle: NSButton.BezelStyle? = nil, - isBordered: Bool? = nil, - toolTip: String? = nil, - action: @escaping () -> Void) - { - self.init(title, action: action) - self.keyEquivalent = keyEquivalent - self.buttonType = buttonType - self.bezelStyle = bezelStyle - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /** Construct a button with an attributed string. */ @@ -119,26 +81,6 @@ public struct KSSNativeButton: NSViewRepresentable, KSSNativeButtonCommonHelper self.action = action } - /** - Construct a button with an attributed string. - */ - @available(*, deprecated, message: "Use init(withAttributedTitle:, action:) plus modifiers") - public init(withAttributedTitle attributedTitle: NSAttributedString, - keyEquivalent: KeyEquivalent? = nil, - buttonType: NSButton.ButtonType? = nil, - bezelStyle: NSButton.BezelStyle? = nil, - isBordered: Bool? = nil, - toolTip: String? = nil, - action: @escaping () -> Void) - { - self.init(withAttributedTitle: attributedTitle, action: action) - self.keyEquivalent = keyEquivalent - self.buttonType = buttonType - self.bezelStyle = bezelStyle - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /** Construct a button with an image. */ @@ -147,30 +89,6 @@ public struct KSSNativeButton: NSViewRepresentable, KSSNativeButtonCommonHelper self.action = action } - /** - Construct a button with an image. - */ - @available(*, deprecated, message: "Use init(withImage:, action:) plus modifiers") - public init(withImage image: NSImage, - alternateImage: NSImage? = nil, - autoInvertImage: Bool = true, - keyEquivalent: KeyEquivalent? = nil, - buttonType: NSButton.ButtonType? = nil, - bezelStyle: NSButton.BezelStyle? = nil, - isBordered: Bool? = nil, - toolTip: String? = nil, - action: @escaping () -> Void) - { - self.init(withImage: image, action: action) - self.nsButtonViewSettings.alternateImage = alternateImage - self.nsButtonViewSettings.autoInvertImage = autoInvertImage - self.keyEquivalent = keyEquivalent - self.buttonType = buttonType - self.bezelStyle = bezelStyle - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /// :nodoc: public func makeNSView(context: NSViewRepresentableContext) -> NSButton { let button = commonMakeButton(context: context) diff --git a/Sources/KSSSwiftUI/KSSToggle.swift b/Sources/KSSSwiftUI/KSSToggle.swift index 77922eb..6ab0e45 100644 --- a/Sources/KSSSwiftUI/KSSToggle.swift +++ b/Sources/KSSSwiftUI/KSSToggle.swift @@ -38,24 +38,6 @@ public struct KSSToggle: NSViewRepresentable, KSSNativeButtonCommonHelper { /// Binding to the item that will reflect the current state of the toggle. @Binding public var isOn: Bool - /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance - /// of the image may be modified if `autoInvertImage` is specified. - @available(*, deprecated, message: "Use nsButtonViewSettings.alternateImage") - public var alternateImage: NSImage? { nsButtonViewSettings.alternateImage } - - /// If set to true, and if `image` or `alternateImage` exist, they will have their colors automatically - /// inverted when we are displaying in "dark mode". This is most useful if they are monochrome images. - @available(*, deprecated, message: "Use nsButtonViewSettings.autoInvertImage") - public var autoInvertImage: Bool { nsButtonViewSettings.autoInvertImage } - - /// Allows the border to be turned on/off. - @available(*, deprecated, message: "Use nsButtonViewSettings.isBordered") - public var isBordered: Bool? { nsButtonViewSettings.isBordered } - - /// Allows a tool tip to be displayed if the cursor hovers over the control for a few moments. - @available(*, deprecated, message: "Use nsButtonViewSettings.toolTip") - public var toolTip: String? { nsButtonViewSettings.toolTip } - let buttonType: NSButton.ButtonType? = .pushOnPushOff let bezelStyle: NSButton.BezelStyle? = .regularSquare @@ -67,20 +49,6 @@ public struct KSSToggle: NSViewRepresentable, KSSNativeButtonCommonHelper { self._isOn = isOn } - /** - Construct a button with a simple string. - */ - @available(*, deprecated, message: "Use init(_, isOn:) plus modifiers") - public init(_ title: String, - isOn: Binding, - isBordered: Bool? = nil, - toolTip: String? = nil) - { - self.init(title, isOn: isOn) - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /** Construct a button with an attributed string. */ @@ -89,20 +57,6 @@ public struct KSSToggle: NSViewRepresentable, KSSNativeButtonCommonHelper { self._isOn = isOn } - /** - Construct a button with an attributed string. - */ - @available(*, deprecated, message: "Use init(withAttributedTitle:, isOn:) plus modifiers") - public init(withAttributedTitle attributedTitle: NSAttributedString, - isOn: Binding, - isBordered: Bool? = nil, - toolTip: String? = nil) - { - self.init(withAttributedTitle: attributedTitle, isOn: isOn) - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /** Construct a button with an image. */ @@ -111,24 +65,6 @@ public struct KSSToggle: NSViewRepresentable, KSSNativeButtonCommonHelper { self._isOn = isOn } - /** - Construct a button with an image. - */ - @available(*, deprecated, message: "Use init(withImage:, isOn:) plus modifiers") - public init(withImage image: NSImage, - isOn: Binding, - alternateImage: NSImage? = nil, - autoInvertImage: Bool = true, - isBordered: Bool? = nil, - toolTip: String? = nil) - { - self.init(withImage: image, isOn: isOn) - self.nsButtonViewSettings.alternateImage = alternateImage - self.nsButtonViewSettings.autoInvertImage = autoInvertImage - self.nsButtonViewSettings.isBordered = isBordered - self.nsButtonViewSettings.toolTip = toolTip - } - /// :nodoc: public func makeNSView(context: NSViewRepresentableContext) -> NSButton { let button = commonMakeButton(context: context) diff --git a/Sources/KSSSwiftUI/ViewExtension.swift b/Sources/KSSSwiftUI/ViewExtension.swift index ceb6dd5..b94705f 100644 --- a/Sources/KSSSwiftUI/ViewExtension.swift +++ b/Sources/KSSSwiftUI/ViewExtension.swift @@ -37,6 +37,16 @@ public extension View { func errorStateIf(_ isInErrorState: Bool) -> some View { modifier(ErrorStateIfModifier(isInErrorState: isInErrorState)) } + + #if canImport(Cocoa) + /** + Adds a tooltip to a view. The tooltip message will popup when the user hovers over the view for a period. + */ + @available(iOS, unavailable) + func toolTip(_ message: String) -> some View { + return overlay(ToolTip(message: message)) + } + #endif } @@ -92,3 +102,19 @@ fileprivate struct ErrorStateIfModifier: ViewModifier { } } } + +#if canImport(Cocoa) +@available(OSX 10.15, *) +fileprivate struct ToolTip: NSViewRepresentable { + let message: String + + func makeNSView(context: NSViewRepresentableContext) -> NSView { + let view = NSView() + view.toolTip = message + return view + } + + func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext) { + } +} +#endif diff --git a/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift b/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift index 6459022..6286382 100644 --- a/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift +++ b/Tests/KSSSwiftUITests/KSSNativeButtonTests.swift @@ -20,19 +20,11 @@ class KSSNativeButtonTests: XCTestCase { var body: some View { VStack { KSSNativeButton("button 1") { print("button 1") } - KSSNativeButton("button 2", - keyEquivalent: .escape, - buttonType: .momentaryLight, - bezelStyle: .circular, - isBordered: true, - toolTip: "this is a tooltip") { print("button 2") } - .nsFont(NSFont.systemFont(ofSize: 10)) - .nsFontSize(12) KSSNativeButton("button 3") { print("button 3") } .nsFont(NSFont.systemFont(ofSize: 10)) .nsFontSize(12) .nsIsBordered(true) - .nsToolTip("this is a tooltip") + .toolTip("this is a tooltip") } } } @@ -47,23 +39,6 @@ class KSSNativeButtonTests: XCTestCase { assertNil { button.buttonType } assertNil { button.bezelStyle } - button = KSSNativeButton("button", - keyEquivalent: .return, - buttonType: .momentaryPushIn, - bezelStyle: .inline, - isBordered: false, - toolTip: "this is a tooltip") { print("hi") } - XCTAssertEqual(button.title, "button") - XCTAssertNil(button.attributedTitle) - XCTAssertNil(button.image) - XCTAssertNil(button.alternateImage) - XCTAssertEqual(button.keyEquivalent, .return) - XCTAssertEqual(button.buttonType, .momentaryPushIn) - XCTAssertEqual(button.bezelStyle, .inline) - XCTAssertFalse(button.isBordered!) - XCTAssertEqual(button.toolTip, "this is a tooltip") - XCTAssertTrue(button.autoInvertImage) - button = KSSNativeButton(withAttributedTitle: NSAttributedString(string: "button")) { print("hi") } assertNil { button.title } assertEqual(to: NSAttributedString(string: "button")) { button.attributedTitle } @@ -72,23 +47,6 @@ class KSSNativeButtonTests: XCTestCase { assertNil { button.buttonType } assertNil { button.bezelStyle } - button = KSSNativeButton(withAttributedTitle: NSAttributedString(string: "button"), - keyEquivalent: .return, - buttonType: .momentaryPushIn, - bezelStyle: .inline, - isBordered: false, - toolTip: "this is a tooltip") { print("hi") } - XCTAssertNil(button.title) - XCTAssertEqual(button.attributedTitle, NSAttributedString(string: "button")) - XCTAssertNil(button.image) - XCTAssertNil(button.alternateImage) - XCTAssertEqual(button.keyEquivalent, .return) - XCTAssertEqual(button.buttonType, .momentaryPushIn) - XCTAssertEqual(button.bezelStyle, .inline) - XCTAssertFalse(button.isBordered!) - XCTAssertEqual(button.toolTip, "this is a tooltip") - XCTAssertTrue(button.autoInvertImage) - button = KSSNativeButton(withImage: NSImage()) { print("hi") } assertNil { button.title } assertNil { button.attributedTitle } @@ -96,25 +54,6 @@ class KSSNativeButtonTests: XCTestCase { assertNil { button.keyEquivalent } assertNil { button.buttonType } assertNil { button.bezelStyle } - - button = KSSNativeButton(withImage: NSImage(), - alternateImage: NSImage(), - autoInvertImage: false, - keyEquivalent: .return, - buttonType: .momentaryPushIn, - bezelStyle: .inline, - isBordered: false, - toolTip: "this is a tooltip") { print("hi") } - XCTAssertNil(button.title) - XCTAssertNil(button.attributedTitle) - XCTAssertNotNil(button.image) - XCTAssertNotNil(button.alternateImage) - XCTAssertEqual(button.keyEquivalent, .return) - XCTAssertEqual(button.buttonType, .momentaryPushIn) - XCTAssertEqual(button.bezelStyle, .inline) - XCTAssertFalse(button.isBordered!) - XCTAssertEqual(button.toolTip, "this is a tooltip") - XCTAssertFalse(button.autoInvertImage) } func testNSCommandModifiers() { @@ -137,18 +76,15 @@ class KSSNativeButtonTests: XCTestCase { assertTrue { button.nsButtonViewSettings.autoInvertImage } assertNil { button.nsButtonViewSettings.isBordered } assertNil { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside } - assertNil { button.nsButtonViewSettings.toolTip } button = button.nsAlternateImage(NSImage()) .nsAutoInvertImage(false) .nsIsBordered(true) .nsShowsBorderOnlyWhileMouseInside(true) - .nsToolTip("tooltip") assertNotNil { button.nsButtonViewSettings.alternateImage } assertFalse { button.nsButtonViewSettings.autoInvertImage } assertTrue { button.nsButtonViewSettings.isBordered! } assertTrue { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside! } - assertEqual(to: "tooltip") { button.nsButtonViewSettings.toolTip } } func testKSSNativeButtonModifiers() { diff --git a/Tests/KSSSwiftUITests/KSSToggleTests.swift b/Tests/KSSSwiftUITests/KSSToggleTests.swift index 63b53e7..69f5f5b 100644 --- a/Tests/KSSSwiftUITests/KSSToggleTests.swift +++ b/Tests/KSSSwiftUITests/KSSToggleTests.swift @@ -23,19 +23,7 @@ class KSSToggleTests : XCTestCase { var body: some View { VStack { KSSToggle("button", isOn: $isOn) - KSSToggle("button", isOn: $isOn, isBordered: true, toolTip: "hi") - KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), - isOn: $isOn, - isBordered: false, - toolTip: "hi") - KSSToggle(withImage: NSImage(), - isOn: $isOn, - alternateImage: NSImage(), - autoInvertImage: false, - isBordered: false, - toolTip: "hi") - .nsFont(NSFont.systemFont(ofSize: 10)) - .nsFontSize(12) + .toolTip("hello world") } } } @@ -47,53 +35,15 @@ class KSSToggleTests : XCTestCase { XCTAssertNil(button.attributedTitle) XCTAssertNil(button.image) - button = KSSToggle("button", - isOn: .constant(true), - isBordered: false, - toolTip: "this is a tooltip") - assertEqual(to: "button") { button.title } - XCTAssertNil(button.attributedTitle) - XCTAssertNil(button.image) - XCTAssertNil(button.alternateImage) - XCTAssertFalse(button.isBordered!) - assertEqual(to: "this is a tooltip") { button.toolTip } - XCTAssertTrue(button.autoInvertImage) - button = KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), isOn: .constant(true)) XCTAssertNil(button.title) assertEqual(to: NSAttributedString(string: "button")) { button.attributedTitle } XCTAssertNil(button.image) - button = KSSToggle(withAttributedTitle: NSAttributedString(string: "button"), - isOn: .constant(true), - isBordered: false, - toolTip: "this is a tooltip") - XCTAssertNil(button.title) - XCTAssertEqual(button.attributedTitle, NSAttributedString(string: "button")) - XCTAssertNil(button.image) - XCTAssertNil(button.alternateImage) - XCTAssertFalse(button.isBordered!) - XCTAssertEqual(button.toolTip, "this is a tooltip") - XCTAssertTrue(button.autoInvertImage) - button = KSSToggle(withImage: NSImage(), isOn: .constant(true)) assertNil { button.title } assertNil { button.attributedTitle } assertNotNil { button.image } - - button = KSSToggle(withImage: NSImage(), - isOn: .constant(true), - alternateImage: NSImage(), - autoInvertImage: false, - isBordered: false, - toolTip: "this is a tooltip") - XCTAssertNil(button.title) - XCTAssertNil(button.attributedTitle) - XCTAssertNotNil(button.image) - XCTAssertNotNil(button.alternateImage) - XCTAssertFalse(button.isBordered!) - XCTAssertEqual(button.toolTip, "this is a tooltip") - XCTAssertFalse(button.autoInvertImage) } func testNSCommandModifiers() { @@ -116,18 +66,15 @@ class KSSToggleTests : XCTestCase { assertTrue { button.nsButtonViewSettings.autoInvertImage } assertNil { button.nsButtonViewSettings.isBordered } assertNil { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside } - assertNil { button.nsButtonViewSettings.toolTip } button = button.nsAlternateImage(NSImage()) .nsAutoInvertImage(false) .nsIsBordered(true) .nsShowsBorderOnlyWhileMouseInside(true) - .nsToolTip("tooltip") assertNotNil { button.nsButtonViewSettings.alternateImage } assertFalse { button.nsButtonViewSettings.autoInvertImage } assertTrue { button.nsButtonViewSettings.isBordered! } assertTrue { button.nsButtonViewSettings.showsBorderOnlyWhileMouseInside! } - assertEqual(to: "tooltip") { button.nsButtonViewSettings.toolTip } } } diff --git a/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift index 5de7465..577efbb 100644 --- a/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift +++ b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift @@ -7,6 +7,7 @@ // import Foundation +import KSSTest import SwiftUI import XCTest From 49af0f9c0fcfd5665512573afa2da2997b169004 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Mon, 7 Sep 2020 16:56:18 -0600 Subject: [PATCH 7/9] Renamed KSSCocoa to KSSNativeUI --- Package.swift | 8 ++++---- README.md | 4 ++-- .../NSApplicationExtension.swift | 0 .../{KSSCocoa => KSSNativeUI}/NSColorExtension.swift | 0 .../{KSSCocoa => KSSNativeUI}/NSFontExtension.swift | 0 .../{KSSCocoa => KSSNativeUI}/NSImageExtension.swift | 0 .../NSViewControllerExtension.swift | 0 .../{KSSCocoa => KSSNativeUI}/NSViewExtension.swift | 0 .../{KSSCocoa => KSSNativeUI}/NSWindowExtension.swift | 0 Sources/KSSNativeUI/Native UI Notes.md | 11 +++++++++++ Sources/KSSSwiftUI/KSSCommandTextField.swift | 3 ++- Sources/KSSSwiftUI/KSSNativeButton.swift | 2 +- Sources/KSSSwiftUI/KSSValidatingView.swift | 3 ++- .../NSApplicationExtensionTests.swift | 3 ++- .../NSImageExtensionTests.swift | 4 +++- 15 files changed, 27 insertions(+), 11 deletions(-) rename Sources/{KSSCocoa => KSSNativeUI}/NSApplicationExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSColorExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSFontExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSImageExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSViewControllerExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSViewExtension.swift (100%) rename Sources/{KSSCocoa => KSSNativeUI}/NSWindowExtension.swift (100%) create mode 100644 Sources/KSSNativeUI/Native UI Notes.md rename Tests/{KSSCocoaTests => KSSNativeUITests}/NSApplicationExtensionTests.swift (96%) rename Tests/{KSSCocoaTests => KSSNativeUITests}/NSImageExtensionTests.swift (98%) diff --git a/Package.swift b/Package.swift index 600fb1a..08524df 100644 --- a/Package.swift +++ b/Package.swift @@ -10,8 +10,8 @@ let package = Package( .iOS(.v13), ], products: [ - .library(name: "KSSCocoa", targets: ["KSSCocoa"]), .library(name: "KSSMap", targets: ["KSSMap"]), + .library(name: "KSSNativeUI", targets: ["KSSNativeUI"]), .library(name: "KSSSwiftUI", targets: ["KSSSwiftUI"]), .library(name: "KSSWeb", targets: ["KSSWeb"]), ], @@ -19,11 +19,11 @@ let package = Package( .package(url: "https://github.com/klassen-software-solutions/KSSCore.git", from: "4.0.0"), ], targets: [ - .target(name: "KSSCocoa", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), + .target(name: "KSSNativeUI", dependencies: [.product(name: "KSSFoundation", package: "KSSCore")]), .target(name: "KSSMap", dependencies: []), - .target(name: "KSSSwiftUI", dependencies: ["KSSCocoa"]), + .target(name: "KSSSwiftUI", dependencies: ["KSSNativeUI"]), .target(name: "KSSWeb", dependencies: []), - .testTarget(name: "KSSCocoaTests", dependencies: ["KSSCocoa", .product(name: "KSSTest", package: "KSSCore")]), + .testTarget(name: "KSSNativeUITests", dependencies: ["KSSNativeUI", .product(name: "KSSTest", package: "KSSCore")]), .testTarget(name: "KSSSwiftUITests", dependencies: ["KSSSwiftUI", .product(name: "KSSTest", package: "KSSCore")]), .testTarget(name: "KSSWebTests", dependencies: ["KSSWeb"]), ] diff --git a/README.md b/README.md index 097012b..21b191f 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ libraries and a standard Apple development environment. The modules provided by this package are the following: -* _KSSCocoa_ - items that depend on Cocoa * _KSSMap_ - items that depend on MapKit +* _KSSNativeUI - items that depend on either Cocoa (macOS) or UIKit (iOS) * _KSSSwiftUI_ - items that depend on SwiftUI * _KSSWeb_ - items that depend on WebKit @@ -25,5 +25,5 @@ The modules provided by this package are the following: Presently we support the following: * _macOS_ - All modules are available - * _iOS_ - All modules are available, except for `KSSCocoa` and `KSSWeb`. + * _iOS_ - All modules are available, except for `KSSWeb`. diff --git a/Sources/KSSCocoa/NSApplicationExtension.swift b/Sources/KSSNativeUI/NSApplicationExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSApplicationExtension.swift rename to Sources/KSSNativeUI/NSApplicationExtension.swift diff --git a/Sources/KSSCocoa/NSColorExtension.swift b/Sources/KSSNativeUI/NSColorExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSColorExtension.swift rename to Sources/KSSNativeUI/NSColorExtension.swift diff --git a/Sources/KSSCocoa/NSFontExtension.swift b/Sources/KSSNativeUI/NSFontExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSFontExtension.swift rename to Sources/KSSNativeUI/NSFontExtension.swift diff --git a/Sources/KSSCocoa/NSImageExtension.swift b/Sources/KSSNativeUI/NSImageExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSImageExtension.swift rename to Sources/KSSNativeUI/NSImageExtension.swift diff --git a/Sources/KSSCocoa/NSViewControllerExtension.swift b/Sources/KSSNativeUI/NSViewControllerExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSViewControllerExtension.swift rename to Sources/KSSNativeUI/NSViewControllerExtension.swift diff --git a/Sources/KSSCocoa/NSViewExtension.swift b/Sources/KSSNativeUI/NSViewExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSViewExtension.swift rename to Sources/KSSNativeUI/NSViewExtension.swift diff --git a/Sources/KSSCocoa/NSWindowExtension.swift b/Sources/KSSNativeUI/NSWindowExtension.swift similarity index 100% rename from Sources/KSSCocoa/NSWindowExtension.swift rename to Sources/KSSNativeUI/NSWindowExtension.swift diff --git a/Sources/KSSNativeUI/Native UI Notes.md b/Sources/KSSNativeUI/Native UI Notes.md new file mode 100644 index 0000000..9a61c53 --- /dev/null +++ b/Sources/KSSNativeUI/Native UI Notes.md @@ -0,0 +1,11 @@ +# Native UI Notes + +The Native UI library contains items, primarily extensions, that are based on either +Cocoa (macOS) or UIKit (iOS). Often these are containing items that help create applications +in one or the other, but are not covered by SwiftUI. In other cases, `KSSSwiftUI` may have +different implementations of a class using either the Cocoa or UIKit versions of +something in `KSSNativeUI`. + +In terms of availability, you should simply assume that anything that starts with `NS...` +is only available on Cocoa based systems and anything that starts with `UI...` is only +available on iOS based system. diff --git a/Sources/KSSSwiftUI/KSSCommandTextField.swift b/Sources/KSSSwiftUI/KSSCommandTextField.swift index a32a311..255f56b 100644 --- a/Sources/KSSSwiftUI/KSSCommandTextField.swift +++ b/Sources/KSSSwiftUI/KSSCommandTextField.swift @@ -9,10 +9,11 @@ #if canImport(Cocoa) import Cocoa -import KSSCocoa +import KSSNativeUI import SwiftUI + /** TextField control suitable for entering command line type items. diff --git a/Sources/KSSSwiftUI/KSSNativeButton.swift b/Sources/KSSSwiftUI/KSSNativeButton.swift index f456055..924f84d 100644 --- a/Sources/KSSSwiftUI/KSSNativeButton.swift +++ b/Sources/KSSSwiftUI/KSSNativeButton.swift @@ -9,7 +9,7 @@ import os import Cocoa -import KSSCocoa +import KSSNativeUI import SwiftUI diff --git a/Sources/KSSSwiftUI/KSSValidatingView.swift b/Sources/KSSSwiftUI/KSSValidatingView.swift index ab44e4a..e036b27 100644 --- a/Sources/KSSSwiftUI/KSSValidatingView.swift +++ b/Sources/KSSSwiftUI/KSSValidatingView.swift @@ -9,10 +9,11 @@ import os import Foundation -import KSSCocoa +import KSSNativeUI import SwiftUI + /** This protocol specifies the API required in order to support the `errorHighlight` and `validator` view modifiers. diff --git a/Tests/KSSCocoaTests/NSApplicationExtensionTests.swift b/Tests/KSSNativeUITests/NSApplicationExtensionTests.swift similarity index 96% rename from Tests/KSSCocoaTests/NSApplicationExtensionTests.swift rename to Tests/KSSNativeUITests/NSApplicationExtensionTests.swift index a4ec978..89d0e89 100644 --- a/Tests/KSSCocoaTests/NSApplicationExtensionTests.swift +++ b/Tests/KSSNativeUITests/NSApplicationExtensionTests.swift @@ -1,9 +1,10 @@ #if canImport(Cocoa) import XCTest -import KSSCocoa import KSSTest +import KSSNativeUI + class NSApplicationExtensionTests: XCTestCase { func testMetadata() { assertEqual(to: "xctest") { NSApplication.shared.name } diff --git a/Tests/KSSCocoaTests/NSImageExtensionTests.swift b/Tests/KSSNativeUITests/NSImageExtensionTests.swift similarity index 98% rename from Tests/KSSCocoaTests/NSImageExtensionTests.swift rename to Tests/KSSNativeUITests/NSImageExtensionTests.swift index cc96b01..048a269 100644 --- a/Tests/KSSCocoaTests/NSImageExtensionTests.swift +++ b/Tests/KSSNativeUITests/NSImageExtensionTests.swift @@ -1,9 +1,11 @@ #if canImport(Cocoa) import XCTest -import KSSCocoa import KSSTest +import KSSNativeUI + + class NSImageExtensionTests: XCTestCase { func testInitFromInputStream() { var image = NSImage(fromInputStream: streamFromEncodedString(plusSymbolEncodedString)) From ded005bb8c4fafb636fca511587cd1141ca30dfd Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Mon, 7 Sep 2020 17:54:28 -0600 Subject: [PATCH 8/9] Fixed all the TODO items Mostly this meant changing the way some Color overrides were done to handle both Cocoa and UIKit. --- Dependencies/prereqs-licenses.json | 2 +- ...wift => NSColor+errorHighlightColor.swift} | 2 +- .../UIColor+errorHighlightColor.swift | 19 +++++++++++ .../Color+errorHighlightColor.swift | 33 +++++++++++++++++++ Sources/KSSSwiftUI/KSSCommandTextField.swift | 3 +- .../KSSSwiftUI/KSSNSButtonViewSettable.swift | 2 ++ .../KSSSwiftUI/KSSNSControlViewSettable.swift | 2 ++ Sources/KSSSwiftUI/KSSURLTextField.swift | 10 +----- Sources/KSSSwiftUI/KSSValidatingView.swift | 24 ++++++++------ Sources/KSSSwiftUI/ViewExtension.swift | 7 +--- .../KSSURLTextFieldTests.swift | 8 +---- .../KSSSwiftUITests/ViewExtensionTests.swift | 29 ++++++++++++++++ 12 files changed, 106 insertions(+), 35 deletions(-) rename Sources/KSSNativeUI/{NSColorExtension.swift => NSColor+errorHighlightColor.swift} (94%) create mode 100644 Sources/KSSNativeUI/UIColor+errorHighlightColor.swift create mode 100644 Sources/KSSSwiftUI/Color+errorHighlightColor.swift create mode 100644 Tests/KSSSwiftUITests/ViewExtensionTests.swift diff --git a/Dependencies/prereqs-licenses.json b/Dependencies/prereqs-licenses.json index 2c26e60..a5e44a2 100644 --- a/Dependencies/prereqs-licenses.json +++ b/Dependencies/prereqs-licenses.json @@ -16,6 +16,6 @@ "generated": { "process": "license-scanner", "project": "KSSCoreUI", - "time": "2020-09-07T15:59:11.440315-06:00" + "time": "2020-09-07T16:55:55.642169-06:00" } } \ No newline at end of file diff --git a/Sources/KSSNativeUI/NSColorExtension.swift b/Sources/KSSNativeUI/NSColor+errorHighlightColor.swift similarity index 94% rename from Sources/KSSNativeUI/NSColorExtension.swift rename to Sources/KSSNativeUI/NSColor+errorHighlightColor.swift index a750ea8..25ec827 100644 --- a/Sources/KSSNativeUI/NSColorExtension.swift +++ b/Sources/KSSNativeUI/NSColor+errorHighlightColor.swift @@ -1,5 +1,5 @@ // -// NSColorExtension.swift +// NSColor+errorHighlightColor.swift // HTTPMonitor // // Created by Steven W. Klassen on 2020-08-13. diff --git a/Sources/KSSNativeUI/UIColor+errorHighlightColor.swift b/Sources/KSSNativeUI/UIColor+errorHighlightColor.swift new file mode 100644 index 0000000..ee73041 --- /dev/null +++ b/Sources/KSSNativeUI/UIColor+errorHighlightColor.swift @@ -0,0 +1,19 @@ +// +// UIColor+errorHighlightColor.swift +// +// +// Created by Steven W. Klassen on 2020-09-07. +// + +#if canImport(UIKit) + +import UIKit + +public extension UIColor { + /// Specifies the color to be used for highlighting errors. Typically this would be used as a background, + /// but it can also be used as a foreground color if you add `.withAlphaComponent(1)` to make + /// it stand out better. + class var errorHighlightColor: UIColor { UIColor.systemYellow.withAlphaComponent(0.50) } +} + +#endif diff --git a/Sources/KSSSwiftUI/Color+errorHighlightColor.swift b/Sources/KSSSwiftUI/Color+errorHighlightColor.swift new file mode 100644 index 0000000..abe8667 --- /dev/null +++ b/Sources/KSSSwiftUI/Color+errorHighlightColor.swift @@ -0,0 +1,33 @@ +// +// Color+errorHighlightColor.swift +// +// +// Created by Steven W. Klassen on 2020-09-07. +// + +import SwiftUI + +#if canImport(Cocoa) +import Cocoa +#endif + +#if canImport(UIKit) +import UIKit +#endif + + +@available(OSX 10.15, *) +public extension Color { + /// Specifies the color to be used for highlighting errors. Typically this would be used as a background, + /// but it can also be used as a foreground color if you add `.withAlphaComponent(1)` to make + /// it stand out better. + static var errorHighlightColor: Color { + #if canImport(Cocoa) + return Color(NSColor.errorHighlightColor) + #elseif canImport(UIKit) + return Color(UIColor.errorHighlightColor) + #else + return Color.yellow.opacity(0.5) + #endif + } +} diff --git a/Sources/KSSSwiftUI/KSSCommandTextField.swift b/Sources/KSSSwiftUI/KSSCommandTextField.swift index 255f56b..9b08075 100644 --- a/Sources/KSSSwiftUI/KSSCommandTextField.swift +++ b/Sources/KSSSwiftUI/KSSCommandTextField.swift @@ -81,7 +81,8 @@ public struct KSSCommandTextField: NSViewRepresentable, KSSNSControlViewSettable public var validatorFn: ((String) -> Bool)? = nil /// :nodoc: - public var errorHighlightColor: NSColor = NSColor.errorHighlightColor + public typealias ColorType = NSColor + public var errorHighlightColor = ColorType.errorHighlightColor } diff --git a/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift index 246ea32..02bd11b 100644 --- a/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift +++ b/Sources/KSSSwiftUI/KSSNSButtonViewSettable.swift @@ -31,6 +31,7 @@ public protocol KSSNSButtonViewSettable : KSSNSControlViewSettable { This object controls the settings that can be set on an `NSButton` based view. */ @available(OSX 10.15, *) +@available(iOS, unavailable) public class KSSNSButtonViewSettings: NSObject { /// Specifies an alternate image to be displayed when the button is activated. Note that the appearance /// of the image may be modified if `autoInvertImage` is specified. @@ -50,6 +51,7 @@ public class KSSNSButtonViewSettings: NSObject { @available(OSX 10.15, *) +@available(iOS, unavailable) public extension KSSNSButtonViewSettable { /** Apply the `NSButton` based settings, including the `NSControl` settings, to the given control. This will diff --git a/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift b/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift index 438fd71..4a11711 100644 --- a/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift +++ b/Sources/KSSSwiftUI/KSSNSControlViewSettable.swift @@ -29,6 +29,7 @@ public protocol KSSNSControlViewSettable { This object controls the settings that can be set on an `NSControl` based view. */ @available(OSX 10.15, *) +@available(iOS, unavailable) public class KSSNSControlViewSettings: NSObject { /// Specify the font. If `nil` then the controls default font will be used. public var font: NSFont? = nil @@ -42,6 +43,7 @@ public class KSSNSControlViewSettings: NSObject { @available(OSX 10.15, *) +@available(iOS, unavailable) public extension KSSNSControlViewSettable { /** Apply the `NSControl` based settings to the given control. This will return true if any of the settings diff --git a/Sources/KSSSwiftUI/KSSURLTextField.swift b/Sources/KSSSwiftUI/KSSURLTextField.swift index 43bf919..cda782d 100644 --- a/Sources/KSSSwiftUI/KSSURLTextField.swift +++ b/Sources/KSSSwiftUI/KSSURLTextField.swift @@ -6,10 +6,6 @@ // Released under the MIT license. // -#if canImport(Cocoa) -import Cocoa -#endif - import Combine import SwiftUI @@ -96,11 +92,7 @@ public struct KSSURLTextField: View, KSSValidatingView { // MARK: KSSValidatingView Items /// :nodoc: -#if canImport(Cocoa) - public var errorHighlightColor: Color = Color(NSColor.errorHighlightColor) -#else - public var errorHighlightColor: Color = Color.yellow.opacity(0.5) -#endif + public var errorHighlightColor = Color.errorHighlightColor /// :nodoc: public var validatorFn: ((URL) -> Bool)? = nil diff --git a/Sources/KSSSwiftUI/KSSValidatingView.swift b/Sources/KSSSwiftUI/KSSValidatingView.swift index e036b27..0fb81e5 100644 --- a/Sources/KSSSwiftUI/KSSValidatingView.swift +++ b/Sources/KSSSwiftUI/KSSValidatingView.swift @@ -49,7 +49,6 @@ public extension KSSValidatingView { } #if canImport(Cocoa) -// TODO: deprecate this? In theory the more general one below should be sufficient public extension KSSValidatingView where ColorType == NSColor { /** Returns a modified View with the color used for the error highlights set. @@ -62,6 +61,19 @@ public extension KSSValidatingView where ColorType == NSColor { } #endif +#if canImport(UIKit) +public extension KSSValidatingView where ColorType == UIColor { + /** + Returns a modified View with the color used for the error highlights set. + */ + func errorHighlight(_ color: UIColor? = nil) -> Self { + var newView = self + newView.errorHighlightColor = color ?? UIColor.errorHighlightColor + return newView + } +} +#endif + @available(OSX 10.15, *) public extension KSSValidatingView where ColorType == Color { /** @@ -69,15 +81,7 @@ public extension KSSValidatingView where ColorType == Color { */ func errorHighlight(_ color: Color? = nil) -> Self { var newView = self -#if canImport(Cocoa) - newView.errorHighlightColor = color ?? Color(NSColor.errorHighlightColor) -#else - // TODO: Add a UIColor.errorHighlightColor. This may require adding a UIKit - // package, or perhaps renaming KSSCocoa to to something more general - // (perhaps KSSNativeUI?) to allow it to contain lower level Cocoa and UIKit - // items. - newView.errorHighlightColor = color ?? Color.yellow.opacity(0.5) -#endif + newView.errorHighlightColor = color ?? Color.errorHighlightColor return newView } } diff --git a/Sources/KSSSwiftUI/ViewExtension.swift b/Sources/KSSSwiftUI/ViewExtension.swift index b94705f..0acade9 100644 --- a/Sources/KSSSwiftUI/ViewExtension.swift +++ b/Sources/KSSSwiftUI/ViewExtension.swift @@ -80,13 +80,8 @@ fileprivate struct InvertColorIfModifier: ViewModifier { } } -#if canImport(Cocoa) @available(OSX 10.15, *) -fileprivate let errorHighlightColor = Color(NSColor.errorHighlightColor) -#else -// TODO: replace this with a more general solution -fileprivate let errorHighlightColor = Color.yellow.opacity(0.5) -#endif +fileprivate let errorHighlightColor = Color.errorHighlightColor @available(OSX 10.15, *) fileprivate struct ErrorStateIfModifier: ViewModifier { diff --git a/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift index 577efbb..b88e29a 100644 --- a/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift +++ b/Tests/KSSSwiftUITests/KSSURLTextFieldTests.swift @@ -29,14 +29,8 @@ class KSSURLTextFieldTests: XCTestCase { } } - func testConstruction() { - // TODO: make this more general, just check against Color.errorHighligthColor if possible -#if canImport(Cocoa) - let correctColor = Color(NSColor.errorHighlightColor) -#else - let correctColor = Color.yellow.opacity(0.5) -#endif + let correctColor = Color.errorHighlightColor var control = KSSURLTextField(url: .constant(nil)) assertEqual(to: "url") { control.helpText } diff --git a/Tests/KSSSwiftUITests/ViewExtensionTests.swift b/Tests/KSSSwiftUITests/ViewExtensionTests.swift new file mode 100644 index 0000000..51969dd --- /dev/null +++ b/Tests/KSSSwiftUITests/ViewExtensionTests.swift @@ -0,0 +1,29 @@ +// +// ViewExtensionTests.swift +// +// +// Created by Steven W. Klassen on 2020-09-07. +// + +import SwiftUI +import XCTest + +import KSSSwiftUI + + +@available(OSX 10.15, *) +class ViewExtensionTests: XCTestCase { + func testExtensionConstruction() { + // Note that this is just a compile test. We don't have any way at present + // of actually checking the results. + _ = Text("hi") + .visible(true) + .invertColorIf(false) + .errorStateIf(false) + + #if canImport(Cocoa) + _ = Text("hi") + .toolTip("hello world") + #endif + } +} From f7f03a35decc8cc90bb72ab764a147a8d805e256 Mon Sep 17 00:00:00 2001 From: Steven Klassen Date: Mon, 7 Sep 2020 21:03:01 -0600 Subject: [PATCH 9/9] Added the CI --- .github/workflows/on-push.yml | 27 +++++++++++++++++++++++++++ .github/workflows/on-release.yml | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/on-push.yml create mode 100644 .github/workflows/on-release.yml diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml new file mode 100644 index 0000000..5f73e06 --- /dev/null +++ b/.github/workflows/on-push.yml @@ -0,0 +1,27 @@ +name: On Push + +on: [push] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest] + steps: + - uses: actions/checkout@v1 + + - name: Build + if: | + !startsWith(github.event.head_commit.message, 'WIP') + && !startsWith(github.ref, 'refs/tags/') + run: | + git submodule update --init --recursive + make + + - name: Run tests + if: | + !startsWith(github.event.head_commit.message, 'WIP') + && !startsWith(github.ref, 'refs/tags/') + run: | + make check diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml new file mode 100644 index 0000000..fcaca60 --- /dev/null +++ b/.github/workflows/on-release.yml @@ -0,0 +1,25 @@ +name: On Release + +on: + release: + types: [published] + +jobs: + docs: + runs-on: macos-latest + steps: + - uses: actions/checkout@v1 + + - name: Install Jazzy + run: | + sudo gem install jazzy + + - name: Publish Docs + env: + X_URL: ${{ secrets.API_PUBLISHER_URL }} + X_USER: ${{ secrets.API_PUBLISHER_USER }} + X_PW: ${{ secrets.API_PUBLISHER_PASSWORD }} + run: | + git submodule update --init --recursive + make docs + BuildSystem/common/publish.py --url "$X_URL" --user "$X_USER" --password "$X_PW" docs