neko

4.0.0-alpha1


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

dependencies

org.clojure/clojure
1.6.0



(this space intentionally left almost blank)
 

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

(ns neko.-utils
  (:require [clojure.string :as string])
  (:import [java.lang.reflect Method Constructor Field]))

Allows other macros to hard-compile the name of application package in them.

(defmacro app-package-name
  []
  (:neko.init/package-name *compiler-options*))

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#)))))

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

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

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
  (:require [clojure.string :as s]
            [neko.ui :refer [make-ui]]
            [neko.debug :refer [all-activities safe-for-ui]]
            [neko.-utils :as u])
  (:import android.app.Activity
           [android.view View Window]
           android.app.Fragment
           neko.ActivityWithState))

Returns the root view of the given activity.

(defn ^View get-decor-view
  [^Activity activity]
  (.. activity getWindow getDecorView))

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

  • neko.ui tree
  • A view object, which will be used directly
  • An integer presumed to be a valid layout ID.
(defn set-content-view!
  [^Activity activity, view]
  {:pre [(instance? Activity activity)]}
  (cond
   (instance? View view)
   (.setContentView activity ^View view)
   (integer? view)
   (.setContentView activity ^Integer view)
   :else
   (let [dv (get-decor-view activity)]
     (.setTag dv (java.util.HashMap.))
     (.setContentView activity
                      ^View (neko.ui/make-ui-element activity view
                                                     {:id-holder dv})))))

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 booleans whether for each feature that indicates if the feature is supported and now enabled.

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

(defmacro request-window-features!
  [^Activity activity & features]
  {:pre [(every? keyword? features)]}
  `[~@(for [feat features]
        `(.requestWindowFeature
          ~activity ~(symbol (str (.getName Window) "/FEATURE_"
                                  (u/keyword->static-field (name feat))))))])

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

Available optional arguments:

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

:features - window features to be requested for the activity. 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 ^{:forms '[name & options & methods]} defactivity
  [name & args]
  (if (some #{:on-create} args)
    (throw
     (RuntimeException.
      (str "ERROR: This syntax of defactivity is deprecated, please "
           "update it to the new syntax: "
           "https://github.com/clojure-android/neko/wiki/Namespaces#defining-an-activity")))
    (let [[{:keys [extends implements prefix state key features]} methods]
          (loop [args args, options {}, methods {}]
            (cond (empty? args) [options methods]
                  (keyword? (first args))
                  (recur (drop 2 args)
                         (assoc options (first args) (second args))
                         methods)
                  :else
                  (recur (rest args) options
                         (assoc methods (ffirst args) (first args)))))
          sname (u/simple-name name)
          prefix (or prefix (str sname "-"))
          state (or state `(atom {}))]
      `(do
         (gen-class
          :name ~name
          :main false
          :prefix ~prefix
          :init "init"
          :state "state"
          :extends ~(or extends Activity)
          :implements ~(conj implements neko.ActivityWithState)
          :overrides-methods ~(conj (keys methods) 'getState)
          :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})
         ~`(defn ~(symbol (str prefix "init"))
             [] [[] ~state])
         ~`(defn ~(symbol (str prefix "getState"))
             [~(vary-meta 'this assoc :tag name)]
             (.state ~'this))
         ~(when-let [[mname args & body] (get methods 'onCreate)]
            (let [[super-call body] (if (= (ffirst body) '.superOnCreate)
                                      [(first body) (rest body)]
                                      [nil body])]
              `(defn ~(symbol (str prefix mname))
                 [~(vary-meta (first args) assoc :tag name)
                  ~(vary-meta (second args) assoc :tag android.os.Bundle)]
                 ~super-call
                 (.put all-activities '~(.name *ns*) ~'this)
                 ~(when key
                    `(.put all-activities ~key ~'this))
                 ~(when features
                    `(request-window-features! ~'this ~@features))
                 (safe-for-ui ~@body))))
         ~@(for [[_ [mname args & body]] (dissoc methods 'onCreate)]
             `(defn ~(symbol (str prefix mname))
                [~(vary-meta (first args) assoc :tag name)
                 ~@(rest args)]
                (safe-for-ui ~@body)))))))
(defn get-state [^ActivityWithState activity]
  (.getState activity))

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]
     (proxy [Fragment] []
       (onCreateView [inflater container bundle]
         (if (instance? View view)
           view
           (do
             (println "One-argument version is deprecated. Please use (simple-fragment context tree)")
             (make-ui view)))))))
 

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\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"}
  (:require [neko.-utils :as u])
  (:import android.content.Context
           neko.App))

Gets a system service for the given type. Type is a keyword that names the service. Examples include :alarm for the alarm service and :layout-inflater for the layout inflater service.

(defmacro get-service
  {:pre [(keyword? type)]}
  ([type]
   `(get-service neko.App/instance ~type))
  ([context type]
   `(.getSystemService
     ^Context ~context
     ~(symbol (str (.getName Context) "/"
                   (u/keyword->static-field (name type)) "_SERVICE")))))
 

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

(ns neko.data
  (:import android.os.Bundle android.content.Intent
           android.content.SharedPreferences
           neko.App))

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 (name k)))
  (entryAt [this k]
    (clojure.lang.MapEntry. k (.get bundle (name k))))
  (valAt [this k]
    (.get bundle (name k)))
  (valAt [this k default]
    (let [key (name k)]
      (if (.containsKey bundle key)
        (.get bundle (name 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 (name k)))
  (entryAt [this k]
    (clojure.lang.MapEntry. k (.get hmap (name k))))
  (valAt [this k]
    (.get hmap (name k)))
  (valAt [this k default]
    (let [key (name k)]
      (if (.containsKey hmap key)
        (.get hmap (name 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 [_] {}))
 

Utilities for interoperating with SharedPreferences class. The original idea is by Artur Malabarba.

(ns neko.data.shared-prefs
  (:require [clojure.data :as data])
  (:import [android.content Context SharedPreferences SharedPreferences$Editor]
           neko.App))
(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. Possible modes: :private, :world-readable, :world-writeable.

(defn get-shared-preferences
  ([name mode]
   (get-shared-preferences App/instance name mode))
  ([^Context context, 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 put
  [^SharedPreferences$Editor sp-editor, key value]
  (let [key (name 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 (RuntimeException. (str "SharedPreferences doesn't support type: "
                                     (type value)))))))

Links an atom and a SharedPreferences file so that whenever the atom is modified changes are propagated down to SP. Only private mode is supported to avoid inconsistency between the atom and SP.

(defn bind-atom-to-prefs
  [atom prefs-file-name]
  (let [^SharedPreferences sp (get-shared-preferences prefs-file-name :private)]
    (reset! atom (reduce (fn [m [key val]] (assoc m (keyword key) val))
                         {} (.getAll sp)))
    (add-watch atom ::sp-wrapper
               (fn [_ _ old new]
                 (let [^SharedPreferences$Editor editor (.edit sp)
                       [removed added] (data/diff old new)]
                   (doseq [[key _] removed]
                     (.remove editor (name key)))
                   (doseq [[key val] added]
                     (try (put editor key val) (catch RuntimeException ex _)))
                   (.commit editor))))))

Defines a new atom that will be bound to the given SharedPreferences file. The atom can only contain primitive values and strings, and its contents will be persisted between application launches. Be aware that if you add an unsupported value to the atom it will not be saved which can lead to inconsistencies.

(defmacro defpreferences
  [atom-name prefs-file-name]
  `(do (def ~atom-name (atom {}))
       (when App/instance
         (bind-atom-to-prefs ~atom-name ~prefs-file-name))))
 

Alpha - subject to change.

Contains convenience functions to work with SQLite databases Android provides.

(ns neko.data.sqlite
  (:refer-clojure :exclude [update])
  (:require [clojure.string :as string])
  (:import [android.database.sqlite SQLiteDatabase]
           [neko.data.sqlite SQLiteHelper TaggedCursor]
           [android.database Cursor CursorIndexOutOfBoundsException]
           [android.content ContentValues Context]
           [clojure.lang Keyword PersistentVector]
           neko.App))

Database initialization

Mapping of SQLite types to respective Java classes. Byte actually stands for array of bytes, or Blob in SQLite.

(def ^:private supported-types
  {"integer" Integer
   "long" Long
   "text" String
   "boolean" Boolean
   "double" Double
   "blob" 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 a number.")
  (assert (map? (:tables schema)) ":tables should be a map.")
  (assoc schema
    :tables
    (into
     {} (for [[table-name params] (:tables schema)]
          (do
            (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))
            [table-name
             (assoc params
               :columns
               (into
                {} (for [[column-name col-params] (:columns params)]
                     (do
                       (assert (keyword? column-name)
                               (str "Column name should be a keyword: " column-name))
                       (assert (or (map? col-params) (class? Integer))
                               (str "Column type should be a map or a string:"
                                    column-name))
                       (let [col-type (if (string? col-params)
                                        col-params
                                        (:sql-type col-params))
                             java-type (-> (re-matches #"(\w+).*" col-type)
                                           second supported-types)
                             col-params {:type java-type
                                         :sql-type col-type}]
                         (assert java-type
                                 (str "Type is not supported: " (:sql-type col-params)))
                         [column-name col-params])))))])))))

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 ^SQLiteHelper create-helper
  ([schema]
   (create-helper App/instance schema))
  ([^Context context, {:keys [name version tables] :as schema}]
   (SQLiteHelper. (.getApplicationContext context) name version schema
                  (for [table (keys tables)]
                    (db-create-query schema table))
                  (for [^Keyword table (keys tables)]
                    (str "drop table if exists " (.getName table))))))

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

(deftype TaggedDatabase [^SQLiteDatabase db, schema])

Gets a SQLiteDatabase instance from the given helper. Access-mode can be either :read or :write.

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

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 (int 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)))
(defn- qualified-name
  [^Keyword kw]
  (.toString (if (.getNamespace (.sym kw))
               (str (.getNamespace (.sym kw)) "." (.getName (.sym kw)))
               (.getName (.sym kw)))))

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 [qk (qualified-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')" qk v)
      Boolean (format "(%s = %s)" qk (if v 1 0))
      Keyword (format "(%s = %s)" qk (qualified-name v))
      nil (format "(%s is NULL)" qk)
      (format "(%s = %s)" qk 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)))
(defn construct-sql-query [select from where]
  (str "select " (string/join (interpose ", " (map qualified-name select)))
       " from " (cond (string? from) from
                      (sequential? from)
                      (string/join (interpose ", " (map name from)))
                      :else (name from))
       (let [wc (where-clause where)]
         (if-not (empty? wc)
           (str " where " wc) ))))

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

(defn query
  ([^TaggedDatabase tagged-db, from where]
   {:pre [(keyword? from)]}
   (let [columns (->> (get-in (.schema tagged-db) [:tables from :columns])
                      keys)]
     (query tagged-db columns from where)))
  ([^TaggedDatabase tagged-db, column-names from where]
   (let [tables (:tables (.schema tagged-db))
         columns (if (keyword? from)
                   (let [table-cls (:columns (get tables from))]
                     (mapv (fn [cl-name]
                             [cl-name (:type (get table-cls cl-name))])
                           column-names))
                   (mapv (fn [^Keyword kw]
                           (let [cl-name (keyword (.getName kw))
                                 cl (-> (get tables (keyword (.getNamespace kw)))
                                        :columns
                                        (get cl-name))]
                             [kw (:type cl)]))
                         column-names))]
     (TaggedCursor. (.rawQuery ^SQLiteDatabase (.db tagged-db)
                               (construct-sql-query column-names from where) nil)
                    columns))))
(def db-query query)

Reads a single (current) row from TaggedCursor object.

(defn entity-from-cursor
  [^TaggedCursor cursor]
  (reduce-kv
   (fn [data i [column-name type]]
     (assoc data column-name
            (get-value-from-cursor cursor i type)))
   {} (vec (.columns cursor))))

Turns data from TaggedCursor object into a lazy sequence.

(defn seq-cursor
  [^TaggedCursor cursor]
  (.moveToFirst cursor)
  (let [seq-fn (fn seq-fn []
                 (lazy-seq
                  (if (.isAfterLast cursor)
                    (.close cursor)
                    (let [v (entity-from-cursor cursor)]
                      (.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 query output.

(defn query-seq
  {:forms '([tagged-db table-name where] [tagged-db columns from where])}
  [& args]
  (seq-cursor (apply query args)))
(def db-query-seq query-seq)

Executes a SELECT statement against the database on a column and returns a scalar value. column can be either a keyword or string-keyword pair where string denotes the aggregation function.

(defn query-scalar
  [^TaggedDatabase tagged-db column table-name where]
  (let [[aggregator column] (if (vector? column)
                              column [nil column])
        type (get-in (.schema tagged-db)
                     [:tables table-name :columns column :type])
        where-cl (where-clause where)
        query (format "select %s from %s %s"
                      (if aggregator
                        (str aggregator "(" (name column) ")")
                        (name column))
                      (name table-name)
                      (if (seq where-cl)
                        (str "where " where-cl) ""))]
    (with-open [cursor (.rawQuery ^SQLiteDatabase (.db tagged-db) query nil)]
      (try (.moveToFirst ^Cursor cursor)
           (get-value-from-cursor cursor 0 type)
           (catch CursorIndexOutOfBoundsException e nil)))))

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

(defn 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))
(def db-update update)

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

(defn 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)))
(def db-insert insert)

Executed nullary transact-fn in a transaction for batch query execution.

(defn transact*
  [^TaggedDatabase tagged-db, transact-fn]
  (let [^SQLiteDatabase db (.db tagged-db)]
    (try (.beginTransaction db)
         (transact-fn)
         (.setTransactionSuccessful db)
         (finally (.endTransaction db)))))

Wraps the code in beginTransaction-endTransaction calls for batch query execution.

(defmacro transact
  [tagged-db & body]
  `(transact* ~tagged-db (fn [] ~@body)))
 

Contains useful tools to be used while developing the application.

(ns neko.debug
  (:require [neko log notify])
  (:import android.app.Activity
           android.view.WindowManager$LayoutParams
           java.util.WeakHashMap))

Simplify REPL access to Activity objects.

Weak hashmap that contains mapping of namespaces or keywords to Activity objects.

(def ^WeakHashMap all-activities
  (WeakHashMap.))

If called without arguments, returns the activity for the current namespace. A version with one argument will return the activity for the given object (be it a namespace or any other object).

(defmacro ^Activity *a
  ([]
   `(get all-activities '~(.name *ns*)))
  ([key]
   `(get all-activities ~key)))

Exception handling

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)

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 in a do-form.

(defmacro safe-for-ui
  [& body]
  (if (:neko.init/release-build *compiler-options*)
    `(do ~@body)
    `(try ~@body
          (catch Throwable e# (handle-exception-from-ui-thread e#)))))

Wraps the given zero-argument function in safe-for-ui call and returns it, without executing.

(defn safe-for-ui*
  [f]
  (fn [] (safe-for-ui (f))))

A conditional macro that will enforce the screen to stay on while the application is run in the debug mode.

(defmacro keep-screen-on
  [^Activity activity]
  (if (:neko.init/release-build *compiler-options*)
    nil
    `(.addFlags (.getWindow ~activity)
                WindowManager$LayoutParams/FLAG_KEEP_SCREEN_ON)))
 

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
  (:require [neko.listeners.dialog :as listeners]
            [neko.resource :as res]
            neko.ui
            [neko.ui.mapping :refer [defelement]]
            [neko.ui.traits :refer [deftrait]])
  (:import android.app.AlertDialog$Builder))

Takes :positive-text (either string or resource ID) and :positive-callback (function of 2 args: dialog and result), and sets it as the positive button for the dialog.

(deftrait :positive-button
  {:attributes [:positive-text :positive-callback]}
  [^AlertDialog$Builder builder,
   {:keys [positive-text positive-callback]} _]
  (.setPositiveButton builder (res/get-string (.getContext builder) positive-text)
                      (listeners/on-click-call positive-callback)))

Takes :negative-text (either string or resource ID) and :negative-callback (function of 2 args: dialog and result), and sets it as the negative button for the dialog.

(deftrait :negative-button
  {:attributes [:negative-text :negative-callback]}
  [^AlertDialog$Builder builder,
   {:keys [negative-text negative-callback]} _]
  (.setNegativeButton builder (res/get-string (.getContext builder) negative-text)
                      (listeners/on-click-call negative-callback)))

Takes :neutral-text (either string or resource ID) and :neutral-callback (function of 2 args: dialog and result), and sets it as the neutral button for the dialog.

(deftrait :neutral-button
  {:attributes [:neutral-text :neutral-callback]}
  [^AlertDialog$Builder builder,
   {:keys [neutral-text neutral-callback]} _]
  (.setNegativeButton builder (res/get-string (.getContext builder) neutral-text)
                      (listeners/on-click-call neutral-callback)))
(defelement :alert-dialog-builder
  :classname AlertDialog$Builder
  :inherits nil
  :traits [:positive-button :negative-button :neutral-button])

Creates a AlertDialog$Builder options with the given parameters.

(defn ^AlertDialog$Builder alert-dialog-builder
  [context options-map]
  (neko.ui/make-ui context [:alert-dialog-builder options-map]))
 

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 obtaining UI elements by their :id trait. To use the protocol an object has to keep a mapping of IDs to UI widgets. Currently the protocol is supported by Activity and View objects.

(ns neko.find-view
  (:require [neko.activity :refer [get-decor-view]])
  (:import android.view.View
           android.app.Activity))
(defn- nil-or-view?
  [x]
  (or (nil? x)
      (instance? android.view.View x)))

Protocol for finding child views by their :id trait.

(defprotocol ViewFinder
  (find-view [container id]))
(extend-protocol ViewFinder
  View
  (find-view [^View view, id]
    {:post [(nil-or-view? %)]}
    (get (.getTag view) id))

  Activity
  (find-view [^Activity activity, id]
    {:post [(nil-or-view? %)]}
    (find-view (get-decor-view activity) id)))

Same as find-view, but takes a variable number of IDs and returns a vector of found views.

(defn find-views
  [container & ids]
  (map (partial find-view container) ids))
 

Utilities to create Intent objects.

(ns neko.intent
  (:require [neko.-utils :refer [app-package-name]])
  (:import [android.content Context Intent]
           android.os.Bundle))

Puts all values from extras-map into the intent's extras. Returns the Intent object.

(defn put-extras
  [^Intent intent, extras-map]
  (doseq [[key value] extras-map
          :let [key (name key)]]
    (case (type value)
      ;; Non-reflection calls for the most frequent cases.
      Long (.putExtra intent key ^long value)
      Double (.putExtra intent key ^double value)
      String (.putExtra intent key ^String value)
      Boolean (.putExtra intent key ^boolean value)
      Bundle (.putExtra intent key ^Bundle value)
      ;; Else fall back to reflection
      (.putExtra intent key value)))
  intent)

Creates a new Intent object with the supplied extras. In three-arg version classname can be either a Class or a symbol that will be resolved to a class. If symbol starts with dot (like '.MainActivity), application's package name will be prenended.

(defn intent
  ([^String action, extras]
   (put-extras (doto (Intent. action)) extras))
  ([^Context context, classname extras]
   (let [^Class class (if (symbol? classname)
                        (resolve
                         (if (.startsWith ^String (str classname) ".")
                           (symbol (str (app-package-name) classname))
                           classname))
                        classname)]
     (put-extras (doto (Intent. context class)) extras))))
 

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

(ns neko.listeners.adapter-view
  {:author "Daniel Solano Gómez"}
  (:require [neko.debug :refer [safe-for-ui]]))

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
  ^android.widget.AdapterView$OnItemClickListener
  [handler-fn]
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.AdapterView$OnItemClickListener %)]}
  (reify android.widget.AdapterView$OnItemClickListener
    (onItemClick [this parent view position id]
      (safe-for-ui (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
  ^android.widget.AdapterView$OnItemLongClickListener
  [handler-fn]
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.AdapterView$OnItemLongClickListener %)]}
  (reify android.widget.AdapterView$OnItemLongClickListener
    (onItemLongClick [this parent view position id]
      (safe-for-ui (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))
  (^android.widget.AdapterView$OnItemSelectedListener
   [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]
       (safe-for-ui (item-fn parent view position id)))
     (onNothingSelected [this parent]
       (safe-for-ui (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"}
  (:require [neko.debug :refer [safe-for-ui]])
  (: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
  ^android.content.DialogInterface$OnCancelListener
  [handler-fn]
  (reify android.content.DialogInterface$OnCancelListener
    (onCancel [this dialog]
      (safe-for-ui (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
  ^android.content.DialogInterface$OnClickListener
  [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)]
        (safe-for-ui (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
  ^android.content.DialogInterface$OnDismissListener
  [handler-fn]
  (reify android.content.DialogInterface$OnDismissListener
    (onDismiss [this dialog]
      (safe-for-ui (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
  ^android.content.DialogInterface$OnKeyListener
  [handler-fn]
  (reify android.content.DialogInterface$OnKeyListener
    (onKey [this dialog key-code event]
      (safe-for-ui (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
  ^android.content.DialogInterface$OnMultiChoiceClickListener
  [handler-fn]
  (reify android.content.DialogInterface$OnMultiChoiceClickListener
    (onClick [this dialog which checked?]
      (safe-for-ui (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)))
 
(ns neko.listeners.search-view
  (:require [neko.debug :refer [safe-for-ui]])
  (: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
  ^android.widget.SearchView$OnQueryTextListener
  [change-fn submit-fn]
  (reify android.widget.SearchView$OnQueryTextListener
    (onQueryTextChange [this query]
      (safe-for-ui (call-if-nnil change-fn query)))
    (onQueryTextSubmit [this query]
      (safe-for-ui (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"}
  (:require [neko.debug :refer [safe-for-ui]]))

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]
  ^android.widget.TextView$OnEditorActionListener
  {:pre  [(fn? handler-fn)]
   :post [(instance? android.widget.TextView$OnEditorActionListener %)]}
  (reify android.widget.TextView$OnEditorActionListener
    (onEditorAction [this view action-id key-event]
      (safe-for-ui (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"}
  (:require [neko.debug :refer [safe-for-ui]]))

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
  ^android.view.View$OnClickListener
  [handler-fn]
  (reify android.view.View$OnClickListener
    (onClick [this view]
      (safe-for-ui (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
  ^android.view.View$OnCreateContextMenuListener
  [handler-fn]
  (reify android.view.View$OnCreateContextMenuListener
    (onCreateContextMenu [this menu view info]
      (safe-for-ui (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
  ^android.view.View$OnDragListener
  [handler-fn]
  (reify android.view.View$OnDragListener
    (onDrag [this view event]
      (safe-for-ui (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
  ^android.view.View$OnFocusChangeListener
  [handler-fn]
  (reify android.view.View$OnFocusChangeListener
    (onFocusChange [this view focused?]
      (safe-for-ui (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
  ^android.view.View$OnKeyListener
  [handler-fn]
  (reify android.view.View$OnKeyListener
    (onKey [this view key-code event]
      (safe-for-ui (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
  ^android.view.View$OnLayoutChangeListener
  [handler-fn]
  (reify android.view.View$OnLayoutChangeListener
    (onLayoutChange [this view left top right bottom
                     old-left old-top old-right old-bottom]
      (safe-for-ui (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
  ^android.view.View$OnLongClickListener
  [handler-fn]
  (reify android.view.View$OnLongClickListener
    (onLongClick [this view]
      (safe-for-ui (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
  ^android.view.View$OnSystemUiVisibilityChangeListener
  [handler-fn]
  (reify android.view.View$OnSystemUiVisibilityChangeListener
    (onLongClick [this view]
      (safe-for-ui (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
  ^android.view.View$OnTouchListener
  [handler-fn]
  (reify android.view.View$OnTouchListener
    (onTouch [this view event]
      (safe-for-ui (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])

(log/d "Some log string" {:foo 1, :bar 2})
(log/i "Logging to custom tag" [1 2 3] :tag "custom")
(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*))
          ex-form (if exception [exception] ())]
      `(binding [*print-readably* nil]
         (. Log ~logfn ~tag (pr-str ~@strings) ~@ex-form)))))

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
  (:require [neko.-utils :refer [int-id]])
  (:import [android.app Notification NotificationManager PendingIntent]
           [android.content Context Intent]
           android.widget.Toast
           neko.App))

Toasts

Creates a Toast object using a text message and a keyword representing how long a toast should be visible (:short or :long). If length is not provided, it defaults to :long.

(defn toast
  ([message]
   (toast App/instance message :long))
  ([message length]
   (toast App/instance message length))
  ([^Context context, ^String message, length]
   {:pre [(or (= length :short) (= length :long))]}
   (.show
    ^Toast (Toast/makeText context message ^int (case length
                                                  :short Toast/LENGTH_SHORT
                                                  :long Toast/LENGTH_LONG)))))

Notifications

Returns the notification manager instance.

(defn- ^NotificationManager notification-manager
  ([^Context context]
   (.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
  ([context [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
  ([options]
   (notification App/instance options))
  ([context {:keys [icon ticker-text when content-title content-text action]
             :or {icon android.R$drawable/ic_dialog_info
                  when (System/currentTimeMillis)}}]
   (let [notification (Notification. icon ticker-text when)]
     (.setLatestEventInfo notification context content-title content-text
                          (construct-pending-intent context action))
     notification)))

Sends the notification to the status bar. ID can be an integer or a keyword.

(defn fire
  ([id notification]
   (fire App/instance id notification))
  ([context id notification]
   (let [id (if (keyword? id)
              (int-id id)
              id)]
     (.notify (notification-manager context) id notification))))

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

(defn cancel
  ([id]
   (cancel App/instance id))
  ([context id]
   (.cancel (notification-manager context) (if (keyword? id)
                                             (int-id id)
                                             id))))
 

Provides utilities to resolve application resources.

(ns neko.resource
  (:require [neko.-utils :refer [app-package-name]])
  (:import android.content.Context
           android.graphics.drawable.Drawable
           neko.App))

Imports all existing application's R subclasses (R$drawable, R$string etc.) into the current namespace.

(defmacro import-all
  []
  `(do ~@(map (fn [res-type]
                `(try (import '~(-> (app-package-name)
                                    (str ".R$" res-type)
                                    symbol))
                      (catch ClassNotFoundException _# nil)))
              '[anim drawable color layout menu string array plurals style id
                dimen])))
(import-all)

Runtime resource resolution

Gets the localized string for the given resource ID. 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
  {:forms '([res-id & format-args?] [context res-id & format-args?])}
  [& args]
  (let [[^Context context args] (if (instance? Context (first args))
                                  [(first args) (rest args)]
                                  [App/instance args])
        [res-id & format-args] args]
    (cond (not (number? res-id)) res-id
          format-args      (.getString context res-id (to-array format-args))
          :else            (.getString context res-id))))

Gets a Drawable object associated with the given resource ID. If res-id is a Drawable, returns it unchanged.

(defn get-drawable
  ([res-id]
   (get-drawable App/instance res-id))
  ([^Context context, res-id]
   (if-not (number? res-id)
     res-id
     (.getDrawable (.getResources context) res-id))))
 

Utilities used to manage multiple threads on Android.

(ns neko.threading
  (:use [neko.debug :only [safe-for-ui]])
  (:import android.view.View
           android.os.Looper
           android.os.Handler))

UI thread utilities

Returns true if the current thread is a UI thread.

(defn on-ui-thread?
  []
  (identical? (Thread/currentThread)
              (.getThread ^Looper (Looper/getMainLooper))))

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]
  `(if (on-ui-thread?)
     (safe-for-ui ~@body)
     (.post (Handler. (Looper/getMainLooper)) (fn [] (safe-for-ui ~@body)))))

Functional version of on-ui, runs the nullary function on the UI thread.

(defn on-ui*
  [f]
  (on-ui (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 ~view (fn [] ~@body)))

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]
  `(.postDelayed ^View ~view ~millis (fn [] ~@body)))
 
(ns neko.tools.repl
  (:require [neko.log :as log])
  (:import android.content.Context
           android.util.Log
           java.io.FileNotFoundException
           java.util.concurrent.atomic.AtomicLong
           java.util.concurrent.ThreadFactory))

A vector containing all CIDER middleware.

(def cider-middleware
  '[cider.nrepl.middleware.apropos/wrap-apropos
    cider.nrepl.middleware.classpath/wrap-classpath
    cider.nrepl.middleware.complete/wrap-complete
    cider.nrepl.middleware.info/wrap-info
    cider.nrepl.middleware.inspect/wrap-inspect
    cider.nrepl.middleware.macroexpand/wrap-macroexpand
    cider.nrepl.middleware.ns/wrap-ns
    cider.nrepl.middleware.resource/wrap-resource
    cider.nrepl.middleware.stacktrace/wrap-stacktrace
    cider.nrepl.middleware.test/wrap-test
    cider.nrepl.middleware.trace/wrap-trace
    cider.nrepl.middleware.undef/wrap-undef])

Checks if cider-nrepl dependency is present on the classpath.

(defn cider-available?
  []
  (try (require 'cider.nrepl.version)
       true
       (catch FileNotFoundException e false)))

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))))))

Some non-critical CIDER and nREPL dependencies cannot be used on Android as-is, so they have to be tranquilized.

(defn- patch-unsupported-dependencies
  []
  (let [curr-ns (ns-name *ns*)]
    (ns dynapath.util)
    (defn add-classpath! [& _])
    (defn addable-classpath [& _])
    (in-ns curr-ns)))

Initializes compliment sources if their namespaces are present.

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

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
  [middleware & repl-args]
  (binding [*ns* (create-ns 'user)]
    (refer-clojure)
    (patch-unsupported-dependencies)
    (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)
              :handler (apply (resolve 'default-handler)
                              (map (fn [sym]
                                     (require (symbol (namespace sym)))
                                     (resolve sym))
                                   middleware))
              repl-args))))

Expands into nREPL server initialization if conditions are met.

(defmacro start-nrepl-server
  [args]
  (when (or (not (:neko.init/release-build *compiler-options*))
            (:neko.init/start-nrepl-server *compiler-options*))
    (let [build-port (:neko.init/nrepl-port *compiler-options*)
          mware (when (cider-available?)
                  (list `quote
                        (or (:neko.init/nrepl-middleware *compiler-options*)
                            cider-middleware)))]
      `(let [port# (or ~(:port args) ~build-port 9999)
             args# (assoc ~args :port port#)]
         (try (apply start-repl ~mware (mapcat identity args#))
              (neko.log/i "Nrepl started at port" port#)
              (catch Exception ex#
                (neko.log/e "Failed to start nREPL" :exception ex#)))))))

Entry point to neko.tools.repl namespace from Java code.

(defn init
  [& {:as args}]
  (start-nrepl-server args))
 

Tools for defining and manipulating Android UI elements.

(ns neko.ui
  (:require [neko.ui.mapping :as kw]
            [neko.ui.traits :refer [apply-trait]]
            [neko.-utils :refer [keyword->setter reflect-setter
                                 reflect-constructor]])
  (:import android.content.res.Configuration
           neko.App))

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 an activity instance, and 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].

(defn make-ui
  [activity tree]
  (make-ui-element activity 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 {}))

Compatibility with Android XML UI facilities.

Renders a View object for the given XML layout ID.

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

Utilities

Returns either :portrait, :landscape, :square, or :undefined depending on the current orientation of the device.

(defn get-screen-orientation
  ([]
   (get-screen-orientation App/instance))
  ([context]
   (condp = (.. context (getResources) (getConfiguration) orientation)
     Configuration/ORIENTATION_PORTRAIT :portrait
     Configuration/ORIENTATION_LANDSCAPE :landscape
     Configuration/ORIENTATION_SQUARE :square
     :undefined)))
 

Contains custom adapters for ListView and Spinner.

(ns neko.ui.adapters
  (:require [neko.debug :refer [safe-for-ui]]
            [neko.data.sqlite :refer [entity-from-cursor]]
            [neko.threading :refer [on-ui]]
            [neko.ui :refer [make-ui-element]])
  (:import android.view.View
           neko.data.sqlite.TaggedCursor
           [neko.ui.adapters InterchangeableListAdapter TaggedCursorAdapter]))

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 context. 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 [context]
                     (or (safe-for-ui
                          (let [view (create-view-fn context)]
                            (if (instance? View view)
                              view
                              (make-ui-element
                               context view
                               {:container-type :abs-listview-layout}))))
                         (android.view.View. context)))
         adapter (InterchangeableListAdapter.
                  create-fn
                  (fn [pos view parent data]
                    (safe-for-ui (update-view-fn pos view parent data)))
                  (access-fn @ref-type))]
     (add-watch ref-type ::adapter-watch
                (fn [_ __ ___ new-state]
                  (on-ui (.setData adapter (access-fn new-state)))))
     adapter)))

Takes a context, a function that creates a View, and a function that updates a view according to the element, and a TaggedCursor instance or cursor-producing function. Returns an Adapter object that displays cursor contents.

create-view-fn is a nullary function that returns a UI tree or a View. update-view-fn is a function of three arguments: view to update, cursor, and data extracted from the cursor. cursor-or-cursor-fn can be a nullary function that returns a TaggedCursor cursor object when called, or just a cursor. In the former case you can refresh adapter by calling `(.updateCursor adapter), in the latter you have to call(.updateCursor adapter new-cursor)`.

(defn cursor-adapter
  [context create-view-fn update-view-fn cursor-or-cursor-fn]
  {:pre [(fn? create-view-fn) (fn? update-view-fn)
         (or (fn? cursor-or-cursor-fn)
             (instance? TaggedCursor cursor-or-cursor-fn))]}
  (let [create-fn (fn [context]
                    (or (safe-for-ui
                         (let [view (create-view-fn)]
                           (if (instance? View view)
                             view
                             (make-ui-element
                              context view
                              {:container-type :abs-listview-layout}))))
                        (android.view.View. context)))]
    (TaggedCursorAdapter.
     context create-fn
     (fn [view cursor data]
       (safe-for-ui (update-view-fn view cursor data)))
     cursor-or-cursor-fn)))

Updates cursor in a given TaggedCursorAdapter. Second argument is necessary if the adapter was created with a cursor rather than cursor-fn.

(defn update-cursor
  ([^TaggedCursorAdapter cursor-adapter]
   (.updateCursor cursor-adapter))
  ([^TaggedCursorAdapter cursor-adapter new-cursor]
   (.updateCursor cursor-adapter new-cursor)))
 

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 ScrollView FrameLayout
            Gallery]
           android.app.ProgressDialog
           android.view.inputmethod.EditorInfo
           [android.view View ViewGroup$LayoutParams Gravity]))

This atom keeps all the relations inside the map.

(def ^{:private true} keyword-mapping
  (atom
   ;; UI widgets
   {:view {:classname android.view.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
                    :frame-layout-params :gallery-layout-params]
           :value-namespaces
           {:gravity android.view.Gravity
            :visibility android.view.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}
    :relative-layout {:classname android.widget.RelativeLayout
                      :inherits :view-group}
    :frame-layout {:classname android.widget.FrameLayout
                   :inherits :view-group}
    :edit-text {:classname android.widget.EditText
                :inherits :view
                :values {:number      EditorInfo/TYPE_CLASS_NUMBER
                         :datetime    EditorInfo/TYPE_CLASS_DATETIME
                         :text        EditorInfo/TYPE_CLASS_TEXT
                         :phone       EditorInfo/TYPE_CLASS_PHONE
                         :go          EditorInfo/IME_ACTION_GO
                         :done        EditorInfo/IME_ACTION_DONE
                         :unspecified EditorInfo/IME_ACTION_UNSPECIFIED
                         :send        EditorInfo/IME_ACTION_SEND
                         :search      EditorInfo/IME_ACTION_SEARCH
                         :previous    EditorInfo/IME_ACTION_PREVIOUS
                         :next        EditorInfo/IME_ACTION_NEXT}}
    :progress-bar {:classname android.widget.ProgressBar
                   :inherits  :view
                   :value-namespaces {:visibility android.view.View}}
    :progress-bar-large {:inherits  :progress-bar
                         :constructor-args [nil android.R$attr/progressBarStyleLarge]}
    :text-view {:classname android.widget.TextView
                :inherits :view
                :value-namespaces
                {:ellipsize android.text.TextUtils$TruncateAt}
                :traits [:text :text-size :on-editor-action]}
    :list-view {:classname android.widget.ListView
                :inherits :view-group
                :traits [:on-item-click]}
    :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}
    :scroll-view {:classname android.widget.ScrollView
                  :inherits :view}
    :gallery {:classname android.widget.Gallery
              :inherits :view-group
              :traits [:on-item-click]}
    ;; 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.FrameLayout :frame-layout
    android.widget.EditText :edit-text
    android.widget.TextView :text-view
    android.widget.ListView :list-view
    android.widget.ImageView :image-view
    android.webkit.WebView :web-view
    android.widget.ScrollView :scroll-view
    android.widget.Gallery :gallery
    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 [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, :container-type.

(defn defelement
  [kw-name & {:as args}]
  (swap! keyword-mapping
         #(assoc % kw-name
                 (let [parent (if (contains? args :inherits)
                                (:inherits args)
                                :view)
                       classname (if (contains? args :classname)
                                   (:classname args)
                                   (get-in % [parent :classname]))]
                   (assoc args :inherits parent :classname classname))))
  (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.debug :refer [safe-for-ui]]
            [neko.ui :as ui]
            [neko.ui.mapping :refer [defelement]]
            [neko.ui.traits :refer [deftrait]]
            [neko.-utils :refer [call-if-nnil int-id]])
  (: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 (int-id (or (:id attributes) Menu/NONE))
             order (int-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]
      (safe-for-ui (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. Activity instance must be provided via :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 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.listeners.view :as view-listeners]
            [neko.listeners.text-view :as text-view-listeners]
            [neko.listeners.adapter-view :as adapter-view]
            neko.listeners.search-view
            [neko.-utils :refer [memoized int-id]])
  (:import [android.widget LinearLayout$LayoutParams ListView TextView SearchView
            ImageView RelativeLayout RelativeLayout$LayoutParams
            AbsListView$LayoutParams FrameLayout$LayoutParams Gallery$LayoutParams]
           [android.view View ViewGroup$LayoutParams
            ViewGroup$MarginLayoutParams]
           android.graphics.Bitmap android.graphics.drawable.Drawable
           android.net.Uri
           android.util.TypedValue
           android.content.Context
           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]))

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 or a resource ID representing a string resource provided to :text attribute.

(deftrait :text
  [^TextView wdg, {:keys [text] :or {text ""}} _]
  (.setText wdg ^CharSequence (res/get-string (.getContext wdg) 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]
   (.. context (getResources) (getDisplayMetrics))))
(defn to-dimension [context value]
  (if (vector? value)
    (Math/round
     ^float (TypedValue/applyDimension
             (kw->unit-id (second value))
             (first value) (get-display-metrics context)))
    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)
    Uri (.setImageURI wdg image)
    ;; Otherwise assume `image` to be resource ID.
    (.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
  [context, ^ViewGroup$MarginLayoutParams params, attribute-map]
  (let [common (to-dimension context (attribute-map :layout-margin 0))
        [l t r b] (map #(to-dimension context (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 (->> (or layout-width :wrap)
                        (kw/value :layout-params)
                        (to-dimension (.getContext wdg)))
        ^int height (->> (or layout-height :wrap)
                         (kw/value :layout-params)
                         (to-dimension (.getContext wdg)))]
    (.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 [^int width (->> (or layout-width :wrap)
                        (kw/value :layout-params)
                        (to-dimension (.getContext wdg)))
        ^int height (->> (or layout-height :wrap)
                         (kw/value :layout-params)
                         (to-dimension (.getContext wdg)))
        weight (or layout-weight 0)
        params (LinearLayout$LayoutParams. width height weight)]
    (apply-margins-to-layout-params (.getContext wdg) 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 (->> (or layout-width :wrap)
                        (kw/value :layout-params)
                        (to-dimension (.getContext wdg)))
        ^int height (->> (or layout-height :wrap)
                         (kw/value :layout-params)
                         (to-dimension (.getContext wdg)))
        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 (int-id (attr-name attributes)))))
    (apply-margins-to-layout-params (.getContext wdg) 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 (->> (or layout-width :wrap)
                        (kw/value :layout-params)
                        (to-dimension (.getContext wdg)))
        ^int height (->> (or layout-height :wrap)
                         (kw/value :layout-params)
                         (to-dimension (.getContext wdg)))]
    (.setLayoutParams
     wdg (if layout-view-type
           (AbsListView$LayoutParams. width height layout-view-type)
           (AbsListView$LayoutParams. width height)))))
(deftrait :gallery-layout-params
  {:attributes [:layout-width :layout-height]
   :applies? (= container-type :gallery)}
  [^View wdg, {:keys [layout-width layout-height]
               :as attributes}
   {:keys [container-type]}]
  (let [^int width (->> (or layout-width :wrap)
                        (kw/value :layout-params)
                        (to-dimension (.getContext wdg)))
        ^int height (->> (or layout-height :wrap)
                         (kw/value :layout-params)
                         (to-dimension (.getContext wdg)))]
    (.setLayoutParams wdg (Gallery$LayoutParams. width height))))

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

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

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]}
  [^View wdg {:keys [padding padding-bottom padding-left
                     padding-right padding-top]} _]
  (let [ctx (.getContext wdg)]
    (.setPadding wdg
                 (to-dimension ctx (or padding-left padding 0))
                 (to-dimension ctx (or padding-top padding 0))
                 (to-dimension ctx (or padding-right padding 0))
                 (to-dimension ctx (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 _ __]
  (let [kw (kw/keyword-by-classname (type wdg))
        container-type (-> (kw/get-keyword-mapping) kw :container-type)]
    {:options-fn #(assoc % :container-type (or container-type kw))}))

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))))

Takes :on-editor-action attribute, which should be function of three arguments, and sets it as OnEditorAction for the TexView widget

(deftrait :on-editor-action
  [^TextView wdg, {:keys [on-editor-action]} _]
  (.setOnEditorActionListener wdg (text-view-listeners/on-editor-action-call on-editor-action)))

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 (int-id id))
  (when id-holder
    (.put ^HashMap (.getTag id-holder) id wdg)))

Takes :on-item-click attribute, which should be function of four arguments

parent   AdapterView of the originating click
view     Item view
position Item view position
id       Item view row id

and sets it as an OnItemClickListener for the widget.

(deftrait :on-item-click
  [^ListView wdg, {:keys [on-item-click]} _]
  (.setOnItemClickListener wdg (adapter-view/on-item-click-call on-item-click)))