neko

3.0.1


Neko is a toolkit designed to make Android development using Clojure easier and more fun.

dependencies

org.clojure-android/clojure
1.5.1-jb



(this space intentionally left almost blank)
 

Internal utilities used by Neko, not intended for external consumption.

(ns neko.-utils
  {:author "Daniel Solano Gómez"}
  (:require [clojure.string :as string])
  (:import [java.lang.reflect Method Constructor Field]))

Takes a defn definition and memoizes it preserving its metadata.

(defmacro memoized
  [func-def]
  (let [fn-name (second func-def)]
    `(do ~func-def
         (let [meta# (meta (var ~fn-name))]
           (def ~fn-name (memoize ~fn-name))
           (reset-meta! (var ~fn-name) meta#)))))

Takes a keyword and converts it to a field name by getting the name from the keyword, converting all hyphens to underscores, capitalizing all letters, and applying the transformation function.

(defn static-field-value
  ([^Class class field xform]
   {:pre [(class? class)
          (keyword? field)
          (fn? xform)]}
   (let [field (.. (name field) (replace \- \_) toUpperCase)
         field ((fn [x] {:post [(string? %)]} (xform x)) field)
         field (.getField class ^String field)]
     (.get field nil)))
  ([class field]
   (static-field-value class field identity)))

Convenient method for testing if the argument is an integer or a keyword.

(defn integer-or-keyword?
  [x]
  (or (integer? x)
      (keyword? x)))

Takes a possibly package-qualified class name symbol and returns a simple class name from it.

(defn simple-name
  [full-class-name]
  (nth (re-find #"(.*\.)?(.+)" (str full-class-name)) 2))

Takes a string and upper-cases the first letter in it.

(defn capitalize
  [s]
  (str (.toUpperCase (subs s 0 1)) (subs s 1)))

Takes a string lower-cases the first letter in it.

(defn unicaseize
  [s]
  (str (.toLowerCase (subs s 0 1)) (subs s 1)))

Takes a keyword and transforms it into a static field name.

All letters in keyword are capitalized, and all dashes are replaced with underscores.

(memoized
 (defn keyword->static-field
   [kw]
   (.toUpperCase (string/replace (name kw) \- \_))))

Takes a keyword and transforms its name into camelCase.

(defn keyword->camelcase
  [kw]
  (let [[first & rest] (string/split (name kw) #"-")]
    (string/join (cons first (map capitalize rest)))))

Takes a keyword and transforms it into a setter method name.

Transforms keyword name into camelCase, capitalizes the first character and appends "set" at the beginning.

(memoized
 (defn keyword->setter
   [kw]
   (->> (keyword->camelcase kw)
        capitalize
        (str "set"))))

Expands into check whether function is defined, then executes it and returns true or just returns false otherwise.

(defmacro call-if-nnil
  [f & arguments]
  `(if ~f
     (do (~f ~@arguments)
         true)
     false))

Reflection functions

(defn class-or-type [cl]
  (condp = cl
    Boolean Boolean/TYPE
    Integer Integer/TYPE
    Long Integer/TYPE
    Double Double/TYPE
    Float Float/TYPE
    Character Character/TYPE
    cl))

Returns a Method object for the given UI object class, method name and the first argument type.

(defn ^Method reflect-setter
  [^Class widget-type, ^String method-name, ^Class value-type]
  (if-not (= widget-type Object)
    (let [value-type (class-or-type value-type)
          all-value-types (cons value-type (supers value-type))]
      (loop [[t & r] all-value-types]
        (if t
          (if-let [method (try
                            (.getDeclaredMethod widget-type method-name
                                                (into-array Class [t]))
                            (catch NoSuchMethodException e nil))]
            method
            (recur r))
          (reflect-setter (.getSuperclass widget-type)
                          method-name value-type))))
    (throw
     (NoSuchMethodException. (format "Couldn't find method .%s for argument %s)"
                                     method-name (.getName value-type))))))

Returns a Constructor object for the given UI object class

(defn ^Constructor reflect-constructor
  [^Class widget-type constructor-arg-types]
  (.getConstructor widget-type (into-array Class constructor-arg-types)))

Returns a field value for the given UI object class and the name of the field.

(defn reflect-field
  [^Class widget-type, ^String field-name]
  (.get ^Field (.getDeclaredField widget-type field-name) nil))
 

Provides utilities to manipulate application ActionBar.

(ns neko.action-bar
  (:require neko.ui)
  (:use [neko.ui.mapping :only [defelement]]
        [neko.ui.traits :only [deftrait]]
        [neko.-utils :only [call-if-nnil]])
  (:import [android.app ActionBar ActionBar$Tab ActionBar$TabListener]
           android.app.Activity android.app.Fragment android.R$id))

Listener helpers

Creates a TabListener from the provided functions for selected, unselected and reselected events.

(defn tab-listener
  [& {:keys [on-tab-selected on-tab-unselected on-tab-reselected]}]
  (reify ActionBar$TabListener
    (onTabSelected [this tab ft]
      (call-if-nnil on-tab-selected tab ft))
    (onTabUnselected [this tab ft]
      (call-if-nnil on-tab-unselected tab ft))
    (onTabReselected [this tab ft]
      (call-if-nnil on-tab-reselected tab ft))))

Creates a TabListener that shows the specified fragment on selecting and hides it on deselecting.

(defn simple-tab-listener
  [tag, ^Fragment fragment]
  (reify ActionBar$TabListener
    (onTabReselected [this tab ft])
    (onTabUnselected [this tab ft]
      (when (.isAdded fragment)
        (.detach ft fragment)))
    (onTabSelected [this tab ft]
      (if (.isDetached fragment)
        (.attach ft fragment)
        (.add ft R$id/content fragment tag)))))

Functions for declarative definition

(defelement :action-bar
  :classname android.app.ActionBar
  :inherits nil
  :traits [:tabs :display-options]
  :values {:standard ActionBar/NAVIGATION_MODE_STANDARD
           :list ActionBar/NAVIGATION_MODE_LIST
           :tabs ActionBar/NAVIGATION_MODE_TABS})
(defelement :action-bar-tab
  :classname android.app.ActionBar$Tab
  :inherits nil
  :traits [:tab-listener])

Takes :tabs attribute which should be a sequence of tab definitions. Each tab definition is itself a sequence of :tab keyword and an attribute map. Creates tabs for the definitions and adds the tabs to the action bar.

(deftrait :tabs
  [^ActionBar action-bar, {:keys [tabs]} _]
  (doseq [[_ tab-attributes] tabs
          :let [tab (.newTab action-bar)]]
    (neko.ui/apply-attributes :action-bar-tab tab tab-attributes {})
    (.addTab action-bar tab)))

Returns an integer value for the given keyword, or the value itself.

(defn display-options-value
  [value]
  (if (keyword? value)
    (case value
      :home-as-up  ActionBar/DISPLAY_HOME_AS_UP
      :show-home   ActionBar/DISPLAY_SHOW_HOME
      :show-custom ActionBar/DISPLAY_SHOW_CUSTOM
      :show-title  ActionBar/DISPLAY_SHOW_TITLE
      :use-logo    ActionBar/DISPLAY_USE_LOGO)
    value))

Takes :display-options attribute, which could be an integer value or one of the following keywords: :home-as-up, :show-home, :show-custom, :show-title, :use-logo, or a vector with these values, to which bit-or operation will be applied.

(deftrait :display-options
  [^ActionBar action-bar, {:keys [display-options]} _]
  (let [value (if (vector? display-options)
                (apply bit-or (map display-options-value display-options))
                (display-options-value display-options))]
    (.setDisplayOptions action-bar value)))

Takes :tab-listener attribute which should be TabListener object and sets it to the tab. Attribute could also be a fragment, in which case a listener would be created that shows and hides the fragment on tab selection and deselection respectively.

(deftrait :tab-listener
  [^ActionBar$Tab tab, {:keys [tab-listener]} _]
  (let [listener (if (instance? Fragment tab-listener)
                   (simple-tab-listener (str tab-listener) tab-listener)
                   tab-listener)]
    (.setTabListener tab listener)))

Configures activity's action bar according to the attributes provided in key-value fashion. For more information, see (describe :action-bar).

(defn setup-action-bar
  [^Activity activity, attributes-map]
  (let [action-bar (.getActionBar activity)]
    (neko.ui/apply-attributes :action-bar action-bar attributes-map {})))
 

Utilities to aid in working with an activity.

(ns neko.activity
  {:author "Daniel Solano Gómez"}
  (:import android.app.Activity
           android.view.View
           android.app.Fragment)
  (:require neko.init)
  (:use [neko.ui :only [make-ui]]
        neko.-utils))

The current activity to operate on.

(def
  ^{:doc 
    :dynamic true}
  *activity*)

Evaluates body such that activity is bound to the given activity.

(defmacro with-activity
  [activity & body]
  `(binding [*activity* ~activity]
     ~@body))

Determines whether the argument is an instance of Activity.

(defn activity?
  [x]
  (instance? Activity x))

Ensures that the calling context has a valid activity var.

(defn has-*activity*?
  []
  (and (bound? #'*activity*)
       (activity? *activity*)))

Sets the content for the activity. The view may be one of:

  • A view object, which will be used directly
  • An integer presumed to be a valid layout ID.
(defn set-content-view!
  ([view]
   {:pre [(or (instance? View view)
              (integer? view))]}
   (set-content-view! *activity* view))
  ([^Activity activity view]
   {:pre [(activity? activity)
          (or (instance? View view)
              (integer? view))]}
   (cond
     (instance? View view)
       (.setContentView activity ^View view)
     (integer? view)
       (.setContentView activity ^Integer view))))

Requests the given features for the activity. The features should be keywords such as :no-title or :indeterminate-progress corresponding FEATURENOTITLE and FEATUREINDETERMINATEPROGRESS, respectively. Returns a sequence of boolean values corresponding to each feature, where a true value indicates the requested feature is supported and now enabled.

If within a with-activity form, supplying an activity as the first argument is not necessary.

This function should be called before set-content-view!.

(defn request-window-features!
  {:arglists '([& features] [activity & features])}
  [activity & features]
  {:pre  [(or (activity? activity)
              (and (keyword? activity)
                   (has-*activity*?)))
          (every? keyword? features)]
   :post [%
          (every? (fn [x] (instance? Boolean x)) %)]}
  (let [[^Activity activity features]
          (if (instance? Activity activity)
            [activity features]
            [*activity* (cons activity features)])
        keyword->int (fn [k]
                       (static-field-value android.view.Window
                                           k
                                           #(str "FEATURE_" %)))
        request-feature  (fn [k]
                           (try
                             (.requestWindowFeature activity (keyword->int k))
                             (catch NoSuchFieldException _
                               (throw (IllegalArgumentException.
                                        (format "‘%s’ is not a valid feature."
                                                k))))))]
    (doall (map request-feature features))))

Creates an activity with the given full package-qualified name. Optional arguments should be provided in a key-value fashion.

Available optional arguments:

:extends, :prefix - same as for gen-class.

:def - symbol to bind the Activity object to in the onCreate method. Relevant only if :create is used.

:on-create - takes a two-argument function. Generates a handler for activity's onCreate event which automatically calls the superOnCreate method and creates a var with the name denoted by :def (or activity's lower-cased name by default) to store the activity object. Then calls the provided function onto the Application object.

:on-start, :on-restart, :on-resume, :on-pause, :on-stop, :on-destroy - same as :on-create but require a one-argument function.

(defmacro defactivity
  [name & {:keys [extends prefix on-create on-create-options-menu
                  on-options-item-selected on-activity-result
                  on-new-intent def state]
           :as options}]
  (let [options (or options {}) ;; Handle no-options case
        sname (simple-name name)
        prefix (or prefix (str sname "-"))
        def (or def (symbol (unicaseize sname)))]
    `(do
       (gen-class
        :name ~name
        :main false
        :prefix ~prefix
        ~@(when state
            '(:init "init" :state "state"))
        :extends ~(or extends Activity)
        :exposes-methods {~'onCreate ~'superOnCreate
                          ~'onStart ~'superOnStart
                          ~'onRestart ~'superOnRestart
                          ~'onResume ~'superOnResume
                          ~'onPause ~'superOnPause
                          ~'onStop ~'superOnStop
                          ~'onCreateContextMenu ~'superOnCreateContextMenu
                          ~'onContextItemSelected ~'superOnContextItemSelected
                          ~'onCreateOptionsMenu ~'superOnCreateOptionsMenu
                          ~'onOptionsItemSelected ~'superOnOptionsItemSelected
                          ~'onActivityResult ~'superOnActivityResult
                          ~'onNewIntent ~'superOnNewIntent
                          ~'onDestroy ~'superOnDestroy})
       ~(when state
          `(defn ~(symbol (str prefix "init"))
             [] [[] ~state]))
       ~(when on-create
          `(defn ~(symbol (str prefix "onCreate"))
             [~(vary-meta 'this assoc :tag name),
              ^android.os.Bundle ~'savedInstanceState]
             (.superOnCreate ~'this ~'savedInstanceState)
             ~(when (and (not (:neko.init/release-build *compiler-options*))
                         def)
                `(def ~(vary-meta def assoc :tag name) ~'this))
             (neko.init/init (.getApplicationContext ~'this))
             (~on-create ~'this ~'savedInstanceState)))
       ~(when on-create-options-menu
          `(defn ~(symbol (str prefix "onCreateOptionsMenu"))
             [~(vary-meta 'this assoc :tag name),
              ^android.view.Menu ~'menu]
             (.superOnCreateOptionsMenu ~'this ~'menu)
             (~on-create-options-menu ~'this ~'menu)
             true))
       ~(when on-options-item-selected
          `(defn ~(symbol (str prefix "onOptionsItemSelected"))
             [~(vary-meta 'this assoc :tag name),
              ^android.view.MenuItem ~'item]
             (~on-options-item-selected ~'this ~'item)
             true))
       ~(when on-activity-result
          `(defn ~(symbol (str prefix "onActivityResult"))
             [~(vary-meta 'this assoc :tag name),
              ^int ~'requestCode,
              ^int ~'resultCode,
              ^android.content.Intent ~'intent]
             (.superOnActivityResult ~'this ~'requestCode ~'resultCode ~'intent)
             (~on-activity-result ~'this ~'requestCode ~'resultCode ~'intent)))
       ~(when on-new-intent
          `(defn ~(symbol (str prefix "onNewIntent"))
             [~(vary-meta 'this assoc :tag name),
              ^android.content.Intent ~'intent]
             (.superOnNewIntent ~'this ~'intent)
             (~on-new-intent ~'this ~'intent)))
       ~@(map #(let [func (options %)
                     event-name (keyword->camelcase %)]
                 (when func
                   `(defn ~(symbol (str prefix event-name))
                      [~(vary-meta 'this assoc :tag name)]
                      (~(symbol (str ".super" (capitalize event-name))) ~'this)
                      (~func ~'this))))
              [:on-start :on-restart :on-resume
               :on-pause :on-stop :on-destroy]))))

Creates a fragment which contains the specified view. If a UI tree was provided, it is inflated and then set as fragment's view.

(defn simple-fragment
  ([context tree]
     (simple-fragment (make-ui context tree)))
  ([view-or-tree]
     (proxy [Fragment] []
       (onCreateView [inflater container bundle]
         (if (instance? View view-or-tree)
           view-or-tree
           (make-ui view-or-tree))))))
 

Contains tools to create and manipulate Application instances. This namespace is deprecated and exists only for backward compatibility purposes.

(ns neko.application
  (:require neko.init)
  (:import android.app.Application
           android.content.Context))
(defmacro defapplication
  [& args]
  (throw (Exception. "defapplication is deprecated, please define
  Application class from Java. Default `:on-create` moved to
  `init-application`.")))

DEPRECATED: Performs necessary preparations for Neko and REPL development. You should call neko.init/init instead.

(defn init-application
  [context]
  (neko.init/init context))
 

Utility functions for managing the compilation environment when using version of Clojure that supports dynamic compilation on the Dalvik virtual machine.

To use this namespace, you need to call init with a context, such as an activity object. This will create a cache directory where temporary files will be placed and will set the 'clojure.compile.path' system property and the 'compile-path' var. If the cache directory already exists, it will be cleaned out.

Note that additional invocations to init within the same process will not have any effect.

(ns neko.compilation
  {:author "Daniel Solano Gómez"}
  (:use [neko.resource :only [get-resource]])
  (:import android.content.Context
           [java.io File FileNotFoundException]))

Whether or not compilation has been initialized.

(def #^{:doc 
        :private true}
  cache-dir (atom nil))

The default name of the cache directory.

(def 
  default-cache-dir "clojure_cache")

Predicate for determining if a given file name is a cache file.

(defn- cache-file?
  [^String name]
  (and (.startsWith name "repl-")
       (or (.endsWith name ".dex")
           (.endsWith name ".jar"))))

Clears all DEX and JAR files from the cache directory.

(defn clear-cache
  []
  (locking cache-dir
    (let [^File dir @cache-dir
          delete-file (fn [^String name] (.delete (File. dir name)))]
      (when dir
        (->>
          (.list dir)
          (filter cache-file?)
          (map delete-file)
          (dorun))))))
(defn get-data-readers [^Context context]
  (when-let [readers-file (try (.open (.getAssets context) "data_readers.clj")
                               (catch FileNotFoundException e nil))]
    (->> readers-file
         slurp
         read-string
         (map (fn [[k v]] [k (resolve v)]))
         (into {}))))

Initializes the compilation path, creating or cleaning cache directory as necessary.

(defn init
  ([^Context context dir-name]
   (locking cache-dir
     (when-not @cache-dir
       (let [dir  (.getDir context dir-name Context/MODE_PRIVATE)
             path (.getAbsolutePath dir)]
         (reset! cache-dir dir)
         (System/setProperty "clojure.compile.path" path)
         (alter-var-root #'clojure.core/*data-readers*
                         (constantly (get-data-readers context)))
         (alter-var-root #'clojure.core/*compile-path* (constantly path))))))
  ([context]
   (init context default-cache-dir)))
 

Compliment source for keywords that represent Android resources.

(ns neko.compliment.android-resources
  (:require [neko.context :as app]
            [clojure.string :as string]
            [neko.resource :as droid-res])
  (:import java.lang.reflect.Field))

Tests if prefix is a keyword.

(defn keyword-symbol?
  [x]
  (re-matches #":.*" x))

Stores cached resources to allow faster lookup.

(def ^{:doc 
       :private true}
  resource-cache (atom {}))

Resource types to be completed.

(def ^{:doc 
       :private true}
  resource-types [:id :string :drawable :layout])

Replaces all underscores in resource name to dashes.

(defn res->keyword
  [res-name]
  (string/replace res-name \_ \-))

Candidates

Returns a map for package pkg-name where keys are resource keywords and values are R class members. type defines which resources should be returned.

If append-ns is true, add namespace name to resource keywords.

(defn- get-package-resources
  [pkg-name type append-ns]
  (into {}
        (when-let [^Class cls ((resolve 'compliment.utils/resolve-class)
                               (symbol (str pkg-name ".R$" (name type))))]
          (for [^Field field (.getDeclaredFields cls)
                :let [field-name (.getName field)]]
            [(if append-ns
               (str ":" pkg-name "/" (res->keyword field-name))
               (str ":" (res->keyword field-name)))
             (list field)]))))

Saves resource keywords for pkg-name to the cache.

(defn populate-cache
  [pkg-name append-ns]
  (swap! resource-cache assoc pkg-name
         (apply merge-with concat
                (map #(get-package-resources pkg-name % append-ns)
                     resource-types))))

Returns a list of resource keywords for the given pkg-name. Populates cache if it is empty.

(defn get-resource-cache
  [pkg-name]
  (let [append-ns (boolean pkg-name)
        pkg-name (or pkg-name (.getPackageName app/context))]
    (when-not (@resource-cache pkg-name)
      (populate-cache pkg-name append-ns))
    (@resource-cache pkg-name)))

Returns a list of resource keywords completions for the keyword prefix. If prefix doesn't have a namespace, assumes application package to be a source.

(defn candidates
  [^String prefix, ns context]
  (when (keyword-symbol? prefix)
    (let [[_ pkg-name] (re-matches #":(.+)/.*" prefix)]
      (for [^String res-str (keys (get-resource-cache pkg-name))
            :when (.startsWith res-str prefix)]
        res-str))))

Documentation

Returns a docstring for the given R class field member.

(defn- get-field-doc
  [^Field field]
  (let [[_ pkg type name] (re-matches #".+ ([^$]+)\$(\w+)\.(.+)" (str field))]
    (str (string/capitalize type) " resource: " pkg "/" name
         (when (= type "string")
           (str " = \"" (droid-res/get-string (.get field nil)) "\""))
         "\n")))

Returns a docstring for the given symbol-str in package pkg-name.

(defn get-resource-doc
  [pkg-name symbol-str]
  (when-let [ress ((get-resource-cache pkg-name) symbol-str)]
    (string/join (map get-field-doc ress))))

Tries to get a docstring for the given completion candidate.

(defn doc
  [^String symbol-str, ns]
  (when (keyword-symbol? symbol-str)
    (let [[_ pkg-name] (re-matches #":(.+)/.*" symbol-str)]
      (get-resource-doc pkg-name symbol-str))))

Source definition

Initializes this completion source if Compliment is available.

(defn init-source
  []
  (try (require 'compliment.core)
       ((resolve 'compliment.sources/defsource) ::android-resources
        :candidates #'candidates
        :doc #'doc)
       (catch Exception ex nil)))
 

Compliment source for keywords that represent UI widget keywords and their traits.

(ns neko.compliment.ui-widgets-and-attributes
  (:require [neko.ui.mapping :as mapping]
            [neko.ui.traits :as traits]
            [neko.doc :as doc]
            [neko.resource :as droid-res]
            [clojure.string :as string]
            [clojure.set :as set]))

Tests if prefix is a keyword.

(defn keyword-symbol?
  [x]
  (re-matches #":.*" x))

Checks if the context is a widget definition, attribute map definition or neither. If context is an attribute map, tries finding current widget's name.

(defn process-context
  [context]
  (let [[level1 level2] context]
    (cond (and (vector? (:form level1)) (= (:idx level1) 0))
          {:type :widget}
          (and (map? (:form level1)) (= (:map-role level1) :key))
          {:type :attr,
           :widget (when (and (vector? (:form level2))
                              (= (:idx level2) 1))
                     (let [w-name (first (:form level2))]
                       (when (and (keyword? w-name)
                                  ((mapping/get-keyword-mapping) w-name))
                         w-name)))})))

Candidates

Returns a list all possible attributes for the given widget keyword.

(defn get-widget-attributes
  [widget-kw]
  (let [attributes (:attributes (meta #'neko.ui.traits/apply-trait))]
    (if widget-kw
      (let [all-traits (mapping/all-traits widget-kw)]
        (for [[att w-traits] attributes
              :when (not (empty? (set/intersection w-traits (set all-traits))))]
          att))
      (keys attributes))))

Returns a list of widget keywords or attribute keywords depending on context.

(defn candidates
  [^String prefix, ns context]
  (when (keyword-symbol? prefix)
    (let [ctx (process-context context)
          cands (cond
                 (nil? ctx) []
                 (= (:type ctx) :widget) (keys (mapping/get-keyword-mapping))
                 (= (:type ctx) :attr) (get-widget-attributes (:widget ctx)))]
      (for [^String kw-str (map str cands)
            :when (.startsWith kw-str prefix)]
        kw-str))))

Documentation

Tries to get a docstring for the given completion candidate.

(defn doc
  [^String symbol-str, ns]
  (when (keyword-symbol? symbol-str)
    (let [kw (keyword (subs symbol-str 1))
          kw-mapping (mapping/get-keyword-mapping)
          attributes (:attributes (meta #'neko.ui.traits/apply-trait))]
      (cond (kw-mapping kw)
            (doc/get-element-doc kw kw-mapping false)
            (attributes kw)
            (->> (attributes kw)
                 (map doc/get-trait-doc)
                 (interpose "\n")
                 string/join)))))

Source definition

Initializes this completion source if Compliment is available.

(defn init-source
  []
  (try (require 'compliment.core)
       ((resolve 'compliment.sources/defsource) ::neko-ui-keywords
        :candidates #'candidates
        :doc #'doc)
       (catch Exception ex nil)))
 

Utilities to aid in working with a context.

(ns neko.context
  {:author "Daniel Solano Gómez"}
  (:use [clojure.string :only [upper-case]])
  (:import android.content.Context))

Stores Application instance that acts as context.

(def 
  ^Context context)

Gets a system service from the context. The type argument is a keyword that names the service type. Examples include :alarm for the alarm service and :layout-inflater for the layout inflater service.

(defmacro get-service
  [type]
  {:pre [(keyword? type)]}
  `(.getSystemService
    context
    ~(symbol (str (.getName Context) "/" (upper-case (name type)) "_SERVICE"))))

Inflates the layout with the given ID.

(defn inflate-layout
  [id]
  {:pre [(integer? id)]
   :post [(instance? android.view.View %)]}
  (.. android.view.LayoutInflater
      (from context)
      (inflate ^Integer id nil)))
 

Contains utilities to manipulate data that is passed between Android entities via Bundles and Intents.

(ns neko.data
  (:refer-clojure :exclude [assoc!])
  (:use [neko.context :only [context]])
  (:import android.os.Bundle android.content.Intent
           android.content.SharedPreferences
           android.content.SharedPreferences$Editor
           android.content.Context))

If given a string returns itself, otherwise transforms a argument into a string.

(defprotocol GenericExtrasKey
  (generic-key [key]))
(extend-protocol GenericExtrasKey
  String
  (generic-key [s] s)

  clojure.lang.Keyword
  (generic-key [k] (.getName k)))

This type acts as a wrapper around Bundle instance to be able to access it like an ordinary map.

(deftype MapLikeBundle [^Bundle bundle]
  clojure.lang.Associative
  (containsKey [this k]
    (.containsKey bundle (generic-key k)))
  (entryAt [this k]
    (clojure.lang.MapEntry. k (.get bundle (generic-key k))))
  (valAt [this k]
    (.get bundle (generic-key k)))
  (valAt [this k default]
    (let [key (generic-key k)]
      (if (.containsKey bundle key)
        (.get bundle (generic-key key))
        default)))
  (seq [this]
    (map (fn [k] [k (.get bundle k)])
         (.keySet bundle))))

This type wraps a HashMap just redirecting the calls to the respective HashMap methods. The only useful thing it does is allowing to use keyword keys instead of string ones.

(deftype MapLikeHashMap [^java.util.HashMap hmap]
  clojure.lang.Associative
  (containsKey [this k]
    (.containsKey hmap (generic-key k)))
  (entryAt [this k]
    (clojure.lang.MapEntry. k (.get hmap (generic-key k))))
  (valAt [this k]
    (.get hmap (generic-key k)))
  (valAt [this k default]
    (let [key (generic-key k)]
      (if (.containsKey hmap key)
        (.get hmap (generic-key key))
        default)))
  (seq [this]
    (map (fn [k] [k (.get hmap k)])
         (.keySet hmap))))

A protocol that helps to wrap objects of different types into MapLikeBundle.

(defprotocol MapLike
  (like-map [this]))
(extend-protocol MapLike
  Bundle
  (like-map [b]
    (MapLikeBundle. b))

  Intent
  (like-map [i]
    (if-let [bundle (.getExtras i)]
      (MapLikeBundle. bundle)
      {}))

  SharedPreferences
  (like-map [sp]
    (MapLikeHashMap. (.getAll sp)))

  nil
  (like-map [_] {}))

SharedPreferences utilities

(def ^:private sp-access-modes {:private Context/MODE_PRIVATE
                                :world-readable Context/MODE_WORLD_READABLE
                                :world-writeable Context/MODE_WORLD_WRITEABLE})

Returns the SharedPreferences object for the given name.

(defn get-shared-preferences
  [name mode]
  {:pre [(or (number? mode) (contains? sp-access-modes mode))]}
  (let [mode (if (number? mode)
               mode (sp-access-modes mode))]
    (.getSharedPreferences context name mode)))

Puts the value into the SharedPreferences editor instance. Accepts limited number of data types supported by SharedPreferences.

(defn ^SharedPreferences$Editor assoc!
  [^SharedPreferences$Editor sp-editor, key value]
  (let [key (generic-key key)]
    (condp #(= (type %2) %1) value
      java.lang.Boolean (.putBoolean sp-editor key value)
      java.lang.Float  (.putFloat sp-editor key value)
      java.lang.Double (.putFloat sp-editor key (float value))
      java.lang.Integer (.putInt sp-editor key value)
      java.lang.Long    (.putLong sp-editor key value)
      java.lang.String (.putString sp-editor key value)
      ;; else
      (throw (Exception. (str "SharedPreferences doesn't support type: "
                              (type value)))))))

Puts value of an arbitrary Clojure data type into given SharedPreferences editor instance. Data is printed into a string and stored as a string value.

(defn ^SharedPreferences$Editor assoc-arbitrary!
  [^SharedPreferences$Editor sp-editor key value]
  (let [key (generic-key key)]
    (.putString sp-editor key (pr-str value))))

Gets a string by given key from a SharedPreferences HashMap (wrapped with like-map) and transforms it into a data value using Clojure reader.

(defn get-arbitrary
  [sp-map key]
  (when-let [val (get sp-map key)]
   (read-string val)))
 

Alpha - subject to change.

Contains convenience functions to work with SQLite databases Android provides.

(ns neko.data.sqlite
  (:require [clojure.string :as string])
  (:use [neko.context :only [context]])
  (:import [android.database.sqlite SQLiteDatabase SQLiteOpenHelper]
           android.database.Cursor
           android.content.ContentValues
           [clojure.lang Keyword PersistentVector]))

Database initialization

Set of types available to be stored in a database. Byte actually stands for array of bytes, or Blob in SQLite.

(def ^{:private true
       :doc }
  supported-types #{Integer Long String Boolean Double Byte})

Creates a schema from arguments and validates it.

(defn make-schema
  [& {:as schema}]
  (assert (string? (:name schema)) ":name should be a String.")
  (assert (number? (:version schema)) ":version should be an number.")
  (assert (map? (:tables schema)) ":tables should be a map.")
  (doseq [[table-name params] (:tables schema)]
    (assert (keyword? table-name)
            (str "Table name should be a keyword: " table-name))
    (assert (map? params) (str "Table parameters should be a map: " table-name))
    (assert (map? (:columns params))
            (str "Table parameters should contain columns map: " table-name))
    (doseq [[column-name col-params] (:columns params)]
      (assert (keyword? column-name)
              (str "Column name should be a keyword: " column-name))
      (assert (map? col-params)
              (str "Column parameters should be a map: " column-name))
      (assert (supported-types (:type col-params))
              (str "Type is not supported: " (:type col-params)))
      (assert (:sql-type col-params)
              (str "SQL type should be specified: " column-name))))
  schema)

Generates a table creation query from the provided schema and table name.

(defn- db-create-query
  [schema table-name]
  (->> (get-in schema [:tables table-name :columns])
       (map (fn [[col params]]
              (str (name col) " " (:sql-type params))))
       (interpose ", ")
       string/join
       (format "create table %s (%s);" (name table-name))))

Creates a SQLiteOpenHelper instance for a given schema.

Helper will recreate database if the current schema version and database version mismatch.

(defn ^SQLiteOpenHelper create-helper
  [{:keys [name version tables] :as schema}]
  (proxy [SQLiteOpenHelper] [context name nil version]
    (onCreate [^SQLiteDatabase db]
      (doseq [table (keys tables)]
        (.execSQL db (db-create-query schema table))))
    (onUpgrade [^SQLiteDatabase db old new]
      (doseq [^Keyword table (keys tables)]
        (.execSQL db (str "drop table if exists " (.getName table))))
      (.onCreate ^SQLiteOpenHelper this db))))

A wrapper around SQLiteDatabase to keep database and its schema together.

(deftype TaggedDatabase [db schema])

Returns SQLiteDatabase instance for the given schema. Access-mode could be either :read or :write.

(defn get-database
  [schema access-mode]
  {:pre [(#{:read :write} access-mode)]}
  (let [helper (create-helper schema)]
    (TaggedDatabase. (case access-mode
                       :read (.getReadableDatabase helper)
                       :write (.getWritableDatabase helper))
                     schema)))

Data-SQL transformers

Takes a map of column keywords to values and creates a ContentValues instance from it.

(defn- map-to-content
  [^TaggedDatabase tagged-db table data-map]
  (let [^ContentValues cv (ContentValues.)]
    (doseq [[col {type :type}] (get-in (.schema tagged-db)
                                       [:tables table :columns])
            :when (contains? data-map col)]
      (let [value (get data-map col)]
        (condp = type
          Integer (.put cv (name col) ^Integer value)
          Long (.put cv (name col) ^Long value)
          Double (.put cv (name col) ^Double value)
          String (.put cv (name col) ^String value)
          Boolean (.put cv (name col) ^Boolean value)
          Byte (.put cv (name col) ^bytes value))))
    cv))

Gets a single value out of the cursor from the specified column.

(defn- get-value-from-cursor
  [^Cursor cur i type]
  (condp = type
    Boolean (= (.getInt cur i) 1)
    Integer (.getInt cur i)
    Long (.getLong cur i)
    String (.getString cur i)
    Double (.getDouble cur i)
    Byte (.getBlob cur i)))

Transforms a key-value pair into a proper SQL comparison/assignment statement.

For example, it will put single quotes around String value. The value could also be a vector that looks like `[:or value1 value2 ...], in which case it will be transformed intokey = value1 OR key = value2 ...`. Nested vectors is supported.

(defn- keyval-to-sql
  [k v]
  (let [k (name k)]
   (condp #(= % (type %2)) v
     PersistentVector (let [[op & values] v]
                        (->> values
                             (map (partial keyval-to-sql k))
                             (interpose (str " " (name op) " "))
                             string/join))
     String (format "(%s = '%s')" k v)
     Boolean (format "(%s = %s)" k (if v 1 0))
     nil (format "(%s is NULL)" k)
     (format "(%s = %s)" k v))))

SQL operations

Takes a map of column keywords to values and generates a WHERE clause from it.

(defn- where-clause
  [where]
  (if (string? where)
    where
    (->> where
         (map (partial apply keyval-to-sql))
         (interpose " AND ")
         string/join)))

Executes SELECT statement against the database and returns a Cursor object with the results. where argument should be a map of column keywords to values.

(defn db-query
  [^TaggedDatabase tagged-db table-name where]
  (let [columns (->> (get-in (.schema tagged-db) [:tables table-name :columns])
                     keys
                     (map name)
                     into-array)]
    (.query ^SQLiteDatabase (.db tagged-db) (name table-name) columns
            (where-clause where) nil nil nil nil)))

Turns data from Cursor object into a lazy sequence. Takes database argument in order to get schema from it.

(defn seq-cursor
  [^TaggedDatabase tagged-db, table-name, ^Cursor cursor]
  (.moveToFirst cursor)
  (let [columns (get-in (.schema tagged-db) [:tables table-name :columns])
        seq-fn (fn seq-fn []
                 (lazy-seq
                  (when-not (.isAfterLast cursor)
                    (let [v (reduce-kv
                             (fn [data i [column-name {type :type}]]
                               (assoc data column-name
                                      (get-value-from-cursor cursor i type)))
                             {} (vec columns))]
                      (.moveToNext cursor)
                      (cons v (seq-fn))))))]
    (seq-fn)))

Executes a SELECT statement against the database and returns the result in a sequence. Same as calling seq-cursor on db-query output.

(defn db-query-seq
  [^TaggedDatabase tagged-db table-name where]
  (seq-cursor tagged-db table-name (db-query tagged-db table-name where)))

Executes UPDATE query against the database generated from set and where clauses given as maps where keys are column keywords.

(defn db-update
  [^TaggedDatabase tagged-db table-name set where]
  (.update ^SQLiteDatabase (.db tagged-db) (name table-name)
           (map-to-content tagged-db table-name set)
           (where-clause where) nil))

Executes INSERT query against the database generated from data-map where keys are column keywords.

(defn db-insert
  [^TaggedDatabase tagged-db table-name data-map]
  (.insert ^SQLiteDatabase (.db tagged-db) (name table-name) nil
           (map-to-content tagged-db table-name data-map)))
 

Contains useful tools to be used while developing the application.

(ns neko.debug
  (:require [neko log notify]))

This atom stores the last exception happened on the UI thread.

(def ^:private ui-exception (atom nil))

Displays an exception message using a Toast and stores the exception for the future reference.

(defn handle-exception-from-ui-thread
  [e]
  (reset! ui-exception e)
  (neko.log/e "Exception raised on UI thread." :exception e)
  (neko.notify/toast (str e) :long))

Returns an uncaught exception happened on UI thread.

(defn ui-e
  [] @ui-exception)
(defmacro catch-all-exceptions [func]
  (if (:neko.init/release-build *compiler-options*)
    `(~func)
    `(try (~func)
          (catch Throwable e#
            (handle-exception-from-ui-thread e#)))))

Wraps the given function inside a try..catch block and notify user using a Toast if an exception happens.

(defn safe-for-ui*
  [f]
  (catch-all-exceptions f))

A conditional macro that will protect the application from crashing if the code provided in body crashes on UI thread in the debug build. If the build is a release one returns body as is.

(defmacro safe-for-ui
  [& body]
  `(safe-for-ui* (fn [] ~@body)))
 

Helps build and manage alert dialogs. The core functionality of this namespace is built around the AlertDialogBuilder protocol. This allows using the protocol with the FunctionalAlertDialogBuilder generated by new-builder as well as the AlertDialog.Builder class provided by the Android platform.

In general, it is preferable to use the functional version of the builder as it is immutable. Using the protocol with an AlertDialog.Builder object works by mutating the object.

(ns neko.dialog.alert
  {:author "Daniel Solano Gómez"}
  (:import android.app.AlertDialog$Builder)
  (:use neko.context))

Defines the functionality needed to build new alert dialogues.

(defprotocol AlertDialogBuilder
  (create [builder]
    "Actually creates the AlertDialog.")
  (get-builder-object [builder]
    "Returns an instance of AlertDialog.Builder with the properties from this
    builder.")
  (with-cancellation [builder cancellable?]
    "Sets whether or not the dialog may be canceled."))
(defrecord FunctionalAlertDialogBuilder
  [^android.content.Context context
   ^boolean cancellable])

Predicate used for testing whether a new builder is a functional builder but is different from the original builder.

(defn- new-builder?
  [old-builder new-builder]
  (and (instance? FunctionalAlertDialogBuilder old-builder)
       (not (identical? old-builder new-builder))))
(extend-type FunctionalAlertDialogBuilder
  AlertDialogBuilder
  (create [this]
    {:post [(instance? android.app.AlertDialog %)]}
    (.create ^AlertDialog$Builder (get-builder-object this)))

  (get-builder-object [this]
    {:post [(instance? AlertDialog$Builder %)]}
    (doto (AlertDialog$Builder. (.context this))
      (.setCancelable (.cancellable this))
      ))

  (with-cancellation [this cancellable?]
    {:post [(new-builder? this %)
            (= (:cancellable %) cancellable?)]}
    (assoc this :cancellable (boolean cancellable?)))
  )
(extend-type AlertDialog$Builder
  AlertDialogBuilder
  (create [this]
    {:post [(instance? android.app.AlertDialog %)]}
    (.create this))

  (get-builder-object [this]
    {:post [(identical? this %)]}
    this)

  (with-cancellation [this cancellable?]
    {:post [(identical? this %)]}
    (.setCancelable this (boolean cancellable?)))
  )

Creates a new functional alert dialog builder.

(defn new-builder
  []
  {:post [(instance? FunctionalAlertDialogBuilder %)]}
  (FunctionalAlertDialogBuilder. context true))
 

This namespace contains functions that help the developer with documentation for different parts of neko.

(ns neko.doc
  (:require [neko.ui.traits :as traits]
            [neko.ui.mapping :as mapping])
  (:use [clojure.string :only [join]]))

Returns a docstring for the given trait keyword.

(defn get-trait-doc
  [trait]
  (when-let [doc (get-in (meta #'traits/apply-trait)
                            [:trait-doc trait])]
    (str trait " - " doc)))

Returns a docsting generated from the element mapping. Verbose flag switches the detailed description of element's traits.

(defn get-element-doc
  [el-type el-mapping verbose?]
  (let [{:keys [classname attributes values]} el-mapping
        traits (mapping/all-traits el-type)]
   (format "%s - %s\n%s%s%s\n"
           el-type
           (or (and classname (.getName ^Class classname))
               "no matching class")
           (if (empty? traits) ""
               (format "Implements traits: %s\n"
                       (if verbose?
                         (join (for [t traits]
                                 (str "\n" (get-trait-doc t))))
                         (join (map (partial str "\n  ") traits)))))
           (if (empty? attributes) ""
               (format "Default attributes: %s\n"
                       (pr-str attributes)))
           (if (empty? values) ""
               (format "Special values: %s\n"
                       (pr-str values))))))

Describes the given keyword. If it reprenents UI element's name then describe the element. If optional second argument equals :verbose, describe all its traits as well. If a trait keyword is given, describes the trait. No-arguments version briefly describes all available UI elements.

(defn describe
  ([]
     (let [all-elements (mapping/get-keyword-mapping)]
       (doseq [[el-type parameters] all-elements]
         (print (get-element-doc el-type parameters false)))))
  ([kw]
     (describe kw nil))
  ([kw modifier]
     (let [parameters ((mapping/get-keyword-mapping) kw)
           trait-doc (get-trait-doc kw)]
       (cond
        parameters (print "Elements found:\n"
                          (get-element-doc kw parameters (= modifier :verbose)))
        trait-doc (print "\nTraits found:\n" trait-doc)
        :else (print (str "No elements or traits were found for " kw))))))
 

Home of the ViewFinder protocol, which should simplify and unify use of the findViewById method introduced in various Android classes. To use the protocol, you need an object that supports findViewById, and these are:

  • activities
  • dialogs
  • views
  • windows

    Given one of these objects, you can (find-view obj id).

    In addition, if within a with-activity form, you can leave out the activity argument, simplifying the above call to:

    (find-view R$id/my_view)

(ns neko.find-view
  {:author "Daniel Solano Gómez"}
  (:use neko.activity))
(defn- nil-or-view?
  [x]
  (or (nil? x)
      (instance? android.view.View x)))

Protocol for finding child views by an ID.

(defprotocol ViewFinder
  (find-view [id] [finder id]
    "The two-arg version is the general version used outside of any context.
    The one-arg version is designed for use within a (with-activity)
    context."))
(extend-protocol ViewFinder
  android.view.Window
  (find-view [window id]
    {:pre  [(integer? id)]
     :post [(nil-or-view? %)]}
    (.findViewById window id))

  android.app.Activity
  (find-view [activity id]
    {:pre  [(integer? id)]
     :post [(nil-or-view? %)]}
    (.findViewById activity id))

  android.view.View
  (find-view [view id]
    {:pre  [(integer? id)]
     :post [(nil-or-view? %)]}
    (.findViewById view id))

  android.app.Dialog
  (find-view [dialog id]
    {:pre  [(integer? id)]
     :post [(nil-or-view? %)]}
    (.findViewById dialog id))

  Integer
  (find-view [id]
    {:pre [(has-*activity*?)]
     :post [(nil-or-view? %)]}
    (find-view *activity* id))

  Long
  (find-view [id]
    {:pre [(has-*activity*?)]
     :post [(nil-or-view? %)]}
    (find-view *activity* (.intValue id))))
 

Contains functions for neko initialization and setting runtime options.

(ns neko.init
  (:require [neko context log resource compilation threading])
  (:import android.content.Context
           java.util.concurrent.atomic.AtomicLong
           java.util.concurrent.ThreadFactory))

Expands into dynamic compilation initialization if conditions are met.

(defmacro
  ^{:private true
    :doc }
  enable-dynamic-compilation
  [context classes-dir]
  (when (or (not (::release-build *compiler-options*))
            (::start-nrepl-sever *compiler-options*)
            (::enable-dynamic-compilation *compiler-options*))
    `(neko.compilation/init ~context ~classes-dir)))

Returns a new ThreadFactory with increased stack size. It is used to substitute nREPL's native configure-thread-factory on Android platform.

(defn android-thread-factory
  []
  (let [counter (AtomicLong. 0)]
    (reify ThreadFactory
      (newThread [_ runnable]
        (doto (Thread. (.getThreadGroup (Thread/currentThread))
                       runnable
                       (format "nREPL-worker-%s" (.getAndIncrement counter))
                       1048576) ;; Hardcoded stack size of 1Mb
          (.setDaemon true))))))

Starts a remote nREPL server. Creates a user namespace because nREPL expects it to be there while initializing. References nrepl's start-server function on demand because the project can be compiled without nrepl dependency.

(defn start-repl
  [& repl-args]
  (binding [*ns* (create-ns 'user)]
    (refer-clojure)
    (use 'clojure.tools.nrepl.server)
    (require '[clojure.tools.nrepl.middleware.interruptible-eval :as ie])
    (with-redefs-fn {(resolve 'ie/configure-thread-factory)
                     android-thread-factory}
      #(apply (resolve 'start-server) repl-args))))

Expands into nREPL server initialization if conditions are met.

(defmacro
  ^{:private true
    :doc }
  start-nrepl-server
  [port other-args]
  (when (or (not (::release-build *compiler-options*))
            (::start-nrepl-sever *compiler-options*))
    (let [build-port (::nrepl-port *compiler-options*)]
      `(let [port# (or ~port ~build-port 9999)]
         (apply start-repl :port port# ~other-args)
         (neko.log/i "Nrepl started at port" port#)))))

Initializes compliment sources if theirs namespaces are present.

(defn enable-compliment-sources
  []
  (try (require 'neko.compliment.android-resources)
       ((resolve 'neko.compliment.android-resources/init-source))
       (require 'neko.compliment.ui-widgets-and-attributes)
       ((resolve 'neko.compliment.ui-widgets-and-attributes/init-source))
       (catch Exception ex nil)))

Represents if initialization was already performed.

(def ^{:doc 
       :private true}
  initialized? (atom false))

Initializes neko library.

Initializes compilation facilities and runs nREPL server if appropriate. Takes the application context and optional arguments in key-value fashion. The value of :classes-dir specifies the path where neko should store compiled files. Other optional arguments are directly feeded to the nREPL's start-server function.

(defn init
  [context & {:keys [classes-dir port] :or {classes-dir "classes"}
              :as args}]
  (when-not @initialized?
    (alter-var-root #'neko.context/context (constantly context))
    (alter-var-root #'neko.resource/package-name
                    (constantly (.getPackageName ^Context context)))
    (enable-dynamic-compilation context classes-dir)
    ;; Ensure that `:port` is provided, pass all other arguments as-is.
    (start-nrepl-server port (mapcat identity (dissoc args :classes-dir :port)))
    (neko.threading/init-threading)
    (enable-compliment-sources)
    (reset! initialized? true)))
 

Utility functions and macros for creating listeners corresponding to the android.widget.AdapterView class.

(ns neko.listeners.adapter-view
  {:author "Daniel Solano Gómez"})

Takes a function and yields an AdapterView.OnItemClickListener object that will invoke the function. This function must take the following four arguments:

parent the AdapterView where the click happened view the view within the AdapterView that was clicked position the position of the view in the adapter id the row id of the item that was clicked

(defn on-item-click-call
  [handler-fn]
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.AdapterView$OnItemClickListener %)]}
  (reify android.widget.AdapterView$OnItemClickListener
    (onItemClick [this parent view position id]
      (handler-fn parent view position id))))

Takes a body of expressions and yields an AdapterView.OnItemClickListener object that will invoke the body. The body takes the following implicit arguments:

parent the AdapterView where the click happened view the view within the AdapterView that was clicked position the position of the view in the adapter id the row id of the item that was clicked

(defmacro on-item-click
  [& body]
  `(on-item-click-call (fn [~'parent ~'view ~'position ~'id] ~@body)))

Takes a function and yields an AdapterView.OnItemLongClickListener object that will invoke the function. This function must take the following four arguments:

parent the AdapterView where the click happened view the view within the AdapterView that was clicked position the position of the view in the adapter id the row id of the item that was clicked

The function should evaluate to a logical true value if it has consumed the long click; otherwise logical false.

(defn on-item-long-click-call
  [handler-fn]
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.AdapterView$OnItemLongClickListener %)]}
  (reify android.widget.AdapterView$OnItemLongClickListener
    (onItemLongClick [this parent view position id]
      (boolean (handler-fn parent view position id)))))

Takes a body of expressions and yields an AdapterView.OnItemLongClickListener object that will invoke the body. The body takes the following implicit arguments:

parent the AdapterView where the click happened view the view within the AdapterView that was clicked position the position of the view in the adapter id the row id of the item that was clicked

The body should evaluate to a logical true value if it has consumed the long click; otherwise logical false.

(defmacro on-item-long-click
  [& body]
  `(on-item-long-click-call (fn [~'parent ~'view ~'position ~'id] ~@body)))

Takes one or two functions and yields an AdapterView.OnItemSelectedListener object that will invoke the functions. The first function will be called to handle the onItemSelected(…) method and must take the following four arguments:

parent the AdapterView where the selection happened view the view within the AdapterView that was clicked position the position of the view in the adapter id the row id of the item that was selected

If a second function is provided, it will be called when the selection disappears from the view. It takes a single argument, the AdapterView that now contains no selected item.

(defn on-item-selected-call
  ([item-fn]
   {:pre  [(fn? item-fn)]
    :post [(instance? android.widget.AdapterView$OnItemSelectedListener %)]}
   (on-item-selected-call item-fn nil))
  ([item-fn nothing-fn]
   {:pre  [(fn? item-fn)
           (or (nil? nothing-fn)
               (fn? nothing-fn))]
    :post [(instance? android.widget.AdapterView$OnItemSelectedListener %)]}
   (reify android.widget.AdapterView$OnItemSelectedListener
     (onItemSelected [this parent view position id]
       (item-fn parent view position id))
     (onNothingSelected [this parent]
       (when nothing-fn
         (nothing-fn parent))))))

Takes a body of expressions and yields an AdapterView.OnItemSelectedListener object that will invoke the body The body takes the following implicit arguments:

type either :item corresponding an onItemSelected(…) call or :nothing corresponding to an onNothingSelected(…) call parent the AdapterView where the selection happened or now contains no selected item view the view within the AdapterView that was clicked. If type is :nothing, this will be nil position the position of the view in the adapter. If type is :nothing, this will be nil. id the row id of the item that was selected. If type is :nothing, this will be nil.

(defmacro on-item-selected
  [& body]
  `(let [handler-fn# (fn [~'type ~'parent ~'view ~'position ~'id]
                       ~@body)]
     (on-item-selected-call
       (fn ~'item-handler [parent# view# position# id#]
         (handler-fn# :item parent# view# position# id#))
       (fn ~'nothing-handler [parent#]
         (handler-fn# :nothing parent# nil nil nil)))))
 

Utility functions and macros for setting listeners corresponding to the android.content DialogInterface interface.

(ns neko.listeners.dialog
  {:author "Daniel Solano Gómez"}
  (:import android.content.DialogInterface))

Takes a function and yields a DialogInterface.OnCancelListener object that will invoke the function. This function must take one argument, the dialog that was canceled.

(defn on-cancel-call
  [handler-fn]
  (reify android.content.DialogInterface$OnCancelListener
    (onCancel [this dialog]
      (handler-fn dialog))))

Takes a body of expressions and yields a DialogInterface.OnCancelListener object that will invoke the body. The body takes an implicit argument 'dialog' that is the dialog that was canceled.

(defmacro on-cancel
  [& body]
  `(on-cancel-call (fn [~'dialog] ~@body)))

Takes a function and yields a DialogInterface.OnCancelListener object that will invoke the function. This function must take two arguments:

dialog: the dialog that received the click which: the button that was clicked (one of :negative, :neutral, or :positive) or the position of the item that was clicked

(defn on-click-call
  [handler-fn]
  (reify android.content.DialogInterface$OnClickListener
    (onClick [this dialog which]
      (let [which (condp = which
                    DialogInterface/BUTTON_NEGATIVE :negative
                    DialogInterface/BUTTON_NEUTRAL  :neutral
                    DialogInterface/BUTTON_POSITIVE :positive
                    which)]
        (handler-fn dialog which)))))

Takes a body of expressions and yields a DialogInterface.OnCancelListener object that will invoke the function. The body will take the following two implicit arguments:

dialog: the dialog that received the click which: the button that was clicked (one of :negative, :neutral, or :positive) or the position of the item that was clicked

(defmacro on-click
  [& body]
  `(on-click-call (fn [~'dialog ~'which] ~@body)))

Takes a function and yields a DialogInterface.OnDismissListener object that will invoke the function. This function must take one argument, the dialog that was dismissed.

(defn on-dismiss-call
  [handler-fn]
  (reify android.content.DialogInterface$OnDismissListener
    (onDismiss [this dialog]
      (handler-fn dialog))))

Takes a body of expressions and yields a DialogInterface.OnDismissListener object that will invoke the body. The body takes an implicit argument 'dialog' that is the dialog that was dismissed.

(defmacro on-dismiss
  [& body]
  `(on-dismiss-call (fn [~'dialog] ~@body)))

Takes a function and yields a DialogInterface.OnKeyListener object that will invoke the function. This function must take the following three arguments:

dialog: the dialog the key has been dispatched to key-code: the code for the physical key that was pressed event: the KeyEvent object containing full information about the event

The function should evaluate to a logical true value if it has consumed the event, otherwise logical false.

(defn on-key-call
  [handler-fn]
  (reify android.content.DialogInterface$OnKeyListener
    (onKey [this dialog key-code event]
      (boolean (handler-fn dialog key-code event)))))

Takes a body of expressions and yields a DialogInterface.OnKeyListener object that will invoke the body. The body takes the following three implicit arguments:

dialog: the dialog the key has been dispatched to key-code: the code for the physical key that was pressed event: the KeyEvent object containing full information about the event

The body should evaluate to a logical true value if it has consumed the event, otherwise logical false.

(defmacro on-key
  [& body]
  `(on-key-call (fn [~'dialog ~'key-code ~'event] ~@body)))

Takes a function and yields a DialogInterface.OnMultiChoiceClickListener object that will invoke the function. This function must take the following three arguments:

dialog: the dialog where the selection was made which: the position of the item in the list that was clicked checked?: true if the click checked the item, else false

(defn on-multi-choice-click-call
  [handler-fn]
  (reify android.content.DialogInterface$OnMultiChoiceClickListener
    (onClick [this dialog which checked?]
      (handler-fn dialog which checked?))))

Takes a body of expressions and yields a DialogInterface.OnMultiChoiceClickListener object that will invoke the body. The body takes the following three implicit arguments:

dialog: the dialog where the selection was made which: the position of the item in the list that was clicked checked?: true if the click checked the item, else false

(defmacro on-multi-choice-click
  [& body]
  `(on-multi-choice-click-call (fn [~'dialog ~'which ~'checked?] ~@body)))
(comment -- OnShowListener is added in API level 8)
 
(ns neko.listeners.search-view
  (:use [neko.-utils :only [call-if-nnil]])
  (:import android.widget.SearchView))

Takes onQueryTextChange and onQueryTextSubmit functions and yields a SearchView.OnQueryTextListener object that will invoke the functions. Both functions take string argument, a query that was entered.

(defn on-query-text-call
  [change-fn submit-fn]
  (reify android.widget.SearchView$OnQueryTextListener
    (onQueryTextChange [this query]
      (call-if-nnil change-fn query))
    (onQueryTextSubmit [this query]
      (call-if-nnil submit-fn query))))
 

Uility functions and macros for creating listeners corresponding to the android.widget.TextView class.

(ns neko.listeners.text-view
  {:author "Daniel Solano Gómez"})

Takes a function and yields a TextView.OnEditorActionListener object that will invoke the function. This function must take the following three arguments:

view the view that was clicked action-id identifier of the action, this will be either the identifier you supplied or EditorInfo/IME_NULL if being called to the enter key being pressed key-event if triggered by an enter key, this is the event; otherwise, this is nil

The function should evaluate to a logical true value if it has consumed the action, otherwise logical false.

(defn on-editor-action-call
  [handler-fn]
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.TextView$OnEditorActionListener %)]}
  (reify android.widget.TextView$OnEditorActionListener
    (onEditorAction [this view action-id key-event]
      (boolean (handler-fn view action-id key-event)))))

Takes a body of expressions and yields a TextView.OnEditorActionListener object that will invoke the body. The body takes the following implicit arguments:

view the view that was clicked action-id identifier of the action, this will be either the identifier you supplied or EditorInfo/IME_NULL if being called to the enter key being pressed key-event if triggered by an enter key, this is the event; otherwise, this is nil

The body should evaluate to a logical true value if it has consumed the action, otherwise logical false.

(defmacro on-editor-action
  [& body]
  `(on-editor-action-call (fn [~'view ~'action-id ~'key-event] ~@body)))
 

Utility functions and macros for setting listeners corresponding to the android.view.View class.

(ns neko.listeners.view
  {:author "Daniel Solano Gómez"})

Takes a function and yields a View.OnClickListener object that will invoke the function. This function must take one argument, the view that was clicked.

(defn on-click-call
  [handler-fn]
  (reify android.view.View$OnClickListener
    (onClick [this view]
      (handler-fn view))))

Takes a body of expressions and yields a View.OnClickListener object that will invoke the body. The body takes an implicit argument 'view' that is the view that was clicked.

(defmacro on-click
  [& body]
  `(on-click-call (fn [~'view] ~@body)))

Takes a function and yields a View.OnCreateContextMenuListener object that will invoke the function. This function must take the following three arguments:

menu: the context menu that is being built view: the view for which the context menu is being built info: extra information about the item for which the context menu should be shown. This information will vary depending on the class of view.

(defn on-create-context-menu-call
  [handler-fn]
  (reify android.view.View$OnCreateContextMenuListener
    (onCreateContextMenu [this menu view info]
      (handler-fn menu view info))))

Takes a body of expressions and yields a View.OnCreateContextMenuListener object that will invoke the body. The body takes the following three implicit arguments:

menu: the context menu that is being built view: the view for which the context menu is being built info: extra information about the item for which the context menu should be shown. This information will vary depending on the class of view.

(defmacro on-create-context-menu
  [& body]
  `(on-create-context-menu-call (fn [~'menu ~'view ~'info] ~@body)))

Takes a function and yields a View.OnDragListener object that will invoke the function. This function must take the two arguments described in on-drag and should return a boolean.Takes a body of expressions and yields a View.OnDragListener object that will invoke the body. The body takes the following two implicit arguments:

view: the view that received the drag event event: the DragEvent object for the drag event

(comment -- Introduced in SDK version 11 (Honeycomb)
(defn on-drag-call
  [handler-fn]
  (reify android.view.View$OnDragListener
    (onDrag [this view event]
      (handler-fn view event))))
(defmacro on-drag
  "Takes a body of expressions and yields a View.OnDragListener object that
  will invoke the body.  The body takes the following two implicit arguments:
  view:  the view that received the drag event
  event: the DragEvent object for the drag event"
  [& body]
  `(on-drag-call (fn [~'view ~'event] ~@body))))

Takes a function and yields a View.OnFocusChangeListener object that will invoke the function. This function must take the following two arguments:

view: the view whose state has changed focused?: the new focused state for view

(defn on-focus-change-call
  [handler-fn]
  (reify android.view.View$OnFocusChangeListener
    (onFocusChange [this view focused?]
      (handler-fn view focused?))))

Takes a body of expressions and yields a View.OnFocusChangeListener object that will invoke the body. The body takes the following two implicit arguments:

view: the view whose state has changed focused?: the new focused state for view

(defmacro on-focus-change
  [& body]
  `(on-focus-change-call (fn [~'view ~'focused?] ~@body)))

Takes a function and yields a View.OnKeyListener object that will invoke the function. This function must take the following three arguments:

view: the view the key has been dispatched to key-code: the code for the physical key that was pressed event: the KeyEvent object containing full information about the event

The function should evaluate to a logical true value if it has consumed the event, otherwise logical false.

(defn on-key-call
  [handler-fn]
  (reify android.view.View$OnKeyListener
    (onKey [this view key-code event]
      (boolean (handler-fn view key-code event)))))

Takes a body of expressions and yields a View.OnKeyListener object that will invoke the body. The body takes the following three implicit arguments:

view: the view the key has been dispatched to key-code: the code for the physical key that was pressed event: the KeyEvent object containing full information about the event

The body should evaluate to a logical true value if it has consumed the event, otherwise logical false.

(defmacro on-key
  [& body]
  `(on-key-call (fn [~'view ~'key-code ~'event] ~@body)))

Takes a function and yields a View.OnLayoutChangeListener object that will invoke the function. This function must take the arguments described in on-layout-change.Takes a body of expressions and yields a View.OnLayoutChangeListener object that will invoke the body. The body takes the following implicit arguments:

view: the view whose state has changed left: the new value of the view's left property top: the new value of the view's top property right: the new value of the view's right property bottom: the new value of the view's bottom property old-left: the previous value of the view's left property old-top: the previous value of the view's top property old-right: the previous value of the view's right property old-bottom: the previous value of the view's bottom property

(comment -- as of API level 11
(defn on-layout-change-call
  [handler-fn]
  (reify android.view.View$OnLayoutChangeListener
    (onLayoutChange [this view left top right bottom
                     old-left old-top old-right old-bottom]
      (handler-fn view left top right bottom
                  old-left olt-top old-right old-bottom))))
(defmacro on-layout-change
  "Takes a body of expressions and yields a View.OnLayoutChangeListener
  object that will invoke the body.  The body takes the following implicit
  arguments:
  view:       the view whose state has changed
  left:       the new value of the view's left property
  top:        the new value of the view's top property
  right:      the new value of the view's right property
  bottom:     the new value of the view's bottom property
  old-left:   the previous value of the view's left property
  old-top:    the previous value of the view's top property
  old-right:  the previous value of the view's right property
  old-bottom: the previous value of the view's bottom property"
  [& body]
  `(on-key-call (fn [~'view ~'left ~'top ~'right ~'bottom
                 ~'old-left ~'old-top ~'old-right ~'old-bottom] ~@body))))

Takes a function and yields a View.OnLongClickListener object that will invoke the function. This function must take one argument, the view that was clicked, and must evaluate to a logical true value if it has consumed the long click, otherwise logical false.

(defn on-long-click-call
  [handler-fn]
  (reify android.view.View$OnLongClickListener
    (onLongClick [this view]
      (boolean (handler-fn view)))))

Takes a body of expressions and yields a View.OnLongClickListener object that will invoke the body. The body takes an implicit argument 'view' that is the view that was clicked and held. The body should also evaluate to a logical true value if it consumes the long click, otherwise logical false.

(defmacro on-long-click
  [& body]
  `(on-long-click-call (fn [~'view] ~@body)))

Takes a function and yields a View.OnSystemUiVisibilityChangeListener object that will invoke the function. This function must take one argument, the view that was clicked, and must evaluate to true if it has consumed the long click, false otherwise.Takes a body of expressions and yields a View.OnSystemUiVisibilityChangeListener object that will invoke the body. The body takes an implicit argument 'visibility' which will be either :status-bar-hidden or :status-bar-visible.

(comment -- incomplete -- also for API level 11 (Honeycomb)
(defn on-system-ui-visibility-change-call
  [handler-fn]
  (reify android.view.View$OnSystemUiVisibilityChangeListener
    (onLongClick [this view]
      (handler-fn view))))
(defmacro on-system-ui-visibility-change
  [& body]
  `(on-system-ui-visibility-change-call (fn [~'visibility] ~@body))))

Takes a function and yields a View.OnTouchListener object that will invoke the function. This function must take the following two arguments:

view: the view the touch event has been dispatched to event: the MotionEvent object containing full information about the event

The function should evaluate to a logical true value if it consumes the event, otherwise logical false.

(defn on-touch-call
  [handler-fn]
  (reify android.view.View$OnTouchListener
    (onTouch [this view event]
      (boolean (handler-fn view event)))))

Takes a body of expressions and yields a View.OnTouchListener object that will invoke the body. The body takes the following implicit arguments:

view: the view the touch event has been dispatched to event: the MotionEvent object containing full information about the event

The body should evaluate to a logical value if it consumes the event, otherwise logical false.

(defmacro on-touch
  [& body]
  `(on-touch-call (fn [~'view ~'event] ~@body)))
 

Utility for logging in Android. There are five logging macros: i, d, e, v, w; for different purposes. Each of them takes variable number of arguments and optional keyword arguments at the end: :exception and :tag. If :tag is not provided, current namespace is used instead. Examples:

(require '[neko.log :as log])

(neko.log/d "Some log string" {:foo 1, :bar 2})
(neko.log/i "Logging to custom tag" [1 2 3] :tag "custom")
(neko.log/e "Something went wrong" [1 2 3] :exception ex)
(ns neko.log
  {:author "Adam Clements"}
  (:import android.util.Log))
(defn- logger [logfn priority-kw args]
  (when-not ((set (:neko.init/ignore-log-priority *compiler-options*))
             priority-kw)
    (let [[strings kwargs] (split-with (complement #{:exception :tag}) args)
          {:keys [exception tag]} (if (odd? (count kwargs))
                                    (butlast kwargs)
                                    kwargs)
          tag (or tag (str *ns*))]
      (if exception
        `(. Log ~logfn ~tag (apply str (interpose " " [~@strings])) ~exception)
        `(. Log ~logfn ~tag (apply str (interpose " " [~@strings])))))))

Log an ERROR message, applying pr-str to all the arguments and taking an optional keyword :exception or :tag at the end which will print the exception stacktrace or override the TAG respectively

(defmacro e
  [& args] (logger 'e :error args))

Log a DEBUG message, applying pr-str to all the arguments and taking an optional keyword :exception or :tag at the end which will print the exception stacktrace or override the TAG respectively

(defmacro d
  [& args] (logger 'd :debug args))

Log an INFO message, applying pr-str to all the arguments and taking an optional keyword :exception or :tag at the end which will print the exception stacktrace or override the TAG respectively

(defmacro i
  [& args] (logger 'i :info args))

Log a VERBOSE message, applying pr-str to all the arguments and taking an optional keyword :exception or :tag at the end which will print the exception stacktrace or override the TAG respectively

(defmacro v
  [& args] (logger 'v :verbose args))

Log a WARN message, applying pr-str to all the arguments and taking an optional keyword :exception or :tag at the end which will print the exception stacktrace or override the TAG respectively

(defmacro w
  [& args] (logger 'w :warn args))
 

Provides convenient wrappers for Toast and Notification APIs.

(ns neko.notify
  (:use [neko.context :only [context]])
  (:import android.content.Context android.widget.Toast
           android.app.Notification android.content.Intent
           android.app.PendingIntent android.app.NotificationManager))

Toasts

Stores constants that represent toast's visible timespan.

(def ^{:doc 
       :private true}
  toast-length {:short Toast/LENGTH_SHORT
                :long Toast/LENGTH_LONG})

Creates a Toast object using a text message and a keyword representing how long a toast should be visible (:short or :long). The application context will be used. One-argument version takes only message and assumes length to be :long.

(defn toast
  ([message]
     (toast message :long))
  ([^String message, length]
     {:pre [(contains? toast-length length)]}
     (.show
      ^Toast (Toast/makeText context message ^int (toast-length length)))))

Notifications

(def ^:private default-notification-icon (atom nil))
(defn set-default-notification-icon! [icon]
  (reset! default-notification-icon icon))

Returns the notification manager instance.

(defn- ^NotificationManager notification-manager
  []
  (.getSystemService context Context/NOTIFICATION_SERVICE))

Creates a PendingIntent instance from a vector where the first element is a keyword representing the action type, and the second element is a action string to create an Intent from.

(defn construct-pending-intent
  [[action-type, ^String action]]
  (let [^Intent intent (Intent. action)]
    (case action-type
      :activity (PendingIntent/getActivity context 0 intent 0)
      :broadcast (PendingIntent/getBroadcast context 0 intent 0)
      :service (PendingIntent/getService context 0 intent 0))))

Creates a Notification instance. If icon is not provided uses the default notification icon.

(defn notification
  [& {:keys [icon ticker-text when content-title content-text action]
      :or {icon @default-notification-icon, when (System/currentTimeMillis)}}]
  {:pre [icon]}
  (let [notification (Notification. icon ticker-text when)]
    (.setLatestEventInfo notification context content-title content-text
                         (construct-pending-intent action))
    notification))

This atom stores the mapping of keywords to integer IDs that represent the notification IDs.

(def ^:private notification-ids (atom {}))

A simple counter that will increment by one after each call.

(def ^:private new-id
  (let [ctr (atom 0)]
    (fn []
      (swap! ctr inc)
      @ctr)))

Sends the notification to the status bar. ID is optional and could be either an integer or a keyword.

(defn fire
  ([notification]
     (.notify (notification-manager) (new-id) notification))
  ([id notification]
     (let [id (if (keyword? id)
                (if (contains? @notification-ids id)
                  (@notification-ids id)
                  (let [number-id (new-id)]
                    (swap! notification-ids assoc id number-id)
                    number-id))
                id)]
       (.notify (notification-manager) id notification))))

Removes a notification by the given ID from the status bar.

(defn cancel
  [id]
  (let [id (if (keyword? id)
             (@notification-ids id)
             id)]
    (.cancel (notification-manager) id)))
 

Provides utilities to resolve application resources.

(ns neko.resource
  (:require [clojure.string :as string]
            [neko.context :as context])
  (:import android.content.Context android.graphics.drawable.Drawable))

Runtime resource resolution

(def package-name (:neko.init/package-name *compiler-options*))

Takes the name of the keyword and turns all hyphens and periods to underscores.

(defn- kw-to-res-name
  [kw]
  (-> (name kw)
      (string/replace \- \_)
      (string/replace \. \_)))

Resolves the resource ID of a given type with the given name. For example, to refer to what in Java would be R.string.my_string, you can use:

`(get-resource :string :my-string)`

The type should be a keyword corresponding to a resource type such as :layout, :attr, or :id.

The name should be a keyword. If the keyword has a namespace, it will be used as the package from which to retrieve the resources. Generally, this is not required as the default will be the application package. However, this can be used to access the resources from the platform. For example, the equivalent to android.R.layout.simplelistitem_1 is:

`(get-resource :layout :android/simple-list-item-1)`

The name portion of the name argument will be converted to a string and any hyphens or periods will be transformed to underscores. Note that hyphens are not valid in Android names, but are allowed here to be Clojure friendly.

If the name argument is an integer, it is assumed to be a valid resource ID and will be returned as is without any processing.

(defn get-resource
  ([res-type res-name]
     (get-resource context/context res-type res-name))
  ([^Context context, res-type res-name]
     (let [resid (if (keyword? res-name)
                      (.getIdentifier (.getResources context)
                                      (kw-to-res-name res-name)
                                      (name res-type)
                                      (or (namespace res-name)
                                          (.getPackageName context)))
                      res-name)]
       (if (= resid 0) nil resid))))

Finds the ID for the XML item with the given name. This is simply a convenient way of calling (get-resource :id name).

(defn get-id
  ([res-name]
     (get-resource context/context :id res-name))
  ([^Context context, res-name]
     (get-resource context :id res-name)))

Finds the resource ID for the layout with the given name. This is simply a convenient way of calling (get-resource :layout name).

(defn get-layout
  ([res-name]
     (get-resource context/context :layout res-name))
  ([^Context context, res-name]
     (get-resource context :layout res-name)))

Gets the localized string with the given ID or name from the context. The name will be resolved using get-resource. If res-name is a string, returns it unchanged.

If additional arguments are supplied, the string will be interpreted as a format and the arguments will be applied to the format.

(defn get-string
  [& args]
  (let [[^Context context args] (if (instance? Context (first args))
                                  [(first args) (rest args)]
                                  [context/context args])
        [res-name & format-args] args]
    (if (string? res-name)
      res-name
      (when-let [id (get-resource context :string res-name)]
        (if format-args
          (.getString context id (to-array format-args))
          (.getString context id))))))
(alter-meta! #'get-string
             assoc :arglists '([res-name & format-args?] [context res-name & format-args?]))

Gets a Drawable object associated with the given ID or name from the context. The name will be resolved using get-resource. If res-name is a Drawable, returns it unchanged.

(defn get-drawable
  ([res-name]
     (get-drawable context/context res-name))
  ([^Context context, res-name]
     (if (instance? Drawable res-name)
       res-name
       (when-let [id (get-resource context :drawable res-name)]
         (.getDrawable (.getResources context) id)))))

Compile time resource resolution

Returns a symbol that represents a resource field specified by type and name keywords. If name is not a keyword, just returns it back.

(defn- resource-symbol
  [res-type res-name]
  (if (not (keyword? res-name))
    name
    (let [package (or (namespace res-name) package-name)
          res-type (name res-type)
          res-name (kw-to-res-name res-name)]
      (symbol (str package ".R$" res-type "/" res-name)))))

Resolves a resource identifier by its type and name. This is the same as get-resource, but executes in compile-time.

(defmacro resolve-resource
  [type name]
  {:pre  [(keyword? type)]}
  (resource-symbol type name))

Finds the resource ID for the XML item with the given name in compile time. This is simply a convenient way of calling (resolve-resource :id name).

(defmacro resolve-id
  [name]
  (resource-symbol :id name))
(defn resolve-id-reader
  [name]
  (resource-symbol :id name))

Finds the resource ID for the string with the given name in compile time. This is simply a convenient way of calling `(resolve-resource :string name)`.

(defmacro resolve-string
  [name]
  (resource-symbol :string name))
(defn resolve-string-reader
  [name]
  (resource-symbol :string name))

Finds the resource ID for the layout with the given name in compile time. This is simply a convenient way of calling `(resolve-resource :layout name)`.

(defmacro resolve-layout
  [name]
  (resource-symbol :layout name))
(defn resolve-layout-reader
  [name]
  (resource-symbol :layout name))

Finds the resource ID for the Drawable with the given name in compile time. This is simply a convenient way of calling `(resolve-resource :drawable name)`.

(defmacro resolve-drawable
  [name]
  (resource-symbol :drawable name))
(defn resolve-drawable-reader
  [name]
  (resource-symbol :drawable name))
 

Utilities used to manage multiple threads on Android.

(ns neko.threading
  {:author "Daniel Solano Gómez"}
  (:use [neko.debug :only [safe-for-ui]])
  (:import android.app.Activity
           android.view.View
           clojure.lang.IFn
           java.util.concurrent.TimeUnit
           android.os.Looper
           android.os.Handler))

Initialization

Contains the UI looper Handler object which is used to post tasks to UI thread.

(def ^{:doc 
       :private true}
  ^Handler handler)

Stores UI thread object for quick reference.

(def ^{:doc 
       :private true}
  ^Thread ui-thread)

Initializes handler and ui-thread vars to be used in threading facilities.

(defn init-threading
  []
  (let [^Looper ui-looper (Looper/getMainLooper)]
    (alter-var-root #'handler (constantly (Handler. ui-looper)))
    (alter-var-root #'ui-thread (constantly (.getThread ui-looper)))))

UI thread utilities

Returns true if the current thread is a UI thread.

(defn on-ui-thread?
  []
  (identical? (Thread/currentThread) ui-thread))

Runs the given nullary function on the UI thread. If this function is called on the UI thread, it will evaluate immediately.

(defn on-ui*
  [f]
  (if (on-ui-thread?)
    (f)
    (.post handler (fn [] (safe-for-ui (f))))))

Runs the macro body on the UI thread. If this macro is called on the UI thread, it will evaluate immediately.

(defmacro on-ui
  [& body]
  `(on-ui* (fn [] ~@body)))

Causes the function to be added to the message queue. The function will execute on the UI thread. Returns true if successfully placed in the message queue.

(defn post*
  [^View view, f]
  (.post view f))

Causes the macro body to be added to the message queue. It will execute on the UI thread. Returns true if successfully placed in the message queue.

(defmacro post
  [view & body]
  `(post* ~view (fn [] ~@body)))

Causes the function to be added to the message queue, to be run after the specified amount of time elapses. The function will execute on the UI thread. Returns true if successfully placed in the message queue.

(defn post-delayed*
  [^View view, millis f]
  (.postDelayed view f millis))

Causes the macro body to be added to the message queue. It will execute on the UI thread. Returns true if successfully placed in the message queue.

(defmacro post-delayed
  [view millis & body]
  `(post-delayed* ~view ~millis (fn [] ~@body)))

A map of unit keywords to TimeUnit instances.

(def ^{:doc 
       :private true}
  unit-map
  {; days/hours/minutes added in API level 9
   ;:days    TimeUnit/DAYS
   ;:hours   TimeUnit/HOURS
   ;:minutes TimeUnit/MINUTES
   :seconds TimeUnit/SECONDS
   :millis  TimeUnit/MILLISECONDS
   :micros  TimeUnit/MICROSECONDS
   :nanos   TimeUnit/NANOSECONDS})
 

Tools for defining and manipulating Android UI elements.

(ns neko.ui
  (:use [neko.-utils :only [keyword->setter reflect-setter reflect-constructor]]
        [neko.listeners.view :only [on-click-call]]
        [neko.ui.traits :only [apply-trait]])
  (:require [neko.ui.mapping :as kw]
            [neko.context :as context]))

Attributes

Takes widget keywords name, UI widget object and attributes map after all custom attributes were applied. Transforms each attribute into a call to (.setCapitalizedKey widget value). If value is a keyword then it is looked up in the keyword-mapping or if it is not there, it is perceived as a static field of the class.

(defn apply-default-setters-from-attributes
  [widget-kw widget attributes]
  (doseq [[attribute value] attributes]
    (let [real-value (kw/value widget-kw value attribute)]
      (.invoke (reflect-setter (type widget)
                               (keyword->setter attribute)
                               (type real-value))
               widget (into-array (vector real-value))))))

Takes UI widget keyword, a widget object, a map of attributes and options. Consequently calls apply-trait on all element's traits, in the end calls apply-default-setters-from-attributes on what is left from the attributes map. Returns the updated options map.

Options is a map of additional arguments that come from container elements to their inside elements. Note that all traits of the current element will receive the initial options map, and modifications will only appear visible to the subsequent elements.

(defn apply-attributes
  [widget-kw widget attributes options]
  (loop [[trait & rest] (kw/all-traits widget-kw),
         attrs attributes, new-opts options]
    (if trait
      (let [[attributes-fn options-fn]
            (apply-trait trait widget attrs options)]
        (recur rest (attributes-fn attrs) (options-fn new-opts)))
      (do
        (apply-default-setters-from-attributes widget-kw widget attrs)
        new-opts))))

Widget creation

Constructs a UI widget by a given keyword. Infers a correct constructor for the types of arguments being passed to it.

(defn construct-element
  ([kw context constructor-args]
     (let [element-class (kw/classname kw)]
       (.newInstance (reflect-constructor element-class
                                          (cons android.content.Context
                                                (map type constructor-args)))
                     (to-array (cons context constructor-args))))))

Creates a UI widget based on its keyword name, applies attributes to it, then recursively create its subelements and add them to the widget.

(defn make-ui-element
  [context tree options]
  (if (sequential? tree)
    (let [[widget-kw attributes & inside-elements] tree
          _ (assert (and (keyword? widget-kw) (map? attributes)))
          attributes (merge (kw/default-attributes widget-kw) attributes)
          wdg (if-let [constr (:custom-constructor attributes)]
                (apply constr context (:constructor-args attributes))
                (construct-element widget-kw context
                                   (:constructor-args attributes)))
          new-opts (apply-attributes
                    widget-kw wdg
                    ;; Remove :custom-constructor and
                    ;; :constructor-args since they are not real
                    ;; attributes.
                    (dissoc attributes :constructor-args :custom-constructor)
                    options)]
      (doseq [element inside-elements :when element]
        (.addView ^android.view.ViewGroup wdg
                  (make-ui-element context element new-opts)))
      wdg)
    tree))

Takes a tree of elements and creates Android UI elements according to this tree. A tree has a form of a vector that looks like following:

[element-name map-of-attributes & subelements]

where map-of-attributes is a map of attribute names to their values, and subelement is itself a tree of this form.

Two-argument version takes an arbitrary Context object to use in UI elements constructor.

(defn make-ui
  ([tree]
     (make-ui-element context/context tree {}))
  ([context tree]
     (make-ui-element context tree {})))

Takes a widget and key-value pairs of attributes, and applies these attributes to the widget.

(defn config
  [widget & {:as attributes}]
  (apply-attributes (kw/keyword-by-classname (type widget))
                    widget attributes {}))
 

Contains custom adapters for ListView and Spinner.

(ns neko.ui.adapters
  (:use [neko.threading :only [on-ui]]
        [neko.ui :only [make-ui-element]])
  (:import neko.ui.adapters.InterchangeableListAdapter
           android.view.View))

Takes a function that creates a View, a function that updates a view according to the element and a reference type that stores the data. Returns an Adapter object that displays ref-type contents. When ref-type is updated, Adapter gets updated as well.

create-view-fn is a function of no arguments. update-view-fn is a function of four arguments: element position, view to update, parent view container and the respective data element from the ref-type. access-fn argument is optional, it is called on the value of ref-type to get the list to be displayed.

(defn ref-adapter
  ([create-view-fn update-view-fn ref-type]
     (ref-adapter create-view-fn update-view-fn ref-type identity))
  ([create-view-fn update-view-fn ref-type access-fn]
     {:pre [(fn? create-view-fn) (fn? update-view-fn)
            (instance? clojure.lang.IFn access-fn)
            (instance? clojure.lang.IDeref ref-type)]}
     (let [create-fn (fn []
                       (let [view (create-view-fn)]
                         (if (instance? View view)
                           view
                           (make-ui-element
                            neko.context/context view
                            {:container-type :abs-listview-layout}))))
           adapter (InterchangeableListAdapter. create-fn update-view-fn
                                                (access-fn @ref-type))]
       (add-watch ref-type ::adapter-watch
                  (fn [_ __ ___ new-state]
                    (on-ui (.setData adapter (access-fn new-state)))))
       adapter)))
 

Contains utilities to work with ListView.

(ns neko.ui.listview
  (:import android.util.SparseBooleanArray
           android.widget.ListView))

Returns a vector of indices for items being checked in a ListView. The two-argument version additionally takes a sequence of data elements of the ListView (usually the data provided to the adapter) and returns the vector of only those elements that are checked.

(defn get-checked
  ([^ListView lv]
     (let [^SparseBooleanArray bool-array (.getCheckedItemPositions lv)
           count (.getCount lv)]
       (loop [i 0, result []]
         (if (= i count)
           result
           (if (.get bool-array i)
             (recur (inc i) (conj result i))
             (recur (inc i) result))))))
  ([^ListView lv, items]
     (let [^SparseBooleanArray bool-array (.getCheckedItemPositions lv)
           count (.getCount lv)]
       (loop [i 0, [curr & rest] items, result []]
         (if (= i count)
           result
           (if (.get bool-array i)
             (recur (inc i) rest (conj result curr))
             (recur (inc i) rest result)))))))

Given a sequence of numbers checks the respective ListView elements.

(defn set-checked!
  [^ListView lv, checked-ids]
  (doseq [i checked-ids]
    (.setItemChecked lv i true)))
 

This namespace provides utilities to connect the keywords to the actual UI classes, define the hierarchy relations between the elements and the values for the keywords representing values.

(ns neko.ui.mapping
  (:require [clojure.string :as string])
  (:use [neko.-utils :only [keyword->static-field reflect-field]])
  (:import [android.widget LinearLayout Button EditText ListView SearchView
            ImageView ImageView$ScaleType RelativeLayout]
           android.app.ProgressDialog
           [android.view View ViewGroup$LayoutParams Gravity]))

This atom keeps all the relations inside the map.

(def ^{:private true} keyword-mapping
  (atom
   ;; UI widgets
   {:view {:traits [:def :id :padding :on-click :on-long-click :on-touch
                    :on-create-context-menu :on-key
                    :default-layout-params :linear-layout-params
                    :relative-layout-params :listview-layout-params]
           :value-namespaces
           {:text-alignment View
            :text-direction View
            :visibility View}}
    :view-group {:inherits :view
                 :traits [:container :id-holder]}
    :button {:classname android.widget.Button
             :inherits :text-view
             :attributes {:text "Default button"}}
    :linear-layout {:classname android.widget.LinearLayout
                    :inherits :view-group
                    :value-namespaces
                    {:gravity android.view.Gravity}}
    :relative-layout {:classname android.widget.RelativeLayout
                      :inherits :view-group}
    :edit-text {:classname android.widget.EditText
                :inherits :view}
    :text-view {:classname android.widget.TextView
                :inherits :view
                :value-namespaces
                {:ellipsize android.text.TextUtils$TruncateAt}
                :traits [:text :text-size]}
    :list-view {:classname android.widget.ListView
                :inherits :view-group}
    :search-view {:classname android.widget.SearchView
                  :inherits :view-group
                  :traits [:on-query-text]}
    :image-view {:classname android.widget.ImageView
                 :inherits :view
                 :traits [:image]
                 :value-namespaces
                 {:scale-type android.widget.ImageView$ScaleType}}
    :web-view {:classname android.webkit.WebView
               :inherits :view}
    ;; Other
    :layout-params {:classname ViewGroup$LayoutParams
                    :values {:fill ViewGroup$LayoutParams/FILL_PARENT
                             :wrap ViewGroup$LayoutParams/WRAP_CONTENT}
                    :value-namespaces
                    {:gravity android.view.Gravity}}
    :progress-dialog {:classname android.app.ProgressDialog
                      :values {:horizontal ProgressDialog/STYLE_HORIZONTAL
                               :spinner ProgressDialog/STYLE_SPINNER}}
    }))

Returns the current state of keyword-mapping.

(defn get-keyword-mapping
  []
  @keyword-mapping)
(def ^{:private true} reverse-mapping
  (atom
   {android.widget.Button :button
    android.widget.LinearLayout :linear-layout
    android.widget.RelativeLayout :relative-layout
    android.widget.EditText :edit-text
    android.widget.TextView :text-view
    android.widget.ListView :list-view
    android.app.ProgressDialog :progress-dialog}))

Connects the given keyword to the classname.

(defn set-classname!
  [kw classname]
  (swap! keyword-mapping assoc-in [kw :classname] classname)
  (swap! reverse-mapping assoc-in classname kw))

Gets the classname from the keyword-mapping map if the argument is a keyword. Otherwise considers the argument to already be a classname.

(defn classname
  [classname-or-kw]
  (if (keyword? classname-or-kw)
    (or (get-in @keyword-mapping [classname-or-kw :classname])
        (throw (Exception. (str "The class for " classname-or-kw
                                " isn't present in the mapping."))))
    classname-or-kw))

Returns a keyword name for the given UI widget classname.

(defn keyword-by-classname
  [classname]
  (@reverse-mapping classname))

Defines the kw to implement trait specified with trait-kw.

(defn add-trait!
  [kw trait-kw]
  (swap! keyword-mapping update-in [kw :traits] conj trait-kw))

Returns the list of all unique traits for kw. The list is built recursively.

(defn all-traits
  [kw]
  (let [own-traits (get-in @keyword-mapping [kw :traits])
        parent (get-in @keyword-mapping [kw :inherits])]
    (concat own-traits (when parent
                         (all-traits parent)))))

Associate the value keyword with the provided value for the given keyword representing the UI element.

(defn set-value!
  [element-kw value-kw value]
  (swap! keyword-mapping assoc-in [element-kw :values value-kw] value))

Searches in the keyword mapping for a value denoted by a list of keys. If value is not found, tries searching in a parent.

(defn- recursive-find
  [[element-kw & other :as keys]]
  (if element-kw
    (or (get-in @keyword-mapping keys)
        (recur (cons (get-in @keyword-mapping [element-kw :inherits]) other)))))

If the value is a keyword then returns the value for it from the keyword-mapping. The value is sought in the element itself and all its parents. If the value-keyword isn't present in any element's keyword-mapping, form the value as classname-for-element-kw/CAPITALIZED-VALUE-KW. Classname for keyword can be extracted from :value-namespaces map for element's mapping.

(defn value
  [element-kw value & [attribute]]
  (let [mapping @keyword-mapping]
    (if-not (keyword? value)
      (cond
       (integer? value) (int value)
       (float? value) (float value)
       :else value)
      (or (recursive-find (list element-kw :values value))
          (reflect-field
           (classname
            (or (and attribute
                     (recursive-find (list element-kw
                                           :value-namespaces attribute)))
                element-kw))
           (keyword->static-field value))))))

Adds a default attribute value for the given element.

(defn add-default-atribute-value!
  [element-kw attribute-kw value]
  (swap! keyword-mapping
         update-in [element-kw :attributes attribute-kw] value))

Returns a map of default attributes for the given element keyword and all its parents.

(defn default-attributes
  [element-kw]
  (merge (when element-kw
           (default-attributes (get-in @keyword-mapping
                                       [element-kw :inherits])))
         (get-in @keyword-mapping [element-kw :attributes])))

Defines the element of the given class with the provided name to use in the UI construction. Takes the element's classname, a parent it inherits, a list of traits and a map of specific values as optional arguments.

Optional arguments - :classname, :inherits, :traits, :values, :attributes.

(defn defelement
  [kw-name & {:as args}]
  (swap! keyword-mapping assoc kw-name
         (if-not (contains? args :inherits)
           (assoc args :inherits :view)
           args))
  (if-let [classname (:classname args)]
    (swap! reverse-mapping assoc classname kw-name)))
 

Provides utilities for declarative options menu generation. Intended to replace XML-based menu layouts.

(ns neko.ui.menu
  (:require [neko.context :as ctx]
            [neko.ui :as ui])
  (:use [neko.ui.mapping :only [defelement]]
        [neko.ui.traits :only [deftrait to-id]]
        [neko.-utils :only [call-if-nnil]])
  (:import [android.view Menu MenuItem]
           [android.view View ActionMode$Callback]
           android.app.Activity))

ActionBar menu

Inflates the given MenuBuilder instance with the declared menu item tree. Root of the tree is a sequence that contains element definitions (see doc for neko.ui/make-ui for element definition syntax). Elements supported are :item, :group and :menu.

:item is a default menu element. See supported traits for :item for more information.

:group allows to unite items into a single category in order to later operate on the whole category at once.

:menu element creates a submenu that can in its own turn contain other :item and :group elements. Only one level of submenus is supported. Note that :menu creates an item for itself and can use all the attributes that apply to items.

(defn make-menu
  ([menu tree]
     (make-menu menu Menu/NONE tree))
  ([menu group tree]
     (doseq [[element-kw attributes & subelements] tree
             :when element-kw]
       (let [id (to-id (or (:id attributes) Menu/NONE))
             order (to-id (or (:order attributes) Menu/NONE))]
         (case element-kw
           :group
           (make-menu menu id subelements)
           :item
           (ui/apply-attributes
            :item
            (.add ^Menu menu ^int group ^int id ^int order "")
            (dissoc attributes :id :order) {})
           :menu
           (let [submenu (.addSubMenu ^Menu menu ^int group
                                      ^int id ^int order "")]
             (ui/apply-attributes :item (.getItem submenu)
                                  (dissoc attributes :id :order) {})
             (make-menu submenu subelements)))))))

Contextual menu tools

Stores a mapping of activities to active action modes. After the action mode is finished, it is removed from the mapping.

(def ^{:doc 
       :private true}
  action-modes (atom {}))

Tries starting action mode for an activity if it is not started yet. Takes an activity as first argument, rest arguments should be pairs of keys and functions.

:on-create takes ActionMode and Menu as arguments. :on-prepare takes ActionMode and Menu as arguments. :on-clicked takes ActionMode and MenuItem that was clicked. :on-destroy takes ActionMode as argument.

(defn start-action-mode
  [activity & {:keys [on-create on-prepare on-clicked on-destroy]}]
  (when-not (@action-modes activity)
    (let [callback (reify ActionMode$Callback
                     (onCreateActionMode [this mode menu]
                       (call-if-nnil on-create mode menu))
                     (onPrepareActionMode [this mode menu]
                       (call-if-nnil on-prepare mode menu))
                     (onActionItemClicked [this mode item]
                       (call-if-nnil on-clicked mode item))
                     (onDestroyActionMode [this mode]
                       (swap! action-modes dissoc activity)
                       (call-if-nnil on-destroy mode)))
          am (.startActionMode ^Activity activity callback)]
      (swap! action-modes assoc activity am)
      am)))

Returns action mode for the given activity.

(defn get-action-mode
  [activity]
  (@action-modes activity))

Element definitions and traits

(defelement :item
  :classname MenuItem
  :inherits nil
  :traits [:show-as-action :on-menu-item-click :action-view])

ShowAsAction attribute

Returns an integer value for the given keyword, or the value itself.

(defn show-as-action-value
  [value]
  (if (keyword? value)
    (case value
      :always               MenuItem/SHOW_AS_ACTION_ALWAYS
      :collapse-action-view MenuItem/SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
      :if-room              MenuItem/SHOW_AS_ACTION_IF_ROOM
      :never                MenuItem/SHOW_AS_ACTION_NEVER
      :with-text            MenuItem/SHOW_AS_ACTION_WITH_TEXT)
    value))

Takes :show-as-action attribute, which could be an integer value or one of the following keywords: :always, :collapse-action-view, :if-room, :never, :with-text; or a vector with these values, to which bit-or operation will be applied.

(deftrait :show-as-action
  [^MenuItem wdg, {:keys [show-as-action]} _]
  (let [value (if (vector? show-as-action)
                (apply bit-or (map show-as-action-value show-as-action))
                (show-as-action-value show-as-action))]
    (.setShowAsAction wdg value)))

OnMenuItemClick attribute

Takes a function and yields a MenuItem.OnMenuItemClickListener object that will invoke the function. This function must take one argument, an item that was clicked.

(defn on-menu-item-click-call
  [handler-fn]
  (reify android.view.MenuItem$OnMenuItemClickListener
    (onMenuItemClick [this item]
      (handler-fn item)
      true)))

Takes a body of expressions and yields a MenuItem.OnMenuItemClickListener object that will invoke the body. The body takes an implicit argument 'item' that is the item that was clicked.

(defmacro on-menu-item-click
  [& body]
  `(on-menu-item-click-call (fn [~'item] ~@body)))

Takes :on-click attribute, which should be function of one argument, and sets it as an OnClickListener for the widget.

(deftrait :on-menu-item-click
  {:attributes [:on-click]}
  [^MenuItem wdg, {:keys [on-click]} _]
  (.setOnMenuItemClickListener wdg (on-menu-item-click-call on-click)))

ActionView attribute

Takes :action-view attribute which should either be a View instance or a UI definition tree, and sets it as an action view for the menu item. For UI tree syntax see docs for neko.ui/make-ui. Custom context can be used for UI inflation by providing :context attribute.

(deftrait :action-view
  {:attributes [:action-view :context]
   :applies? (:action-view attrs)}
  [^MenuItem wdg, {:keys [action-view context] :as attrs} _]
  (let [view (if (instance? View action-view)
               action-view
               (ui/make-ui-element (or context ctx/context)
                                   action-view {:menu-item wdg}))]
    (.setActionView wdg ^View view)))
 

Contains trait declarations for various UI elements.

(ns neko.ui.traits
  (:require [neko.ui.mapping :as kw]
            [neko.resource :as res]
            [neko.context :as context]
            [neko.listeners.view :as view-listeners]
            neko.listeners.search-view)
  (:use [neko.-utils :only [memoized]])
  (:import [android.widget LinearLayout$LayoutParams TextView SearchView
            ImageView RelativeLayout RelativeLayout$LayoutParams
            AbsListView$LayoutParams]
           [android.view View ViewGroup$LayoutParams
            ViewGroup$MarginLayoutParams]
           android.graphics.Bitmap android.graphics.drawable.Drawable
           android.net.Uri
           android.util.TypedValue
           java.util.HashMap
           clojure.lang.Keyword))

Infrastructure for traits and attributes

Transforms the given map of attributes into the valid Java-interop code of setters.

trait is the keyword for a transformer function over the attribute map.

object-symbol is an symbol for the UI element to apply setters to.

attributes-map is a map of attributes to their values.

generated-code is an attribute-setter code generated so far. The code this method generates should be appended to it.

options-map is a map of additional options that come from higher level elements to their inside elements. A transformer can use this map to provide some arguments to its own inside elements.

Returns a vector that looks like `[new-generated-code attributes-update-fn options-update-fn].attributes-update-fn` should take attributes map and remove processed attributes from it. options-update-fn should remove old or introduce new options for next-level elements.

(defmulti apply-trait
  (fn [trait widget attributes-map options-map]
    trait))

Appends information about attribute to trait mapping to meta.

(defn add-attributes-to-meta
  [meta attr-list trait]
  (reduce (fn [m att]
            (update-in m [:attributes att]
                       #(if %
                          (conj % trait)
                          #{trait})))
          meta attr-list))

Defines a trait with the given name.

match-pred is a function on attributes map that should return a logical truth if this trait should be executed against the widget and the map. By default it checks if attribute with the same name as trait's is present in the attribute map.

The parameter list is the following: `[widget attributes-map options-map]`.

Body of the trait can optionally return a map with the following keys: :attribute-fn, :options-fn, which values are functions to be applied to attributes map and options map respectively after the trait finishes its work. If they are not provided, attribute-fn defaults to dissoc'ing trait's name from attribute map, and options-fn defaults to identity function.

(defmacro deftrait
  [name & args]
  (let [[docstring args] (if (string? (first args))
                           [(first args) (next args)]
                           [nil args])
        [param-map args] (if (map? (first args))
                           [(first args) (next args)]
                           [{} args])
        attrs-sym (gensym "attributes")
        match-pred (cond (:applies? param-map)
                         (:applies? param-map)
                         (:attributes param-map)
                         `(some ~attrs-sym ~(:attributes param-map))
                         :else `(~name ~attrs-sym))
        [arglist & codegen-body] args
        dissoc-fn (if (:attributes param-map)
                    `(fn [a#] (apply dissoc a# ~(:attributes param-map)))
                    `(fn [a#] (dissoc a# ~name)))]
    `(do
       (alter-meta! #'apply-trait
                    (fn [m#]
                      (-> m#
                          (assoc-in [:trait-doc ~name] ~docstring)
                          (add-attributes-to-meta
                           (or ~(:attributes param-map) [~name]) ~name))))
       (defmethod apply-trait ~name
         [trait# widget# ~attrs-sym options#]
         (let [~arglist [widget# ~attrs-sym options#]]
           (if ~match-pred
             (let [result# (do ~@codegen-body)
                   attr-fn# ~dissoc-fn]
               (if (map? result#)
                 [(:attributes-fn result# attr-fn#)
                  (:options-fn result# identity)]
                 [attr-fn# identity]))
             [identity identity]))))))
(alter-meta! #'deftrait
             assoc :arglists '([name docstring? param-map? [params*] body]))

Utility functions

Makes an ID from arbitrary object by calling .hashCode on it. Returns the absolute value.

(defn to-id
  [obj]
  (Math/abs (.hashCode ^Object obj)))

Implementation of different traits

Def attribute

Takes a symbol provided to :def and binds the widget to it.

Example: [:button {:def ok}] defines a var ok which stores the button object.

(deftrait :def
  [wdg {:keys [def]} _]
  (assert (and (symbol? def) (namespace def)))
  (intern (symbol (namespace def)) (symbol (name def)) wdg))

Basic traits

Sets widget's text to a string, integer ID or a keyword representing the string resource provided to :text attribute.

(deftrait :text
  [^TextView wdg, {:keys [text]} _]
  (.setText wdg ^CharSequence (res/get-string text)))
(defn- kw->unit-id [unit-kw]
  (case unit-kw
    :px TypedValue/COMPLEX_UNIT_PX
    :dp TypedValue/COMPLEX_UNIT_DIP
    :dip TypedValue/COMPLEX_UNIT_DIP
    :sp TypedValue/COMPLEX_UNIT_SP
    :pt TypedValue/COMPLEX_UNIT_PT
    :in TypedValue/COMPLEX_UNIT_IN
    :mm TypedValue/COMPLEX_UNIT_MM
    TypedValue/COMPLEX_UNIT_PX))

Returns Android's DisplayMetrics object from application context.

(memoized
 (defn- get-display-metrics
   []
   (.. context/context (getResources) (getDisplayMetrics))))
(defn to-dimension [value]
  (if (vector? value)
    (Math/round
     ^float (TypedValue/applyDimension (kw->unit-id (second value))
                                       (first value) (get-display-metrics)))
    value))

Takes :text-size attribute which should be either integer or a dimension vector, and sets it to the widget.

(deftrait :text-size
  [^TextView wdg, {:keys [text-size]} _]
  (if (vector? text-size)
    (.setTextSize wdg (kw->unit-id (second text-size)) (first text-size))
    (.setTextSize wdg text-size)))

Takes :image attribute which can be a resource ID, resource keyword, Drawable, Bitmap or URI and sets it ImageView widget's image source.

(deftrait :image
   [^ImageView wdg, {:keys [image]} _]
  (condp instance? image
    Bitmap (.setImageBitmap wdg image)
    Drawable (.setImageDrawable wdg image)
    Keyword (.setImageDrawable wdg (neko.resource/get-drawable image))
    Uri (.setImageURI wdg image)
    ;; Otherwise assume `image` to be resource ID.
    :else (.setImageResource wdg image)))

Layout parameters attributes

(def ^:private margin-attributes [:layout-margin
                                  :layout-margin-left :layout-margin-top
                                  :layout-margin-right :layout-margin-bottom])

Takes a LayoutParams object that implements MarginLayoutParams class and an attribute map, and sets margins for this object.

(defn- apply-margins-to-layout-params
  [^ViewGroup$MarginLayoutParams params, attribute-map]
  (let [common (to-dimension (attribute-map :layout-margin 0))
        [l t r b] (map #(to-dimension (attribute-map % common))
                       (rest margin-attributes))]
    (.setMargins params l t r b)))

Takes :layout-width and :layout-height attributes and sets LayoutParams, if the container type is not specified.

(deftrait :default-layout-params
  {:attributes [:layout-width :layout-height]
   :applies? (and (or layout-width layout-height) (nil? container-type))}
  [^View wdg, {:keys [layout-width layout-height]} {:keys [container-type]}]
  (let [^int width  (kw/value :layout-params (or layout-width  :wrap))
        ^int height (kw/value :layout-params (or layout-height :wrap))]
   (.setLayoutParams wdg (ViewGroup$LayoutParams. width height))))

Takes :layout-width, :layout-height, :layout-weight, :layout-gravity and different layout margin attributes and sets LinearLayout.LayoutParams if current container is LinearLayout. Values could be either numbers of :fill or :wrap.

(deftrait :linear-layout-params
  {:attributes (concat margin-attributes [:layout-width :layout-height
                                          :layout-weight :layout-gravity])
   :applies? (= container-type :linear-layout)}
  [^View wdg, {:keys [layout-width layout-height layout-weight layout-gravity]
               :as attributes}
   {:keys [container-type]}]
  (let [width  (kw/value :layout-params (or layout-width  :wrap))
        height (kw/value :layout-params (or layout-height :wrap))
        weight (or layout-weight 0)
        params (LinearLayout$LayoutParams. width height weight)]
    (apply-margins-to-layout-params params attributes)
    (when layout-gravity
      (set! (. params gravity)
            (kw/value :layout-params layout-gravity :gravity)))
    (.setLayoutParams wdg params)))

Relative layout

(def ^:private relative-layout-attributes
  ;; Hard-coded number values are attributes that appeared since
  ;; Android Jellybean.
  {:standalone {:layout-align-parent-bottom  RelativeLayout/ALIGN_PARENT_BOTTOM
                :layout-align-parent-end     21 ; RelativeLayout/ALIGN_PARENT_END
                :layout-align-parent-left    RelativeLayout/ALIGN_PARENT_LEFT
                :layout-align-parent-right   RelativeLayout/ALIGN_PARENT_RIGHT
                :layout-align-parent-start   20 ; RelativeLayout/ALIGN_PARENT_START
                :layout-align-parent-top     RelativeLayout/ALIGN_PARENT_TOP
                :layout-center-horizontal    RelativeLayout/CENTER_HORIZONTAL
                :layout-center-vertical      RelativeLayout/CENTER_VERTICAL
                :layout-center-in-parent     RelativeLayout/CENTER_IN_PARENT}
   :with-id    {:layout-above                RelativeLayout/ABOVE
                :layout-align-baseline       RelativeLayout/ALIGN_BASELINE
                :layout-align-bottom         RelativeLayout/ALIGN_BOTTOM
                :layout-align-end            19 ; RelativeLayout/ALIGN_END
                :layout-align-left           RelativeLayout/ALIGN_LEFT
                :layout-align-right          RelativeLayout/ALIGN_RIGHT
                :layout-align-start          18 ; RelativeLayout/ALIGN_START
                :layout-align-top            RelativeLayout/ALIGN_TOP
                :layout-below                RelativeLayout/BELOW
                :layout-to-end-of            17 ; RelativeLayout/END_OF
                :layout-to-left-of           RelativeLayout/LEFT_OF
                :layout-to-right-of          RelativeLayout/RIGHT_OF
                :layout-to-start-of          16 ; RelativeLayout/START_OF
                }})
(def ^:private all-relative-attributes
  (apply concat [:layout-width :layout-height
                 :layout-align-with-parent-if-missing]
         (map keys (vals relative-layout-attributes))))
(deftrait :relative-layout-params
  {:attributes (concat all-relative-attributes margin-attributes)
   :applies? (= container-type :relative-layout)}
  [^View wdg, {:keys [layout-width layout-height
                      layout-align-with-parent-if-missing] :as attributes}
   {:keys [container-type]}]
  (let [^int width  (kw/value :layout-params (or layout-width  :wrap))
        ^int height (kw/value :layout-params (or layout-height :wrap))
        lp (RelativeLayout$LayoutParams. width height)]
    (when-not (nil? layout-align-with-parent-if-missing)
      (set! (. lp alignWithParent) layout-align-with-parent-if-missing))
    (doseq [[attr-name attr-id] (:standalone relative-layout-attributes)]
      (when (= (attr-name attributes) true)
        (.addRule lp attr-id)))
    (doseq [[attr-name attr-id] (:with-id relative-layout-attributes)]
      (when (contains? attributes attr-name)
        (.addRule lp attr-id (to-id (attr-name attributes)))))
    (apply-margins-to-layout-params lp attributes)
    (.setLayoutParams wdg lp)))
(deftrait :listview-layout-params
  {:attributes [:layout-width :layout-height :layout-view-type]
   :applies? (= container-type :abs-listview-layout)}
  [^View wdg, {:keys [layout-width layout-height layout-view-type]
               :as attributes}
   {:keys [container-type]}]
  (let [^int width  (kw/value :layout-params (or layout-width  :wrap))
        ^int height (kw/value :layout-params (or layout-height :wrap))]
    (.setLayoutParams
     wdg (if layout-view-type
           (AbsListView$LayoutParams. width height layout-view-type)
           (AbsListView$LayoutParams. width height)))))

Takes :padding, :padding-bottom, :padding-left, :padding-right and :padding-top and set element's padding according to their values. Values might be either integers or vectors like [number unit-kw], where unit keyword is one of the following: :px, :dip, :sp, :pt, :in, :mm.

(deftrait :padding
  {:attributes [:padding :padding-bottom :padding-left
                :padding-right :padding-top]}
  [wdg {:keys [padding padding-bottom padding-left
               padding-right padding-top]} _]
  (.setPadding ^View wdg
               (to-dimension (or padding-left padding 0))
               (to-dimension (or padding-top padding 0))
               (to-dimension (or padding-right padding 0))
               (to-dimension (or padding-bottom padding 0))))

Puts the type of the widget onto the options map so subelement can use the container type to choose the correct LayoutParams instance.

(deftrait :container
  {:applies? (constantly true)}
  [wdg _ __]
  {:options-fn #(assoc % :container-type (kw/keyword-by-classname (type wdg)))})

Listener traits

Takes :on-click attribute, which should be function of one argument, and sets it as an OnClickListener for the widget.

(deftrait :on-click
  [^View wdg, {:keys [on-click]} _]
  (.setOnClickListener wdg (view-listeners/on-click-call on-click)))

Takes :on-create-context-menu attribute, which should be function of three arguments, and sets it as an OnCreateContextMenuListener for the object.

(deftrait :on-create-context-menu
  [^View wdg, {:keys [on-create-context-menu]} _]
  (.setOnCreateContextMenuListener
   wdg (view-listeners/on-create-context-menu-call on-create-context-menu)))

Takes :on-focus-change attribute, which should be function of two arguments, and sets it as an OnFocusChangeListener for the object.

(deftrait :on-focus-change
  [^View wdg, {:keys [on-focus-change]} _]
  (.setOnFocusChangeListener
   wdg (view-listeners/on-focus-change-call on-focus-change)))

Takes :on-key attribute, which should be function of three arguments, and sets it as an OnKeyListener for the widget.

(deftrait :on-key
  [^View wdg, {:keys [on-key]} _]
  (.setOnKeyListener wdg (view-listeners/on-key-call on-key)))

Takes :on-long-click attribute, which should be function of one argument, and sets it as an OnLongClickListener for the widget.

(deftrait :on-long-click
  [^View wdg, {:keys [on-long-click]} _]
  (.setOnLongClickListener
   wdg (view-listeners/on-long-click-call on-long-click)))

Takes :on-touch attribute, which should be function of two arguments, and sets it as an OnTouchListener for the widget.

(deftrait :on-touch
  [^View wdg, {:keys [on-touch]} _]
  (.setOnTouchListener wdg (view-listeners/on-touch-call on-touch)))

Takes :on-query-text-change and :on-query-text-submit attributes, which should be functions of one or two arguments, depending on the context of usage. If widget is used as an action item in a menu, two arguments are passed to the function - query text and the menu item, for which widget is being action item to. Otherwise only query text is passed to the functions.

Then OnQueryTextListener object is created from the functions and set to the widget.

(deftrait :on-query-text
  {:attributes [:on-query-text-change :on-query-text-submit]}
  [^SearchView wdg, {:keys [on-query-text-change on-query-text-submit]}
   {:keys [menu-item]}]
  (.setOnQueryTextListener
   wdg (neko.listeners.search-view/on-query-text-call
        (if (and menu-item on-query-text-change)
          (fn [q] (on-query-text-change q menu-item))
          on-query-text-change)
        (if (and menu-item on-query-text-submit)
          (fn [q] (on-query-text-submit q menu-item))
          on-query-text-submit))))

ID storing traits

Takes :id-holder attribute which should equal true and marks the widget to be a holder of lower-level elements. Elements are stored by their IDs as keys in a map, which is accessible by calling .getTag on the holder widget.

Example:

(def foo (make-ui [:linear-layout {:id-holder true} [:button {:id ::abutton}]])) (::abutton (.getTag foo)) => internal Button widget.

(deftrait :id-holder
  [^View wdg, _ __]
  (.setTag wdg (HashMap.))
  {:options-fn #(assoc % :id-holder wdg)})

Takes :id attribute, which can either be an integer or a keyword (that would be transformed into integer as well) and sets it as widget's ID attribute. Also, if an ID holder was declared in this tree, stores the widget in id-holder's tag (see docs for :id-holdertrait).

(deftrait :id
  [^View wdg, {:keys [id]} {:keys [^View id-holder]}]
  (.setId wdg (to-id id))
  (when id-holder
    (.put ^HashMap (.getTag id-holder) id wdg)))
 
{res/id neko.resource/resolve-id-reader
 res/layout neko.resource/resolve-layout-reader
 res/string neko.resource/resolve-string-reader
 res/drawable neko.resource/resolve-drawable-reader}