<commit_before>/*
 * Copyright 2019 Poly Forest, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.acornui.component

import com.acornui.Disposable
import com.acornui.collection.ListView
import com.acornui.collection.ObservableList
import com.acornui.collection.indexOfFirst2
import com.acornui.collection.indexOfLast2
import com.acornui.component.layout.DataScrollerStyle
import com.acornui.component.layout.algorithm.VerticalLayoutData
import com.acornui.component.layout.algorithm.virtual.ItemRendererOwner
import com.acornui.component.layout.algorithm.virtual.VirtualVerticalLayoutStyle
import com.acornui.component.layout.algorithm.virtual.vDataScroller
import com.acornui.component.scroll.ScrollModel
import com.acornui.component.style.*
import com.acornui.component.text.TextInput
import com.acornui.component.text.selectable
import com.acornui.component.text.textInput
import com.acornui.cursor.StandardCursors
import com.acornui.cursor.cursor
import com.acornui.di.Owned
import com.acornui.di.inject
import com.acornui.di.own
import com.acornui.focus.blurred
import com.acornui.focus.focus
import com.acornui.focus.focusSelf
import com.acornui.input.Ascii
import com.acornui.input.interaction.KeyInteractionRo
import com.acornui.input.interaction.click
import com.acornui.input.keyDown
import com.acornui.math.Bounds
import com.acornui.math.Pad
import com.acornui.popup.PopUpManager
import com.acornui.popup.lift
import com.acornui.recycle.Clearable
import com.acornui.reflect.observable
import com.acornui.signal.Signal0
import com.acornui.signal.bind
import com.acornui.text.StringFormatter
import com.acornui.text.ToStringFormatter
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

open class OptionList<E : Any>(
		owner: Owned
) : ContainerImpl(owner), Clearable {

	constructor(owner: Owned, data: List<E?>) : this(owner) {
		data(data)
	}

	constructor(owner: Owned, data: ObservableList<E?>) : this(owner) {
		data(data)
	}

	/**
	 * If true, when this option list is clicked, this list will open. Clicking the down arrow will still result in a
	 * toggle.
	 * If false (default), this list is only opened when either the down arrow is clicked, or the down key is pressed.
	 */
	var autoOpen: Boolean = false

	private val _input = own(Signal0())

	/**
	 * Dispatched on each input character.
	 * This does not dispatch when selecting an item from the drop down list.
	 */
	val input = _input.asRo()

	private val _changed = own(Signal0())

	/**
	 * Dispatched on value commit.
	 * It is dispatched when the user selects an item, or commits the value of the text input. It is not dispatched
	 * when the selected item or text is programmatically changed.
	 */
	val changed = _changed.asRo()

	/**
	 * The formatter to be used when converting a data element to a string.
	 * This should generally be the same formatter used for the labels in the ItemRenderer elements.
	 */
	var formatter: StringFormatter<E> = ToStringFormatter

	/**
	 * Given the text input's text, returns the matching item in the data list, or null if there are no matches.
	 * By default this will search for a case insensitive match to the item's string result from the [formatter].
	 */
	var textToItem = { text: String ->
		data.firstOrNull {
			if (it != null) {
				val itStr = formatter.format(it)
				itStr.equals(text, ignoreCase = true)
			} else false
		}
	}

	/**
	 * Sets the currently selected item.
	 * Note that this does not invoke [input] or [changed] signals.
	 */
	var selectedItem: E?
		get() = dataScroller.selection.selectedItem
		set(value) {
			dataScroller.selection.selectedItem = value
			textInput.text = if (value == null) "" else formatter.format(value)
		}

	/**
	 * If true, the list will open when typing into the input field.
	 */
	var openOnInput = true

	private val _isOpenChanged = own(Signal0())

	/**
	 * Dispatched when the [isOpen] property has changed.
	 */
	val isOpenChanged = _isOpenChanged.asRo()

	private val textInput: TextInput = textInput {
		input.add {
			if (openOnInput) open()
			scrollModel.value = 0f // Scroll to the top.
			dataScroller.highlighted.selectedItem = null
			setSelectedItemFromText()
			_input.dispatch()
		}
	}

	private var handCursor: Disposable? = null

	/**
	 * If false, the text input will not accept type input, and items may only be selected via the dropdown.
	 */
	var editable: Boolean by observable(true) {
		textInput.editable = it
		textInput.selectable = it
		handCursor?.dispose()
		if (!it) handCursor = cursor(StandardCursors.HAND)
	}

	/**
	 * If true, this option list will use the CommonStyleTags.disabled style tag and have interactivity disabled.
	 */
	var disabled: Boolean by observable(false) {
		interactivityMode = if (it) InteractivityMode.NONE else InteractivityMode.ALL
		disabledTag = it
	}

	/**
	 * The background, as created by [OptionListStyle.background].
	 */
	private var background: UiComponent? = null

	/**
	 * The down arrow, as created by [OptionListStyle.downArrow].
	 */
	private var downArrow: UiComponent? = null

	private val dataScroller = vDataScroller<E> {
		focusEnabled = true
		keyDown().add(::keyDownHandler)
		selection.changed.add { _, newSelection ->
			val value = newSelection.firstOrNull()
			textInput.text = if (value == null) "" else formatter.format(value)
			focus()
			close()
			_changed.dispatch()
		}
	}

	private val listLift = lift {
		focus = false
		+dataScroller layout { fill() }
		onClosed = {<commit_msg>OptionList submit changed signal before close/blur.<commit_after>/*
 * Copyright 2019 Poly Forest, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.acornui.component

import com.acornui.Disposable
import com.acornui.collection.ListView
import com.acornui.collection.ObservableList
import com.acornui.collection.indexOfFirst2
import com.acornui.collection.indexOfLast2
import com.acornui.component.layout.DataScrollerStyle
import com.acornui.component.layout.algorithm.VerticalLayoutData
import com.acornui.component.layout.algorithm.virtual.ItemRendererOwner
import com.acornui.component.layout.algorithm.virtual.VirtualVerticalLayoutStyle
import com.acornui.component.layout.algorithm.virtual.vDataScroller
import com.acornui.component.scroll.ScrollModel
import com.acornui.component.style.*
import com.acornui.component.text.TextInput
import com.acornui.component.text.selectable
import com.acornui.component.text.textInput
import com.acornui.cursor.StandardCursors
import com.acornui.cursor.cursor
import com.acornui.di.Owned
import com.acornui.di.inject
import com.acornui.di.own
import com.acornui.focus.blurred
import com.acornui.focus.focus
import com.acornui.focus.focusSelf
import com.acornui.input.Ascii
import com.acornui.input.interaction.KeyInteractionRo
import com.acornui.input.interaction.click
import com.acornui.input.keyDown
import com.acornui.math.Bounds
import com.acornui.math.Pad
import com.acornui.popup.PopUpManager
import com.acornui.popup.lift
import com.acornui.recycle.Clearable
import com.acornui.reflect.observable
import com.acornui.signal.Signal0
import com.acornui.signal.bind
import com.acornui.text.StringFormatter
import com.acornui.text.ToStringFormatter
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

open class OptionList<E : Any>(
		owner: Owned
) : ContainerImpl(owner), Clearable {

	constructor(owner: Owned, data: List<E?>) : this(owner) {
		data(data)
	}

	constructor(owner: Owned, data: ObservableList<E?>) : this(owner) {
		data(data)
	}

	/**
	 * If true, when this option list is clicked, this list will open. Clicking the down arrow will still result in a
	 * toggle.
	 * If false (default), this list is only opened when either the down arrow is clicked, or the down key is pressed.
	 */
	var autoOpen: Boolean = false

	private val _input = own(Signal0())

	/**
	 * Dispatched on each input character.
	 * This does not dispatch when selecting an item from the drop down list.
	 */
	val input = _input.asRo()

	private val _changed = own(Signal0())

	/**
	 * Dispatched on value commit.
	 * It is dispatched when the user selects an item, or commits the value of the text input. It is not dispatched
	 * when the selected item or text is programmatically changed.
	 */
	val changed = _changed.asRo()

	/**
	 * The formatter to be used when converting a data element to a string.
	 * This should generally be the same formatter used for the labels in the ItemRenderer elements.
	 */
	var formatter: StringFormatter<E> = ToStringFormatter

	/**
	 * Given the text input's text, returns the matching item in the data list, or null if there are no matches.
	 * By default this will search for a case insensitive match to the item's string result from the [formatter].
	 */
	var textToItem = { text: String ->
		data.firstOrNull {
			if (it != null) {
				val itStr = formatter.format(it)
				itStr.equals(text, ignoreCase = true)
			} else false
		}
	}

	/**
	 * Sets the currently selected item.
	 * Note that this does not invoke [input] or [changed] signals.
	 */
	var selectedItem: E?
		get() = dataScroller.selection.selectedItem
		set(value) {
			dataScroller.selection.selectedItem = value
			textInput.text = if (value == null) "" else formatter.format(value)
		}

	/**
	 * If true, the list will open when typing into the input field.
	 */
	var openOnInput = true

	private val _isOpenChanged = own(Signal0())

	/**
	 * Dispatched when the [isOpen] property has changed.
	 */
	val isOpenChanged = _isOpenChanged.asRo()

	private val textInput: TextInput = textInput {
		input.add {
			if (openOnInput) open()
			scrollModel.value = 0f // Scroll to the top.
			dataScroller.highlighted.selectedItem = null
			setSelectedItemFromText()
			_input.dispatch()
		}
	}

	private var handCursor: Disposable? = null

	/**
	 * If false, the text input will not accept type input, and items may only be selected via the dropdown.
	 */
	var editable: Boolean by observable(true) {
		textInput.editable = it
		textInput.selectable = it
		handCursor?.dispose()
		if (!it) handCursor = cursor(StandardCursors.HAND)
	}

	/**
	 * If true, this option list will use the CommonStyleTags.disabled style tag and have interactivity disabled.
	 */
	var disabled: Boolean by observable(false) {
		interactivityMode = if (it) InteractivityMode.NONE else InteractivityMode.ALL
		disabledTag = it
	}

	/**
	 * The background, as created by [OptionListStyle.background].
	 */
	private var background: UiComponent? = null

	/**
	 * The down arrow, as created by [OptionListStyle.downArrow].
	 */
	private var downArrow: UiComponent? = null

	private val dataScroller = vDataScroller<E> {
		focusEnabled = true
		keyDown().add(::keyDownHandler)
		selection.changed.add { _, newSelection ->
			val value = newSelection.firstOrNull()
			textInput.text = if (value == null) "" else formatter.format(value)
			focus()
			_changed.dispatch()
			close()
		}
	}

	private val listLift = lift {
		focus = false
		+dataScroller layout { fill() }
		onClosed = {
{'fraction_non_alphanumeric': 0.09207246135948147, 'fraction_numerical': 0.0024929366794083432, 'mean_word_length': 4.05672268907563, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6890383', 'n_tokens_mistral': 3816, 'n_tokens_neox': 3540, 'n_words': 1165}