aboutsummaryrefslogtreecommitdiff
path: root/library/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/main')
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt12
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt2
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt53
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt97
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt4
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt26
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt7
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt8
-rw-r--r--library/src/main/res/layout/kau_search_item.xml33
-rw-r--r--library/src/main/res/layout/kau_search_view.xml112
-rw-r--r--library/src/main/res/values/dimens.xml3
-rw-r--r--library/src/main/res/values/dimens_search.xml24
-rw-r--r--library/src/main/res/values/ids.xml1
-rw-r--r--library/src/main/res/values/strings_commons.xml1
-rw-r--r--library/src/main/res/values/styles.xml8
15 files changed, 386 insertions, 5 deletions
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
index 89afaab..723af45 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
@@ -6,6 +6,8 @@ import android.support.v7.widget.RecyclerView
import ca.allanwang.kau.R
import ca.allanwang.kau.kpref.items.*
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 2017-06-08.
@@ -21,10 +23,14 @@ fun RecyclerView.setKPrefAdapter(globalOptions: GlobalOptions, builder: KPrefAda
layoutManager = LinearLayoutManager(context)
val adapter = FastItemAdapter<KPrefItemCore>()
adapter.withOnClickListener { v, _, item, _ -> item.onClick(v, v.findViewById(R.id.kau_pref_inner_content)) }
- val items = KPrefAdapterBuilder(globalOptions)
- builder.invoke(items)
- adapter.add(items.list)
this.adapter = adapter
+ doAsync {
+ val items = KPrefAdapterBuilder(globalOptions)
+ builder.invoke(items)
+ uiThread {
+ adapter.add(items.list)
+ }
+ }
return adapter
}
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt
index 9732b98..a782430 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt
@@ -8,6 +8,8 @@ import ca.allanwang.kau.kpref.GlobalOptions
* Created by Allan Wang on 2017-06-14.
*
* Just text with the core options. Extends base preference but has an empty getter and setter
+ * Useful replacement of [KPrefText] when nothing is displayed on the right side,
+ * and when the preference is completely handled by the click
*
*/
class KPrefPlainText(val builder: KPrefPlainTextBuilder) : KPrefItemBase<Unit>(builder) {
diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt
new file mode 100644
index 0000000..0b36a5d
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt
@@ -0,0 +1,53 @@
+package ca.allanwang.kau.searchview
+
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.LazyResettable
+import ca.allanwang.kau.utils.bindView
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-06-23.
+ *
+ * A holder for each individual search item
+ * Contains a [key] which acts as a unique identifier (eg url)
+ * and a [content] which is displayed in the item
+ */
+class SearchItem(val key: String, val content: String = key) : AbstractItem<SearchItem, SearchItem.ViewHolder>() {
+
+ companion object {
+ var foregroundColor: Int?=null
+ }
+
+ override fun getLayoutRes(): Int = R.layout.kau_search_item
+
+ override fun getType(): Int = R.id.kau_item_search
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ if (foregroundColor != null) {
+ holder.text.setTextColor(foregroundColor!!)
+ holder.icon.drawable.setTint(foregroundColor!!)
+ }
+ holder.text.text = content
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ holder.text.text = null
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val icon: ImageView by bindView(R.id.search_icon)
+ val text: TextView by bindView(R.id.search_text)
+
+ init {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
new file mode 100644
index 0000000..d786e13
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
@@ -0,0 +1,97 @@
+package ca.allanwang.kau.searchview
+
+import android.content.Context
+import android.support.annotation.ColorInt
+import android.support.annotation.IdRes
+import android.support.v7.widget.CardView
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.view.Menu
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.ProgressBar
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.*
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import com.mikepenz.google_material_typeface_library.GoogleMaterial
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-23.
+ */
+class SearchView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ //configs
+ var foregroundColor: Int = 0xddffffff.toInt()
+ set(value) {
+ if (field == value) return
+ field = value
+ tintForeground(value)
+ }
+ var navIcon: IIcon? = GoogleMaterial.Icon.gmd_arrow_back
+ set(value) {
+ field = value
+ iconNav.setIcon(value)
+ if (value == null) iconNav.gone()
+ }
+ var micIcon: IIcon? = GoogleMaterial.Icon.gmd_mic
+ set(value) {
+ field = value
+ iconMic.setIcon(value)
+ if (value == null) iconMic.gone()
+ }
+ var clearIcon: IIcon? = GoogleMaterial.Icon.gmd_clear
+ set(value) {
+ field = value
+ iconClear.setIcon(value)
+ if (value == null) iconClear.gone()
+ }
+
+ //views
+ private val shadow: View by bindView(R.id.search_shadow)
+ private val card: CardView by bindView(R.id.search_shadow)
+ private val iconNav: ImageView by bindView(R.id.search_nav)
+ //TODO edittext
+ private val progress: ProgressBar by bindView(R.id.search_progress)
+ private val iconMic: ImageView by bindView(R.id.search_mic)
+ private val iconClear: ImageView by bindView(R.id.search_clear)
+ private val recycler: RecyclerView by bindView(R.id.search_recycler)
+ private val adapter = FastItemAdapter<SearchItem>()
+
+ init {
+ View.inflate(context, R.layout.kau_search_view, this)
+ iconNav.setIcon(navIcon)
+ iconMic.setIcon(micIcon)
+ iconClear.setIcon(clearIcon)
+ tintForeground(foregroundColor)
+ with(recycler) {
+ layoutManager = LinearLayoutManager(context)
+ addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ super.onScrollStateChanged(recyclerView, newState)
+ if (newState == RecyclerView.SCROLL_STATE_DRAGGING) hideKeyboard()
+ }
+ })
+ adapter = this@SearchView.adapter
+ }
+ }
+
+ fun config(action: SearchView.() -> Unit) = action()
+
+ fun bind(menu: Menu, @IdRes id: Int) {
+ val item = menu.findItem(id)
+ if (item.icon == null) item.icon = GoogleMaterial.Icon.gmd_search.toDrawable(context, 20)
+ gone()
+ }
+
+ fun tintForeground(@ColorInt color: Int) {
+ iconNav.drawable.setTint(color)
+ iconMic.drawable.setTint(color)
+ iconClear.drawable.setTint(color)
+ SearchItem.foregroundColor = color
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt
index c74a3ec..7a13685 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt
@@ -53,4 +53,6 @@ fun Activity.setMenuIcons(menu: Menu, @ColorInt color: Int = Color.WHITE, vararg
iicons.forEach { (id, iicon) ->
menu.findItem(id).icon = iicon.toDrawable(this, sizeDp = 20, color = color)
}
-} \ No newline at end of file
+}
+
+fun Activity.hideKeyboard() = currentFocus.hideKeyboard() \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
index d1e527e..3cf3309 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
@@ -67,6 +67,32 @@ fun View.circularReveal(x: Int = 0, y: Int = 0, offset: Long = 0L, radius: Float
anim.start()
}
+fun View.circularHide(x: Int = 0, y: Int = 0, offset: Long = 0L, radius: Float = -1.0f, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ invisible()
+ onFinish?.invoke()
+ return
+ }
+ var r = radius
+ if (r < 0.0f) {
+ r = Math.max(Math.hypot(x.toDouble(), y.toDouble()), Math.hypot((width - x.toDouble()), (height - y.toDouble()))).toFloat()
+ }
+ val anim = ViewAnimationUtils.createCircularReveal(this, x, y, r, 0f).setDuration(duration)
+ anim.startDelay = offset
+ anim.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator?) = onStart?.invoke() ?: Unit
+
+ override fun onAnimationEnd(animation: Animator?) {
+ invisible()
+ onFinish?.invoke() ?: Unit
+ }
+
+ override fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ anim.start()
+}
+
fun View.fadeIn(offset: Long = 0L, duration: Long = 200L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
if (!isAttachedToWindow) {
onStart?.invoke()
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
index 7597f80..31bff97 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
@@ -11,6 +11,8 @@ import android.support.annotation.*
import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.content.ContextCompat
import android.util.TypedValue
+import android.view.View
+import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import ca.allanwang.kau.R
import com.afollestad.materialdialogs.MaterialDialog
@@ -121,4 +123,7 @@ val Context.isNetworkAvailable: Boolean
return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
}
-fun Context.getDip(value: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics) \ No newline at end of file
+fun Context.getDip(value: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics)
+
+val Context.isRtl: Boolean
+ get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
index c64220c..4ec7a4e 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
@@ -1,11 +1,14 @@
package ca.allanwang.kau.utils
+import android.app.Activity
+import android.content.Context
import android.graphics.Color
import android.support.annotation.ColorInt
import android.support.annotation.StringRes
import android.support.design.widget.Snackbar
import android.view.View
import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
@@ -53,3 +56,8 @@ fun ImageView.setIcon(icon: IIcon?, sizeDp: Int = 24, @ColorInt color: Int = Col
setImageDrawable(icon.toDrawable(context, sizeDp = sizeDp, color = color, builder = builder))
}
+fun View.hideKeyboard() {
+ clearFocus()
+ (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(windowToken, 0)
+}
+
diff --git a/library/src/main/res/layout/kau_search_item.xml b/library/src/main/res/layout/kau_search_item.xml
new file mode 100644
index 0000000..fbdc826
--- /dev/null
+++ b/library/src/main/res/layout/kau_search_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/linearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_item_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/search_icon"
+ android:layout_width="@dimen/kau_search_icon"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/kau_search"
+ android:scaleType="center" />
+
+ <TextView
+ android:id="@+id/search_text"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="start|center_vertical"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/kau_search_key_line_16"
+ android:paddingStart="@dimen/kau_search_key_line_8"
+ android:textIsSelectable="false"
+ android:textSize="@dimen/kau_search_text_small" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/library/src/main/res/layout/kau_search_view.xml b/library/src/main/res/layout/kau_search_view.xml
new file mode 100644
index 0000000..799d4df
--- /dev/null
+++ b/library/src/main/res/layout/kau_search_view.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <View
+ android:id="@+id/search_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/search_cardview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/linearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_height"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/search_nav"
+ android:layout_width="@dimen/kau_search_icon"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:scaleType="centerInside" />
+
+ <com.lapism.searchview.SearchEditText
+ android:id="@+id/search_edit_text"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:background="@android:color/transparent"
+ android:ellipsize="end"
+ android:ems="10"
+ android:gravity="start|center_vertical"
+ android:imeOptions="actionSearch|flagNoExtractUi"
+ android:inputType="textNoSuggestions"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/kau_search_key_line_16"
+ android:paddingStart="@dimen/kau_search_key_line_8"
+ android:privateImeOptions="nm"
+ android:textSize="@dimen/kau_search_text_medium"
+ android:windowSoftInputMode="stateAlwaysHidden" />
+
+ <ProgressBar
+ android:id="@+id/search_progress"
+ style="?android:attr/indeterminateProgressStyle"
+ android:layout_width="@dimen/kau_search_progress"
+ android:visibility="gone"
+ android:layout_height="match_parent" />
+
+ <FrameLayout
+ android:layout_width="48dp"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/search_mic"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:visibility="gone"
+ android:scaleType="center" />
+
+ <ImageView
+ android:id="@+id/search_clear"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:scaleType="center" />
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/search_divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_divider"
+ android:background="?android:attr/listDivider" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/search_recycler"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:overScrollMode="never" />
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</merge> \ No newline at end of file
diff --git a/library/src/main/res/values/dimens.xml b/library/src/main/res/values/dimens.xml
index fddb6e3..0c0e749 100644
--- a/library/src/main/res/values/dimens.xml
+++ b/library/src/main/res/values/dimens.xml
@@ -13,4 +13,7 @@
<dimen name="kau_color_circle_size">56dp</dimen>
<dimen name="kau_status_bar_height">24dp</dimen>
+
+ <dimen name="kau_search_dropdownitem_icon_width">32dip</dimen>
+
</resources>
diff --git a/library/src/main/res/values/dimens_search.xml b/library/src/main/res/values/dimens_search.xml
new file mode 100644
index 0000000..3664403
--- /dev/null
+++ b/library/src/main/res/values/dimens_search.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <dimen name="kau_search_height">46dp</dimen>
+
+ <dimen name="kau_search_key_line_8">8dp</dimen>
+ <dimen name="kau_search_key_line_16">16dp</dimen>
+ <dimen name="kau_search_divider">1dp</dimen>
+
+ <dimen name="kau_search_item_margin_text">24dp</dimen>
+ <dimen name="kau_search_item_height">56dp</dimen>
+ <dimen name="kau_search_icon">56dp</dimen>
+ <dimen name="kau_search_progress">24dp</dimen>
+ <dimen name="kau_search_text_small">14sp</dimen>
+ <dimen name="kau_search_text_medium">16sp</dimen>
+ <dimen name="kau_search_reveal">24dp</dimen>
+
+ <dimen name="kau_search_filter_margin_top">4dp</dimen>
+ <dimen name="kau_search_filter_margin_start">12dp</dimen>
+
+ <dimen name="kau_search_menu_item_margin">1dp</dimen>
+ <dimen name="kau_search_menu_item_margin_left_right">2dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml
index 54f01af..cda9faa 100644
--- a/library/src/main/res/values/ids.xml
+++ b/library/src/main/res/values/ids.xml
@@ -7,4 +7,5 @@
<item name="kau_item_pref_color_picker" type="id" />
<item name="kau_item_pref_sub_item" type="id" />
<item name="kau_item_pref_plain_text" type="id" />
+ <item name="kau_item_search" type="id" />
</resources> \ No newline at end of file
diff --git a/library/src/main/res/values/strings_commons.xml b/library/src/main/res/values/strings_commons.xml
index 2673738..b37c1ea 100644
--- a/library/src/main/res/values/strings_commons.xml
+++ b/library/src/main/res/values/strings_commons.xml
@@ -49,4 +49,5 @@ Most resources are verbatim and x represents a formatted item
<string name="kau_x_minutes">%d minutes</string>
<string name="kau_warning">Warning</string>
<string name="kau_yes">Yes</string>
+ <string name="kau_search">Search</string>
</resources>
diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml
new file mode 100644
index 0000000..c8e2197
--- /dev/null
+++ b/library/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="KauSearchView" parent="Widget.AppCompat.SearchView.ActionBar">
+
+ </style>
+
+</resources> \ No newline at end of file