├── .gitignore ├── .travis.yml ├── LICENSE ├── README.org ├── doc ├── big_logo.png └── images │ ├── simple-actionbar.png │ └── tabs-actionbar.png ├── examples └── web-view.clj ├── project.clj ├── src ├── clojure │ └── neko │ │ ├── _utils.clj │ │ ├── action_bar.clj │ │ ├── activity.clj │ │ ├── compliment │ │ └── ui_widgets_and_attributes.clj │ │ ├── context.clj │ │ ├── data.clj │ │ ├── data │ │ ├── shared_prefs.clj │ │ └── sqlite.clj │ │ ├── debug.clj │ │ ├── dialog │ │ └── alert.clj │ │ ├── doc.clj │ │ ├── find_view.clj │ │ ├── intent.clj │ │ ├── listeners │ │ ├── adapter_view.clj │ │ ├── dialog.clj │ │ ├── search_view.clj │ │ ├── text_view.clj │ │ └── view.clj │ │ ├── log.clj │ │ ├── notify.clj │ │ ├── resource.clj │ │ ├── threading.clj │ │ ├── tools │ │ └── repl.clj │ │ ├── ui.clj │ │ └── ui │ │ ├── adapters.clj │ │ ├── listview.clj │ │ ├── mapping.clj │ │ ├── menu.clj │ │ └── traits.clj └── java │ └── neko │ ├── ActivityWithState.java │ ├── App.java │ ├── data │ └── sqlite │ │ ├── SQLiteHelper.java │ │ └── TaggedCursor.java │ └── ui │ └── adapters │ ├── InterchangeableListAdapter.java │ └── TaggedCursorAdapter.java └── test └── neko ├── compliment └── t_ui_widgets_and_attributes.clj ├── data ├── t_shared_prefs.clj └── t_sqlite.clj ├── dialog └── t_alert.clj ├── listeners ├── t_adapter_view.clj ├── t_text_view.clj └── t_view.clj ├── t_activity.clj ├── t_context.clj ├── t_data.clj ├── t_debug.clj ├── t_doc.clj ├── t_find_view.clj ├── t_intent.clj ├── t_log.clj ├── t_notify.clj ├── t_threading.clj ├── t_ui.clj ├── t_utils.clj └── ui ├── t_adapters.clj └── t_listview.clj /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | target/ 3 | gen/ 4 | libs/ 5 | docs/ 6 | checkouts/ 7 | local.properties 8 | clojure.properties 9 | pom.xml 10 | pom.xml.asc 11 | /.lein-* 12 | /.nrepl-port 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | sudo: false 3 | cache: 4 | directories: 5 | - $HOME/.m2 6 | before_install: 7 | - mkdir ~/bin 8 | - wget https://raw.github.com/technomancy/leiningen/stable/bin/lein -P ~/bin/ 9 | - chmod a+x ~/bin/lein 10 | android: 11 | components: 12 | - build-tools-18.1.1 13 | - android-18 14 | - extra-android-m2repository 15 | lein: ~/bin/lein 16 | script: 17 | - DEBUG=1 lein with-profile travis droid local-test 18 | - lein with-profile travis coverage 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | === Eclipse Public License - v 1.0 === 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE (“AGREEMENT”). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE 5 | PROGRAM CONSTITUTES RECIPIENT’S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | ==== 1. DEFINITIONS ==== 8 | 9 | “Contribution” means: 10 | 11 | a. in the case of the initial Contributor, the initial 12 | code and documentation distributed under this Agreement, and 13 | 14 | b. in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | + 20 | where such changes and/or additions to the Program originate from and are 21 | distributed by that particular Contributor. A Contribution ‘originates’ from a 22 | Contributor if it was added to the Program by such Contributor itself or anyone 23 | acting on such Contributor’s behalf. Contributions do not include additions to 24 | the Program which: (i) are separate modules of software distributed in 25 | conjunction with the Program under their own license agreement, and (ii) are 26 | not derivative works of the Program. 27 | 28 | “Contributor” means any person or entity that distributes the Program. 29 | 30 | “Licensed Patents” mean patent claims licensable by a Contributor which are 31 | necessarily infringed by the use or sale of its Contribution alone or when 32 | combined with the Program. 33 | 34 | “Program” means the Contributions distributed in accordance with this 35 | Agreement. 36 | 37 | “Recipient” means anyone who receives the Program under this Agreement, 38 | including all Contributors. 39 | 40 | ==== 2. GRANT OF RIGHTS ==== 41 | 42 | a. Subject to the terms of this Agreement, each Contributor hereby grants 43 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 44 | reproduce, prepare derivative works of, publicly display, publicly perform, 45 | distribute and sublicense the Contribution of such Contributor, if any, and 46 | such derivative works, in source code and object code form. 47 | 48 | b. Subject to the terms of this Agreement, each Contributor hereby grants 49 | Recipient a non-exclusive, worldwide, royalty-free patent license under 50 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 51 | transfer the Contribution of such Contributor, if any, in source code and 52 | object code form. This patent license shall apply to the combination of the 53 | Contribution and the Program if, at the time the Contribution is added by 54 | the Contributor, such addition of the Contribution causes such combination 55 | to be covered by the Licensed Patents. The patent license shall not apply to 56 | any other combinations which include the Contribution. No hardware per se is 57 | licensed hereunder. 58 | 59 | c. Recipient understands that although each Contributor grants the licenses to 60 | its Contributions set forth herein, no assurances are provided by any 61 | Contributor that the Program does not infringe the patent or other 62 | intellectual property rights of any other entity. Each Contributor disclaims 63 | any liability to Recipient for claims brought by any other entity based on 64 | infringement of intellectual property rights or otherwise. As a condition to 65 | exercising the rights and licenses granted hereunder, each Recipient hereby 66 | assumes sole responsibility to secure any other intellectual property rights 67 | needed, if any. For example, if a third party patent license is required to 68 | allow Recipient to distribute the Program, it is Recipient’s responsibility 69 | to acquire that license before distributing the Program. 70 | 71 | d. Each Contributor represents that to its knowledge it has sufficient 72 | copyright rights in its Contribution, if any, to grant the copyright license 73 | set forth in this Agreement. 74 | 75 | ==== 3. REQUIREMENTS ==== 76 | 77 | A Contributor may choose to distribute the Program in object code form under 78 | its own license agreement, provided that: 79 | 80 | a. it complies with the terms and conditions of this Agreement; and 81 | 82 | b. its license agreement: 83 | 84 | i) effectively disclaims on behalf of all Contributors all warranties and 85 | conditions, express and implied, including warranties or conditions of 86 | title and non-infringement, and implied warranties or conditions of 87 | merchantability and fitness for a particular purpose; 88 | 89 | ii) effectively excludes on behalf of all Contributors all liability for 90 | damages, including direct, indirect, special, incidental and 91 | consequential damages, such as lost profits; 92 | 93 | iii) states that any provisions which differ from this Agreement are offered 94 | by that Contributor alone and not by any other party; and 95 | 96 | iv) states that source code for the Program is available from such 97 | Contributor, and informs licensees how to obtain it in a reasonable 98 | manner on or through a medium customarily used for software 99 | exchange. 100 | 101 | When the Program is made available in source code form: 102 | 103 | a. it must be made available under this Agreement; and 104 | 105 | b. a copy of this Agreement must be included with each copy of the Program. 106 | 107 | Contributors may not remove or alter any copyright notices contained within the 108 | Program. 109 | 110 | Each Contributor must identify itself as the originator of its Contribution, if 111 | any, in a manner that reasonably allows subsequent Recipients to identify the 112 | originator of the Contribution. 113 | 114 | ==== 4. COMMERCIAL DISTRIBUTION ==== 115 | 116 | Commercial distributors of software may accept certain responsibilities with 117 | respect to end users, business partners and the like. While this license is 118 | intended to facilitate the commercial use of the Program, the Contributor who 119 | includes the Program in a commercial product offering should do so in a manner 120 | which does not create potential liability for other Contributors. Therefore, if 121 | a Contributor includes the Program in a commercial product offering, such 122 | Contributor (“Commercial Contributor”) hereby agrees to defend and indemnify 123 | every other Contributor (“Indemnified Contributor”) against any losses, damages 124 | and costs (collectively “Losses”) arising from claims, lawsuits and other legal 125 | actions brought by a third party against the Indemnified Contributor to the 126 | extent caused by the acts or omissions of such Commercial Contributor in 127 | connection with its distribution of the Program in a commercial product 128 | offering. The obligations in this section do not apply to any claims or Losses 129 | relating to any actual or alleged intellectual property infringement. In order 130 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 131 | Contributor in writing of such claim, and b) allow the Commercial Contributor 132 | to control, and cooperate with the Commercial Contributor in, the defense and 133 | any related settlement negotiations. The Indemnified Contributor may 134 | participate in any such claim at its own expense. 135 | 136 | For example, a Contributor might include the Program in a commercial product 137 | offering, Product X. That Contributor is then a Commercial Contributor. If that 138 | Commercial Contributor then makes performance claims, or offers warranties 139 | related to Product X, those performance claims and warranties are such 140 | Commercial Contributor’s responsibility alone. Under this section, the 141 | Commercial Contributor would have to defend claims against the other 142 | Contributors related to those performance claims and warranties, and if a court 143 | requires any other Contributor to pay any damages as a result, the Commercial 144 | Contributor must pay those damages. 145 | 146 | v==== 5. NO WARRANTY ==== 147 | 148 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 149 | PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS 150 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 151 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 152 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 153 | responsible for determining the appropriateness of using and 154 | distributing the Program and assumes all risks associated with its 155 | exercise of rights under this Agreement , including but not limited to 156 | the risks and costs of program errors, compliance with applicable laws, 157 | damage to or loss of data, programs or equipment, and unavailability or 158 | interruption of operations. 159 | 160 | ==== 6. DISCLAIMER OF LIABILITY ==== 161 | 162 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 163 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 164 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 165 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 166 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 167 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 168 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 169 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 170 | 171 | ==== 7. GENERAL ==== 172 | 173 | If any provision of this Agreement is invalid or unenforceable under 174 | applicable law, it shall not affect the validity or enforceability of 175 | the remainder of the terms of this Agreement, and without further action 176 | by the parties hereto, such provision shall be reformed to the minimum 177 | extent necessary to make such provision valid and enforceable. 178 | 179 | If Recipient institutes patent litigation against any entity 180 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 181 | Program itself (excluding combinations of the Program with other 182 | software or hardware) infringes such Recipient’s patent(s), then such 183 | Recipient’s rights granted under Section 2(b) shall terminate as of the 184 | date such litigation is filed. 185 | 186 | All Recipient’s rights under this Agreement shall terminate if it 187 | fails to comply with any of the material terms or conditions of this 188 | Agreement and does not cure such failure in a reasonable period of time 189 | after becoming aware of such noncompliance. If all Recipient’s rights 190 | under this Agreement terminate, Recipient agrees to cease use and 191 | distribution of the Program as soon as reasonably practicable. However, 192 | Recipient’s obligations under this Agreement and any licenses granted by 193 | Recipient relating to the Program shall continue and survive. 194 | 195 | Everyone is permitted to copy and distribute copies of this 196 | Agreement, but in order to avoid inconsistency the Agreement is 197 | copyrighted and may only be modified in the following manner. The 198 | Agreement Steward reserves the right to publish new versions (including 199 | revisions) of this Agreement from time to time. No one other than the 200 | Agreement Steward has the right to modify this Agreement. The Eclipse 201 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 202 | assign the responsibility to serve as the Agreement Steward to a 203 | suitable separate entity. Each new version of the Agreement will be 204 | given a distinguishing version number. The Program (including 205 | Contributions) may always be distributed subject to the version of the 206 | Agreement under which it was received. In addition, after a new version 207 | of the Agreement is published, Contributor may elect to distribute the 208 | Program (including its Contributions) under the new version. Except as 209 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 210 | rights or licenses to the intellectual property of any Contributor under 211 | this Agreement, whether expressly, by implication, estoppel or 212 | otherwise. All rights in the Program not expressly granted under this 213 | Agreement are reserved. 214 | 215 | This Agreement is governed by the laws of the State of New York and 216 | the intellectual property laws of the United States of America. No party 217 | to this Agreement will bring a legal action under this Agreement more 218 | than one year after the cause of action arose. Each party waives its 219 | rights to a jury trial in any resulting litigation. 220 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * [[https://raw.githubusercontent.com/clojure-android/neko/master/doc/big_logo.png]] 2 | 3 | [[https://travis-ci.org/clojure-android/neko/][https://travis-ci.org/clojure-android/neko.svg?branch=master]] 4 | [[https://coveralls.io/r/clojure-android/neko][https://coveralls.io/repos/clojure-android/neko/badge.svg?branch=master]] [[http://jarkeeper.com/clojure-android/neko][http://jarkeeper.com/clojure-android/neko/status.png]] 5 | 6 | Neko is a toolkit designed to make Android development using Clojure easier 7 | and more fun. It provides different facilities that wrap Android's Java API 8 | and are more idiomatic to use from Clojure. Neko does not try to wrap every 9 | API class or method, as they are always accessible to a developer through 10 | interop. Instead Neko only offers substitutes to Java API where it really 11 | simplifies the development and gives better understanding of the code. 12 | 13 | Neko is developed under [[http://clojure-android.info/][Clojure-Android initiative]]. 14 | 15 | ** Installation 16 | 17 | Add the following line to the dependencies of your Clojure/Android project: 18 | 19 | [[https://clojars.org/neko][https://clojars.org/neko/latest-version.svg]] 20 | 21 | If you use [[https://github.com/clojure-android/lein-droid][lein-droid]] to create and build your project (which you 22 | should), the Neko dependency will already be there. 23 | 24 | ** Documentation 25 | 26 | For your first dive into Clojure on Android development, see this 27 | [[https://github.com/clojure-android/lein-droid/wiki/Tutorial][Tutorial]]. 28 | 29 | For detailed information on available features and utilities, 30 | please consult the [[https://github.com/alexander-yakushev/neko/wiki][wiki]]. 31 | 32 | Marginalia docs are available [[http://clojure-android.github.io/neko/][here]]. 33 | 34 | ** Building Neko 35 | 36 | If you want to modify/extend Neko source code and then use it in 37 | your applications, you can build it yourself. Use =lein droid jar= 38 | in neko's directory to create a JAR file which then can be pushed 39 | to Clojars under your name or installed into a local Maven repo. 40 | 41 | Make sure that =:target-version= defined in =project.clj= matches 42 | the Android SDK version you have installed. 43 | 44 | If you have problems building Neko, try running =lein clean= first. 45 | Also you can use =DEBUG=1 lein droid jar= for additional build 46 | information. 47 | 48 | ** Acknowledgments 49 | 50 | Neko was originally written by [[https://github.com/sattvik][Daniel Solano Gómez]]. This version is a fork by 51 | Alexander Yakushev as part of Google Summer of Code 2012/2013 participation 52 | project. 53 | 54 | I thank Remco van 't Veer for his [[https://github.com/remvee/clj-android][clj-android]] library which was one of the 55 | pioneer attempts to bring Clojure to Android development. Some features in my 56 | Neko fork are inspired by ideas from his project. 57 | 58 | I also thank all the [[https://github.com/alexander-yakushev/neko/graphs/contributors][contributors]] to Neko. 59 | 60 | Cat icon used in the logo is designed by [[http://www.freepik.com/][Freepik]] from Flaticon.com. 61 | 62 | ** Legal information 63 | 64 | Copyright © 2011-2013 Sattvik Software & Technology Resources, Ltd. 65 | Co., 2012-2015 Alexander Yakushev 66 | 67 | All rights reserved. 68 | 69 | Licensed under Eclipse Public License v1.0. See [[https://github.com/alexander-yakushev/neko/blob/master/LICENSE][LICENSE]]. 70 | -------------------------------------------------------------------------------- /doc/big_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-android/neko/df40df53beb604e3db486fe56c62a5f45a3b7163/doc/big_logo.png -------------------------------------------------------------------------------- /doc/images/simple-actionbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-android/neko/df40df53beb604e3db486fe56c62a5f45a3b7163/doc/images/simple-actionbar.png -------------------------------------------------------------------------------- /doc/images/tabs-actionbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-android/neko/df40df53beb604e3db486fe56c62a5f45a3b7163/doc/images/tabs-actionbar.png -------------------------------------------------------------------------------- /examples/web-view.clj: -------------------------------------------------------------------------------- 1 | (ns org.neko.main 2 | (:require [neko.activity :refer [defactivity set-content-view!]] 3 | [neko.debug :refer [*a]] 4 | [neko.find-view :refer [find-view]] 5 | [neko.threading :refer [on-ui]]) 6 | (:import [android.webkit WebViewClient])) 7 | 8 | (defn make-webview-client 9 | "Generate a class instance of WebViewClient" 10 | [web-view] 11 | (proxy [WebViewClient] [] 12 | (shouldOverrideUrlLoading [view url] 13 | (.loadUrl view url) 14 | true))) 15 | 16 | (defn main-layout 17 | "Create a layout with a WebView" 18 | [activity] 19 | [:web-view {:id ::main-web-view 20 | :layout-width :fill-parent 21 | :layout-height :fill-parent}]) 22 | 23 | (defactivity org.neko.MainActivity 24 | :key :main 25 | :on-create (fn 26 | [this bundle] 27 | (on-ui 28 | (set-content-view! (*a) (main-layout (*a)))) 29 | (on-ui 30 | (let [webview (find-view (*a) ::main-web-view)] 31 | (doto (.getSettings webview) 32 | (.setJavaScriptEnabled true) 33 | (.setBuiltInZoomControls true)) 34 | (doto webview 35 | (.loadUrl "http://clojure-android.info") 36 | (.setWebViewClient (make-webview-client webview))))))) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject neko "4.0.0-alpha5" 2 | :description "Neko is a toolkit designed to make Android development using Clojure easier and more fun." 3 | :url "https://github.com/clojure-android/neko" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure-android/clojure "1.7.0-r2"] 8 | [com.android.support/multidex "1.0.0" :extension "aar"]] 9 | :plugins [[lein-droid "0.4.6"]] 10 | 11 | :source-paths ["src/clojure"] 12 | :java-source-paths ["src/java"] 13 | :javac-options ["-target" "1.6" "-source" "1.6" "-Xlint:-options"] 14 | 15 | :profiles {:default [:android-common] 16 | 17 | :robolectric 18 | [:android-common 19 | {:dependencies [[junit/junit "4.12"] 20 | [org.robolectric/robolectric "3.0"] 21 | [org.clojure-android/droid-test "0.1.1-SNAPSHOT"] 22 | [org.clojure/tools.nrepl "0.2.10"]]}] 23 | 24 | :local-testing 25 | [:robolectric 26 | {:target-path "target/local-testing" 27 | :dependencies [[venantius/ultra "0.3.3"]] 28 | :android {:aot [#"neko.*\.t-.+" "ultra.test"]}}] 29 | 30 | :local-repl 31 | [:robolectric 32 | {:target-path "target/local-repl" 33 | :android {:aot :all-with-unused}}] 34 | 35 | :travis 36 | [:local-testing 37 | {:dependencies [[cloverage "1.0.6" :exclusions [org.clojure/tools.logging]] 38 | [org.clojure-android/tools.logging "0.3.2-r1"]] 39 | :plugins [[lein-shell "0.4.0"]] 40 | :aliases {"coverage" ["do" ["droid" "local-test" "cloverage"] 41 | ["shell" "curl" "-F" 42 | "json_file=@target/coverage/coveralls.json" 43 | "https://coveralls.io/api/v1/jobs"]]} 44 | :android {:sdk-path "/usr/local/android-sdk/" 45 | :aot ["cloverage.coverage"] 46 | :cloverage-exclude-ns ["neko.tools.repl"]}}]} 47 | 48 | 49 | :android {:library true 50 | :target-version 18}) 51 | -------------------------------------------------------------------------------- /src/clojure/neko/_utils.clj: -------------------------------------------------------------------------------- 1 | (ns neko.-utils 2 | "Internal utilities used by Neko, not intended for external consumption." 3 | (:require [clojure.string :as string]) 4 | (:import [java.lang.reflect Method Constructor Field Modifier])) 5 | 6 | (defmacro app-package-name 7 | "Allows other macros to hard-compile the name of application package in them." 8 | [] 9 | (:neko.init/package-name *compiler-options*)) 10 | 11 | (defmacro memoized [inside-defn] 12 | (let [[_ name doc & fdecl] inside-defn 13 | m (assoc (meta name) :doc doc)] 14 | `(def ~(with-meta name m) 15 | (memoize (fn ~@fdecl))))) 16 | 17 | (defn int-id 18 | "Makes an ID from arbitrary object by calling .hashCode on it. 19 | Returns the absolute value." 20 | [obj] 21 | (Math/abs (.hashCode ^Object obj))) 22 | 23 | (defn simple-name 24 | "Takes a possibly package-qualified class name symbol and returns a 25 | simple class name from it." 26 | [full-class-name] 27 | (nth (re-find #"(.*\.)?(.+)" (str full-class-name)) 2)) 28 | 29 | (defn capitalize 30 | "Takes a string and upper-cases the first letter in it." 31 | [s] 32 | (str (.toUpperCase (subs s 0 1)) (subs s 1))) 33 | 34 | (defn unicaseize 35 | "Takes a string lower-cases the first letter in it." 36 | [s] 37 | (str (.toLowerCase (subs s 0 1)) (subs s 1))) 38 | 39 | (memoized 40 | (defn keyword->static-field 41 | "Takes a keyword and transforms it into a static field name. 42 | 43 | All letters in keyword are capitalized, and all dashes are replaced 44 | with underscores." 45 | [kw] 46 | (.toUpperCase (string/replace (name kw) \- \_)))) 47 | 48 | (defn keyword->camelcase 49 | "Takes a keyword and transforms its name into camelCase." 50 | [kw] 51 | (let [[first & rest] (string/split (name kw) #"-")] 52 | (string/join (cons first (map capitalize rest))))) 53 | 54 | (memoized 55 | (defn keyword->setter 56 | "Takes a keyword and transforms it into a setter method name. 57 | 58 | Transforms keyword name into camelCase, capitalizes the first 59 | character and appends \"set\" at the beginning." 60 | [kw] 61 | (->> (keyword->camelcase kw) 62 | capitalize 63 | (str "set")))) 64 | 65 | (defmacro call-if-nnil 66 | "Expands into check whether function is defined, then executes it 67 | and returns true or just returns false otherwise." 68 | [f & arguments] 69 | `(if ~f 70 | (do (~f ~@arguments) 71 | true) 72 | false)) 73 | 74 | ;; Reflection functions 75 | 76 | (defn class-or-type [cl] 77 | (condp = cl 78 | Boolean Boolean/TYPE 79 | Integer Integer/TYPE 80 | Long Integer/TYPE 81 | Double Double/TYPE 82 | Float Float/TYPE 83 | Character Character/TYPE 84 | cl)) 85 | 86 | (defn ^Method reflect-setter 87 | "Returns a Method object for the given UI object class, method name 88 | and the first argument type." 89 | [^Class widget-type, ^String method-name, ^Class value-type] 90 | (if-not (= widget-type Object) 91 | (let [value-type (class-or-type value-type) 92 | all-value-types (cons value-type (supers value-type))] 93 | (loop [[t & r] all-value-types] 94 | (if t 95 | (if-let [method (try 96 | (.getDeclaredMethod widget-type method-name 97 | (into-array Class [t])) 98 | (catch NoSuchMethodException e nil))] 99 | method 100 | (recur r)) 101 | (reflect-setter (.getSuperclass widget-type) 102 | method-name value-type)))) 103 | (throw 104 | (NoSuchMethodException. (format "Couldn't find method .%s for argument %s)" 105 | method-name (.getName value-type)))))) 106 | 107 | (defn ^Constructor reflect-constructor 108 | "Returns a Constructor object for the given UI object class " 109 | [^Class widget-type constructor-arg-types] 110 | (.getConstructor widget-type (into-array Class constructor-arg-types))) 111 | 112 | (defn reflect-field 113 | "Returns a field value for the given UI object class and the name of 114 | the field." 115 | [^Class widget-type, ^String field-name] 116 | (.get ^Field (.getDeclaredField widget-type field-name) nil)) 117 | 118 | (defn list-all-methods 119 | "Returns names of all non-private methods of a class, including methods 120 | declared in its parents." 121 | [^Class c] 122 | (loop [c c, methods ()] 123 | (if c 124 | (recur (.getSuperclass c) 125 | (concat methods 126 | (keep (fn [^Method m] 127 | (when-not (Modifier/isPrivate (.getModifiers m)) 128 | (.getName m))) 129 | (.getDeclaredMethods c)))) 130 | methods))) 131 | 132 | (defn closest-android-ancestor 133 | [^Class c] 134 | (loop [c c] 135 | (when c 136 | (if-let [cand-name 137 | (re-find #"^class (android\..*)" (str c))] 138 | (Class/forName (second cand-name)) 139 | (recur (.getSuperclass c)))))) 140 | -------------------------------------------------------------------------------- /src/clojure/neko/action_bar.clj: -------------------------------------------------------------------------------- 1 | (ns neko.action-bar 2 | "Provides utilities to manipulate application ActionBar." 3 | (:require neko.ui) 4 | (:use [neko.ui.mapping :only [defelement]] 5 | [neko.ui.traits :only [deftrait]] 6 | [neko.-utils :only [call-if-nnil]]) 7 | (:import [android.app ActionBar ActionBar$Tab ActionBar$TabListener] 8 | android.app.Activity android.app.Fragment android.R$id)) 9 | 10 | ;; ## Listener helpers 11 | 12 | (defn tab-listener 13 | "Creates a TabListener from the provided functions for selected, 14 | unselected and reselected events." 15 | [& {:keys [on-tab-selected on-tab-unselected on-tab-reselected]}] 16 | (reify ActionBar$TabListener 17 | (onTabSelected [this tab ft] 18 | (call-if-nnil on-tab-selected tab ft)) 19 | (onTabUnselected [this tab ft] 20 | (call-if-nnil on-tab-unselected tab ft)) 21 | (onTabReselected [this tab ft] 22 | (call-if-nnil on-tab-reselected tab ft)))) 23 | 24 | (defn simple-tab-listener 25 | "Creates a TabListener that shows the specified fragment on 26 | selecting and hides it on deselecting." 27 | [tag, ^Fragment fragment] 28 | (reify ActionBar$TabListener 29 | (onTabReselected [this tab ft]) 30 | (onTabUnselected [this tab ft] 31 | (when (.isAdded fragment) 32 | (.detach ft fragment))) 33 | (onTabSelected [this tab ft] 34 | (if (.isDetached fragment) 35 | (.attach ft fragment) 36 | (.add ft R$id/content fragment tag))))) 37 | 38 | ;; ## Functions for declarative definition 39 | 40 | (defelement :action-bar 41 | :classname android.app.ActionBar 42 | :inherits nil 43 | :traits [:tabs :display-options] 44 | :values {:standard ActionBar/NAVIGATION_MODE_STANDARD 45 | :list ActionBar/NAVIGATION_MODE_LIST 46 | :tabs ActionBar/NAVIGATION_MODE_TABS}) 47 | 48 | (defelement :action-bar-tab 49 | :classname android.app.ActionBar$Tab 50 | :inherits nil 51 | :traits [:tab-listener]) 52 | 53 | (deftrait :tabs 54 | "Takes `:tabs` attribute which should be a sequence of tab 55 | definitions. Each tab definition is itself a sequence of `:tab` 56 | keyword and an attribute map. Creates tabs for the definitions and 57 | adds the tabs to the action bar." 58 | [^ActionBar action-bar, {:keys [tabs]} _] 59 | (doseq [[_ tab-attributes] tabs 60 | :let [tab (.newTab action-bar)]] 61 | (neko.ui/apply-attributes :action-bar-tab tab tab-attributes {}) 62 | (.addTab action-bar tab))) 63 | 64 | (defn display-options-value 65 | "Returns an integer value for the given keyword, or the value itself." 66 | [value] 67 | (if (keyword? value) 68 | (case value 69 | :home-as-up ActionBar/DISPLAY_HOME_AS_UP 70 | :show-home ActionBar/DISPLAY_SHOW_HOME 71 | :show-custom ActionBar/DISPLAY_SHOW_CUSTOM 72 | :show-title ActionBar/DISPLAY_SHOW_TITLE 73 | :use-logo ActionBar/DISPLAY_USE_LOGO) 74 | value)) 75 | 76 | (deftrait :display-options 77 | "Takes `:display-options` attribute, which could be an integer value 78 | or one of the following keywords: `:home-as-up`, `:show-home`, 79 | `:show-custom`, `:show-title`, `:use-logo`, or a vector with these 80 | values, to which bit-or operation will be applied." 81 | [^ActionBar action-bar, {:keys [display-options]} _] 82 | (let [value (if (vector? display-options) 83 | (apply bit-or (map display-options-value display-options)) 84 | (display-options-value display-options))] 85 | (.setDisplayOptions action-bar value))) 86 | 87 | (deftrait :tab-listener 88 | "Takes `:tab-listener` attribute which should be TabListener object 89 | and sets it to the tab. Attribute could also be a fragment, in which 90 | case a listener would be created that shows and hides the fragment 91 | on tab selection and deselection respectively." 92 | [^ActionBar$Tab tab, {:keys [tab-listener]} _] 93 | (let [listener (if (instance? Fragment tab-listener) 94 | (simple-tab-listener (str tab-listener) tab-listener) 95 | tab-listener)] 96 | (.setTabListener tab listener))) 97 | 98 | (defn setup-action-bar 99 | "Configures activity's action bar according to the attributes 100 | provided in key-value fashion. For more information, 101 | see `(describe :action-bar)`." 102 | [^Activity activity, attributes-map] 103 | (let [action-bar (.getActionBar activity)] 104 | (neko.ui/apply-attributes :action-bar action-bar attributes-map {}))) 105 | -------------------------------------------------------------------------------- /src/clojure/neko/activity.clj: -------------------------------------------------------------------------------- 1 | (ns neko.activity 2 | "Utilities to aid in working with an activity." 3 | (:require [clojure.string :as s] 4 | [neko.ui :refer [make-ui]] 5 | [neko.debug :refer [all-activities safe-for-ui]] 6 | [neko.-utils :as u]) 7 | (:import android.app.Activity 8 | [android.view View Window] 9 | android.app.Fragment 10 | neko.ActivityWithState)) 11 | 12 | (defn ^View get-decor-view 13 | "Returns the root view of the given activity." 14 | [^Activity activity] 15 | (.. activity getWindow getDecorView)) 16 | 17 | (defn set-content-view! 18 | "Sets the content for the activity. The view may be one of: 19 | 20 | + neko.ui tree 21 | + A view object, which will be used directly 22 | + An integer presumed to be a valid layout ID." 23 | [^Activity activity, view] 24 | {:pre [(instance? Activity activity)]} 25 | (cond 26 | (instance? View view) 27 | (.setContentView activity ^View view) 28 | 29 | (integer? view) 30 | (.setContentView activity ^Integer view) 31 | 32 | :else 33 | (let [dv (get-decor-view activity)] 34 | (.setTag dv (java.util.HashMap.)) 35 | (.setContentView activity 36 | ^View (neko.ui/make-ui-element activity view 37 | {:id-holder dv}))))) 38 | 39 | (defmacro request-window-features! 40 | "Requests the given features for the activity. The features should be keywords 41 | such as :no-title or :indeterminate-progress corresponding FEATURE_NO_TITLE 42 | and FEATURE_INDETERMINATE_PROGRESS, respectively. Returns a sequence of 43 | booleans whether for each feature that indicates if the feature is supported 44 | and now enabled. 45 | 46 | This macro should be called before set-content-view!." 47 | [^Activity activity & features] 48 | {:pre [(every? keyword? features)]} 49 | `[~@(for [feat features] 50 | `(.requestWindowFeature 51 | ~activity ~(symbol (str (.getName Window) "/FEATURE_" 52 | (u/keyword->static-field (name feat))))))]) 53 | 54 | (defmacro ^{:forms '[name & options & methods]} defactivity 55 | "Creates an activity with the given full package-qualified name. 56 | Optional arguments should be provided in a key-value fashion. 57 | 58 | Available optional arguments: 59 | 60 | :extends, :implements, :prefix - same as for `gen-class`. 61 | 62 | :features - window features to be requested for the activity. 63 | Relevant only if :create is used. 64 | 65 | :on-create - takes a two-argument function. Generates a handler for 66 | activity's `onCreate` event which automatically calls the 67 | superOnCreate method and creates a var with the name denoted by 68 | `:def` (or activity's lower-cased name by default) to store the 69 | activity object. Then calls the provided function onto the 70 | Application object. 71 | 72 | :on-start, :on-restart, :on-resume, :on-pause, :on-stop, :on-destroy 73 | - same as :on-create but require a one-argument function." 74 | [name & args] 75 | (if (some #{:on-create} args) 76 | (throw 77 | (RuntimeException. 78 | (str "ERROR: This syntax of defactivity is deprecated, please " 79 | "update it to the new syntax: " 80 | "https://github.com/clojure-android/neko/wiki/Namespaces#defining-an-activity"))) 81 | (let [[{:keys [extends implements prefix state key features]} methods] 82 | (loop [args args, options {}, methods {}] 83 | (cond (empty? args) [options methods] 84 | 85 | (keyword? (first args)) 86 | (recur (drop 2 args) 87 | (assoc options (first args) (second args)) 88 | methods) 89 | 90 | :else 91 | (recur (rest args) options 92 | (assoc methods (ffirst args) (first args))))) 93 | 94 | sname (u/simple-name name) 95 | prefix (or prefix (str sname "-")) 96 | extends (resolve (or extends 'android.app.Activity)) 97 | state (or state `(atom {})) 98 | release-build? (:neko.init/release-build *compiler-options*) 99 | exposed-methods (if release-build? 100 | (map str (keys methods)) 101 | (u/list-all-methods extends))] 102 | `(do 103 | (gen-class 104 | :name ~name 105 | :main false 106 | :prefix ~prefix 107 | :init "init" 108 | :state "state" 109 | :extends ~(symbol (.getName extends)) 110 | :implements ~(conj implements neko.ActivityWithState) 111 | :overrides-methods ~(when release-build? 112 | (map (fn [[_ [mname args]]] [mname (count args)]) 113 | (assoc methods 'getState '(getState [this])))) 114 | :exposes-methods 115 | ~(->> exposed-methods 116 | distinct 117 | (map (fn [mname] 118 | [(symbol mname) (symbol (str "super" (u/capitalize mname)))])) 119 | (into {}))) 120 | 121 | ~`(defn ~(symbol (str prefix "init")) 122 | [] [[] ~state]) 123 | ~`(defn ~(symbol (str prefix "getState")) 124 | [~(vary-meta 'this assoc :tag name)] 125 | (.state ~'this)) 126 | ~(when-let [[mname args & body] (get methods 'onCreate)] 127 | `(defn ~(symbol (str prefix mname)) 128 | [~(vary-meta (first args) assoc :tag name) 129 | ~(vary-meta (second args) assoc :tag android.os.Bundle)] 130 | (.put all-activities '~(.name *ns*) ~'this) 131 | ~(when key 132 | `(.put all-activities ~key ~'this)) 133 | ~(when features 134 | `(request-window-features! ~'this ~@features)) 135 | (safe-for-ui ~@body))) 136 | ~@(for [[_ [mname args & body]] (dissoc methods 'onCreate)] 137 | `(defn ~(symbol (str prefix mname)) 138 | [~(vary-meta (first args) assoc :tag name) 139 | ~@(rest args)] 140 | (safe-for-ui ~@body))))))) 141 | 142 | (defn get-state [^ActivityWithState activity] 143 | (.getState activity)) 144 | 145 | (defn simple-fragment 146 | "Creates a fragment which contains the specified view. If a UI tree 147 | was provided, it is inflated and then set as fragment's view." 148 | ([context tree] 149 | (simple-fragment (make-ui context tree))) 150 | ([view] 151 | (proxy [Fragment] [] 152 | (onCreateView [inflater container bundle] 153 | (if (instance? View view) 154 | view 155 | (do 156 | (println "One-argument version is deprecated. Please use (simple-fragment context tree)") 157 | (make-ui view))))))) 158 | -------------------------------------------------------------------------------- /src/clojure/neko/compliment/ui_widgets_and_attributes.clj: -------------------------------------------------------------------------------- 1 | (ns neko.compliment.ui-widgets-and-attributes 2 | "Compliment source for keywords that represent UI widget keywords 3 | and their traits." 4 | (:require [neko.ui.mapping :as mapping] 5 | [neko.ui.traits :as traits] 6 | [neko.doc :as doc] 7 | [neko.resource :as droid-res] 8 | [clojure.string :as string] 9 | [clojure.set :as set])) 10 | 11 | (defn keyword-symbol? 12 | "Tests if prefix is a keyword." 13 | [x] 14 | (re-matches #":.*" x)) 15 | 16 | (defn process-context 17 | "Checks if the context is a widget definition, attribute map 18 | definition or neither. If context is an attribute map, tries finding 19 | current widget's name." 20 | [context] 21 | (let [[level1 level2] context] 22 | (cond (and (vector? (:form level1)) (= (:idx level1) 0)) 23 | {:type :widget} 24 | 25 | (and (map? (:form level1)) (= (:map-role level1) :key)) 26 | {:type :attr, 27 | :widget (when (and (vector? (:form level2)) 28 | (= (:idx level2) 1)) 29 | (let [w-name (first (:form level2))] 30 | (when (and (keyword? w-name) 31 | ((mapping/get-keyword-mapping) w-name)) 32 | w-name)))}))) 33 | 34 | ;; ## Candidates 35 | 36 | (defn get-widget-attributes 37 | "Returns a list all possible attributes for the given widget keyword." 38 | [widget-kw] 39 | (let [attributes (:attributes (meta #'neko.ui.traits/apply-trait))] 40 | (if widget-kw 41 | (let [all-traits (mapping/all-traits widget-kw)] 42 | (for [[att w-traits] attributes 43 | :when (not (empty? (set/intersection w-traits (set all-traits))))] 44 | att)) 45 | (keys attributes)))) 46 | 47 | (defn candidates 48 | "Returns a list of widget keywords or attribute keywords depending 49 | on context." 50 | [^String prefix, ns context] 51 | (when (keyword-symbol? prefix) 52 | (let [ctx (process-context context) 53 | cands (cond 54 | (nil? ctx) [] 55 | (= (:type ctx) :widget) (keys (mapping/get-keyword-mapping)) 56 | (= (:type ctx) :attr) (get-widget-attributes (:widget ctx)))] 57 | (for [^String kw-str (map str cands) 58 | :when (.startsWith kw-str prefix)] 59 | kw-str)))) 60 | 61 | ;; ## Documentation 62 | 63 | (defn doc 64 | "Tries to get a docstring for the given completion candidate." 65 | [^String symbol-str, ns] 66 | (when (keyword-symbol? symbol-str) 67 | (let [kw (keyword (subs symbol-str 1)) 68 | kw-mapping (mapping/get-keyword-mapping) 69 | attributes (:attributes (meta #'neko.ui.traits/apply-trait))] 70 | (cond (kw-mapping kw) 71 | (doc/get-element-doc kw (kw-mapping kw) false) 72 | 73 | (attributes kw) 74 | (->> (attributes kw) 75 | (map doc/get-trait-doc) 76 | (interpose "\n\n") 77 | string/join))))) 78 | 79 | ;; ## Source definition 80 | 81 | (defn init-source 82 | "Initializes this completion source if Compliment is available." 83 | [] 84 | (try (require 'compliment.core) 85 | ((resolve 'compliment.sources/defsource) ::neko-ui-keywords 86 | :candidates #'candidates 87 | :doc #'doc) 88 | (catch Exception ex nil))) 89 | -------------------------------------------------------------------------------- /src/clojure/neko/context.clj: -------------------------------------------------------------------------------- 1 | (ns neko.context 2 | "Utilities to aid in working with a context." 3 | {:author "Daniel Solano Gómez"} 4 | (:require [neko.-utils :as u]) 5 | (:import android.content.Context 6 | neko.App)) 7 | 8 | (defmacro get-service 9 | "Gets a system service for the given type. Type is a keyword that names the 10 | service. Examples include :alarm for the alarm service and 11 | :layout-inflater for the layout inflater service." 12 | {:pre [(keyword? type)]} 13 | ([type] 14 | `(get-service neko.App/instance ~type)) 15 | ([context type] 16 | `(.getSystemService 17 | ^Context ~context 18 | ~(symbol (str (.getName Context) "/" 19 | (u/keyword->static-field (name type)) "_SERVICE"))))) 20 | -------------------------------------------------------------------------------- /src/clojure/neko/data.clj: -------------------------------------------------------------------------------- 1 | (ns neko.data 2 | "Contains utilities to manipulate data that is passed between 3 | Android entities via Bundles and Intents." 4 | (:import android.os.Bundle android.content.Intent 5 | android.content.SharedPreferences 6 | neko.App)) 7 | 8 | ;; This type acts as a wrapper around Bundle instance to be able to 9 | ;; access it like an ordinary map. 10 | ;; 11 | (deftype MapLikeBundle [^Bundle bundle] 12 | clojure.lang.Associative 13 | (containsKey [this k] 14 | (.containsKey bundle (name k))) 15 | (entryAt [this k] 16 | (clojure.lang.MapEntry. k (.get bundle (name k)))) 17 | (valAt [this k] 18 | (.get bundle (name k))) 19 | (valAt [this k default] 20 | (let [key (name k)] 21 | (if (.containsKey bundle key) 22 | (.get bundle (name key)) 23 | default))) 24 | (seq [this] 25 | (map (fn [k] [(keyword k) (.get bundle k)]) 26 | (.keySet bundle)))) 27 | 28 | ;; This type wraps a HashMap just redirecting the calls to the 29 | ;; respective HashMap methods. The only useful thing it does is 30 | ;; allowing to use keyword keys instead of string ones. 31 | ;; 32 | (deftype MapLikeHashMap [^java.util.HashMap hmap] 33 | clojure.lang.Associative 34 | (containsKey [this k] 35 | (.containsKey hmap (name k))) 36 | (entryAt [this k] 37 | (clojure.lang.MapEntry. k (.get hmap (name k)))) 38 | (valAt [this k] 39 | (.get hmap (name k))) 40 | (valAt [this k default] 41 | (let [key (name k)] 42 | (if (.containsKey hmap key) 43 | (.get hmap (name key)) 44 | default))) 45 | (seq [this] 46 | (map (fn [k] [(keyword k) (.get hmap k)]) 47 | (.keySet hmap)))) 48 | 49 | (defprotocol MapLike 50 | "A protocol that helps to wrap objects of different types into 51 | MapLikeBundle." 52 | (like-map [this])) 53 | 54 | (extend-protocol MapLike 55 | Bundle 56 | (like-map [b] 57 | (MapLikeBundle. b)) 58 | 59 | Intent 60 | (like-map [i] 61 | (if-let [bundle (.getExtras i)] 62 | (MapLikeBundle. bundle) 63 | {})) 64 | 65 | SharedPreferences 66 | (like-map [sp] 67 | (MapLikeHashMap. (.getAll sp))) 68 | 69 | nil 70 | (like-map [_] {})) 71 | -------------------------------------------------------------------------------- /src/clojure/neko/data/shared_prefs.clj: -------------------------------------------------------------------------------- 1 | (ns neko.data.shared-prefs 2 | "Utilities for interoperating with SharedPreferences class. The original idea 3 | is by Artur Malabarba." 4 | (:require [clojure.data :as data]) 5 | (:import [android.content Context SharedPreferences SharedPreferences$Editor] 6 | neko.App)) 7 | 8 | (def ^:private sp-access-modes {:private Context/MODE_PRIVATE 9 | :world-readable Context/MODE_WORLD_READABLE 10 | :world-writeable Context/MODE_WORLD_WRITEABLE}) 11 | 12 | (defn get-shared-preferences 13 | "Returns the SharedPreferences object for the given name. Possible modes: 14 | `:private`, `:world-readable`, `:world-writeable`." 15 | ([name mode] 16 | (get-shared-preferences App/instance name mode)) 17 | ([^Context context, name mode] 18 | {:pre [(or (number? mode) (contains? sp-access-modes mode))]} 19 | (let [mode (if (number? mode) 20 | mode (sp-access-modes mode))] 21 | (.getSharedPreferences context name mode)))) 22 | 23 | (defn ^SharedPreferences$Editor put 24 | "Puts the value into the SharedPreferences editor instance. Accepts 25 | limited number of data types supported by SharedPreferences." 26 | [^SharedPreferences$Editor sp-editor, key value] 27 | (let [key (name key)] 28 | (condp #(= (type %2) %1) value 29 | java.lang.Boolean (.putBoolean sp-editor key value) 30 | java.lang.Float (.putFloat sp-editor key value) 31 | java.lang.Double (.putFloat sp-editor key (float value)) 32 | java.lang.Integer (.putInt sp-editor key value) 33 | java.lang.Long (.putLong sp-editor key value) 34 | java.lang.String (.putString sp-editor key value) 35 | ;; else 36 | (throw (RuntimeException. (str "SharedPreferences doesn't support type: " 37 | (type value))))))) 38 | 39 | (defn bind-atom-to-prefs 40 | "Links an atom and a SharedPreferences file so that whenever the atom is 41 | modified changes are propagated down to SP. Only private mode is supported to 42 | avoid inconsistency between the atom and SP." 43 | [atom prefs-file-name] 44 | (let [^SharedPreferences sp (get-shared-preferences prefs-file-name :private)] 45 | (reset! atom (reduce (fn [m [key val]] (assoc m (keyword key) val)) 46 | {} (.getAll sp))) 47 | (add-watch atom ::sp-wrapper 48 | (fn [_ _ old new] 49 | (let [^SharedPreferences$Editor editor (.edit sp) 50 | [removed added] (data/diff old new)] 51 | (doseq [[key _] removed] 52 | (.remove editor (name key))) 53 | (doseq [[key val] added] 54 | (try (put editor key val) (catch RuntimeException ex _))) 55 | (.commit editor)))))) 56 | 57 | (defmacro defpreferences 58 | "Defines a new atom that will be bound to the given SharedPreferences file. 59 | The atom can only contain primitive values and strings, and its contents will 60 | be persisted between application launches. Be aware that if you add an 61 | unsupported value to the atom it will not be saved which can lead to 62 | inconsistencies." 63 | [atom-name prefs-file-name] 64 | `(do (def ~atom-name (atom {})) 65 | (when App/instance 66 | (bind-atom-to-prefs ~atom-name ~prefs-file-name)))) 67 | -------------------------------------------------------------------------------- /src/clojure/neko/data/sqlite.clj: -------------------------------------------------------------------------------- 1 | (ns neko.data.sqlite 2 | "Alpha - subject to change. 3 | 4 | Contains convenience functions to work with SQLite databases Android 5 | provides." 6 | (:refer-clojure :exclude [update]) 7 | (:require [clojure.string :as string]) 8 | (:import [android.database.sqlite SQLiteDatabase] 9 | [neko.data.sqlite SQLiteHelper TaggedCursor] 10 | [android.database Cursor CursorIndexOutOfBoundsException] 11 | [android.content ContentValues Context] 12 | [clojure.lang Keyword PersistentVector] 13 | neko.App)) 14 | 15 | ;; ### Database initialization 16 | 17 | (def ^:private supported-types 18 | "Mapping of SQLite types to respective Java classes. Byte actually stands for 19 | array of bytes, or Blob in SQLite." 20 | {"integer" Integer 21 | "long" Long 22 | "text" String 23 | "boolean" Boolean 24 | "double" Double 25 | "blob" Byte}) 26 | 27 | (defn make-schema 28 | "Creates a schema from arguments and validates it." 29 | [& {:as schema}] 30 | (assert (string? (:name schema)) ":name should be a String.") 31 | (assert (number? (:version schema)) ":version should be a number.") 32 | (assert (map? (:tables schema)) ":tables should be a map.") 33 | (assoc schema 34 | :tables 35 | (into 36 | {} (for [[table-name params] (:tables schema)] 37 | (do 38 | (assert (keyword? table-name) 39 | (str "Table name should be a keyword: " table-name)) 40 | (assert (map? params) 41 | (str "Table parameters should be a map: " table-name)) 42 | (assert (map? (:columns params)) 43 | (str "Table parameters should contain columns map: " table-name)) 44 | [table-name 45 | (assoc params 46 | :columns 47 | (into 48 | {} (for [[column-name col-params] (:columns params)] 49 | (do 50 | (assert (keyword? column-name) 51 | (str "Column name should be a keyword: " column-name)) 52 | (assert (or (map? col-params) (class? Integer)) 53 | (str "Column type should be a map or a string:" 54 | column-name)) 55 | (let [col-type (if (string? col-params) 56 | col-params 57 | (:sql-type col-params)) 58 | java-type (-> (re-matches #"(\w+).*" col-type) 59 | second supported-types) 60 | col-params {:type java-type 61 | :sql-type col-type}] 62 | (assert java-type 63 | (str "Type is not supported: " (:sql-type col-params))) 64 | [column-name col-params])))))]))))) 65 | 66 | (defn- db-create-query 67 | "Generates a table creation query from the provided schema and table 68 | name." 69 | [schema table-name] 70 | (->> (get-in schema [:tables table-name :columns]) 71 | (map (fn [[col params]] 72 | (str (name col) " " (:sql-type params)))) 73 | (interpose ", ") 74 | string/join 75 | (format "create table %s (%s);" (name table-name)))) 76 | 77 | (defn ^SQLiteHelper create-helper 78 | "Creates a SQLiteOpenHelper instance for a given schema. 79 | 80 | Helper will recreate database if the current schema version and 81 | database version mismatch." 82 | ([schema] 83 | (create-helper App/instance schema)) 84 | ([^Context context, {:keys [name version tables] :as schema}] 85 | (SQLiteHelper. (.getApplicationContext context) name version schema 86 | (for [table (keys tables)] 87 | (db-create-query schema table)) 88 | (for [^Keyword table (keys tables)] 89 | (str "drop table if exists " (.getName table)))))) 90 | 91 | ;; A wrapper around SQLiteDatabase to keep database and its schema 92 | ;; together. 93 | ;; 94 | (deftype TaggedDatabase [^SQLiteDatabase db, schema]) 95 | 96 | (defn get-database 97 | "Gets a SQLiteDatabase instance from the given helper. Access-mode can be 98 | either `:read` or `:write`." 99 | [^SQLiteHelper helper, access-mode] 100 | {:pre [(#{:read :write} access-mode)]} 101 | (TaggedDatabase. (case access-mode 102 | :read (.getReadableDatabase helper) 103 | :write (.getWritableDatabase helper)) 104 | (.schema helper))) 105 | 106 | ;; ### Data-SQL transformers 107 | 108 | (defn- map-to-content 109 | "Takes a map of column keywords to values and creates a 110 | ContentValues instance from it." 111 | [^TaggedDatabase tagged-db table data-map] 112 | (let [^ContentValues cv (ContentValues.)] 113 | (doseq [[col {type :type}] (get-in (.schema tagged-db) 114 | [:tables table :columns]) 115 | :when (contains? data-map col)] 116 | (let [value (get data-map col)] 117 | (condp = type 118 | Integer (.put cv (name col) ^Integer (int value)) 119 | Long (.put cv (name col) ^Long value) 120 | Double (.put cv (name col) ^Double value) 121 | String (.put cv (name col) ^String value) 122 | Boolean (.put cv (name col) ^Boolean value) 123 | Byte (.put cv (name col) ^bytes value)))) 124 | cv)) 125 | 126 | (defn- get-value-from-cursor 127 | "Gets a single value out of the cursor from the specified column." 128 | [^Cursor cur i type] 129 | (condp = type 130 | Boolean (= (.getInt cur i) 1) 131 | Integer (.getInt cur i) 132 | Long (.getLong cur i) 133 | String (.getString cur i) 134 | Double (.getDouble cur i) 135 | Byte (.getBlob cur i))) 136 | 137 | (defn- qualified-name 138 | [^Keyword kw] 139 | (.toString (if (.getNamespace (.sym kw)) 140 | (str (.getNamespace (.sym kw)) "." (.getName (.sym kw))) 141 | (.getName (.sym kw))))) 142 | 143 | (defn- keyval-to-sql 144 | "Transforms a key-value pair into a proper SQL comparison/assignment 145 | statement. 146 | 147 | For example, it will put single quotes around String value. The 148 | value could also be a vector that looks like `[:or value1 value2 149 | ...]`, in which case it will be transformed into `key = value1 OR 150 | key = value2 ...`. Nested vectors is supported." 151 | [k v] 152 | (let [qk (qualified-name k)] 153 | (condp #(= % (type %2)) v 154 | PersistentVector (let [[op & values] v] 155 | (->> values 156 | (map (partial keyval-to-sql k)) 157 | (interpose (str " " (name op) " ")) 158 | string/join)) 159 | String (format "(%s = '%s')" qk v) 160 | Boolean (format "(%s = %s)" qk (if v 1 0)) 161 | Keyword (format "(%s = %s)" qk (qualified-name v)) 162 | nil (format "(%s is NULL)" qk) 163 | (format "(%s = %s)" qk v)))) 164 | 165 | ;; ### SQL operations 166 | 167 | (defn- where-clause 168 | "Takes a map of column keywords to values and generates a WHERE 169 | clause from it." 170 | [where] 171 | (if (string? where) 172 | where 173 | (->> where 174 | (map #(str "(" (apply keyval-to-sql %) ")")) 175 | (interpose " AND ") 176 | string/join))) 177 | 178 | (defn construct-sql-query [select from where] 179 | (str "select " (string/join (interpose ", " (map qualified-name select))) 180 | " from " (cond (string? from) from 181 | 182 | (sequential? from) 183 | (string/join (interpose ", " (map name from))) 184 | 185 | :else (name from)) 186 | (let [wc (where-clause where)] 187 | (if-not (empty? wc) 188 | (str " where " wc) "")))) 189 | 190 | (defn query 191 | "Executes SELECT statement against the database and returns a TaggedCursor 192 | object with the results. `where` argument should be a map of column keywords 193 | to values." 194 | ([^TaggedDatabase tagged-db, from where] 195 | {:pre [(keyword? from)]} 196 | (let [columns (->> (get-in (.schema tagged-db) [:tables from :columns]) 197 | keys)] 198 | (query tagged-db columns from where))) 199 | ([^TaggedDatabase tagged-db, column-names from where] 200 | (let [tables (:tables (.schema tagged-db)) 201 | columns (if (keyword? from) 202 | (let [table-cls (:columns (get tables from))] 203 | (mapv (fn [cl-name] 204 | [cl-name (:type (get table-cls cl-name))]) 205 | column-names)) 206 | (mapv (fn [^Keyword kw] 207 | (let [cl-name (keyword (.getName kw)) 208 | cl (-> (get tables (keyword (.getNamespace kw))) 209 | :columns 210 | (get cl-name))] 211 | [kw (:type cl)])) 212 | column-names))] 213 | (TaggedCursor. (.rawQuery ^SQLiteDatabase (.db tagged-db) 214 | (construct-sql-query column-names from where) nil) 215 | columns)))) 216 | (def db-query query) 217 | 218 | (defn entity-from-cursor 219 | "Reads a single (current) row from TaggedCursor object." 220 | [^TaggedCursor cursor] 221 | (reduce-kv 222 | (fn [data i [column-name type]] 223 | (assoc data column-name 224 | (get-value-from-cursor cursor i type))) 225 | {} (vec (.columns cursor)))) 226 | 227 | (defn seq-cursor 228 | "Turns data from TaggedCursor object into a lazy sequence." 229 | [^TaggedCursor cursor] 230 | (.moveToFirst cursor) 231 | (let [seq-fn (fn seq-fn [] 232 | (lazy-seq 233 | (if (.isAfterLast cursor) 234 | (.close cursor) 235 | (let [v (entity-from-cursor cursor)] 236 | (.moveToNext cursor) 237 | (cons v (seq-fn))))))] 238 | (seq-fn))) 239 | 240 | (defn query-seq 241 | "Executes a SELECT statement against the database and returns the 242 | result in a sequence. Same as calling `seq-cursor` on `query` output." 243 | {:forms '([tagged-db table-name where] [tagged-db columns from where])} 244 | [& args] 245 | (seq-cursor (apply query args))) 246 | (def db-query-seq query-seq) 247 | 248 | (defn query-scalar 249 | "Executes a SELECT statement against the database on a column and returns a 250 | scalar value. `column` can be either a keyword or string-keyword pair where 251 | string denotes the aggregation function." 252 | [^TaggedDatabase tagged-db column table-name where] 253 | (let [[aggregator column] (if (vector? column) 254 | column [nil column]) 255 | type (get-in (.schema tagged-db) 256 | [:tables table-name :columns column :type]) 257 | where-cl (where-clause where) 258 | query (format "select %s from %s %s" 259 | (if aggregator 260 | (str aggregator "(" (name column) ")") 261 | (name column)) 262 | (name table-name) 263 | (if (seq where-cl) 264 | (str "where " where-cl) ""))] 265 | (with-open [cursor (.rawQuery ^SQLiteDatabase (.db tagged-db) query nil)] 266 | (try (.moveToFirst ^Cursor cursor) 267 | (get-value-from-cursor cursor 0 type) 268 | (catch CursorIndexOutOfBoundsException e nil))))) 269 | 270 | (defn update 271 | "Executes UPDATE query against the database generated from set and 272 | where clauses given as maps where keys are column keywords." 273 | [^TaggedDatabase tagged-db table-name set where] 274 | (.update ^SQLiteDatabase (.db tagged-db) (name table-name) 275 | (map-to-content tagged-db table-name set) 276 | (where-clause where) nil)) 277 | (def db-update update) 278 | 279 | (defn insert 280 | "Executes INSERT query against the database generated from data-map 281 | where keys are column keywords." 282 | [^TaggedDatabase tagged-db table-name data-map] 283 | (.insert ^SQLiteDatabase (.db tagged-db) (name table-name) nil 284 | (map-to-content tagged-db table-name data-map))) 285 | (def db-insert insert) 286 | 287 | (defn delete 288 | "Executes DELETE query against the database on the given table with the given 289 | where clauses." 290 | [^TaggedDatabase tagged-db table-name where] 291 | (.delete ^SQLiteDatabase (.db tagged-db) (name table-name) 292 | (where-clause where) nil)) 293 | 294 | 295 | (defn transact* 296 | "Executed nullary transact-fn in a transaction for batch query execution." 297 | [^TaggedDatabase tagged-db, transact-fn] 298 | (let [^SQLiteDatabase db (.db tagged-db)] 299 | (try (.beginTransaction db) 300 | (transact-fn) 301 | (.setTransactionSuccessful db) 302 | (finally (.endTransaction db))))) 303 | 304 | (defmacro transact 305 | "Wraps the code in beginTransaction-endTransaction calls for batch query 306 | execution." 307 | [tagged-db & body] 308 | `(transact* ~tagged-db (fn [] ~@body))) 309 | -------------------------------------------------------------------------------- /src/clojure/neko/debug.clj: -------------------------------------------------------------------------------- 1 | (ns neko.debug 2 | "Contains useful tools to be used while developing the application." 3 | (:require [neko log notify]) 4 | (:import android.app.Activity 5 | android.view.WindowManager$LayoutParams 6 | java.util.WeakHashMap)) 7 | 8 | ;;; Simplify REPL access to Activity objects. 9 | 10 | (def ^WeakHashMap all-activities 11 | "Weak hashmap that contains mapping of namespaces or 12 | keywords to Activity objects." 13 | (WeakHashMap.)) 14 | 15 | (defmacro ^Activity *a 16 | "If called without arguments, returns the activity for the current 17 | namespace. A version with one argument will return the activity for 18 | the given object (be it a namespace or any other object)." 19 | ([] 20 | `(get all-activities '~(.name *ns*))) 21 | ([key] 22 | `(get all-activities ~key))) 23 | 24 | ;;; Exception handling 25 | 26 | ;; This atom stores the last exception happened on the UI thread. 27 | ;; 28 | (def ^:private ui-exception (atom nil)) 29 | 30 | (defn handle-exception-from-ui-thread 31 | "Displays an exception message using a Toast and stores the 32 | exception for the future reference." 33 | [e] 34 | (reset! ui-exception e) 35 | (neko.log/e "Exception raised on UI thread." :exception e) 36 | (neko.notify/toast (str e) :long)) 37 | 38 | (defn ui-e 39 | "Returns an uncaught exception happened on UI thread." 40 | [] @ui-exception) 41 | 42 | (defmacro safe-for-ui 43 | "A conditional macro that will protect the application from crashing 44 | if the code provided in `body` crashes on UI thread in the debug 45 | build. If the build is a release one returns `body` in a `do`-form." 46 | [& body] 47 | (if (:neko.init/release-build *compiler-options*) 48 | `(do ~@body) 49 | `(try ~@body 50 | (catch Throwable e# (handle-exception-from-ui-thread e#) 51 | false)))) 52 | 53 | (defn safe-for-ui* 54 | "Wraps the given zero-argument function in `safe-for-ui` call and returns it, 55 | without executing." 56 | [f] 57 | (fn [] (safe-for-ui (f)))) 58 | 59 | (defmacro keep-screen-on 60 | "A conditional macro that will enforce the screen to stay on while the 61 | application is run in the debug mode." 62 | [^Activity activity] 63 | (if (:neko.init/release-build *compiler-options*) 64 | nil 65 | `(.addFlags (.getWindow ~activity) 66 | WindowManager$LayoutParams/FLAG_KEEP_SCREEN_ON))) 67 | -------------------------------------------------------------------------------- /src/clojure/neko/dialog/alert.clj: -------------------------------------------------------------------------------- 1 | (ns neko.dialog.alert 2 | "Helps build and manage alert dialogs. The core functionality of this 3 | namespace is built around the AlertDialogBuilder protocol. This allows using 4 | the protocol with the FunctionalAlertDialogBuilder generated by new-builder 5 | as well as the AlertDialog.Builder class provided by the Android platform. 6 | 7 | In general, it is preferable to use the functional version of the builder as 8 | it is immutable. Using the protocol with an AlertDialog.Builder object works 9 | by mutating the object." 10 | (:require [neko.listeners.dialog :as listeners] 11 | [neko.resource :as res] 12 | neko.ui 13 | [neko.ui.mapping :refer [defelement]] 14 | [neko.ui.traits :refer [deftrait]]) 15 | (:import android.app.AlertDialog$Builder)) 16 | 17 | (deftrait :positive-button 18 | "Takes :positive-text (either string or resource ID) 19 | and :positive-callback (function of 2 args: dialog and result), and sets it as 20 | the positive button for the dialog." 21 | {:attributes [:positive-text :positive-callback]} 22 | [^AlertDialog$Builder builder, 23 | {:keys [positive-text positive-callback]} _] 24 | (.setPositiveButton builder (res/get-string (.getContext builder) positive-text) 25 | (listeners/on-click-call positive-callback))) 26 | 27 | (deftrait :negative-button 28 | "Takes :negative-text (either string or resource ID) 29 | and :negative-callback (function of 2 args: dialog and result), and sets it as 30 | the negative button for the dialog." 31 | {:attributes [:negative-text :negative-callback]} 32 | [^AlertDialog$Builder builder, 33 | {:keys [negative-text negative-callback]} _] 34 | (.setNegativeButton builder (res/get-string (.getContext builder) negative-text) 35 | (listeners/on-click-call negative-callback))) 36 | 37 | (deftrait :neutral-button 38 | "Takes :neutral-text (either string or resource ID) 39 | and :neutral-callback (function of 2 args: dialog and result), and sets it as 40 | the neutral button for the dialog." 41 | {:attributes [:neutral-text :neutral-callback]} 42 | [^AlertDialog$Builder builder, 43 | {:keys [neutral-text neutral-callback]} _] 44 | (.setNeutralButton builder (res/get-string (.getContext builder) neutral-text) 45 | (listeners/on-click-call neutral-callback))) 46 | 47 | (defelement :alert-dialog-builder 48 | :classname AlertDialog$Builder 49 | :inherits nil 50 | :traits [:positive-button :negative-button :neutral-button]) 51 | 52 | (defn ^AlertDialog$Builder alert-dialog-builder 53 | "Creates a AlertDialog$Builder options with the given parameters." 54 | [context options-map] 55 | (neko.ui/make-ui context [:alert-dialog-builder options-map])) 56 | -------------------------------------------------------------------------------- /src/clojure/neko/doc.clj: -------------------------------------------------------------------------------- 1 | (ns neko.doc 2 | "This namespace contains functions that help the developer with 3 | documentation for different parts of neko." 4 | (:require [neko.ui.traits :as traits] 5 | [neko.ui.mapping :as mapping]) 6 | (:use [clojure.string :only [join]])) 7 | 8 | (defn get-trait-doc 9 | "Returns a docstring for the given trait keyword." 10 | [trait] 11 | (when-let [doc (get-in (meta #'traits/apply-trait) 12 | [:trait-doc trait])] 13 | (str trait " - " doc))) 14 | 15 | (defn get-element-doc 16 | "Returns a docsting generated from the element mapping. Verbose flag 17 | switches the detailed description of element's traits." 18 | [el-type el-mapping verbose?] 19 | (let [{:keys [classname attributes values]} el-mapping 20 | traits (mapping/all-traits el-type)] 21 | (format "%s - %s\n%s%s%s\n" 22 | el-type 23 | (or (and classname (.getName ^Class classname)) 24 | "no matching class") 25 | (if (empty? traits) "" 26 | (format "Implements traits: %s\n" 27 | (if verbose? 28 | (join (for [t traits] 29 | (str "\n" (get-trait-doc t)))) 30 | (join (map (partial str "\n ") traits))))) 31 | (if (empty? attributes) "" 32 | (format "Default attributes: %s\n" 33 | (pr-str attributes))) 34 | (if (empty? values) "" 35 | (format "Special values: %s\n" 36 | (pr-str values)))))) 37 | 38 | (defn describe 39 | "Describes the given keyword. If it reprenents UI element's name 40 | then describe the element. If optional second argument equals 41 | `:verbose`, describe all its traits as well. If a trait keyword is 42 | given, describes the trait. No-arguments version briefly describes 43 | all available UI elements." 44 | ([] 45 | (let [all-elements (mapping/get-keyword-mapping)] 46 | (doseq [[el-type parameters] all-elements] 47 | (print (get-element-doc el-type parameters false))))) 48 | ([kw] 49 | (describe kw nil)) 50 | ([kw modifier] 51 | (let [parameters ((mapping/get-keyword-mapping) kw) 52 | trait-doc (get-trait-doc kw)] 53 | (cond 54 | parameters (print "Elements found:\n" 55 | (get-element-doc kw parameters (= modifier :verbose))) 56 | trait-doc (print "\nTraits found:\n" trait-doc) 57 | :else (print (str "No elements or traits were found for " kw)))))) 58 | -------------------------------------------------------------------------------- /src/clojure/neko/find_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.find-view 2 | "Home of the ViewFinder protocol, which should simplify and unify 3 | obtaining UI elements by their :id trait. To use the protocol an 4 | object has to keep a mapping of IDs to UI widgets. Currently the 5 | protocol is supported by Activity and View objects." 6 | (:require [neko.activity :refer [get-decor-view]]) 7 | (:import android.view.View 8 | android.app.Activity)) 9 | 10 | (defprotocol ViewFinder 11 | "Protocol for finding child views by their `:id` trait." 12 | (find-view [container id])) 13 | 14 | (extend-protocol ViewFinder 15 | View 16 | (find-view [^View view, id] 17 | (get (.getTag view) id)) 18 | 19 | Activity 20 | (find-view [^Activity activity, id] 21 | (find-view (get-decor-view activity) id))) 22 | 23 | (defn find-views 24 | "Same as `find-view`, but takes a variable number of IDs and returns 25 | a vector of found views." 26 | [container & ids] 27 | (map (partial find-view container) ids)) 28 | -------------------------------------------------------------------------------- /src/clojure/neko/intent.clj: -------------------------------------------------------------------------------- 1 | (ns neko.intent 2 | "Utilities to create Intent objects." 3 | (:require [neko.-utils :refer [app-package-name]]) 4 | (:import [android.content Context Intent] 5 | android.os.Bundle)) 6 | 7 | (defn put-extras 8 | "Puts all values from extras-map into the intent's extras. Returns the Intent 9 | object." 10 | [^Intent intent, extras-map] 11 | (doseq [[key value] extras-map 12 | :let [key (name key)]] 13 | (condp #(= % (type %2)) value 14 | ;; Non-reflection calls for the most frequent cases. 15 | Long (.putExtra intent key ^long value) 16 | Double (.putExtra intent key ^double value) 17 | String (.putExtra intent key ^String value) 18 | Boolean (.putExtra intent key ^boolean value) 19 | Bundle (.putExtra intent key ^Bundle value) 20 | ;; Else fall back to reflection 21 | (.putExtra intent key value))) 22 | intent) 23 | 24 | (defn intent 25 | "Creates a new Intent object with the supplied extras. In three-arg version 26 | `classname` can be either a Class or a symbol that will be resolved to a 27 | class. If symbol starts with dot (like '.MainActivity), application's package 28 | name will be prenended." 29 | ([^String action, extras] 30 | (put-extras (doto (Intent. action)) extras)) 31 | ([^Context context, classname extras] 32 | (let [^Class class (if (symbol? classname) 33 | (resolve 34 | (if (.startsWith ^String (str classname) ".") 35 | (symbol (str (app-package-name) classname)) 36 | classname)) 37 | classname)] 38 | (put-extras (doto (Intent. context class)) extras)))) 39 | 40 | -------------------------------------------------------------------------------- /src/clojure/neko/listeners/adapter_view.clj: -------------------------------------------------------------------------------- 1 | ; Copyright © 2011 Sattvik Software & Technology Resources, Ltd. Co. 2 | ; All rights reserved. 3 | ; 4 | ; This program and the accompanying materials are made available under the 5 | ; terms of the Eclipse Public License v1.0 which accompanies this distribution, 6 | ; and is available at . 7 | ; 8 | ; By using this software in any fashion, you are agreeing to be bound by the 9 | ; terms of this license. You must not remove this notice, or any other, from 10 | ; this software. 11 | 12 | (ns neko.listeners.adapter-view 13 | "Utility functions and macros for creating listeners corresponding to the 14 | android.widget.AdapterView class." 15 | {:author "Daniel Solano Gómez"} 16 | (:require [neko.debug :refer [safe-for-ui]])) 17 | 18 | (defn on-item-click-call 19 | "Takes a function and yields an AdapterView.OnItemClickListener object that 20 | will invoke the function. This function must take the following four 21 | arguments: 22 | 23 | parent the AdapterView where the click happened 24 | view the view within the AdapterView that was clicked 25 | position the position of the view in the adapter 26 | id the row id of the item that was clicked" 27 | ^android.widget.AdapterView$OnItemClickListener 28 | [handler-fn] 29 | {:pre [(fn? handler-fn)] 30 | :post [(instance? android.widget.AdapterView$OnItemClickListener %)]} 31 | (reify android.widget.AdapterView$OnItemClickListener 32 | (onItemClick [this parent view position id] 33 | (safe-for-ui (handler-fn parent view position id))))) 34 | 35 | (defmacro on-item-click 36 | "Takes a body of expressions and yields an AdapterView.OnItemClickListener 37 | object that will invoke the body. The body takes the following implicit 38 | arguments: 39 | 40 | parent the AdapterView where the click happened 41 | view the view within the AdapterView that was clicked 42 | position the position of the view in the adapter 43 | id the row id of the item that was clicked" 44 | [& body] 45 | `(on-item-click-call (fn [~'parent ~'view ~'position ~'id] ~@body))) 46 | 47 | (defn on-item-long-click-call 48 | "Takes a function and yields an AdapterView.OnItemLongClickListener object 49 | that will invoke the function. This function must take the following four 50 | arguments: 51 | 52 | parent the AdapterView where the click happened 53 | view the view within the AdapterView that was clicked 54 | position the position of the view in the adapter 55 | id the row id of the item that was clicked 56 | 57 | The function should evaluate to a logical true value if it has consumed the 58 | long click; otherwise logical false." 59 | ^android.widget.AdapterView$OnItemLongClickListener 60 | [handler-fn] 61 | {:pre [(fn? handler-fn)] 62 | :post [(instance? android.widget.AdapterView$OnItemLongClickListener %)]} 63 | (reify android.widget.AdapterView$OnItemLongClickListener 64 | (onItemLongClick [this parent view position id] 65 | (safe-for-ui (boolean (handler-fn parent view position id)))))) 66 | 67 | (defmacro on-item-long-click 68 | "Takes a body of expressions and yields an 69 | AdapterView.OnItemLongClickListener object that will invoke the body. The 70 | body takes the following implicit arguments: 71 | 72 | parent the AdapterView where the click happened 73 | view the view within the AdapterView that was clicked 74 | position the position of the view in the adapter 75 | id the row id of the item that was clicked 76 | 77 | The body should evaluate to a logical true value if it has consumed the long 78 | click; otherwise logical false." 79 | [& body] 80 | `(on-item-long-click-call (fn [~'parent ~'view ~'position ~'id] ~@body))) 81 | 82 | (defn on-item-selected-call 83 | "Takes one or two functions and yields an AdapterView.OnItemSelectedListener object 84 | that will invoke the functions. The first function will be called to handle the 85 | onItemSelected(…) method and must take the following four arguments: 86 | 87 | parent the AdapterView where the selection happened 88 | view the view within the AdapterView that was clicked 89 | position the position of the view in the adapter 90 | id the row id of the item that was selected 91 | 92 | If a second function is provided, it will be called when the selection 93 | disappears from the view. It takes a single argument, the AdapterView that 94 | now contains no selected item." 95 | ([item-fn] 96 | {:pre [(fn? item-fn)] 97 | :post [(instance? android.widget.AdapterView$OnItemSelectedListener %)]} 98 | (on-item-selected-call item-fn nil)) 99 | (^android.widget.AdapterView$OnItemSelectedListener 100 | [item-fn nothing-fn] 101 | {:pre [(fn? item-fn) 102 | (or (nil? nothing-fn) 103 | (fn? nothing-fn))] 104 | :post [(instance? android.widget.AdapterView$OnItemSelectedListener %)]} 105 | (reify android.widget.AdapterView$OnItemSelectedListener 106 | (onItemSelected [this parent view position id] 107 | (safe-for-ui (item-fn parent view position id))) 108 | (onNothingSelected [this parent] 109 | (safe-for-ui (when nothing-fn 110 | (nothing-fn parent))))))) 111 | 112 | (defmacro on-item-selected 113 | "Takes a body of expressions and yields an AdapterView.OnItemSelectedListener 114 | object that will invoke the body The body takes the following implicit 115 | arguments: 116 | 117 | type either :item corresponding an onItemSelected(…) call or :nothing 118 | corresponding to an onNothingSelected(…) call 119 | parent the AdapterView where the selection happened or now contains no 120 | selected item 121 | view the view within the AdapterView that was clicked. If type is 122 | :nothing, this will be nil 123 | position the position of the view in the adapter. If type is :nothing, this 124 | will be nil. 125 | id the row id of the item that was selected. If type is :nothing, 126 | this will be nil." 127 | [& body] 128 | `(let [handler-fn# (fn [~'type ~'parent ~'view ~'position ~'id] 129 | ~@body)] 130 | (on-item-selected-call 131 | (fn ~'item-handler [parent# view# position# id#] 132 | (handler-fn# :item parent# view# position# id#)) 133 | (fn ~'nothing-handler [parent#] 134 | (handler-fn# :nothing parent# nil nil nil))))) 135 | -------------------------------------------------------------------------------- /src/clojure/neko/listeners/dialog.clj: -------------------------------------------------------------------------------- 1 | ; Copyright © 2011 Sattvik Software & Technology Resources, Ltd. Co. 2 | ; All rights reserved. 3 | ; 4 | ; This program and the accompanying materials are made available under the 5 | ; terms of the Eclipse Public License v1.0 which accompanies this distribution, 6 | ; and is available at . 7 | ; 8 | ; By using this software in any fashion, you are agreeing to be bound by the 9 | ; terms of this license. You must not remove this notice, or any other, from 10 | ; this software. 11 | 12 | (ns neko.listeners.dialog 13 | "Utility functions and macros for setting listeners corresponding to the 14 | android.content DialogInterface interface." 15 | {:author "Daniel Solano Gómez"} 16 | (:require [neko.debug :refer [safe-for-ui]]) 17 | (:import android.content.DialogInterface)) 18 | 19 | (defn on-cancel-call 20 | "Takes a function and yields a DialogInterface.OnCancelListener object that 21 | will invoke the function. This function must take one argument, the dialog 22 | that was canceled." 23 | ^android.content.DialogInterface$OnCancelListener 24 | [handler-fn] 25 | (reify android.content.DialogInterface$OnCancelListener 26 | (onCancel [this dialog] 27 | (safe-for-ui (handler-fn dialog))))) 28 | 29 | (defmacro on-cancel 30 | "Takes a body of expressions and yields a DialogInterface.OnCancelListener object that 31 | will invoke the body. The body takes an implicit argument 'dialog' that is the 32 | dialog that was canceled." 33 | [& body] 34 | `(on-cancel-call (fn [~'dialog] ~@body))) 35 | 36 | (defn on-click-call 37 | "Takes a function and yields a DialogInterface.OnCancelListener object that 38 | will invoke the function. This function must take two arguments: 39 | 40 | dialog: the dialog that received the click 41 | which: the button that was clicked (one of :negative, :neutral, or 42 | :positive) or the position of the item that was clicked" 43 | ^android.content.DialogInterface$OnClickListener 44 | [handler-fn] 45 | (reify android.content.DialogInterface$OnClickListener 46 | (onClick [this dialog which] 47 | (let [which (condp = which 48 | DialogInterface/BUTTON_NEGATIVE :negative 49 | DialogInterface/BUTTON_NEUTRAL :neutral 50 | DialogInterface/BUTTON_POSITIVE :positive 51 | which)] 52 | (safe-for-ui (handler-fn dialog which)))))) 53 | 54 | (defmacro on-click 55 | "Takes a body of expressions and yields a DialogInterface.OnCancelListener 56 | object that will invoke the function. The body will take the following two 57 | implicit arguments: 58 | 59 | dialog: the dialog that received the click 60 | which: the button that was clicked (one of :negative, :neutral, or 61 | :positive) or the position of the item that was clicked" 62 | [& body] 63 | `(on-click-call (fn [~'dialog ~'which] ~@body))) 64 | 65 | (defn on-dismiss-call 66 | "Takes a function and yields a DialogInterface.OnDismissListener object that 67 | will invoke the function. This function must take one argument, the dialog 68 | that was dismissed." 69 | ^android.content.DialogInterface$OnDismissListener 70 | [handler-fn] 71 | (reify android.content.DialogInterface$OnDismissListener 72 | (onDismiss [this dialog] 73 | (safe-for-ui (handler-fn dialog))))) 74 | 75 | (defmacro on-dismiss 76 | "Takes a body of expressions and yields a DialogInterface.OnDismissListener 77 | object that will invoke the body. The body takes an implicit argument 78 | 'dialog' that is the dialog that was dismissed." 79 | [& body] 80 | `(on-dismiss-call (fn [~'dialog] ~@body))) 81 | 82 | (defn on-key-call 83 | "Takes a function and yields a DialogInterface.OnKeyListener object that will 84 | invoke the function. This function must take the following three arguments: 85 | 86 | dialog: the dialog the key has been dispatched to 87 | key-code: the code for the physical key that was pressed 88 | event: the KeyEvent object containing full information about the event 89 | 90 | The function should evaluate to a logical true value if it has consumed the 91 | event, otherwise logical false." 92 | ^android.content.DialogInterface$OnKeyListener 93 | [handler-fn] 94 | (reify android.content.DialogInterface$OnKeyListener 95 | (onKey [this dialog key-code event] 96 | (safe-for-ui (boolean (handler-fn dialog key-code event)))))) 97 | 98 | (defmacro on-key 99 | "Takes a body of expressions and yields a DialogInterface.OnKeyListener 100 | object that will invoke the body. The body takes the following three 101 | implicit arguments: 102 | 103 | dialog: the dialog the key has been dispatched to 104 | key-code: the code for the physical key that was pressed 105 | event: the KeyEvent object containing full information about the event 106 | 107 | The body should evaluate to a logical true value if it has consumed the 108 | event, otherwise logical false." 109 | [& body] 110 | `(on-key-call (fn [~'dialog ~'key-code ~'event] ~@body))) 111 | 112 | (defn on-multi-choice-click-call 113 | "Takes a function and yields a DialogInterface.OnMultiChoiceClickListener 114 | object that will invoke the function. This function must take the following 115 | three arguments: 116 | 117 | dialog: the dialog where the selection was made 118 | which: the position of the item in the list that was clicked 119 | checked?: true if the click checked the item, else false" 120 | ^android.content.DialogInterface$OnMultiChoiceClickListener 121 | [handler-fn] 122 | (reify android.content.DialogInterface$OnMultiChoiceClickListener 123 | (onClick [this dialog which checked?] 124 | (safe-for-ui (handler-fn dialog which checked?))))) 125 | 126 | (defmacro on-multi-choice-click 127 | "Takes a body of expressions and yields a 128 | DialogInterface.OnMultiChoiceClickListener object that will invoke the body. 129 | The body takes the following three implicit arguments: 130 | 131 | dialog: the dialog where the selection was made 132 | which: the position of the item in the list that was clicked 133 | checked?: true if the click checked the item, else false" 134 | [& body] 135 | `(on-multi-choice-click-call (fn [~'dialog ~'which ~'checked?] ~@body))) 136 | -------------------------------------------------------------------------------- /src/clojure/neko/listeners/search_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.listeners.search-view 2 | (:require [neko.debug :refer [safe-for-ui]]) 3 | (:use [neko.-utils :only [call-if-nnil]]) 4 | (:import android.widget.SearchView)) 5 | 6 | (defn on-query-text-call 7 | "Takes onQueryTextChange and onQueryTextSubmit functions and yields 8 | a SearchView.OnQueryTextListener object that will invoke the 9 | functions. Both functions take string argument, a query that was 10 | entered." 11 | ^android.widget.SearchView$OnQueryTextListener 12 | [change-fn submit-fn] 13 | (reify android.widget.SearchView$OnQueryTextListener 14 | (onQueryTextChange [this query] 15 | (safe-for-ui (call-if-nnil change-fn query))) 16 | (onQueryTextSubmit [this query] 17 | (safe-for-ui (call-if-nnil submit-fn query))))) 18 | -------------------------------------------------------------------------------- /src/clojure/neko/listeners/text_view.clj: -------------------------------------------------------------------------------- 1 | ; Copyright © 2011 Sattvik Software & Technology Resources, Ltd. Co. 2 | ; All rights reserved. 3 | ; 4 | ; This program and the accompanying materials are made available under the 5 | ; terms of the Eclipse Public License v1.0 which accompanies this distribution, 6 | ; and is available at . 7 | ; 8 | ; By using this software in any fashion, you are agreeing to be bound by the 9 | ; terms of this license. You must not remove this notice, or any other, from 10 | ; this software. 11 | 12 | (ns neko.listeners.text-view 13 | "Uility functions and macros for creating listeners corresponding to the 14 | android.widget.TextView class." 15 | {:author "Daniel Solano Gómez"} 16 | (:require [neko.debug :refer [safe-for-ui]])) 17 | 18 | (defn on-editor-action-call 19 | "Takes a function and yields a TextView.OnEditorActionListener object that 20 | will invoke the function. This function must take the following three 21 | arguments: 22 | 23 | view the view that was clicked 24 | action-id identifier of the action, this will be either the identifier you 25 | supplied or EditorInfo/IME_NULL if being called to the enter key 26 | being pressed 27 | key-event if triggered by an enter key, this is the event; otherwise, this 28 | is nil 29 | 30 | The function should evaluate to a logical true value if it has consumed the 31 | action, otherwise logical false." 32 | [handler-fn] 33 | ^android.widget.TextView$OnEditorActionListener 34 | {:pre [(fn? handler-fn)] 35 | :post [(instance? android.widget.TextView$OnEditorActionListener %)]} 36 | (reify android.widget.TextView$OnEditorActionListener 37 | (onEditorAction [this view action-id key-event] 38 | (safe-for-ui (boolean (handler-fn view action-id key-event)))))) 39 | 40 | (defmacro on-editor-action 41 | "Takes a body of expressions and yields a TextView.OnEditorActionListener 42 | object that will invoke the body. The body takes the following implicit 43 | arguments: 44 | 45 | view the view that was clicked 46 | action-id identifier of the action, this will be either the identifier you 47 | supplied or EditorInfo/IME_NULL if being called to the enter key 48 | being pressed 49 | key-event if triggered by an enter key, this is the event; otherwise, this 50 | is nil 51 | 52 | The body should evaluate to a logical true value if it has consumed the 53 | action, otherwise logical false." 54 | [& body] 55 | `(on-editor-action-call (fn [~'view ~'action-id ~'key-event] ~@body))) 56 | -------------------------------------------------------------------------------- /src/clojure/neko/listeners/view.clj: -------------------------------------------------------------------------------- 1 | ; Copyright © 2011 Sattvik Software & Technology Resources, Ltd. Co. 2 | ; All rights reserved. 3 | ; 4 | ; This program and the accompanying materials are made available under the 5 | ; terms of the Eclipse Public License v1.0 which accompanies this distribution, 6 | ; and is available at . 7 | ; 8 | ; By using this software in any fashion, you are agreeing to be bound by the 9 | ; terms of this license. You must not remove this notice, or any other, from 10 | ; this software. 11 | 12 | (ns neko.listeners.view 13 | "Utility functions and macros for setting listeners corresponding to the 14 | android.view.View class." 15 | {:author "Daniel Solano Gómez"} 16 | (:require [neko.debug :refer [safe-for-ui]])) 17 | 18 | (defn on-click-call 19 | "Takes a function and yields a View.OnClickListener object that will invoke 20 | the function. This function must take one argument, the view that was 21 | clicked." 22 | ^android.view.View$OnClickListener 23 | [handler-fn] 24 | (reify android.view.View$OnClickListener 25 | (onClick [this view] 26 | (safe-for-ui (handler-fn view))))) 27 | 28 | (defmacro on-click 29 | "Takes a body of expressions and yields a View.OnClickListener object that 30 | will invoke the body. The body takes an implicit argument 'view' that is the 31 | view that was clicked." 32 | [& body] 33 | `(on-click-call (fn [~'view] ~@body))) 34 | 35 | (defn on-create-context-menu-call 36 | "Takes a function and yields a View.OnCreateContextMenuListener object that 37 | will invoke the function. This function must take the following three 38 | arguments: 39 | 40 | menu: the context menu that is being built 41 | view: the view for which the context menu is being built 42 | info: extra information about the item for which the context menu should be 43 | shown. This information will vary depending on the class of view." 44 | ^android.view.View$OnCreateContextMenuListener 45 | [handler-fn] 46 | (reify android.view.View$OnCreateContextMenuListener 47 | (onCreateContextMenu [this menu view info] 48 | (safe-for-ui (handler-fn menu view info))))) 49 | 50 | (defmacro on-create-context-menu 51 | "Takes a body of expressions and yields a View.OnCreateContextMenuListener 52 | object that will invoke the body. The body takes the following three 53 | implicit arguments: 54 | 55 | menu: the context menu that is being built 56 | view: the view for which the context menu is being built 57 | info: extra information about the item for which the context menu should be 58 | shown. This information will vary depending on the class of view." 59 | [& body] 60 | `(on-create-context-menu-call (fn [~'menu ~'view ~'info] ~@body))) 61 | 62 | (defn on-drag-call 63 | "Takes a function and yields a View.OnDragListener object that will invoke 64 | the function. This function must take the two arguments described in 65 | on-drag and should return a boolean." 66 | ^android.view.View$OnDragListener 67 | [handler-fn] 68 | (reify android.view.View$OnDragListener 69 | (onDrag [this view event] 70 | (safe-for-ui (handler-fn view event))))) 71 | 72 | (defmacro on-drag 73 | "Takes a body of expressions and yields a View.OnDragListener object that 74 | will invoke the body. The body takes the following two implicit arguments: 75 | 76 | view: the view that received the drag event 77 | event: the DragEvent object for the drag event" 78 | [& body] 79 | `(on-drag-call (fn [~'view ~'event] ~@body))) 80 | 81 | (defn on-focus-change-call 82 | "Takes a function and yields a View.OnFocusChangeListener object that will 83 | invoke the function. This function must take the following two arguments: 84 | 85 | view: the view whose state has changed 86 | focused?: the new focused state for view" 87 | ^android.view.View$OnFocusChangeListener 88 | [handler-fn] 89 | (reify android.view.View$OnFocusChangeListener 90 | (onFocusChange [this view focused?] 91 | (safe-for-ui (handler-fn view focused?))))) 92 | 93 | (defmacro on-focus-change 94 | "Takes a body of expressions and yields a View.OnFocusChangeListener object 95 | that will invoke the body. The body takes the following two implicit 96 | arguments: 97 | 98 | view: the view whose state has changed 99 | focused?: the new focused state for view" 100 | [& body] 101 | `(on-focus-change-call (fn [~'view ~'focused?] ~@body))) 102 | 103 | (defn on-key-call 104 | "Takes a function and yields a View.OnKeyListener object that will invoke the 105 | function. This function must take the following three arguments: 106 | 107 | view: the view the key has been dispatched to 108 | key-code: the code for the physical key that was pressed 109 | event: the KeyEvent object containing full information about the event 110 | 111 | The function should evaluate to a logical true value if it has consumed the 112 | event, otherwise logical false." 113 | ^android.view.View$OnKeyListener 114 | [handler-fn] 115 | (reify android.view.View$OnKeyListener 116 | (onKey [this view key-code event] 117 | (safe-for-ui (boolean (handler-fn view key-code event)))))) 118 | 119 | (defmacro on-key 120 | "Takes a body of expressions and yields a View.OnKeyListener object that will 121 | invoke the body. The body takes the following three implicit arguments: 122 | 123 | view: the view the key has been dispatched to 124 | key-code: the code for the physical key that was pressed 125 | event: the KeyEvent object containing full information about the event 126 | 127 | The body should evaluate to a logical true value if it has consumed the 128 | event, otherwise logical false." 129 | [& body] 130 | `(on-key-call (fn [~'view ~'key-code ~'event] ~@body))) 131 | 132 | (comment -- as of API level 11 133 | (defn on-layout-change-call 134 | "Takes a function and yields a View.OnLayoutChangeListener object that 135 | will invoke the function. This function must take the arguments described 136 | in on-layout-change." 137 | ^android.view.View$OnLayoutChangeListener 138 | [handler-fn] 139 | (reify android.view.View$OnLayoutChangeListener 140 | (onLayoutChange [this view left top right bottom 141 | old-left old-top old-right old-bottom] 142 | (safe-for-ui (handler-fn view left top right bottom 143 | old-left olt-top old-right old-bottom))))) 144 | 145 | (defmacro on-layout-change 146 | "Takes a body of expressions and yields a View.OnLayoutChangeListener 147 | object that will invoke the body. The body takes the following implicit 148 | arguments: 149 | 150 | view: the view whose state has changed 151 | left: the new value of the view's left property 152 | top: the new value of the view's top property 153 | right: the new value of the view's right property 154 | bottom: the new value of the view's bottom property 155 | old-left: the previous value of the view's left property 156 | old-top: the previous value of the view's top property 157 | old-right: the previous value of the view's right property 158 | old-bottom: the previous value of the view's bottom property" 159 | [& body] 160 | `(on-key-call (fn [~'view ~'left ~'top ~'right ~'bottom 161 | ~'old-left ~'old-top ~'old-right ~'old-bottom] ~@body)))) 162 | 163 | (defn on-long-click-call 164 | "Takes a function and yields a View.OnLongClickListener object that will 165 | invoke the function. This function must take one argument, the view that was 166 | clicked, and must evaluate to a logical true value if it has consumed the 167 | long click, otherwise logical false." 168 | ^android.view.View$OnLongClickListener 169 | [handler-fn] 170 | (reify android.view.View$OnLongClickListener 171 | (onLongClick [this view] 172 | (safe-for-ui (boolean (handler-fn view)))))) 173 | 174 | (defmacro on-long-click 175 | "Takes a body of expressions and yields a View.OnLongClickListener object 176 | that will invoke the body. The body takes an implicit argument 'view' that 177 | is the view that was clicked and held. The body should also evaluate to a 178 | logical true value if it consumes the long click, otherwise logical false." 179 | [& body] 180 | `(on-long-click-call (fn [~'view] ~@body))) 181 | 182 | (comment -- incomplete -- also for API level 11 (Honeycomb) 183 | (defn on-system-ui-visibility-change-call 184 | "Takes a function and yields a View.OnSystemUiVisibilityChangeListener object 185 | that will invoke the function. This function must take one argument, the 186 | view that was clicked, and must evaluate to true if it has consumed the long 187 | click, false otherwise." 188 | ^android.view.View$OnSystemUiVisibilityChangeListener 189 | [handler-fn] 190 | (reify android.view.View$OnSystemUiVisibilityChangeListener 191 | (onLongClick [this view] 192 | (safe-for-ui (handler-fn view))))) 193 | 194 | (defmacro on-system-ui-visibility-change 195 | "Takes a body of expressions and yields a 196 | View.OnSystemUiVisibilityChangeListener object that will invoke the body. 197 | The body takes an implicit argument 'visibility' which will be either 198 | :status-bar-hidden or :status-bar-visible." 199 | [& body] 200 | `(on-system-ui-visibility-change-call (fn [~'visibility] ~@body)))) 201 | 202 | (defn on-touch-call 203 | "Takes a function and yields a View.OnTouchListener object that will invoke 204 | the function. This function must take the following two arguments: 205 | 206 | view: the view the touch event has been dispatched to 207 | event: the MotionEvent object containing full information about the event 208 | 209 | The function should evaluate to a logical true value if it consumes the 210 | event, otherwise logical false." 211 | ^android.view.View$OnTouchListener 212 | [handler-fn] 213 | (reify android.view.View$OnTouchListener 214 | (onTouch [this view event] 215 | (safe-for-ui (boolean (handler-fn view event)))))) 216 | 217 | (defmacro on-touch 218 | "Takes a body of expressions and yields a View.OnTouchListener object that 219 | will invoke the body. The body takes the following implicit arguments: 220 | 221 | view: the view the touch event has been dispatched to 222 | event: the MotionEvent object containing full information about the event 223 | 224 | The body should evaluate to a logical value if it consumes the event, 225 | otherwise logical false." 226 | [& body] 227 | `(on-touch-call (fn [~'view ~'event] ~@body))) 228 | -------------------------------------------------------------------------------- /src/clojure/neko/log.clj: -------------------------------------------------------------------------------- 1 | (ns neko.log 2 | "Utility for logging in Android. There are five logging macros: `i`, 3 | `d`, `e`, `v`, `w`; for different purposes. Each of them takes 4 | variable number of arguments and optional keyword arguments at the 5 | end: `:exception` and `:tag`. If `:tag` is not provided, current 6 | namespace is used instead. Examples: 7 | 8 | (require '[neko.log :as log]) 9 | 10 | (log/d \"Some log string\" {:foo 1, :bar 2}) 11 | (log/i \"Logging to custom tag\" [1 2 3] :tag \"custom\") 12 | (log/e \"Something went wrong\" [1 2 3] :exception ex)" 13 | {:author "Adam Clements"} 14 | (:import android.util.Log)) 15 | 16 | (defn- logger [logfn priority-kw args] 17 | (when-not ((set (:neko.init/ignore-log-priority *compiler-options*)) 18 | priority-kw) 19 | (let [[strings kwargs] (split-with (complement #{:exception :tag}) args) 20 | {:keys [exception tag]} (if (odd? (count kwargs)) 21 | (butlast kwargs) 22 | kwargs) 23 | tag (or tag (str *ns*)) 24 | ex-form (if exception [exception] ())] 25 | `(binding [*print-readably* nil] 26 | (. Log ~logfn ~tag (pr-str ~@strings) ~@ex-form))))) 27 | 28 | (defmacro e 29 | "Log an ERROR message, applying pr-str to all the arguments and taking 30 | an optional keyword :exception or :tag at the end which will print the 31 | exception stacktrace or override the TAG respectively" 32 | [& args] (logger 'e :error args)) 33 | 34 | (defmacro d 35 | "Log a DEBUG message, applying pr-str to all the arguments and taking 36 | an optional keyword :exception or :tag at the end which will print the 37 | exception stacktrace or override the TAG respectively" 38 | [& args] (logger 'd :debug args)) 39 | 40 | (defmacro i 41 | "Log an INFO message, applying pr-str to all the arguments and taking 42 | an optional keyword :exception or :tag at the end which will print the 43 | exception stacktrace or override the TAG respectively" 44 | [& args] (logger 'i :info args)) 45 | 46 | (defmacro v 47 | "Log a VERBOSE message, applying pr-str to all the arguments and taking 48 | an optional keyword :exception or :tag at the end which will print the 49 | exception stacktrace or override the TAG respectively" 50 | [& args] (logger 'v :verbose args)) 51 | 52 | (defmacro w 53 | "Log a WARN message, applying pr-str to all the arguments and taking 54 | an optional keyword :exception or :tag at the end which will print the 55 | exception stacktrace or override the TAG respectively" 56 | [& args] (logger 'w :warn args)) 57 | -------------------------------------------------------------------------------- /src/clojure/neko/notify.clj: -------------------------------------------------------------------------------- 1 | (ns neko.notify 2 | "Provides convenient wrappers for Toast and Notification APIs." 3 | (:require [neko.-utils :refer [int-id]]) 4 | (:import [android.app Notification NotificationManager PendingIntent] 5 | [android.content Context Intent] 6 | android.widget.Toast 7 | neko.App)) 8 | 9 | ;; ### Toasts 10 | 11 | (defn toast 12 | "Creates a Toast object using a text message and a keyword representing how 13 | long a toast should be visible (`:short` or `:long`). If length is not 14 | provided, it defaults to :long." 15 | ([message] 16 | (toast App/instance message :long)) 17 | ([message length] 18 | (toast App/instance message length)) 19 | ([^Context context, ^String message, length] 20 | {:pre [(or (= length :short) (= length :long))]} 21 | (.show 22 | ^Toast (Toast/makeText context message ^int (case length 23 | :short Toast/LENGTH_SHORT 24 | :long Toast/LENGTH_LONG))))) 25 | 26 | ;; ### Notifications 27 | 28 | (defn- ^NotificationManager notification-manager 29 | "Returns the notification manager instance." 30 | ([^Context context] 31 | (.getSystemService context Context/NOTIFICATION_SERVICE))) 32 | 33 | (defn construct-pending-intent 34 | "Creates a PendingIntent instance from a vector where the first 35 | element is a keyword representing the action type, and the second 36 | element is a action string to create an Intent from." 37 | ([context [action-type, ^String action]] 38 | (let [^Intent intent (Intent. action)] 39 | (case action-type 40 | :activity (PendingIntent/getActivity context 0 intent 0) 41 | :broadcast (PendingIntent/getBroadcast context 0 intent 0) 42 | :service (PendingIntent/getService context 0 intent 0))))) 43 | 44 | (defn notification 45 | "Creates a Notification instance. If icon is not provided uses the 46 | default notification icon." 47 | ([options] 48 | (notification App/instance options)) 49 | ([context {:keys [icon ticker-text when content-title content-text action] 50 | :or {icon android.R$drawable/ic_dialog_info 51 | when (System/currentTimeMillis)}}] 52 | (let [notification (Notification. icon ticker-text when)] 53 | (.setLatestEventInfo notification context content-title content-text 54 | (construct-pending-intent context action)) 55 | notification))) 56 | 57 | (defn fire 58 | "Sends the notification to the status bar. ID can be an integer or a keyword." 59 | ([id notification] 60 | (fire App/instance id notification)) 61 | ([context id notification] 62 | (let [id (if (keyword? id) 63 | (int-id id) 64 | id)] 65 | (.notify (notification-manager context) id notification)))) 66 | 67 | (defn cancel 68 | "Removes a notification by the given ID from the status bar." 69 | ([id] 70 | (cancel App/instance id)) 71 | ([context id] 72 | (.cancel (notification-manager context) (if (keyword? id) 73 | (int-id id) 74 | id)))) 75 | -------------------------------------------------------------------------------- /src/clojure/neko/resource.clj: -------------------------------------------------------------------------------- 1 | (ns neko.resource 2 | "Provides utilities to resolve application resources." 3 | (:require [neko.-utils :refer [app-package-name]]) 4 | (:import android.content.Context 5 | android.graphics.drawable.Drawable 6 | neko.App)) 7 | 8 | (defmacro import-all 9 | "Imports all existing application's R subclasses (R$drawable, R$string etc.) 10 | into the current namespace." 11 | [] 12 | `(do ~@(map (fn [res-type] 13 | `(try (import '~(-> (app-package-name) 14 | (str ".R$" res-type) 15 | symbol)) 16 | (catch ClassNotFoundException _# nil))) 17 | '[anim drawable color layout menu string array plurals style id 18 | dimen raw]))) 19 | 20 | (import-all) 21 | ;; ## Runtime resource resolution 22 | 23 | (defn get-string 24 | "Gets the localized string for the given resource ID. If res-name is a string, 25 | returns it unchanged. If additional arguments are supplied, the string will be 26 | interpreted as a format and the arguments will be applied to the format." 27 | {:forms '([res-id & format-args?] [context res-id & format-args?])} 28 | [& args] 29 | (let [[^Context context args] (if (instance? Context (first args)) 30 | [(first args) (rest args)] 31 | [App/instance args]) 32 | [res-id & format-args] args] 33 | (cond (not (number? res-id)) res-id 34 | format-args (.getString context res-id (to-array format-args)) 35 | :else (.getString context res-id)))) 36 | 37 | (defn get-drawable 38 | "Gets a Drawable object associated with the given resource ID. If res-id is a 39 | Drawable, returns it unchanged." 40 | ([res-id] 41 | (get-drawable App/instance res-id)) 42 | 43 | ([^Context context, res-id] 44 | (if-not (number? res-id) 45 | res-id 46 | (.getDrawable (.getResources context) res-id)))) 47 | -------------------------------------------------------------------------------- /src/clojure/neko/threading.clj: -------------------------------------------------------------------------------- 1 | (ns neko.threading 2 | "Utilities used to manage multiple threads on Android." 3 | (:use [neko.debug :only [safe-for-ui]]) 4 | (:import android.view.View 5 | android.os.Looper 6 | android.os.Handler)) 7 | 8 | ;; ### UI thread utilities 9 | 10 | (defn on-ui-thread? 11 | "Returns true if the current thread is a UI thread." 12 | [] 13 | (identical? (Thread/currentThread) 14 | (.getThread ^Looper (Looper/getMainLooper)))) 15 | 16 | (defmacro on-ui 17 | "Runs the macro body on the UI thread. If this macro is called on the UI 18 | thread, it will evaluate immediately." 19 | [& body] 20 | `(if (on-ui-thread?) 21 | (safe-for-ui ~@body) 22 | (.post (Handler. (Looper/getMainLooper)) (fn [] (safe-for-ui ~@body))))) 23 | 24 | (defn on-ui* 25 | "Functional version of `on-ui`, runs the nullary function on the UI thread." 26 | [f] 27 | (on-ui (f))) 28 | 29 | (defmacro post 30 | "Causes the macro body to be added to the message queue. It will execute on 31 | the UI thread. Returns true if successfully placed in the message queue." 32 | [view & body] 33 | `(.post ^View ~view (fn [] ~@body))) 34 | 35 | (defmacro post-delayed 36 | "Causes the macro body to be added to the message queue. It will execute on 37 | the UI thread. Returns true if successfully placed in the message queue." 38 | [view millis & body] 39 | `(.postDelayed ^View ~view (fn [] ~@body) ~millis)) 40 | -------------------------------------------------------------------------------- /src/clojure/neko/tools/repl.clj: -------------------------------------------------------------------------------- 1 | (ns neko.tools.repl 2 | (:require [neko.log :as log]) 3 | (:import android.content.Context 4 | android.util.Log 5 | java.io.FileNotFoundException 6 | java.util.concurrent.atomic.AtomicLong 7 | java.util.concurrent.ThreadFactory)) 8 | 9 | (def cider-middleware 10 | "A vector containing all CIDER middleware." 11 | '[cider.nrepl.middleware.apropos/wrap-apropos 12 | cider.nrepl.middleware.complete/wrap-complete 13 | cider.nrepl.middleware.info/wrap-info 14 | cider.nrepl.middleware.inspect/wrap-inspect 15 | cider.nrepl.middleware.macroexpand/wrap-macroexpand 16 | cider.nrepl.middleware.ns/wrap-ns 17 | cider.nrepl.middleware.resource/wrap-resource 18 | cider.nrepl.middleware.stacktrace/wrap-stacktrace 19 | cider.nrepl.middleware.test/wrap-test 20 | cider.nrepl.middleware.trace/wrap-trace 21 | cider.nrepl.middleware.undef/wrap-undef]) 22 | 23 | (defn cider-available? 24 | "Checks if cider-nrepl dependency is present on the classpath." 25 | [] 26 | (try (require 'cider.nrepl.version) 27 | true 28 | (catch FileNotFoundException e false))) 29 | 30 | (defn android-thread-factory 31 | "Returns a new ThreadFactory with increased stack size. It is used to 32 | substitute nREPL's native `configure-thread-factory` on Android platform." 33 | [] 34 | (let [counter (AtomicLong. 0)] 35 | (reify ThreadFactory 36 | (newThread [_ runnable] 37 | (doto (Thread. (.getThreadGroup (Thread/currentThread)) 38 | runnable 39 | (format "nREPL-worker-%s" (.getAndIncrement counter)) 40 | 1048576) ;; Hardcoded stack size of 1Mb 41 | (.setDaemon true)))))) 42 | 43 | (defn- patch-unsupported-dependencies 44 | "Some non-critical CIDER and nREPL dependencies cannot be used on Android 45 | as-is, so they have to be tranquilized." 46 | [] 47 | (let [curr-ns (ns-name *ns*)] 48 | (ns dynapath.util) 49 | (defn add-classpath! [& _]) 50 | (defn addable-classpath [& _]) 51 | (in-ns curr-ns))) 52 | 53 | (defn enable-compliment-sources 54 | "Initializes compliment sources if their namespaces are present." 55 | [] 56 | (try (require 'neko.compliment.ui-widgets-and-attributes) 57 | ((resolve 'neko.compliment.ui-widgets-and-attributes/init-source)) 58 | (catch Exception ex nil))) 59 | 60 | (defn start-repl 61 | "Starts a remote nREPL server. Creates a `user` namespace because nREPL 62 | expects it to be there while initializing. References nrepl's `start-server` 63 | function on demand because the project can be compiled without nrepl 64 | dependency." 65 | [middleware & repl-args] 66 | (binding [*ns* (create-ns 'user)] 67 | (refer-clojure) 68 | (patch-unsupported-dependencies) 69 | (use 'clojure.tools.nrepl.server) 70 | ;; Hack nREPL version to avoid CIDER complaining about it. 71 | (require 'clojure.tools.nrepl) 72 | (alter-var-root (resolve 'clojure.tools.nrepl/version) 73 | (constantly {:version-string "0.2.10", :qualifier "", 74 | :incremental "10", :minor "2", :major "0"})) 75 | (require '[clojure.tools.nrepl.middleware.interruptible-eval :as ie]) 76 | (with-redefs-fn {(resolve 'ie/configure-thread-factory) 77 | android-thread-factory} 78 | #(apply (resolve 'start-server) 79 | :handler (apply (resolve 'default-handler) 80 | (map (fn [sym] 81 | (require (symbol (namespace sym))) 82 | (resolve sym)) 83 | middleware)) 84 | repl-args)))) 85 | 86 | (defmacro start-nrepl-server 87 | "Expands into nREPL server initialization if conditions are met." 88 | [args] 89 | (when (or (not (:neko.init/release-build *compiler-options*)) 90 | (:neko.init/start-nrepl-server *compiler-options*)) 91 | (let [build-port (:neko.init/nrepl-port *compiler-options*) 92 | mware (when (cider-available?) 93 | (list `quote 94 | (or (:neko.init/nrepl-middleware *compiler-options*) 95 | cider-middleware)))] 96 | `(let [port# (or ~(:port args) ~build-port 9999) 97 | args# (assoc ~args :port port#)] 98 | (try (apply start-repl ~mware (mapcat identity args#)) 99 | (neko.log/i "Nrepl started at port" port#) 100 | (catch Exception ex# 101 | (neko.log/e "Failed to start nREPL" :exception ex#))))))) 102 | 103 | (defn init 104 | "Entry point to neko.tools.repl namespace from Java code." 105 | [& {:as args}] 106 | (start-nrepl-server args)) 107 | -------------------------------------------------------------------------------- /src/clojure/neko/ui.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui 2 | "Tools for defining and manipulating Android UI elements." 3 | (:require [neko.ui.mapping :as kw] 4 | [neko.ui.traits :refer [apply-trait]] 5 | [neko.-utils :refer [keyword->setter reflect-setter 6 | reflect-constructor]]) 7 | (:import android.content.res.Configuration 8 | neko.App)) 9 | 10 | ;; ## Attributes 11 | 12 | (defn apply-default-setters-from-attributes 13 | "Takes widget keywords name, UI widget object and attributes map 14 | after all custom attributes were applied. Transforms each attribute 15 | into a call to (.set_CapitalizedKey_ widget value). If value is a 16 | keyword then it is looked up in the keyword-mapping or if it is not 17 | there, it is perceived as a static field of the class." 18 | [widget-kw widget attributes] 19 | (doseq [[attribute value] attributes] 20 | (let [real-value (kw/value widget-kw value attribute)] 21 | (.invoke (reflect-setter (type widget) 22 | (keyword->setter attribute) 23 | (type real-value)) 24 | widget (into-array (vector real-value)))))) 25 | 26 | (defn apply-attributes 27 | "Takes UI widget keyword, a widget object, a map of attributes and 28 | options. Consequently calls `apply-trait` on all element's traits, 29 | in the end calls `apply-default-setters-from-attributes` on what is 30 | left from the attributes map. Returns the updated options map. 31 | 32 | Options is a map of additional arguments that come from container 33 | elements to their inside elements. Note that all traits of the 34 | current element will receive the initial options map, and 35 | modifications will only appear visible to the subsequent elements." 36 | [widget-kw widget attributes options] 37 | (loop [[trait & rest] (kw/all-traits widget-kw), 38 | attrs attributes, new-opts options] 39 | (if trait 40 | (let [[attributes-fn options-fn] 41 | (apply-trait trait widget attrs options)] 42 | (recur rest (attributes-fn attrs) (options-fn new-opts))) 43 | (do 44 | (apply-default-setters-from-attributes widget-kw widget attrs) 45 | new-opts)))) 46 | 47 | ;; ## Widget creation 48 | 49 | (defn construct-element 50 | "Constructs a UI widget by a given keyword. Infers a correct 51 | constructor for the types of arguments being passed to it." 52 | ([kw context constructor-args] 53 | (let [element-class (kw/classname kw)] 54 | (.newInstance (reflect-constructor element-class 55 | (cons android.content.Context 56 | (map type constructor-args))) 57 | (to-array (cons context constructor-args)))))) 58 | 59 | (defn make-ui-element 60 | "Creates a UI widget based on its keyword name, applies attributes 61 | to it, then recursively create its subelements and add them to the 62 | widget." 63 | [context tree options] 64 | (if (sequential? tree) 65 | (let [[widget-kw attributes & inside-elements] tree 66 | _ (assert (and (keyword? widget-kw) (map? attributes))) 67 | attributes (merge (kw/default-attributes widget-kw) attributes) 68 | wdg (if-let [constr (:custom-constructor attributes)] 69 | (apply constr context (:constructor-args attributes)) 70 | (construct-element widget-kw context 71 | (:constructor-args attributes))) 72 | new-opts (apply-attributes 73 | widget-kw wdg 74 | ;; Remove :custom-constructor and 75 | ;; :constructor-args since they are not real 76 | ;; attributes. 77 | (dissoc attributes :constructor-args :custom-constructor) 78 | options)] 79 | (doseq [element inside-elements :when element] 80 | (.addView ^android.view.ViewGroup wdg 81 | (make-ui-element context element new-opts))) 82 | wdg) 83 | tree)) 84 | 85 | (defn make-ui 86 | "Takes an activity instance, and a tree of elements and creates Android UI 87 | elements according to this tree. A tree has a form of a vector that looks like 88 | following: 89 | 90 | `[element-name map-of-attributes & subelements]`." 91 | [activity tree] 92 | (make-ui-element activity tree {})) 93 | 94 | (defn config 95 | "Takes a widget and key-value pairs of attributes, and applies these 96 | attributes to the widget." 97 | [widget & {:as attributes}] 98 | (apply-attributes (kw/keyword-by-classname (type widget)) 99 | widget attributes {})) 100 | 101 | ;; ## Compatibility with Android XML UI facilities. 102 | 103 | (defn inflate-layout 104 | "Renders a View object for the given XML layout ID." 105 | [activity id] 106 | {:pre [(integer? id)] 107 | :post [(instance? android.view.View %)]} 108 | (.. android.view.LayoutInflater 109 | (from activity) 110 | (inflate ^Integer id nil))) 111 | 112 | ;; ## Utilities 113 | 114 | (defn get-screen-orientation 115 | "Returns either :portrait, :landscape, :square, or :undefined depending on the 116 | current orientation of the device." 117 | ([] 118 | (get-screen-orientation App/instance)) 119 | ([context] 120 | (condp = (.. context (getResources) (getConfiguration) orientation) 121 | Configuration/ORIENTATION_PORTRAIT :portrait 122 | Configuration/ORIENTATION_LANDSCAPE :landscape 123 | Configuration/ORIENTATION_SQUARE :square 124 | :undefined))) 125 | -------------------------------------------------------------------------------- /src/clojure/neko/ui/adapters.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.adapters 2 | "Contains custom adapters for ListView and Spinner." 3 | (:require [neko.debug :refer [safe-for-ui]] 4 | [neko.data.sqlite :refer [entity-from-cursor]] 5 | [neko.threading :refer [on-ui]] 6 | [neko.ui :refer [make-ui-element]]) 7 | (:import android.view.View 8 | neko.data.sqlite.TaggedCursor 9 | [neko.ui.adapters InterchangeableListAdapter TaggedCursorAdapter])) 10 | 11 | (defn ref-adapter 12 | "Takes a function that creates a View, a function that updates a 13 | view according to the element and a reference type that stores the 14 | data. Returns an Adapter object that displays ref-type contents. 15 | When ref-type is updated, Adapter gets updated as well. 16 | 17 | `create-view-fn` is a function of context. `update-view-fn` is 18 | a function of four arguments: element position, view to update, 19 | parent view container and the respective data element from the 20 | ref-type. `access-fn` argument is optional, it is called on the 21 | value of ref-type to get the list to be displayed." 22 | ([create-view-fn update-view-fn ref-type] 23 | (ref-adapter create-view-fn update-view-fn ref-type identity)) 24 | ([create-view-fn update-view-fn ref-type access-fn] 25 | {:pre [(fn? create-view-fn) (fn? update-view-fn) 26 | (instance? clojure.lang.IFn access-fn) 27 | (instance? clojure.lang.IDeref ref-type)]} 28 | (let [create-fn (fn [context] 29 | (or (safe-for-ui 30 | (let [view (create-view-fn context)] 31 | (if (instance? View view) 32 | view 33 | (make-ui-element 34 | context view 35 | {:container-type :abs-listview-layout})))) 36 | (android.view.View. context))) 37 | adapter (InterchangeableListAdapter. 38 | create-fn 39 | (fn [pos view parent data] 40 | (safe-for-ui (update-view-fn pos view parent data))) 41 | (access-fn @ref-type))] 42 | (add-watch ref-type ::adapter-watch 43 | (fn [_ __ ___ new-state] 44 | (on-ui (.setData adapter (access-fn new-state))))) 45 | adapter))) 46 | 47 | (defn cursor-adapter 48 | "Takes a context, a function that creates a View, and a function that updates 49 | a view according to the element, and a TaggedCursor instance or 50 | cursor-producing function. Returns an Adapter object that displays cursor 51 | contents. 52 | 53 | `create-view-fn` is a nullary function that returns a UI tree or a View. 54 | `update-view-fn` is a function of three arguments: view to update, cursor, and 55 | data extracted from the cursor. `cursor-or-cursor-fn` can be a nullary 56 | function that returns a TaggedCursor cursor object when called, or just a 57 | cursor. In the former case you can refresh adapter by calling `(.updateCursor 58 | adapter)`, in the latter you have to call `(.updateCursor adapter 59 | new-cursor)`." 60 | [context create-view-fn update-view-fn cursor-or-cursor-fn] 61 | {:pre [(fn? create-view-fn) (fn? update-view-fn) 62 | (or (fn? cursor-or-cursor-fn) 63 | (instance? TaggedCursor cursor-or-cursor-fn))]} 64 | (let [create-fn (fn [context] 65 | (or (safe-for-ui 66 | (let [view (create-view-fn)] 67 | (if (instance? View view) 68 | view 69 | (make-ui-element 70 | context view 71 | {:container-type :abs-listview-layout})))) 72 | (android.view.View. context)))] 73 | (TaggedCursorAdapter. 74 | context create-fn 75 | (fn [view cursor data] 76 | (safe-for-ui (update-view-fn view cursor data))) 77 | cursor-or-cursor-fn))) 78 | 79 | (defn update-cursor 80 | "Updates cursor in a given TaggedCursorAdapter. Second argument is necessary 81 | if the adapter was created with a cursor rather than cursor-fn." 82 | ([^TaggedCursorAdapter cursor-adapter] 83 | (.updateCursor cursor-adapter)) 84 | ([^TaggedCursorAdapter cursor-adapter new-cursor] 85 | (.updateCursor cursor-adapter new-cursor))) 86 | -------------------------------------------------------------------------------- /src/clojure/neko/ui/listview.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.listview 2 | "Contains utilities to work with ListView." 3 | (:import android.util.SparseBooleanArray 4 | android.widget.ListView)) 5 | 6 | (defn get-checked 7 | "Returns a vector of indices for items being checked in a ListView. 8 | The two-argument version additionally takes a sequence of data 9 | elements of the ListView (usually the data provided to the adapter) 10 | and returns the vector of only those elements that are checked. " 11 | ([^ListView lv] 12 | (let [^SparseBooleanArray bool-array (.getCheckedItemPositions lv) 13 | count (.getCount lv)] 14 | (loop [i 0, result []] 15 | (if (= i count) 16 | result 17 | (if (.get bool-array i) 18 | (recur (inc i) (conj result i)) 19 | (recur (inc i) result)))))) 20 | ([^ListView lv, items] 21 | (let [^SparseBooleanArray bool-array (.getCheckedItemPositions lv) 22 | count (.getCount lv)] 23 | (loop [i 0, [curr & rest] items, result []] 24 | (if (= i count) 25 | result 26 | (if (.get bool-array i) 27 | (recur (inc i) rest (conj result curr)) 28 | (recur (inc i) rest result))))))) 29 | 30 | (defn set-checked! 31 | "Given a sequence of numbers checks the respective ListView 32 | elements." 33 | [^ListView lv, checked-ids] 34 | (doseq [i checked-ids] 35 | (.setItemChecked lv i true))) 36 | -------------------------------------------------------------------------------- /src/clojure/neko/ui/mapping.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.mapping 2 | "This namespace provides utilities to connect the keywords to the 3 | actual UI classes, define the hierarchy relations between the 4 | elements and the values for the keywords representing values." 5 | (:require [clojure.string :as string]) 6 | (:use [neko.-utils :only [keyword->static-field reflect-field]]) 7 | (:import [android.widget LinearLayout Button CheckBox EditText ListView 8 | SearchView ImageView ImageView$ScaleType RelativeLayout ScrollView 9 | FrameLayout Gallery GridView] 10 | android.app.ProgressDialog 11 | android.view.inputmethod.EditorInfo 12 | [android.view View ViewGroup$LayoutParams Gravity])) 13 | 14 | ;; This atom keeps all the relations inside the map. 15 | (def ^{:private true} keyword-mapping 16 | (atom 17 | ;; UI widgets 18 | {:view {:classname android.view.View 19 | :traits [:def :id :padding :on-click :on-drag :on-long-click 20 | :on-touch :on-create-context-menu :on-key :on-focus-change 21 | :default-layout-params :linear-layout-params 22 | :relative-layout-params :listview-layout-params 23 | :frame-layout-params :gallery-layout-params] 24 | :value-namespaces 25 | {:gravity android.view.Gravity 26 | :visibility android.view.View}} 27 | :view-group {:inherits :view 28 | :traits [:container :id-holder]} 29 | :button {:classname android.widget.Button 30 | :inherits :text-view 31 | :attributes {:text "Default button"}} 32 | :check-box {:classname android.widget.CheckBox 33 | :inherits :text-view} 34 | :linear-layout {:classname android.widget.LinearLayout 35 | :inherits :view-group} 36 | :relative-layout {:classname android.widget.RelativeLayout 37 | :inherits :view-group} 38 | :frame-layout {:classname android.widget.FrameLayout 39 | :inherits :view-group} 40 | :edit-text {:classname android.widget.EditText 41 | :inherits :view 42 | :values {:number EditorInfo/TYPE_CLASS_NUMBER 43 | :datetime EditorInfo/TYPE_CLASS_DATETIME 44 | :text EditorInfo/TYPE_CLASS_TEXT 45 | :phone EditorInfo/TYPE_CLASS_PHONE 46 | :go EditorInfo/IME_ACTION_GO 47 | :done EditorInfo/IME_ACTION_DONE 48 | :unspecified EditorInfo/IME_ACTION_UNSPECIFIED 49 | :send EditorInfo/IME_ACTION_SEND 50 | :search EditorInfo/IME_ACTION_SEARCH 51 | :previous EditorInfo/IME_ACTION_PREVIOUS 52 | :next EditorInfo/IME_ACTION_NEXT}} 53 | :progress-bar {:classname android.widget.ProgressBar 54 | :inherits :view 55 | :value-namespaces {:visibility android.view.View}} 56 | :text-view {:classname android.widget.TextView 57 | :inherits :view 58 | :value-namespaces 59 | {:ellipsize android.text.TextUtils$TruncateAt} 60 | :traits [:text :text-size :on-editor-action]} 61 | :list-view {:classname android.widget.ListView 62 | :inherits :view-group 63 | :traits [:on-item-click]} 64 | :search-view {:classname android.widget.SearchView 65 | :inherits :view-group 66 | :traits [:on-query-text]} 67 | :image-view {:classname android.widget.ImageView 68 | :inherits :view 69 | :traits [:image] 70 | :value-namespaces 71 | {:scale-type android.widget.ImageView$ScaleType}} 72 | :web-view {:classname android.webkit.WebView 73 | :inherits :view} 74 | :scroll-view {:classname android.widget.ScrollView 75 | :inherits :view} 76 | :gallery {:classname android.widget.Gallery 77 | :inherits :view-group 78 | :traits [:on-item-click]} 79 | :grid-view {:classname android.widget.GridView 80 | :inherits :view-group 81 | :traits [:on-item-click]} 82 | 83 | ;; Other 84 | :layout-params {:classname ViewGroup$LayoutParams 85 | :values {:fill ViewGroup$LayoutParams/FILL_PARENT 86 | :wrap ViewGroup$LayoutParams/WRAP_CONTENT} 87 | :value-namespaces 88 | {:gravity android.view.Gravity}} 89 | :progress-dialog {:classname android.app.ProgressDialog 90 | :values {:horizontal ProgressDialog/STYLE_HORIZONTAL 91 | :spinner ProgressDialog/STYLE_SPINNER}} 92 | })) 93 | 94 | (defn get-keyword-mapping 95 | "Returns the current state of `keyword-mapping`." 96 | [] 97 | @keyword-mapping) 98 | 99 | (def ^{:private true} reverse-mapping 100 | (atom 101 | {android.widget.Button :button 102 | android.widget.LinearLayout :linear-layout 103 | android.widget.RelativeLayout :relative-layout 104 | android.widget.FrameLayout :frame-layout 105 | android.widget.EditText :edit-text 106 | android.widget.TextView :text-view 107 | android.widget.ListView :list-view 108 | android.widget.ImageView :image-view 109 | android.webkit.WebView :web-view 110 | android.widget.ScrollView :scroll-view 111 | android.widget.Gallery :gallery 112 | android.widget.GridView :grid-view 113 | android.widget.ProgressBar :progress-bar 114 | android.app.ProgressDialog :progress-dialog})) 115 | 116 | (defn set-classname! 117 | "Connects the given keyword to the classname." 118 | [kw classname] 119 | (swap! keyword-mapping assoc-in [kw :classname] classname) 120 | (swap! reverse-mapping assoc-in classname kw)) 121 | 122 | (defn classname 123 | "Gets the classname from the keyword-mapping map if the argument is 124 | a keyword. Otherwise considers the argument to already be a 125 | classname." 126 | [classname-or-kw] 127 | (if (keyword? classname-or-kw) 128 | (or (get-in @keyword-mapping [classname-or-kw :classname]) 129 | (throw (Exception. (str "The class for " classname-or-kw 130 | " isn't present in the mapping.")))) 131 | classname-or-kw)) 132 | 133 | (defn keyword-by-classname 134 | "Returns a keyword name for the given UI widget classname." 135 | [classname] 136 | (@reverse-mapping classname)) 137 | 138 | (defn add-trait! 139 | "Defines the `kw` to implement trait specified with `trait-kw`." 140 | [kw trait-kw] 141 | (swap! keyword-mapping update-in [kw :traits] conj trait-kw)) 142 | 143 | (defn all-traits 144 | "Returns the list of all unique traits for `kw`. The list is built 145 | recursively." 146 | [kw] 147 | (let [own-traits (get-in @keyword-mapping [kw :traits]) 148 | parent (get-in @keyword-mapping [kw :inherits])] 149 | (concat own-traits (when parent 150 | (all-traits parent))))) 151 | 152 | (defn set-value! 153 | "Associate the value keyword with the provided value for the given 154 | keyword representing the UI element." 155 | [element-kw value-kw value] 156 | (swap! keyword-mapping assoc-in [element-kw :values value-kw] value)) 157 | 158 | (defn- recursive-find 159 | "Searches in the keyword mapping for a value denoted by a list of 160 | keys. If value is not found, tries searching in a parent." 161 | [[element-kw & other :as keys]] 162 | (if element-kw 163 | (or (get-in @keyword-mapping keys) 164 | (recur (cons (get-in @keyword-mapping [element-kw :inherits]) other))))) 165 | 166 | (defn value 167 | "If the value is a keyword then returns the value for it from the 168 | keyword-mapping. The value is sought in the element itself and all 169 | its parents. If the value-keyword isn't present in any element's 170 | keyword-mapping, form the value as 171 | `classname-for-element-kw/CAPITALIZED-VALUE-KW`. Classname for 172 | keyword can be extracted from :value-namespaces map for element's 173 | mapping." 174 | [element-kw value & [attribute]] 175 | (let [mapping @keyword-mapping] 176 | (if-not (keyword? value) 177 | (cond 178 | (integer? value) (int value) 179 | (float? value) (float value) 180 | :else value) 181 | (or (recursive-find (list element-kw :values value)) 182 | (reflect-field 183 | (classname 184 | (or (and attribute 185 | (recursive-find [element-kw :value-namespaces attribute])) 186 | element-kw)) 187 | (keyword->static-field value)))))) 188 | 189 | (defn add-default-atribute-value! 190 | "Adds a default attribute value for the given element." 191 | [element-kw attribute-kw value] 192 | (swap! keyword-mapping 193 | update-in [element-kw :attributes attribute-kw] value)) 194 | 195 | (defn default-attributes 196 | "Returns a map of default attributes for the given element keyword 197 | and all its parents." 198 | [element-kw] 199 | (merge (when element-kw 200 | (default-attributes (get-in @keyword-mapping 201 | [element-kw :inherits]))) 202 | (get-in @keyword-mapping [element-kw :attributes]))) 203 | 204 | (defn defelement 205 | "Defines the element of the given class with the provided name to 206 | use in the UI construction. Takes the element's classname, a parent 207 | it inherits, a list of traits and a map of specific values as 208 | optional arguments. 209 | 210 | Optional arguments 211 | - :classname, :inherits, :traits, :values, :attributes, :container-type." 212 | [kw-name & {:as args}] 213 | (swap! keyword-mapping 214 | #(assoc % kw-name 215 | (let [parent (if (contains? args :inherits) 216 | (:inherits args) 217 | :view) 218 | classname (if (contains? args :classname) 219 | (:classname args) 220 | (get-in % [parent :classname]))] 221 | (assoc args :inherits parent :classname classname)))) 222 | (if-let [classname (:classname args)] 223 | (swap! reverse-mapping assoc classname kw-name))) 224 | -------------------------------------------------------------------------------- /src/clojure/neko/ui/menu.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.menu 2 | "Provides utilities for declarative options menu generation. 3 | Intended to replace XML-based menu layouts." 4 | (:require [neko.debug :refer [safe-for-ui]] 5 | [neko.ui :as ui] 6 | [neko.ui.mapping :refer [defelement]] 7 | [neko.ui.traits :refer [deftrait]] 8 | [neko.-utils :refer [call-if-nnil int-id]]) 9 | (:import [android.view Menu MenuItem] 10 | [android.view View ActionMode$Callback] 11 | android.app.Activity)) 12 | 13 | ;; ## ActionBar menu 14 | 15 | (defn make-menu 16 | "Inflates the given MenuBuilder instance with the declared menu item 17 | tree. Root of the tree is a sequence that contains element 18 | definitions (see doc for `neko.ui/make-ui` for element definition 19 | syntax). Elements supported are `:item`, `:group` and `:menu`. 20 | 21 | `:item` is a default menu element. See supported traits for :item for 22 | more information. 23 | 24 | `:group` allows to unite items into a single category in order to 25 | later operate on the whole category at once. 26 | 27 | `:menu` element creates a submenu that can in its own turn contain 28 | other `:item` and `:group` elements. Only one level of submenus is 29 | supported. Note that :menu creates an item for itself and can use 30 | all the attributes that apply to items. " 31 | ([menu tree] 32 | (make-menu menu Menu/NONE tree)) 33 | ([menu group tree] 34 | (doseq [[element-kw attributes & subelements] tree 35 | :when element-kw] 36 | (let [id (int-id (or (:id attributes) Menu/NONE)) 37 | order (int-id (or (:order attributes) Menu/NONE))] 38 | (case element-kw 39 | :group 40 | (make-menu menu id subelements) 41 | 42 | :item 43 | (ui/apply-attributes 44 | :item 45 | (.add ^Menu menu ^int group ^int id ^int order "") 46 | (dissoc attributes :id :order) {}) 47 | 48 | :menu 49 | (let [submenu (.addSubMenu ^Menu menu ^int group 50 | ^int id ^int order "")] 51 | (ui/apply-attributes :item (.getItem submenu) 52 | (dissoc attributes :id :order) {}) 53 | (make-menu submenu subelements))))))) 54 | 55 | ;; ## Contextual menu tools 56 | 57 | (def ^{:doc "Stores a mapping of activities to active action modes. 58 | After the action mode is finished, it is removed from the mapping." 59 | :private true} 60 | action-modes (atom {})) 61 | 62 | (defn start-action-mode 63 | "Tries starting action mode for an activity if it is not started 64 | yet. Takes an activity as first argument, rest arguments should be 65 | pairs of keys and functions. 66 | 67 | `:on-create` takes ActionMode and Menu as arguments. 68 | `:on-prepare` takes ActionMode and Menu as arguments. 69 | `:on-clicked` takes ActionMode and MenuItem that was clicked. 70 | `:on-destroy` takes ActionMode as argument." 71 | [activity & {:keys [on-create on-prepare on-clicked on-destroy]}] 72 | (when-not (@action-modes activity) 73 | (let [callback (reify ActionMode$Callback 74 | (onCreateActionMode [this mode menu] 75 | (call-if-nnil on-create mode menu)) 76 | (onPrepareActionMode [this mode menu] 77 | (call-if-nnil on-prepare mode menu)) 78 | (onActionItemClicked [this mode item] 79 | (call-if-nnil on-clicked mode item)) 80 | (onDestroyActionMode [this mode] 81 | (swap! action-modes dissoc activity) 82 | (call-if-nnil on-destroy mode))) 83 | am (.startActionMode ^Activity activity callback)] 84 | (swap! action-modes assoc activity am) 85 | am))) 86 | 87 | (defn get-action-mode 88 | "Returns action mode for the given activity." 89 | [activity] 90 | (@action-modes activity)) 91 | 92 | ;; ## Element definitions and traits 93 | 94 | (defelement :item 95 | :classname MenuItem 96 | :inherits nil 97 | :traits [:show-as-action :on-menu-item-click :action-view]) 98 | 99 | ;; ### ShowAsAction attribute 100 | 101 | (defn show-as-action-value 102 | "Returns an integer value for the given keyword, or the value itself." 103 | [value] 104 | (if (keyword? value) 105 | (case value 106 | :always MenuItem/SHOW_AS_ACTION_ALWAYS 107 | :collapse-action-view MenuItem/SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW 108 | :if-room MenuItem/SHOW_AS_ACTION_IF_ROOM 109 | :never MenuItem/SHOW_AS_ACTION_NEVER 110 | :with-text MenuItem/SHOW_AS_ACTION_WITH_TEXT) 111 | value)) 112 | 113 | (deftrait :show-as-action 114 | "Takes :show-as-action attribute, which could be an integer value or 115 | one of the following keywords: :always, :collapse-action-view, 116 | :if-room, :never, :with-text; or a vector with these values, to 117 | which bit-or operation will be applied." 118 | [^MenuItem wdg, {:keys [show-as-action]} _] 119 | (let [value (if (vector? show-as-action) 120 | (apply bit-or 0 (map show-as-action-value show-as-action)) 121 | (show-as-action-value show-as-action))] 122 | (.setShowAsAction wdg value))) 123 | 124 | ;; ### OnMenuItemClick attribute 125 | 126 | (defn on-menu-item-click-call 127 | "Takes a function and yields a MenuItem.OnMenuItemClickListener 128 | object that will invoke the function. This function must take one 129 | argument, an item that was clicked." 130 | [handler-fn] 131 | (reify android.view.MenuItem$OnMenuItemClickListener 132 | (onMenuItemClick [this item] 133 | (safe-for-ui (handler-fn item)) 134 | true))) 135 | 136 | (defmacro on-menu-item-click 137 | "Takes a body of expressions and yields a 138 | MenuItem.OnMenuItemClickListener object that will invoke the body. 139 | The body takes an implicit argument 'item' that is the item that was 140 | clicked." 141 | [& body] 142 | `(on-menu-item-click-call (fn [~'item] ~@body))) 143 | 144 | (deftrait :on-menu-item-click 145 | "Takes :on-click attribute, which should be function of one 146 | argument, and sets it as an OnClickListener for the widget." 147 | {:attributes [:on-click]} 148 | [^MenuItem wdg, {:keys [on-click]} _] 149 | (.setOnMenuItemClickListener wdg (on-menu-item-click-call on-click))) 150 | 151 | ;; ### ActionView attribute 152 | 153 | (deftrait :action-view 154 | "Takes `:action-view` attribute which should either be a View instance or a UI 155 | definition tree, and sets it as an action view for the menu item. For UI tree 156 | syntax see docs for `neko.ui/make-ui`. Activity instance must be provided via 157 | `:context` attribute." 158 | {:attributes [:action-view :context] 159 | :applies? (:action-view attrs)} 160 | [^MenuItem wdg, {:keys [action-view context] :as attrs} _] 161 | (let [view (if (instance? View action-view) 162 | action-view 163 | (ui/make-ui-element context action-view {:menu-item wdg}))] 164 | (.setActionView wdg ^View view))) 165 | -------------------------------------------------------------------------------- /src/clojure/neko/ui/traits.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.traits 2 | "Contains trait declarations for various UI elements." 3 | (:require [neko.ui.mapping :as kw] 4 | [neko.resource :as res] 5 | [neko.listeners.view :as view-listeners] 6 | [neko.listeners.text-view :as text-view-listeners] 7 | [neko.listeners.adapter-view :as adapter-view] 8 | neko.listeners.search-view 9 | [neko.-utils :refer [memoized int-id closest-android-ancestor]]) 10 | (:import [android.widget LinearLayout$LayoutParams ListView TextView SearchView 11 | ImageView RelativeLayout RelativeLayout$LayoutParams AdapterView 12 | AbsListView$LayoutParams FrameLayout$LayoutParams Gallery$LayoutParams] 13 | [android.view View ViewGroup$LayoutParams 14 | ViewGroup$MarginLayoutParams] 15 | android.graphics.Bitmap android.graphics.drawable.Drawable 16 | android.net.Uri 17 | android.util.TypedValue 18 | android.content.Context 19 | java.util.HashMap 20 | clojure.lang.Keyword)) 21 | 22 | ;; ## Infrastructure for traits and attributes 23 | 24 | (defmulti apply-trait 25 | "Transforms the given map of attributes into the valid Java-interop 26 | code of setters. 27 | 28 | `trait` is the keyword for a transformer function over the attribute map. 29 | 30 | `object-symbol` is an symbol for the UI element to apply setters to. 31 | 32 | `attributes-map` is a map of attributes to their values. 33 | 34 | `generated-code` is an attribute-setter code generated so far. The 35 | code this method generates should be appended to it. 36 | 37 | `options-map` is a map of additional options that come from higher 38 | level elements to their inside elements. A transformer can use this 39 | map to provide some arguments to its own inside elements. 40 | 41 | Returns a vector that looks like `[new-generated-code 42 | attributes-update-fn options-update-fn]`. `attributes-update-fn` 43 | should take attributes map and remove processed attributes from it. 44 | `options-update-fn` should remove old or introduce new options for 45 | next-level elements." 46 | (fn [trait widget attributes-map options-map] 47 | trait)) 48 | 49 | (defn add-attributes-to-meta 50 | "Appends information about attribute to trait mapping to `meta`." 51 | [meta attr-list trait] 52 | (reduce (fn [m att] 53 | (update-in m [:attributes att] 54 | #(if % 55 | (conj % trait) 56 | #{trait}))) 57 | meta attr-list)) 58 | 59 | (defmacro deftrait 60 | "Defines a trait with the given name. 61 | 62 | `match-pred` is a function on attributes map that should return a 63 | logical truth if this trait should be executed against the widget 64 | and the map. By default it checks if attribute with the same name as 65 | trait's is present in the attribute map. 66 | 67 | The parameter list is the following: `[widget attributes-map 68 | options-map]`. 69 | 70 | Body of the trait can optionally return a map with the following 71 | keys: `:attribute-fn`, `:options-fn`, which values are functions to 72 | be applied to attributes map and options map respectively after the 73 | trait finishes its work. If they are not provided, `attribute-fn` 74 | defaults to dissoc'ing trait's name from attribute map, and 75 | `options-fn` defaults to identity function." 76 | [name & args] 77 | (let [[docstring args] (if (string? (first args)) 78 | [(first args) (next args)] 79 | [nil args]) 80 | [param-map args] (if (map? (first args)) 81 | [(first args) (next args)] 82 | [{} args]) 83 | attrs-sym (gensym "attributes") 84 | match-pred (cond (:applies? param-map) 85 | (:applies? param-map) 86 | 87 | (:attributes param-map) 88 | `(some ~attrs-sym ~(:attributes param-map)) 89 | 90 | :else `(~name ~attrs-sym)) 91 | [arglist & codegen-body] args 92 | dissoc-fn (if (:attributes param-map) 93 | `(fn [a#] (apply dissoc a# ~(:attributes param-map))) 94 | `(fn [a#] (dissoc a# ~name)))] 95 | `(do 96 | (alter-meta! #'apply-trait 97 | (fn [m#] 98 | (-> m# 99 | (assoc-in [:trait-doc ~name] ~docstring) 100 | (add-attributes-to-meta 101 | (or ~(:attributes param-map) [~name]) ~name)))) 102 | (defmethod apply-trait ~name 103 | [trait# widget# ~attrs-sym options#] 104 | (let [~arglist [widget# ~attrs-sym options#]] 105 | (if ~match-pred 106 | (let [result# (do ~@codegen-body) 107 | attr-fn# ~dissoc-fn] 108 | (if (map? result#) 109 | [(:attributes-fn result# attr-fn#) 110 | (:options-fn result# identity)] 111 | [attr-fn# identity])) 112 | [identity identity])))))) 113 | 114 | (alter-meta! #'deftrait 115 | assoc :arglists '([name docstring? param-map? [params*] body])) 116 | 117 | ;; ## Implementation of different traits 118 | 119 | ;; ### Def attribute 120 | 121 | (deftrait :def 122 | "Takes a symbol provided to `:def` and binds the widget to it. 123 | 124 | Example: `[:button {:def ok}]` defines a var `ok` which stores the 125 | button object." 126 | [wdg {:keys [def]} _] 127 | (assert (and (symbol? def) (namespace def))) 128 | (intern (symbol (namespace def)) (symbol (name def)) wdg)) 129 | 130 | ;; ### Basic traits 131 | 132 | (deftrait :text 133 | "Sets widget's text to a string or a resource ID representing a string 134 | resource provided to `:text` attribute." 135 | [^TextView wdg, {:keys [text] :or {text ""}} _] 136 | (.setText wdg ^CharSequence (res/get-string (.getContext wdg) text))) 137 | 138 | (defn- kw->unit-id [unit-kw] 139 | (case unit-kw 140 | :px TypedValue/COMPLEX_UNIT_PX 141 | :dp TypedValue/COMPLEX_UNIT_DIP 142 | :dip TypedValue/COMPLEX_UNIT_DIP 143 | :sp TypedValue/COMPLEX_UNIT_SP 144 | :pt TypedValue/COMPLEX_UNIT_PT 145 | :in TypedValue/COMPLEX_UNIT_IN 146 | :mm TypedValue/COMPLEX_UNIT_MM 147 | TypedValue/COMPLEX_UNIT_PX)) 148 | 149 | (memoized 150 | (defn- get-display-metrics 151 | "Returns Android's DisplayMetrics object from application context." 152 | [^Context context] 153 | (.. context (getResources) (getDisplayMetrics)))) 154 | 155 | (defn to-dimension [context value] 156 | (if (vector? value) 157 | (Math/round 158 | ^float (TypedValue/applyDimension 159 | (kw->unit-id (second value)) 160 | (first value) (get-display-metrics context))) 161 | value)) 162 | 163 | (deftrait :text-size 164 | "Takes `:text-size` attribute which should be either integer or a 165 | dimension vector, and sets it to the widget." 166 | [^TextView wdg, {:keys [text-size]} _] 167 | (if (vector? text-size) 168 | (.setTextSize wdg (kw->unit-id (second text-size)) (first text-size)) 169 | (.setTextSize wdg text-size))) 170 | 171 | (deftrait :image 172 | "Takes `:image` attribute which can be a resource ID, resource 173 | keyword, Drawable, Bitmap or URI and sets it ImageView widget's 174 | image source." [^ImageView wdg, {:keys [image]} _] 175 | (condp instance? image 176 | Bitmap (.setImageBitmap wdg image) 177 | Drawable (.setImageDrawable wdg image) 178 | Uri (.setImageURI wdg image) 179 | ;; Otherwise assume `image` to be resource ID. 180 | (.setImageResource wdg image))) 181 | 182 | ;; ### Layout parameters attributes 183 | 184 | (def ^:private margin-attributes [:layout-margin 185 | :layout-margin-left :layout-margin-top 186 | :layout-margin-right :layout-margin-bottom]) 187 | 188 | (defn- apply-margins-to-layout-params 189 | "Takes a LayoutParams object that implements MarginLayoutParams 190 | class and an attribute map, and sets margins for this object." 191 | [context, ^ViewGroup$MarginLayoutParams params, attribute-map] 192 | (let [common (to-dimension context (attribute-map :layout-margin 0)) 193 | [l t r b] (map #(to-dimension context (attribute-map % common)) 194 | (rest margin-attributes))] 195 | (.setMargins params l t r b))) 196 | 197 | (deftrait :default-layout-params 198 | "Takes `:layout-width` and `:layout-height` attributes and sets 199 | LayoutParams, if the container type is not specified." 200 | {:attributes [:layout-width :layout-height] 201 | :applies? (and (or layout-width layout-height) (nil? container-type))} 202 | [^View wdg, {:keys [layout-width layout-height]} {:keys [container-type]}] 203 | (let [^int width (->> (or layout-width :wrap) 204 | (kw/value :layout-params) 205 | (to-dimension (.getContext wdg))) 206 | ^int height (->> (or layout-height :wrap) 207 | (kw/value :layout-params) 208 | (to-dimension (.getContext wdg)))] 209 | (.setLayoutParams wdg (ViewGroup$LayoutParams. width height)))) 210 | 211 | (deftrait :linear-layout-params 212 | "Takes `:layout-width`, `:layout-height`, `:layout-weight`, 213 | `:layout-gravity` and different layout margin attributes and sets 214 | LinearLayout.LayoutParams if current container is LinearLayout. 215 | Values could be either numbers of `:fill` or `:wrap`." 216 | {:attributes (concat margin-attributes [:layout-width :layout-height 217 | :layout-weight :layout-gravity]) 218 | :applies? (= container-type :linear-layout)} 219 | [^View wdg, {:keys [layout-width layout-height layout-weight layout-gravity] 220 | :as attributes} 221 | {:keys [container-type]}] 222 | (let [^int width (->> (or layout-width :wrap) 223 | (kw/value :layout-params) 224 | (to-dimension (.getContext wdg))) 225 | ^int height (->> (or layout-height :wrap) 226 | (kw/value :layout-params) 227 | (to-dimension (.getContext wdg))) 228 | weight (or layout-weight 0) 229 | params (LinearLayout$LayoutParams. width height weight)] 230 | (apply-margins-to-layout-params (.getContext wdg) params attributes) 231 | (when layout-gravity 232 | (set! (. params gravity) 233 | (kw/value :layout-params layout-gravity :gravity))) 234 | (.setLayoutParams wdg params))) 235 | 236 | ;; #### Relative layout 237 | 238 | (def ^:private relative-layout-attributes 239 | ;; Hard-coded number values are attributes that appeared since 240 | ;; Android Jellybean. 241 | {:standalone {:layout-align-parent-bottom RelativeLayout/ALIGN_PARENT_BOTTOM 242 | :layout-align-parent-end 21 ; RelativeLayout/ALIGN_PARENT_END 243 | :layout-align-parent-left RelativeLayout/ALIGN_PARENT_LEFT 244 | :layout-align-parent-right RelativeLayout/ALIGN_PARENT_RIGHT 245 | :layout-align-parent-start 20 ; RelativeLayout/ALIGN_PARENT_START 246 | :layout-align-parent-top RelativeLayout/ALIGN_PARENT_TOP 247 | :layout-center-horizontal RelativeLayout/CENTER_HORIZONTAL 248 | :layout-center-vertical RelativeLayout/CENTER_VERTICAL 249 | :layout-center-in-parent RelativeLayout/CENTER_IN_PARENT} 250 | :with-id {:layout-above RelativeLayout/ABOVE 251 | :layout-align-baseline RelativeLayout/ALIGN_BASELINE 252 | :layout-align-bottom RelativeLayout/ALIGN_BOTTOM 253 | :layout-align-end 19 ; RelativeLayout/ALIGN_END 254 | :layout-align-left RelativeLayout/ALIGN_LEFT 255 | :layout-align-right RelativeLayout/ALIGN_RIGHT 256 | :layout-align-start 18 ; RelativeLayout/ALIGN_START 257 | :layout-align-top RelativeLayout/ALIGN_TOP 258 | :layout-below RelativeLayout/BELOW 259 | :layout-to-end-of 17 ; RelativeLayout/END_OF 260 | :layout-to-left-of RelativeLayout/LEFT_OF 261 | :layout-to-right-of RelativeLayout/RIGHT_OF 262 | :layout-to-start-of 16 ; RelativeLayout/START_OF 263 | }}) 264 | 265 | (def ^:private all-relative-attributes 266 | (apply concat [:layout-width :layout-height 267 | :layout-align-with-parent-if-missing] 268 | (map keys (vals relative-layout-attributes)))) 269 | 270 | (deftrait :relative-layout-params 271 | {:attributes (concat all-relative-attributes margin-attributes) 272 | :applies? (= container-type :relative-layout)} 273 | [^View wdg, {:keys [layout-width layout-height 274 | layout-align-with-parent-if-missing] :as attributes} 275 | {:keys [container-type]}] 276 | (let [^int width (->> (or layout-width :wrap) 277 | (kw/value :layout-params) 278 | (to-dimension (.getContext wdg))) 279 | ^int height (->> (or layout-height :wrap) 280 | (kw/value :layout-params) 281 | (to-dimension (.getContext wdg))) 282 | lp (RelativeLayout$LayoutParams. width height)] 283 | (when-not (nil? layout-align-with-parent-if-missing) 284 | (set! (. lp alignWithParent) layout-align-with-parent-if-missing)) 285 | (doseq [[attr-name attr-id] (:standalone relative-layout-attributes)] 286 | (when (= (attr-name attributes) true) 287 | (.addRule lp attr-id))) 288 | (doseq [[attr-name attr-id] (:with-id relative-layout-attributes)] 289 | (when (contains? attributes attr-name) 290 | (.addRule lp attr-id (int-id (attr-name attributes))))) 291 | (apply-margins-to-layout-params (.getContext wdg) lp attributes) 292 | (.setLayoutParams wdg lp))) 293 | 294 | (deftrait :listview-layout-params 295 | {:attributes [:layout-width :layout-height :layout-view-type] 296 | :applies? (= container-type :abs-listview-layout)} 297 | [^View wdg, {:keys [layout-width layout-height layout-view-type] 298 | :as attributes} 299 | {:keys [container-type]}] 300 | (let [^int width (->> (or layout-width :wrap) 301 | (kw/value :layout-params) 302 | (to-dimension (.getContext wdg))) 303 | ^int height (->> (or layout-height :wrap) 304 | (kw/value :layout-params) 305 | (to-dimension (.getContext wdg)))] 306 | (.setLayoutParams 307 | wdg (if layout-view-type 308 | (AbsListView$LayoutParams. width height layout-view-type) 309 | (AbsListView$LayoutParams. width height))))) 310 | 311 | (deftrait :gallery-layout-params 312 | {:attributes [:layout-width :layout-height] 313 | :applies? (= container-type :gallery)} 314 | [^View wdg, {:keys [layout-width layout-height] 315 | :as attributes} 316 | {:keys [container-type]}] 317 | (let [^int width (->> (or layout-width :wrap) 318 | (kw/value :layout-params) 319 | (to-dimension (.getContext wdg))) 320 | ^int height (->> (or layout-height :wrap) 321 | (kw/value :layout-params) 322 | (to-dimension (.getContext wdg)))] 323 | (.setLayoutParams wdg (Gallery$LayoutParams. width height)))) 324 | 325 | (deftrait :frame-layout-params 326 | "Takes `:layout-width`, `:layout-height`, `:layout-gravity` and different 327 | layout margin attributes and sets FrameLayout.LayoutParams if current 328 | container is FrameLayout. Values could be either numbers of `:fill` or 329 | `:wrap`." {:attributes (concat margin-attributes [:layout-width :layout-height 330 | :layout-gravity]) 331 | :applies? (= container-type :frame-layout)} 332 | [^View wdg, {:keys [layout-width layout-height layout-gravity] 333 | :as attributes} 334 | {:keys [container-type]}] 335 | (let [^int width (->> (or layout-width :wrap) 336 | (kw/value :layout-params) 337 | (to-dimension (.getContext wdg))) 338 | ^int height (->> (or layout-height :wrap) 339 | (kw/value :layout-params) 340 | (to-dimension (.getContext wdg))) 341 | params (FrameLayout$LayoutParams. width height)] 342 | (apply-margins-to-layout-params (.getContext wdg) params attributes) 343 | (when layout-gravity 344 | (set! (. params gravity) 345 | (kw/value :layout-params layout-gravity :gravity))) 346 | (.setLayoutParams wdg params))) 347 | 348 | (deftrait :padding 349 | "Takes `:padding`, `:padding-bottom`, `:padding-left`, 350 | `:padding-right` and `:padding-top` and set element's padding 351 | according to their values. Values might be either integers or 352 | vectors like `[number unit-kw]`, where unit keyword is one of the 353 | following: :px, :dip, :sp, :pt, :in, :mm." 354 | {:attributes [:padding :padding-bottom :padding-left 355 | :padding-right :padding-top]} 356 | [^View wdg {:keys [padding padding-bottom padding-left 357 | padding-right padding-top]} _] 358 | (let [ctx (.getContext wdg)] 359 | (.setPadding wdg 360 | (to-dimension ctx (or padding-left padding 0)) 361 | (to-dimension ctx (or padding-top padding 0)) 362 | (to-dimension ctx (or padding-right padding 0)) 363 | (to-dimension ctx (or padding-bottom padding 0))))) 364 | 365 | (deftrait :container 366 | "Puts the type of the widget onto the options map so subelement can 367 | use the container type to choose the correct LayoutParams instance." 368 | {:applies? (constantly true)} 369 | [wdg _ __] 370 | (let [kw (kw/keyword-by-classname (closest-android-ancestor (class wdg))) 371 | container-type (-> (kw/get-keyword-mapping) kw :container-type)] 372 | {:options-fn #(assoc % :container-type (or container-type kw))})) 373 | 374 | ;; ### Listener traits 375 | 376 | (deftrait :on-click 377 | "Takes :on-click attribute, which should be function of one 378 | argument, and sets it as an OnClickListener for the widget." 379 | [^View wdg, {:keys [on-click]} _] 380 | (.setOnClickListener wdg (view-listeners/on-click-call on-click))) 381 | 382 | (deftrait :on-create-context-menu 383 | "Takes :on-create-context-menu attribute, which should be function 384 | of three arguments, and sets it as an OnCreateContextMenuListener 385 | for the object." 386 | [^View wdg, {:keys [on-create-context-menu]} _] 387 | (.setOnCreateContextMenuListener 388 | wdg (view-listeners/on-create-context-menu-call on-create-context-menu))) 389 | 390 | (deftrait :on-focus-change 391 | "Takes :on-focus-change attribute, which should be function of two 392 | arguments, and sets it as an OnFocusChangeListener for the object." 393 | [^View wdg, {:keys [on-focus-change]} _] 394 | (.setOnFocusChangeListener 395 | wdg (view-listeners/on-focus-change-call on-focus-change))) 396 | 397 | (deftrait :on-key 398 | "Takes :on-key attribute, which should be function of three 399 | arguments, and sets it as an OnKeyListener for the widget." 400 | [^View wdg, {:keys [on-key]} _] 401 | (.setOnKeyListener wdg (view-listeners/on-key-call on-key))) 402 | 403 | (deftrait :on-long-click 404 | "Takes :on-long-click attribute, which should be function of one 405 | argument, and sets it as an OnLongClickListener for the widget." 406 | [^View wdg, {:keys [on-long-click]} _] 407 | (.setOnLongClickListener 408 | wdg (view-listeners/on-long-click-call on-long-click))) 409 | 410 | (deftrait :on-touch 411 | "Takes :on-touch attribute, which should be function of two 412 | arguments, and sets it as an OnTouchListener for the widget." 413 | [^View wdg, {:keys [on-touch]} _] 414 | (.setOnTouchListener wdg (view-listeners/on-touch-call on-touch))) 415 | 416 | (deftrait :on-drag 417 | "Takes :on-drag attribute, which should be function of two 418 | arguments, and sets it as an OnDragListener for the widget." 419 | [^View wdg, {:keys [on-drag]} _] 420 | (.setOnDragListener wdg (view-listeners/on-drag-call on-drag))) 421 | 422 | (deftrait :on-query-text 423 | "Takes `:on-query-text-change` and `:on-query-text-submit` 424 | attributes, which should be functions of one or two arguments, 425 | depending on the context of usage. If widget is used as an action 426 | item in a menu, two arguments are passed to the function - query 427 | text and the menu item, for which widget is being action item to. 428 | Otherwise only query text is passed to the functions. 429 | 430 | Then OnQueryTextListener object is created from the functions and 431 | set to the widget." 432 | {:attributes [:on-query-text-change :on-query-text-submit]} 433 | [^SearchView wdg, {:keys [on-query-text-change on-query-text-submit]} 434 | {:keys [menu-item]}] 435 | (.setOnQueryTextListener 436 | wdg (neko.listeners.search-view/on-query-text-call 437 | (if (and menu-item on-query-text-change) 438 | (fn [q] (on-query-text-change q menu-item)) 439 | on-query-text-change) 440 | (if (and menu-item on-query-text-submit) 441 | (fn [q] (on-query-text-submit q menu-item)) 442 | on-query-text-submit)))) 443 | 444 | (deftrait :on-editor-action 445 | "Takes :on-editor-action attribute, which should be function 446 | of three arguments, and sets it as OnEditorAction for the 447 | TexView widget" 448 | [^TextView wdg, {:keys [on-editor-action]} _] 449 | (.setOnEditorActionListener wdg (text-view-listeners/on-editor-action-call on-editor-action))) 450 | 451 | ;; ### ID storing traits 452 | 453 | (deftrait :id-holder 454 | "Takes `:id-holder` attribute which should equal true and marks the 455 | widget to be a holder of lower-level elements. Elements are stored 456 | by their IDs as keys in a map, which is accessible by calling 457 | `.getTag` on the holder widget. 458 | 459 | Example: 460 | 461 | (def foo (make-ui [:linear-layout {:id-holder true} 462 | [:button {:id ::abutton}]])) 463 | (::abutton (.getTag foo)) => internal Button widget." 464 | [^View wdg, _ __] 465 | (.setTag wdg (HashMap.)) 466 | {:options-fn #(assoc % :id-holder wdg)}) 467 | 468 | (deftrait :id 469 | "Takes `:id` attribute, which can either be an integer or a 470 | keyword (that would be transformed into integer as well) and sets it 471 | as widget's ID attribute. Also, if an ID holder was declared in 472 | this tree, stores the widget in id-holder's tag (see docs for 473 | `:id-holder`trait)." 474 | [^View wdg, {:keys [id]} {:keys [^View id-holder]}] 475 | (.setId wdg (int-id id)) 476 | (when id-holder 477 | (.put ^HashMap (.getTag id-holder) id wdg))) 478 | 479 | (deftrait :on-item-click 480 | "Takes :on-item-click attribute, which should be function of four arguments 481 | 482 | parent AdapterView of the originating click 483 | view Item view 484 | position Item view position 485 | id Item view row id 486 | 487 | and sets it as an OnItemClickListener for the widget." 488 | [^AdapterView wdg, {:keys [on-item-click]} _] 489 | (.setOnItemClickListener wdg (adapter-view/on-item-click-call on-item-click))) 490 | 491 | -------------------------------------------------------------------------------- /src/java/neko/ActivityWithState.java: -------------------------------------------------------------------------------- 1 | package neko; 2 | 3 | public interface ActivityWithState { 4 | 5 | public Object getState(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/java/neko/App.java: -------------------------------------------------------------------------------- 1 | package neko; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.multidex.MultiDex; 6 | import android.util.Log; 7 | import clojure.lang.RT; 8 | import clojure.lang.Var; 9 | import clojure.lang.IFn; 10 | import java.lang.reflect.Method; 11 | 12 | public class App extends Application { 13 | 14 | private static String TAG = "neko.App"; 15 | public static Application instance; 16 | 17 | @Override 18 | public void onCreate() { 19 | instance = this; 20 | try { 21 | Class dalvikCLclass = Class.forName("clojure.lang.DalvikDynamicClassLoader"); 22 | Method setContext = dalvikCLclass.getMethod("setContext", Context.class); 23 | setContext.invoke(null, this); 24 | } catch (ClassNotFoundException e) { 25 | Log.i(TAG, "DalvikDynamicClassLoader is not found, probably Skummet is used."); 26 | } catch (Exception e) { 27 | Log.e(TAG, "setContext method not found, check if your Clojure dependency is correct."); 28 | } 29 | } 30 | 31 | // This method is only necessary for asynchronous loading. Clojure is 32 | // perfectly capable of loading itself the first time anything from it is 33 | // called. 34 | public static void loadClojure() { 35 | IFn load = (IFn)RT.var("clojure.core", "load"); 36 | load.invoke("/neko/activity"); 37 | 38 | try { 39 | load.invoke("/neko/tools/repl"); 40 | IFn init = (IFn)RT.var("neko.tools.repl", "init"); 41 | init.invoke(); 42 | } catch (Exception e) { 43 | Log.i(TAG, "Could not find neko.tools.repl, probably Skummet is used."); 44 | } 45 | } 46 | 47 | public static void loadAsynchronously(final String activityClass, final Runnable callback) { 48 | new Thread(Thread.currentThread().getThreadGroup(), 49 | new Runnable(){ 50 | @Override 51 | public void run() { 52 | loadClojure(); 53 | 54 | try { 55 | Class.forName(activityClass); 56 | } catch (ClassNotFoundException e) { 57 | Log.e(TAG, "Failed loading activity " + activityClass, e); 58 | } 59 | 60 | callback.run(); 61 | } 62 | }, 63 | "ClojureLoadingThread", 64 | 1048576 // = 1MB, thread stack size in bytes 65 | ).start(); 66 | } 67 | 68 | @Override 69 | protected void attachBaseContext(Context base) { 70 | super.attachBaseContext(base); 71 | MultiDex.install(this); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/java/neko/data/sqlite/SQLiteHelper.java: -------------------------------------------------------------------------------- 1 | package neko.data.sqlite; 2 | 3 | import android.database.sqlite.SQLiteDatabase; 4 | import android.database.sqlite.SQLiteOpenHelper; 5 | import android.content.Context; 6 | import java.util.List; 7 | import clojure.lang.IFn; 8 | 9 | public class SQLiteHelper extends SQLiteOpenHelper { 10 | 11 | private final List createQueriesList; 12 | private final List dropTablesList; 13 | public final Object schema; 14 | 15 | public SQLiteHelper(Context context, String name, int version, Object schema, 16 | List createQueriesList, 17 | List dropTablesList) { 18 | super(context, name, null, version); 19 | this.schema = schema; 20 | this.createQueriesList = createQueriesList; 21 | this.dropTablesList = dropTablesList; 22 | } 23 | 24 | @Override 25 | public void onCreate(SQLiteDatabase db) { 26 | for (String q : createQueriesList) { 27 | db.execSQL(q); 28 | } 29 | } 30 | 31 | @Override 32 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 33 | for (String q : dropTablesList) { 34 | db.execSQL(q); 35 | } 36 | this.onCreate(db); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/java/neko/data/sqlite/TaggedCursor.java: -------------------------------------------------------------------------------- 1 | package neko.data.sqlite; 2 | 3 | import android.database.Cursor; 4 | import android.database.CursorWrapper; 5 | 6 | public class TaggedCursor extends CursorWrapper { 7 | 8 | public final Object columns; 9 | 10 | public TaggedCursor(Cursor cursor, Object columns) { 11 | super(cursor); 12 | this.columns = columns; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/java/neko/ui/adapters/InterchangeableListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2012 Alexander Yakushev 3 | * All rights reserved. 4 | * 5 | * This program and the accompanying materials are made available under the 6 | * terms of the Eclipse Public License v1.0 which accompanies this 7 | * distribution, and is available at 8 | * . 9 | * 10 | * By using this software in any fashion, you are agreeing to be bound by the 11 | * terms of this license. You must not remove this notice, or any other, from 12 | * this software. 13 | */ 14 | package neko.ui.adapters; 15 | 16 | import android.widget.BaseAdapter; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import clojure.lang.IFn; 20 | import java.util.List; 21 | 22 | public class InterchangeableListAdapter extends BaseAdapter { 23 | 24 | private IFn createViewFn; 25 | private IFn updateViewFn; 26 | private List data; 27 | 28 | public InterchangeableListAdapter(IFn createViewFn, IFn updateViewFn, 29 | List initialData) { 30 | super(); 31 | this.createViewFn = createViewFn; 32 | this.updateViewFn = updateViewFn; 33 | this.data = initialData; 34 | } 35 | 36 | public int getCount() { 37 | return data.size(); 38 | } 39 | 40 | public Object getItem(int position) { 41 | return data.get(position); 42 | } 43 | 44 | public long getItemId(int position) { 45 | return position; 46 | } 47 | 48 | public boolean hasStableIds() { 49 | return false; 50 | } 51 | 52 | public boolean isEmpty() { 53 | return data.isEmpty(); 54 | } 55 | 56 | public View getView(int position, View convertView, ViewGroup parent) { 57 | View view = convertView; 58 | if (view == null) { 59 | view = (View)createViewFn.invoke(parent.getContext()); 60 | } 61 | updateViewFn.invoke(position, view, parent, data.get(position)); 62 | return view; 63 | } 64 | 65 | public void setData(List newData) { 66 | data = newData; 67 | notifyDataSetInvalidated(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/java/neko/ui/adapters/TaggedCursorAdapter.java: -------------------------------------------------------------------------------- 1 | package neko.ui.adapters; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.CursorAdapter; 8 | import clojure.lang.IFn; 9 | import clojure.lang.RT; 10 | import neko.data.sqlite.TaggedCursor; 11 | 12 | public class TaggedCursorAdapter extends CursorAdapter { 13 | 14 | private IFn createViewFn; 15 | private IFn updateViewFn; 16 | private IFn updateCursorFn; 17 | private IFn entityFromCursor; 18 | 19 | public TaggedCursorAdapter(Context context, IFn createViewFn, 20 | IFn updateViewFn, Object cursorOrFn) { 21 | super(context, null); 22 | Cursor cursor; 23 | this.createViewFn = createViewFn; 24 | this.updateViewFn = updateViewFn; 25 | this.entityFromCursor = (IFn)RT.var("neko.data.sqlite", "entity-from-cursor").deref(); 26 | if (cursorOrFn instanceof TaggedCursor) 27 | updateCursor((TaggedCursor)cursorOrFn); 28 | else { 29 | this.updateCursorFn = (IFn)cursorOrFn; 30 | updateCursor(); 31 | } 32 | } 33 | 34 | @Override 35 | public View newView(Context context, Cursor cursor, ViewGroup parent) { 36 | return (View)createViewFn.invoke(context); 37 | } 38 | 39 | @Override 40 | public void bindView(View view, Context context, Cursor cursor) { 41 | updateViewFn.invoke(view, cursor, entityFromCursor.invoke(cursor)); 42 | } 43 | 44 | public Object getItem(int position) { 45 | return entityFromCursor.invoke(super.getItem(position)); 46 | } 47 | 48 | public void updateCursor() { 49 | if (updateCursorFn == null) 50 | throw new RuntimeException("Zero-argument updateCursor() needs adapter to be created with cursor-fn"); 51 | updateCursor((TaggedCursor)updateCursorFn.invoke()); 52 | } 53 | 54 | public void updateCursor(TaggedCursor cursor) { 55 | changeCursor(cursor); 56 | notifyDataSetChanged(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /test/neko/compliment/t_ui_widgets_and_attributes.clj: -------------------------------------------------------------------------------- 1 | (ns neko.compliment.t-ui-widgets-and-attributes 2 | (:require [neko.compliment.ui-widgets-and-attributes :as comp] 3 | [clojure.test :refer :all])) 4 | 5 | (deftest candidates 6 | (let [widget-ctx '[{:form [__prefix__ {}], :idx 0}] 7 | attr-ctx '[{:form {__prefix__ nil}, :idx nil, :map-role :key} 8 | {:form [:text-view {__prefix__ nil}], :idx 1}] 9 | attr2-ctx '[{:form {__prefix__ nil}, :idx nil, :map-role :key} 10 | {:form [:nothing {__prefix__ nil}], :idx 1}] 11 | bad-ctx '[{:form {:text __prefix__}, :idx nil, :map-role :val} 12 | {:form [:text-view {:text __prefix__}], :idx 1}] 13 | include (fn [subelems list] (every? (fn [x] (some #(= % x) list)) subelems))] 14 | (testing "widgets" 15 | (is (include [":view" ":view-group"] (comp/candidates ":vi" *ns* widget-ctx))) 16 | (is (include [":progress-bar"] (comp/candidates ":progress" *ns* widget-ctx))) 17 | (is (= [] (comp/candidates ":bollocks" *ns* widget-ctx)))) 18 | 19 | (testing "attributes" 20 | (is (include [":text" ":text-size"] (comp/candidates ":te" *ns* attr-ctx))) 21 | (is (include [":layout-margin-left" ":layout-margin" ":layout-margin-top" 22 | ":layout-margin-right" ":layout-margin-bottom"] 23 | (comp/candidates ":layout-marg" *ns* attr-ctx))) 24 | ;; Unparsed widget-kw yields all attributes 25 | (is (= [":image"] (comp/candidates ":ima" *ns* attr2-ctx))) 26 | ;; Wrong widget kw 27 | (is (= [] (comp/candidates ":image" *ns* attr-ctx))) 28 | ) 29 | 30 | (testing "bad-context" 31 | (is (= [] (comp/candidates ":text" *ns* bad-ctx))) 32 | (is (= [] (comp/candidates ":vi" *ns* bad-ctx)))))) 33 | 34 | (deftest docs 35 | (is (re-matches #"(?s):text-view - android.widget.TextView\n.+" 36 | (comp/doc ":text-view" *ns*))) 37 | (is (re-matches #"(?s)^:id-holder -.+" (comp/doc ":id-holder" *ns*))) 38 | (is (re-matches #"(?s)^:linear-layout-params - .+" (comp/doc ":layout-weight" *ns*))) 39 | (is (= nil (comp/doc ":unrelated" *ns*)))) 40 | 41 | (deftest initialization 42 | (testing "wouldn't die without compliment" 43 | (is (= nil (comp/init-source))))) 44 | -------------------------------------------------------------------------------- /test/neko/data/t_shared_prefs.clj: -------------------------------------------------------------------------------- 1 | (ns neko.data.t-shared-prefs 2 | (:require [clojure.test :refer :all] 3 | [neko.data.shared-prefs :as sp]) 4 | (:import [android.content SharedPreferences SharedPreferences$Editor] 5 | org.robolectric.RuntimeEnvironment 6 | neko.App)) 7 | 8 | (set! App/instance RuntimeEnvironment/application) 9 | 10 | (deftest get-shared-preferences 11 | (is (instance? SharedPreferences (sp/get-shared-preferences "test" :private))) 12 | (is (instance? SharedPreferences (sp/get-shared-preferences "test" :world-writeable)))) 13 | 14 | (deftest edit-shared-preferences 15 | (let [prefs (sp/get-shared-preferences "test2" :private)] 16 | (-> (.edit prefs) 17 | (sp/put :foo "foo") 18 | (sp/put :bar 42) 19 | .commit) 20 | (is (= "foo" (.getString prefs "foo" ""))) 21 | (is (= 42 (.getLong prefs "bar" -1))))) 22 | 23 | (deftest defpreferences 24 | (sp/defpreferences sp-atom "test3") 25 | (is (instance? clojure.lang.Atom sp-atom)) 26 | (is (= {} @sp-atom)) 27 | (reset! sp-atom {:foo "foo" :bar 42}) 28 | (let [prefs (sp/get-shared-preferences "test3" :private)] 29 | (is (= "foo" (.getString prefs "foo" ""))) 30 | (is (= 42 (.getLong prefs "bar" -1)))) 31 | (swap! sp-atom assoc :foo "Foo") 32 | (let [prefs (sp/get-shared-preferences "test3" :private)] 33 | (is (= "Foo" (.getString prefs "foo" "")))) 34 | (swap! sp-atom dissoc :bar) 35 | (let [prefs (sp/get-shared-preferences "test3" :private)] 36 | (is (= -1 (.getLong prefs "bar" -1)))) 37 | (reset! sp-atom {}) 38 | (let [prefs (sp/get-shared-preferences "test3" :private)] 39 | (is (empty? (.getAll prefs))))) 40 | -------------------------------------------------------------------------------- /test/neko/data/t_sqlite.clj: -------------------------------------------------------------------------------- 1 | (ns neko.data.t-sqlite 2 | (:require [clojure.test :refer :all] 3 | [neko.data.sqlite :as db]) 4 | (:import android.app.Activity 5 | org.robolectric.RuntimeEnvironment 6 | neko.App)) 7 | 8 | (set! App/instance RuntimeEnvironment/application) 9 | 10 | (def schema 11 | (db/make-schema 12 | :name "test.db" 13 | :version 1 14 | :tables {:employees {:columns {:_id "integer primary key" 15 | :name "text not null" 16 | :vacation "boolean" 17 | :certificate "blob" 18 | :boss_id "integer"}} 19 | :bosses {:columns {:_id "integer primary key" 20 | :name "text not null"}}})) 21 | 22 | (deftest sqlite 23 | (def helper (db/create-helper (Activity.) schema)) 24 | (def db (db/get-database helper :write)) 25 | 26 | (db/transact* 27 | db (fn [] 28 | (db/insert db :employees {:name "Shelley Levene" 29 | :vacation false 30 | :certificate (.getBytes "quick brown fox")}) 31 | (db/insert db :employees {:name "Dave Moss" 32 | :vacation false}) 33 | (db/insert db :employees {:name "Ricky Roma" 34 | :vacation false}))) 35 | 36 | (is (= () (db/query-seq db :employees {:vacation true}))) 37 | 38 | (db/update db :employees {:vacation true} 39 | {:_id [:or 1 2]}) 40 | 41 | (is (= [{:_id 1, :name "Shelley Levene", :vacation true} 42 | {:_id 2, :name "Dave Moss", :vacation true}] 43 | (map #(select-keys % [:_id :name :vacation]) 44 | (db/query-seq db :employees {:vacation true})))) 45 | (is (= [{:name "Shelley Levene", :vacation true} 46 | {:name "Dave Moss", :vacation true}] 47 | (db/query-seq db [:name :vacation] :employees {:vacation true}))) 48 | 49 | (db/transact db 50 | (let [willid (db/insert db :bosses {:name "John Williamson"}) 51 | mmid (db/insert db :bosses {:name "Mitch and Murray"})] 52 | (db/update db :employees {:boss_id willid} {}) 53 | (db/insert db :employees {:name "Blake", :boss_id mmid, :vacation false}))) 54 | 55 | ;; For all employees not on vacation get their bosses. 56 | (is (= [{:employees/name "Ricky Roma", :bosses/name "John Williamson"} 57 | {:employees/name "Blake", :bosses/name "Mitch and Murray"}] 58 | (db/query-seq db [:employees/name :bosses/name] 59 | [:employees :bosses] 60 | {:employees/vacation false 61 | :employees/boss_id :bosses/_id}))) 62 | 63 | (is (= "Shelley Levene" (db/query-scalar db :name :employees {:_id 1}))) 64 | (is (= 4 (db/query-scalar db ["count" :_id] :employees nil)))) 65 | -------------------------------------------------------------------------------- /test/neko/dialog/t_alert.clj: -------------------------------------------------------------------------------- 1 | (ns neko.dialog.t-alert 2 | (:require [neko.dialog.alert :as dlg] 3 | [neko.activity :refer [defactivity]] 4 | [neko.listeners.dialog :as listeners] 5 | neko.t-activity 6 | [coa.droid-test :refer [deftest]] 7 | [clojure.test :refer :all :exclude [deftest]]) 8 | (:import android.app.AlertDialog 9 | android.content.DialogInterface 10 | [org.robolectric Robolectric RuntimeEnvironment])) 11 | 12 | (deftype OnClick [callback] 13 | android.content.DialogInterface$OnClickListener 14 | (onClick [this dialog which] 15 | (callback dialog which))) 16 | 17 | (deftest alert-dialog-builder 18 | (let [;; Need to redef a listener, otherwise Cloverage freaks out. 19 | dialog (with-redefs [listeners/on-click-call (fn [x] (OnClick. x))] 20 | (-> (dlg/alert-dialog-builder 21 | RuntimeEnvironment/application 22 | {:message "Dialog message" 23 | :cancelable true 24 | :positive-text "OK" 25 | :positive-callback (fn [dialog res] (is true)) 26 | :negative-text "Cancel" 27 | :negative-callback (fn [dialog res] (is true) (.cancel dialog)) 28 | :neutral-text "Maybe" 29 | :neutral-callback (fn [dialog res] (is true))}) 30 | .create))] 31 | (is (instance? AlertDialog dialog)) 32 | (.show dialog) 33 | (is (.isShowing dialog)) 34 | (.performClick (.getButton dialog DialogInterface/BUTTON_POSITIVE)) 35 | (.performClick (.getButton dialog DialogInterface/BUTTON_NEUTRAL)) 36 | (.performClick (.getButton dialog DialogInterface/BUTTON_NEGATIVE)) 37 | (is (not (.isShowing dialog))))) 38 | -------------------------------------------------------------------------------- /test/neko/listeners/t_adapter_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.listeners.t-adapter-view 2 | (:require [clojure.test :refer :all :exclude [deftest]] 3 | [neko.listeners.adapter-view :as l] 4 | [coa.droid-test :refer [deftest]]) 5 | (:import android.widget.ListView 6 | org.robolectric.RuntimeEnvironment)) 7 | 8 | (defmacro test-listener [& body] 9 | `(let [~'v (ListView. RuntimeEnvironment/application) 10 | ~'called (fn [] (is true))] 11 | ~@body)) 12 | 13 | (deftest on-item-click 14 | (test-listener 15 | (.onItemClick (l/on-item-click (is (= position 3)) 16 | (is (= id 20))) 17 | v v 3 20)) 18 | 19 | (test-listener 20 | (.onItemClick (l/on-item-click-call (fn [_ __ pos id] 21 | (is (= pos 3)) 22 | (is (= id 20)))) 23 | v v 3 20))) 24 | 25 | (deftest on-item-long-click 26 | (test-listener 27 | (.onItemLongClick (l/on-item-long-click (is (= position 3)) 28 | (is (= id 20))) 29 | v v 3 20)) 30 | 31 | (test-listener 32 | (.onItemLongClick (l/on-item-long-click-call (fn [_ __ pos id] 33 | (is (= pos 3)) 34 | (is (= id 20)))) 35 | v v 3 20))) 36 | 37 | (deftest on-item-selected 38 | (test-listener 39 | (.onItemSelected (l/on-item-selected (is (= position 3)) 40 | (is (= id 20))) 41 | v v 3 20)) 42 | 43 | (test-listener 44 | (.onItemSelected (l/on-item-selected-call (fn [_ __ pos id] 45 | (is (= pos 3)) 46 | (is (= id 20)))) 47 | v v 3 20))) 48 | -------------------------------------------------------------------------------- /test/neko/listeners/t_text_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.listeners.t-text-view 2 | (:require [clojure.test :refer :all :exclude [deftest]] 3 | [neko.listeners.text-view :as l] 4 | [coa.droid-test :refer [deftest]]) 5 | (:import [android.view KeyEvent MotionEvent] 6 | android.widget.TextView 7 | neko.App)) 8 | 9 | (defmacro test-listener [& body] 10 | `(let [~'v (TextView. App/instance) 11 | ~'called (fn [] (is true))] 12 | ~@body)) 13 | 14 | (deftest on-editor-action-call 15 | (let [test-code KeyEvent/KEYCODE_EQUALS 16 | test-event (KeyEvent. KeyEvent/ACTION_DOWN test-code)] 17 | (test-listener 18 | (.onEditorAction (l/on-editor-action (called) 19 | (is (= 42 action-id)) 20 | (is (= key-event test-event))) 21 | v 42 test-event)) 22 | 23 | (test-listener 24 | (.onEditorAction (l/on-editor-action-call (fn [_ action-id key-event] 25 | (called) 26 | (is (= 42 action-id)) 27 | (is (= key-event test-event)))) 28 | v 42 test-event)))) 29 | -------------------------------------------------------------------------------- /test/neko/listeners/t_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.listeners.t-view 2 | (:require [clojure.test :refer :all :exclude [deftest]] 3 | [neko.listeners.view :as l] 4 | [coa.droid-test :refer [deftest]]) 5 | (:import [android.view KeyEvent MotionEvent] 6 | org.robolectric.RuntimeEnvironment 7 | neko.App)) 8 | 9 | (defmacro test-listener [& body] 10 | `(let [~'v (android.view.View. RuntimeEnvironment/application) 11 | ~'called (fn [] (is true))] 12 | ~@body)) 13 | 14 | (deftest on-click 15 | (test-listener 16 | (.setOnClickListener v (l/on-click (called))) 17 | (.performClick v)) 18 | 19 | (test-listener 20 | (.setOnClickListener v (l/on-click-call (fn [_] (called)))) 21 | (.performClick v))) 22 | 23 | (deftest on-long-click 24 | (test-listener 25 | (.setOnLongClickListener v (l/on-long-click (called))) 26 | (.performLongClick v)) 27 | 28 | (test-listener 29 | (.setOnLongClickListener v (l/on-long-click-call (fn [_] (called)))) 30 | (.performLongClick v))) 31 | 32 | (deftest on-touch 33 | (let [touch-event (MotionEvent/obtain 50 300 MotionEvent/ACTION_UP 0 0 0)] 34 | (test-listener (.onTouch (l/on-touch (called)) v touch-event)) 35 | (test-listener (.onTouch (l/on-touch-call (fn [_ __] (called))) v touch-event)))) 36 | 37 | (deftest on-key 38 | (let [test-code KeyEvent/KEYCODE_EQUALS 39 | test-event (KeyEvent. KeyEvent/ACTION_DOWN test-code)] 40 | (test-listener 41 | (.onKey (l/on-key (called) 42 | (is (= key-code test-code))) 43 | v test-code test-event)) 44 | 45 | (test-listener 46 | (.onKey (l/on-key-call (fn [_ code __] (called) 47 | (is (= code test-code)))) 48 | v test-code test-event)))) 49 | -------------------------------------------------------------------------------- /test/neko/t_activity.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-activity 2 | (:require [clojure.test :refer :all :exclude [deftest]] 3 | [neko.activity :refer [defactivity] :as a] 4 | [neko.debug :as dbg] 5 | [neko.ui :as ui] 6 | [neko.find-view :refer [find-view]] 7 | [coa.droid-test :refer [deftest]]) 8 | (:import android.app.Activity 9 | android.os.Bundle 10 | android.view.View 11 | coa.droid_test.Helpers 12 | [android.widget LinearLayout TextView] 13 | [org.robolectric Robolectric RuntimeEnvironment] 14 | [org.robolectric.util ActivityController ComponentController])) 15 | 16 | (def simple-ui [:linear-layout {:orientation :vertical} 17 | [:text-view {:id ::tv 18 | :text "test"}]]) 19 | 20 | (defn top-level-view [activity] 21 | (-> (a/get-decor-view activity) 22 | (.findViewById android.R$id/content) 23 | (.getChildAt 0))) 24 | 25 | (defn make-activity [] 26 | (Robolectric/setupActivity Activity)) 27 | 28 | (deftest set-content-view 29 | (testing "set View objects" 30 | (let [activity (make-activity) 31 | view (View. RuntimeEnvironment/application)] 32 | (a/set-content-view! activity view) 33 | (is (= view (top-level-view activity))))) 34 | 35 | (testing "set layout IDs" 36 | (let [activity (make-activity)] 37 | (a/set-content-view! activity android.R$layout/simple_list_item_1) 38 | (is (= TextView (type (.findViewById activity android.R$id/text1)))))) 39 | 40 | (testing "set neko.ui trees" 41 | (let [activity (make-activity) 42 | neko-view (ui/make-ui RuntimeEnvironment/application simple-ui)] 43 | (is (nil? (find-view activity ::tv))) 44 | (a/set-content-view! activity simple-ui) 45 | (is (= TextView (type (find-view activity ::tv))))))) 46 | 47 | (deftest request-window-features 48 | (testing "empty" 49 | (let [activity (make-activity)] 50 | (is (= [] (a/request-window-features! activity))))) 51 | 52 | (testing "one feature" 53 | (defactivity neko.TestActivity 54 | (onCreate [this bundle] 55 | (.superOnCreate this bundle) 56 | (is (= [true] (a/request-window-features! this :progress))))) 57 | (Robolectric/setupActivity neko.TestActivity)) 58 | 59 | (testing "multiple features" 60 | (defactivity neko.TestActivity 61 | (onCreate [this bundle] 62 | (.superOnCreate this bundle) 63 | (is (= [true true] (a/request-window-features! this :progress :no-title))))) 64 | (Robolectric/setupActivity neko.TestActivity))) 65 | 66 | (definterface TestInterface 67 | (ifaceMethod [])) 68 | 69 | (deftest defactivity-tests 70 | (defactivity neko.DefActivity 71 | :implements [neko.t_activity.TestInterface] 72 | :key :defact 73 | :request-features [:no-title :progress] 74 | 75 | (onCreate [this bundle] 76 | (.superOnCreate this bundle) 77 | (is (instance? Activity this)) 78 | (is (= this (get dbg/all-activities :defact))) 79 | (is (= this (get dbg/all-activities 'neko.t-activity))) 80 | (is (= {} @(a/get-state this))) 81 | (swap! (a/get-state this) assoc :test "test")) 82 | 83 | (onStart [this] 84 | (.superOnStart this) 85 | (is (= "test" (:test @(a/get-state this))))) 86 | 87 | (onStop [this] 88 | (.superOnStop this) 89 | (is true)) 90 | 91 | (ifaceMethod [this] 92 | (is true))) 93 | 94 | (let [controller (-> (Robolectric/buildActivity neko.DefActivity) 95 | .setup .stop) 96 | activity (Helpers/getActivity controller)] 97 | (.ifaceMethod activity))) 98 | -------------------------------------------------------------------------------- /test/neko/t_context.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-context 2 | (:require [clojure.test :refer :all] 3 | [neko.context :as context]) 4 | (:import [android.app AlarmManager NotificationManager Activity] 5 | org.robolectric.RuntimeEnvironment 6 | neko.App)) 7 | 8 | (set! App/instance RuntimeEnvironment/application) 9 | 10 | (deftest sanity-check 11 | (is (= (.getApplication (Activity.)) 12 | RuntimeEnvironment/application)) 13 | (is (= (.getApplicationContext (.getApplication (Activity.))) 14 | RuntimeEnvironment/application))) 15 | 16 | (deftest get-service 17 | (is (instance? NotificationManager (context/get-service :notification))) 18 | (is (instance? AlarmManager 19 | (context/get-service RuntimeEnvironment/application :alarm)))) 20 | 21 | -------------------------------------------------------------------------------- /test/neko/t_data.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-data 2 | (:require [neko.data :as data] 3 | [neko.data.shared-prefs :as sp] 4 | [neko.intent :as intent] 5 | [clojure.test :refer :all]) 6 | (:import org.robolectric.RuntimeEnvironment 7 | neko.App)) 8 | 9 | (deftest bundle-like-map 10 | (let [extras {:user "Joe" :age 37} 11 | intent (intent/intent "foo.MAIN" extras)] 12 | (is (= (:user extras) (:user (data/like-map intent)))) 13 | (is (= extras (into {} (data/like-map intent)))) 14 | (is (= extras (into {} (data/like-map (.getExtras intent))))) 15 | 16 | (is (= {} (into {} (data/like-map (intent/intent "bar.MAIN" {}))))) 17 | (is (= {} (data/like-map nil))))) 18 | 19 | (set! App/instance RuntimeEnvironment/application) 20 | 21 | (deftest sp-like-map 22 | (let [sp (sp/get-shared-preferences "testprefs" :private)] 23 | (-> sp .edit 24 | (sp/put :name "Longcat") 25 | (sp/put :length-in-feet 10000) 26 | .commit) 27 | (is (= {:name "Longcat", :length-in-feet 10000} (into {} (data/like-map sp)))))) 28 | 29 | -------------------------------------------------------------------------------- /test/neko/t_debug.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-debug 2 | (:require [neko.debug :as dbg] 3 | [neko.activity :refer [defactivity]] 4 | [clojure.test :refer :all]) 5 | (:import [org.robolectric Robolectric RuntimeEnvironment] 6 | org.robolectric.shadows.ShadowToast 7 | android.view.WindowManager$LayoutParams 8 | neko.App)) 9 | 10 | (deftest *a-and-keep-screen 11 | (defactivity neko.DebugActivity 12 | :key :test-debug 13 | (onCreate [this bundle] 14 | (.superOnCreate this bundle) 15 | (dbg/keep-screen-on this))) 16 | 17 | (let [activity (Robolectric/setupActivity neko.DebugActivity)] 18 | (is (= activity (dbg/*a))) 19 | (is (= activity (dbg/*a :test-debug))) 20 | (is (not= 0 (bit-and WindowManager$LayoutParams/FLAG_KEEP_SCREEN_ON 21 | (.getForcedWindowFlags (.getWindow activity))))))) 22 | 23 | (set! App/instance RuntimeEnvironment/application) 24 | 25 | (deftest safe-for-ui 26 | (ShadowToast/reset) 27 | (is (thrown? ArithmeticException (/ 1 0))) 28 | (is (= 1 (dbg/safe-for-ui (/ 2 2)))) 29 | (is (not (dbg/safe-for-ui (/ 1 0)))) 30 | (is (= 1 (ShadowToast/shownToastCount))) 31 | (is (instance? ArithmeticException (dbg/ui-e)))) 32 | 33 | (deftest safe-for-ui* 34 | (ShadowToast/reset) 35 | (let [wrapped (dbg/safe-for-ui* (fn [] (/ 1 0)))] 36 | (is (function? wrapped)) 37 | (is (= 0 (ShadowToast/shownToastCount))) ;; Not called yet 38 | (wrapped) 39 | (is (= 1 (ShadowToast/shownToastCount))) ;; Failed 40 | (is (instance? ArithmeticException (dbg/ui-e))))) 41 | -------------------------------------------------------------------------------- /test/neko/t_doc.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-doc 2 | (:require [neko.doc :as doc] 3 | [clojure.test :refer :all]) 4 | (:import java.io.StringWriter)) 5 | 6 | (defmacro capture-out [& body] 7 | `(let [out# (StringWriter.)] 8 | (binding [*out* out#] 9 | ~@body) 10 | (str out#))) 11 | 12 | (deftest describe 13 | (is (instance? String (capture-out (doc/describe)))) 14 | (is (re-matches #"(?s)^\nTraits found:\n :on-click.+" (capture-out (doc/describe :on-click)))) 15 | (is (re-matches #"(?s)^Elements found:\n :edit-text.+" (capture-out (doc/describe :edit-text)))) 16 | (is (re-matches #"^No elements or traits.+" (capture-out (doc/describe :nothing)))) 17 | (is (< 2000 (count (capture-out (doc/describe :text-view :verbose)))))) 18 | -------------------------------------------------------------------------------- /test/neko/t_find_view.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-find-view 2 | (:require [clojure.test :refer :all] 3 | [neko.find-view :as fv] 4 | [neko.ui :as ui]) 5 | (:import [android.widget TextView EditText Button] 6 | org.robolectric.RuntimeEnvironment 7 | neko.App)) 8 | 9 | (def simple-ui [:linear-layout {:id-holder true 10 | :orientation :vertical} 11 | [:relative-layout {:id ::rel 12 | :id-holder true} 13 | [:text-view {:id ::tv-in-rel 14 | :text "test"}] 15 | [:edit-text {:id ::et-in-rel 16 | :hint "test"}]] 17 | [:button {:id ::but 18 | :text "Button"}]]) 19 | 20 | (deftest find-view 21 | (let [view (ui/make-ui RuntimeEnvironment/application simple-ui)] 22 | (is (instance? Button (fv/find-view view ::but))) 23 | (is (nil? (fv/find-view view ::tv-in-rel))) 24 | 25 | (let [rel (fv/find-view view ::rel) 26 | [tv et] (fv/find-views rel ::tv-in-rel ::et-in-rel)] 27 | (is (instance? TextView tv)) 28 | (is (instance? EditText et))))) 29 | 30 | -------------------------------------------------------------------------------- /test/neko/t_intent.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-intent 2 | (:require [clojure.test :refer :all] 3 | [neko.intent :as intent] 4 | neko.t-activity) 5 | (:import android.content.Intent 6 | android.os.Bundle 7 | org.robolectric.RuntimeEnvironment)) 8 | 9 | (deftest intent-creation 10 | (is (instance? Intent (intent/intent "foo.bar.MAIN" {}))) 11 | (is (instance? Intent (intent/intent RuntimeEnvironment/application 12 | neko.DefActivity {:user "Joe"}))) 13 | ;; Can't really test with the (app-package-name) 14 | (is (thrown? NullPointerException (intent/intent RuntimeEnvironment/application 15 | '.DefActivity {:user "Joe"})))) 16 | 17 | (deftest put-extras 18 | (let [i (intent/intent "foo.MAIN" {:user "Joe", :age 37, :gpa 4.5, :rank (int 3) 19 | :employed true :bundle (Bundle.)})] 20 | (is (instance? Bundle (.getExtras i))) 21 | (is (= "Joe" (.getString (.getExtras i) "user"))) 22 | (is (= 37 (.getLong (.getExtras i) "age"))) 23 | (is (= 4.5 (.getDouble (.getExtras i) "gpa"))) 24 | (is (= 3 (.getInt (.getExtras i) "rank"))) 25 | (is (= true (.getBoolean (.getExtras i) "employed"))) 26 | (is (instance? Bundle (.getBundle (.getExtras i) "bundle"))))) 27 | -------------------------------------------------------------------------------- /test/neko/t_log.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-log 2 | (:require [clojure.test :refer :all] 3 | [coa.droid-test :refer [unstub]] 4 | [neko.log :as log]) 5 | (:import org.robolectric.shadows.ShadowLog 6 | android.util.Log)) 7 | 8 | (ShadowLog/setupLogging) 9 | 10 | (defmacro capture-log [& body] 11 | `(let [stream# (java.io.ByteArrayOutputStream.)] 12 | (ShadowLog/reset) 13 | (set! ShadowLog/stream (java.io.PrintStream. stream#)) 14 | ~@body 15 | (.close stream#) 16 | (str stream#))) 17 | 18 | (deftest simple-logging-test 19 | (is (= (capture-log (log/d "message")) 20 | "D/neko.t-log: message\n")) 21 | (is (= (capture-log (log/e "message")) 22 | "E/neko.t-log: message\n")) 23 | (is (= (capture-log (log/i "message")) 24 | "I/neko.t-log: message\n")) 25 | (is (= (capture-log (log/v "message")) 26 | "V/neko.t-log: message\n")) 27 | (is (= (capture-log (log/w "message")) 28 | "W/neko.t-log: message\n"))) 29 | 30 | (deftest extra-options-test 31 | (is (= (capture-log (log/d "message" :tag "tag")) 32 | "D/tag: message\n")) 33 | 34 | (let [e (Exception.)] 35 | (is (.startsWith (capture-log (log/e "message" :tag "tag" :exception e)) 36 | "E/tag: message\n")) 37 | (is (= (.throwable (.get (ShadowLog/getLogs) 0)) e)))) 38 | 39 | (deftest string-concatenation-and-pprint 40 | (is (= (capture-log (log/i "quick" "brown" "fox" :tag "tag")) 41 | "I/tag: quick brown fox\n")) 42 | 43 | (is (= (capture-log (log/v "Lazy list is expanded:" (take 5 (range 100)) :tag "tag")) 44 | "V/tag: Lazy list is expanded: (0 1 2 3 4)\n"))) 45 | -------------------------------------------------------------------------------- /test/neko/t_notify.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-notify 2 | (:require [clojure.test :refer :all] 3 | [neko.context :as ctx] 4 | [neko.notify :as notify] 5 | [neko.-utils :as u]) 6 | (:import android.app.Activity 7 | org.robolectric.Shadows 8 | org.robolectric.shadows.ShadowToast 9 | neko.App)) 10 | 11 | (deftest disguised-toast 12 | (ShadowToast/reset) 13 | (is (= 0 (ShadowToast/shownToastCount))) 14 | (notify/toast "Disguised toast" :short) 15 | (notify/toast App/instance "Disguised toast" :long) 16 | (notify/toast "Disguised toast" :long) 17 | (notify/toast (Activity.) "Disguised toast" :long) 18 | (is (= 4 (ShadowToast/shownToastCount)))) 19 | 20 | (deftest notifications 21 | (let [nm (ctx/get-service :notification) 22 | n (notify/notification {:content-title "Title" 23 | :content-text "Text" 24 | :action [:activity "foo.bar.MAIN"]})] 25 | (notify/fire ::test n) 26 | (is (= n (.getNotification (Shadows/shadowOf nm) nil (u/int-id ::test)))) 27 | (notify/cancel ::test) 28 | (is (= nil (.getNotification (Shadows/shadowOf nm) nil (u/int-id ::test)))))) 29 | -------------------------------------------------------------------------------- /test/neko/t_threading.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-threading 2 | (:require [clojure.test :refer :all] 3 | [coa.droid-test :as dt] 4 | [neko.threading :as t]) 5 | (:import android.view.View 6 | org.robolectric.RuntimeEnvironment 7 | neko.App)) 8 | 9 | (deftest ui-thread 10 | (is (t/on-ui-thread?)) 11 | 12 | ;; Should execute immediately. 13 | (let [thread (Thread/currentThread)] 14 | (t/on-ui 15 | (is (= thread (Thread/currentThread))))) 16 | 17 | (future 18 | (is (not (t/on-ui-thread?))) 19 | (t/on-ui 20 | (is (t/on-ui-thread?)))) 21 | 22 | (future 23 | (t/on-ui* 24 | (fn [] (is (t/on-ui-thread?)))))) 25 | 26 | (dt/deftest post 27 | (let [pr (promise)] 28 | (t/post (View. RuntimeEnvironment/application) 29 | (is (t/on-ui-thread?)) 30 | (deliver pr :success)) 31 | (is (= :success (deref pr 10000 :fail)))) 32 | 33 | ;; Can't really test post-delayed from Robolectric 34 | (t/post-delayed (View. RuntimeEnvironment/application) 1000 nil)) 35 | -------------------------------------------------------------------------------- /test/neko/t_ui.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-ui 2 | (:require [neko.ui :as ui] 3 | [neko.activity :refer [defactivity]] 4 | [neko.-utils :as utils] 5 | [clojure.test :refer :all]) 6 | (:import [android.widget Button LinearLayout TextView] 7 | android.content.pm.ActivityInfo 8 | android.view.View 9 | neko.App 10 | [org.robolectric Robolectric RuntimeEnvironment])) 11 | 12 | (deftest apply-default-setters-from-attributes 13 | (let [v (Button. RuntimeEnvironment/application)] 14 | (is (= View/VISIBLE (.getVisibility v))) 15 | (is (.isEnabled v)) 16 | (ui/apply-default-setters-from-attributes 17 | :button v {:visibility View/GONE, :enabled false}) 18 | (is (not (.isEnabled v))) 19 | (is (= View/GONE (.getVisibility v))))) 20 | 21 | (deftest apply-attributes 22 | (let [v (Button. RuntimeEnvironment/application)] 23 | (is (.isEnabled v)) 24 | (is (= "" (.getText v))) 25 | (ui/apply-attributes :button v 26 | {:enabled false, :text "hello"} {}) 27 | (is (not (.isEnabled v))) 28 | (is (= "hello" (.getText v)))) 29 | 30 | (let [ll (LinearLayout. RuntimeEnvironment/application)] 31 | (is (= {:container-type :linear-layout 32 | :id-holder ll} 33 | (ui/apply-attributes :linear-layout ll {:id-holder true} {}))))) 34 | 35 | (deftest construct-element 36 | (is (instance? Button (ui/construct-element 37 | :button RuntimeEnvironment/application [])))) 38 | 39 | (deftest make-ui-element 40 | (is (thrown? AssertionError (ui/make-ui-element RuntimeEnvironment/application 41 | ['button {:foo "bar"}] {}))) 42 | (is (thrown? AssertionError (ui/make-ui-element RuntimeEnvironment/application 43 | [:button [:foo "bar"]] {}))) 44 | (let [v (Button. RuntimeEnvironment/application)] 45 | (= v (ui/make-ui-element RuntimeEnvironment/application v {}))) 46 | 47 | (let [v (ui/make-ui-element RuntimeEnvironment/application 48 | [:button {:text "hello"}] {})] 49 | (instance? Button v) 50 | (is (= "hello" (.getText v)))) 51 | 52 | (let [v (ui/make-ui-element RuntimeEnvironment/application 53 | [:button {:custom-constructor 54 | (fn [ctx] (doto (Button. ctx) 55 | (.setVisibility View/GONE)))}] 56 | {})] 57 | (instance? Button v) 58 | (is (= View/GONE (.getVisibility v))))) 59 | 60 | (deftest make-ui 61 | (let [v (ui/make-ui RuntimeEnvironment/application 62 | [:linear-layout {:id-holder true 63 | :orientation :vertical} 64 | [:button {:id ::hello 65 | :text "hello"}]])] 66 | (is (instance? LinearLayout v)) 67 | (is (= LinearLayout/VERTICAL (.getOrientation v))) 68 | (let [tag (.getTag v) 69 | b (get tag ::hello)] 70 | (is (instance? Button b)) 71 | (is (= (utils/int-id ::hello) (.getId b))) 72 | (is (= "hello" (.getText b)))))) 73 | 74 | (deftest config 75 | (let [v (ui/make-ui RuntimeEnvironment/application [:button {:text "hello"}])] 76 | (is (= View/VISIBLE (.getVisibility v))) 77 | (is (= "hello" (.getText v))) 78 | (ui/config v :text "updated" :visibility View/GONE) 79 | (is (= "updated" (.getText v))) 80 | (is (= View/GONE (.getVisibility v))))) 81 | 82 | (deftest inflate-layout 83 | (is (instance? TextView (ui/inflate-layout RuntimeEnvironment/application 84 | android.R$layout/simple_list_item_1)))) 85 | 86 | (set! App/instance RuntimeEnvironment/application) 87 | 88 | (deftest get-screen-orientation 89 | ;; Robolectric always returns :undefined on orientation, oh well 90 | (is (= :undefined (ui/get-screen-orientation RuntimeEnvironment/application))) 91 | (is (= :undefined (ui/get-screen-orientation)))) 92 | -------------------------------------------------------------------------------- /test/neko/t_utils.clj: -------------------------------------------------------------------------------- 1 | (ns neko.t-utils 2 | (:require [clojure.test :refer :all] 3 | [neko.-utils :as u])) 4 | 5 | (deftest int-id 6 | (is (= (u/int-id :foo) (u/int-id :foo))) 7 | (is (not= (u/int-id :foo) (u/int-id :bar))) 8 | (every? pos? (map u/int-id [:foo :bar :baz ::qux :foo/bar :q.w.e.r]))) 9 | 10 | (deftest simple-name 11 | (is (= "Context" (u/simple-name 'android.context.Context))) 12 | (is (= "Activity" (u/simple-name 'Activity))) 13 | (is (= "App" (u/simple-name 'neko.App)))) 14 | 15 | (deftest capitalize 16 | (is (= "Foo" (u/capitalize "foo"))) 17 | (is (= "OnCreate" (u/capitalize "onCreate")))) 18 | 19 | (deftest unicaseize 20 | (is (= "onCreate" (u/unicaseize "OnCreate")))) 21 | 22 | (deftest keyword->static-field 23 | (is (= "VERTICAL" (u/keyword->static-field :vertical))) 24 | (is (= "SCREEN_SIZE" (u/keyword->static-field :screen-size)))) 25 | 26 | (deftest keyword->camelcase 27 | (is (= "onClick" (u/keyword->camelcase :on-click))) 28 | (is (= "getPositiveButton" (u/keyword->camelcase :get-positive-button)))) 29 | 30 | (deftest keyword->setter 31 | (is (= "setOnClickListener" (u/keyword->setter :on-click-listener))) 32 | (is (= "setPositiveButton" (u/keyword->setter :positive-button)))) 33 | 34 | (deftest reflect-setter 35 | (is (instance? java.lang.reflect.Method (u/reflect-setter String "indexOf" Integer/TYPE))) 36 | (is (thrown? NoSuchMethodException (u/reflect-setter String "nonExisting" Integer/TYPE)))) 37 | 38 | (deftest call-if-nnil 39 | (let [f nil] 40 | (is (not (u/call-if-nnil f 1 2)))) 41 | (let [f +] 42 | (is (u/call-if-nnil f 1 2)))) 43 | 44 | (u/memoized 45 | (defn plus "Adds two numbers" 46 | [x y] 47 | (Thread/sleep 500) 48 | (+ x y))) 49 | 50 | (deftest memoized 51 | (let [a (System/currentTimeMillis) 52 | _ (is (= 5 (plus 2 3))) 53 | b (System/currentTimeMillis) 54 | _ (is (= 5 (plus 2 3))) 55 | c (System/currentTimeMillis)] 56 | (is (< 300 (- b a))) 57 | (is (> 100 (- c b))))) 58 | -------------------------------------------------------------------------------- /test/neko/ui/t_adapters.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.t-adapters 2 | (:require [neko.ui.adapters :refer :all] 3 | [neko.ui :refer [config]] 4 | [neko.data.sqlite :as db] 5 | [clojure.test :refer :all]) 6 | (:import [neko.ui.adapters InterchangeableListAdapter TaggedCursorAdapter] 7 | [android.widget ListView TextView] 8 | neko.App)) 9 | 10 | (deftest ref-adapter-tests 11 | (testing "simple adapter" 12 | (let [ref (atom ["one" "two" "three"]) 13 | adapter (ref-adapter (fn [ctx] 14 | [:text-view {}]) 15 | (fn [pos v _ data] 16 | (config v :text data)) 17 | ref)] 18 | (instance? InterchangeableListAdapter adapter) 19 | (is (= 3 (.getCount adapter))) 20 | (let [item (.getView adapter 0 nil (ListView. App/instance))] 21 | (is (instance? TextView item)) 22 | (is (= "one" (.getText item)))) 23 | (swap! ref conj "four") 24 | (let [new-item (.getView adapter 3 nil (ListView. App/instance))] 25 | (is (instance? TextView new-item)) 26 | (is (= "four" (.getText new-item)))))) 27 | 28 | (testing "ref access-fn" 29 | (let [ref (atom {:items ["one" "two" "three"] :extra 42}) 30 | adapter (ref-adapter (fn [ctx] 31 | [:text-view {}]) 32 | (fn [pos v _ data] 33 | (config v :text data)) 34 | ref 35 | :items)] 36 | (is (= 3 (.getCount adapter))) 37 | (swap! ref update-in [:items] conj "four") 38 | (is (= 4 (.getCount adapter))))) 39 | 40 | (testing "doesn't die on exceptions" 41 | (let [ref (atom {:items ["one" "two" "three"] :extra 42}) 42 | adapter (ref-adapter (fn [_] (/ 1 0)) (constantly nil) 43 | ref :items)] 44 | (is (= 3 (.getCount adapter))) 45 | (let [item (.getView adapter 0 nil (ListView. App/instance))] 46 | ;; Should return a dummy view 47 | (is (instance? android.view.View item)))) 48 | (let [ref (atom {:items ["one" "two" "three"] :extra 42}) 49 | adapter (ref-adapter (constantly (TextView. App/instance)) 50 | (fn [_ _ _ _] (/ 1 0)) ref :items)] 51 | (is (= 3 (.getCount adapter))) 52 | (let [item (.getView adapter 0 nil (ListView. App/instance))] 53 | ;; Item test shouldn't change 54 | (is (= "" (.getText item)))))) 55 | 56 | (testing "wrong inputs" 57 | (is (thrown? AssertionError (ref-adapter nil nil nil nil))) 58 | (is (thrown? AssertionError (ref-adapter (fn []) nil nil nil))) 59 | (is (thrown? AssertionError (ref-adapter (fn []) (fn []) [1 2 3] nil))) 60 | (is (thrown? AssertionError (ref-adapter (fn []) (fn []) (atom []) 42))))) 61 | 62 | (deftest cursor-adapter-tests 63 | (let [schema (db/make-schema 64 | :name "adapters_test.db" 65 | :version 1 66 | :tables {:numbers {:columns {:_id "integer primary key" 67 | :name "text not null"}}}) 68 | helper (db/create-helper App/instance schema) 69 | db (db/get-database helper :write) 70 | get-view (fn [adapter] (let [cursor (.getCursor adapter) 71 | v (.newView adapter App/instance cursor 72 | (ListView. App/instance))] 73 | (.bindView adapter v App/instance cursor) 74 | v))] 75 | (db/transact 76 | db (doall (map #(db/insert db :numbers {:name %}) 77 | ["one" "two" "three"]))) 78 | (testing "explicit cursor" 79 | (let [cursor (db/query db :numbers {}) 80 | adapter (cursor-adapter App/instance 81 | (fn [] [:text-view {}]) 82 | (fn [v _ data] (config v :text (:name data))) 83 | cursor)] 84 | 85 | (instance? TaggedCursorAdapter adapter) 86 | (is (= 3 (.getCount adapter))) 87 | (.moveToFirst cursor) 88 | (let [item (get-view adapter)] 89 | (is (instance? TextView item)) 90 | (is (= "one" (.getText item)))) 91 | 92 | (db/insert db :numbers {:name "four"}) 93 | (is (= 3 (.getCount adapter))) ;; Not changed because we haven't updated 94 | 95 | (update-cursor adapter (db/query db :numbers {})) 96 | (is (= 4 (.getCount adapter))) 97 | (.moveToFirst (.getCursor adapter)) 98 | (dotimes [i 3] (.moveToNext (.getCursor adapter))) 99 | (let [new-item (get-view adapter)] 100 | (is (instance? TextView new-item)) 101 | (is (= "four" (.getText new-item)))))) 102 | 103 | (testing "cursor-fn" 104 | (let [adapter (cursor-adapter App/instance 105 | (fn [] [:text-view {}]) 106 | (fn [v _ data] (config v :text (str data))) 107 | (fn [] (db/query db :numbers {})))] 108 | (is (= 4 (.getCount adapter))) 109 | (db/insert db :numbers {:name "four"}) 110 | (is (= 4 (.getCount adapter))) ;; Not changed because we haven't updated 111 | (update-cursor adapter) 112 | (is (= 5 (.getCount adapter))))) 113 | 114 | (testing "doesn't die on exceptions" 115 | (let [adapter (cursor-adapter App/instance (fn [] (/ 1 0)) 116 | (constantly nil) 117 | (db/query db :numbers {}))] 118 | (is (= 5 (.getCount adapter))) 119 | (.moveToFirst (.getCursor adapter)) 120 | (is (instance? android.view.View (get-view adapter)))) 121 | 122 | (let [adapter (cursor-adapter App/instance (fn [] (TextView. App/instance)) 123 | (fn [_ _ _] (/ 1 0)) 124 | (db/query db :numbers {}))] 125 | (is (= 5 (.getCount adapter))) 126 | ;; Item test shouldn't change 127 | (.moveToFirst (.getCursor adapter)) 128 | (is (= "" (.getText (get-view adapter)))))))) 129 | -------------------------------------------------------------------------------- /test/neko/ui/t_listview.clj: -------------------------------------------------------------------------------- 1 | (ns neko.ui.t-listview 2 | (:require [neko.ui.listview :as lv] 3 | [neko.ui :as ui] 4 | [neko.ui.adapters :refer [ref-adapter]] 5 | [clojure.test :refer :all]) 6 | (:import [android.widget ListView CheckBox] 7 | neko.App)) 8 | 9 | (deftest get-checked 10 | (let [ref (atom (range 5)) 11 | adapter (ref-adapter (fn [c] (CheckBox. c)) 12 | (fn [pos v _ data] (.setText v (str data))) 13 | ref) 14 | v (ui/make-ui App/instance 15 | [:list-view {:adapter adapter 16 | :choice-mode ListView/CHOICE_MODE_MULTIPLE}])] 17 | (is (= [] (lv/get-checked v))) 18 | (.setItemChecked v 1 true) 19 | (.setItemChecked v 3 true) 20 | (is (= [1 3] (lv/get-checked v))) 21 | (is (= ["one" "three"] (lv/get-checked v ["zero" "one" "two" "three" "four"]))))) 22 | 23 | (deftest set-checked 24 | (let [ref (atom (range 10)) 25 | adapter (ref-adapter (fn [c] (CheckBox. c)) 26 | (fn [pos v _ data] (.setText v (str data))) 27 | ref) 28 | v (ui/make-ui App/instance 29 | [:list-view {:adapter adapter 30 | :choice-mode ListView/CHOICE_MODE_MULTIPLE}])] 31 | (is (= [] (lv/get-checked v))) 32 | (lv/set-checked! v [4 5 6]) 33 | (is (= [4 5 6] (lv/get-checked v))))) 34 | --------------------------------------------------------------------------------