All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tools.aqua.bgw.util.ComponentViewGrid.kt Maven / Gradle / Ivy

There is a newer version: 0.5
Show newest version
/*
 *    Copyright 2021 The BoardGameWork Authors
 *    SPDX-License-Identifier: Apache-2.0
 *
 *    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.
 */

@file:Suppress("MemberVisibilityCanBePrivate", "unused", "Unchecked_Cast", "DuplicatedCode", "TooManyFunctions")

package tools.aqua.bgw.util

import tools.aqua.bgw.components.ComponentView
import tools.aqua.bgw.components.layoutviews.GridPane.Companion.COLUMN_WIDTH_AUTO
import tools.aqua.bgw.components.layoutviews.GridPane.Companion.ROW_HEIGHT_AUTO
import tools.aqua.bgw.core.Alignment

/**
 * Internal class [ComponentViewGrid] boxing grid structure.
 *
 * @constructor Creates a [ComponentViewGrid].
 */
internal class ComponentViewGrid(
	rows: Int,
	columns: Int
) : Iterable> {
	
	//region Attributes
	/**
	 * Current number of rows.
	 */
	var rows = rows
		private set
	
	/**
	 * Current number of columns.
	 */
	var columns = columns
		private set
	
	/**
	 * Grid array containing contents.
	 */
	private var grid = Array(columns) { Array(rows) { null } }
	
	/**
	 * Grid array containing centering behaviors for individual cells.
	 */
	private var centeringModes = Array(columns) { Array(rows) { Alignment.CENTER } }
	
	/**
	 * Row heights.
	 */
	private var rowHeights = DoubleArray(rows) { -1.0 }
	
	/**
	 * Column widths.
	 */
	private var columnWidths = DoubleArray(columns) { -1.0 }
	//endregion
	
	//region Get/Set operators
	/**
	 * Returns grid cell content.
	 *
	 * @param columnIndex Column index of cell.
	 * @param rowIndex Row index of cell.
	 */
	operator fun get(columnIndex: Int, rowIndex: Int): T? = grid[columnIndex][rowIndex] as? T
	
	
	/**
	 * Sets grid cell content.
	 *
	 * @param columnIndex Column index of cell.
	 * @param rowIndex Row index of cell.
	 *
	 * @throws IllegalArgumentException if [columnIndex] is out of grid range.
	 */
	operator fun set(columnIndex: Int, rowIndex: Int, value: T?) {
		require(columnIndex in 0 until columns && rowIndex in 0 until rows) {
			"Indices exceed grid bounds."
		}
		
		grid[columnIndex][rowIndex] = value
	}
	//endregion
	
	//region Get/Set center modes
	/**
	 * Returns grid cell centering mode.
	 *
	 * @param columnIndex Column index of cell.
	 * @param rowIndex Row index of cell.
	 */
	fun getCellCenterMode(columnIndex: Int, rowIndex: Int): Alignment = centeringModes[columnIndex][rowIndex]
	
	/**
	 * Sets grid cell centering mode.
	 *
	 * @param columnIndex Column index of cell.
	 * @param rowIndex Row index of cell.
	 *
	 * @throws IllegalArgumentException If [columnIndex] or [rowIndex] is out of grid range.
	 */
	fun setCellCenterMode(columnIndex: Int, rowIndex: Int, alignment: Alignment) {
		require(columnIndex in 0 until columns && rowIndex in 0 until rows) {
			"Indices exceed grid bounds."
		}
		
		centeringModes[columnIndex][rowIndex] = alignment
	}
	
	/**
	 * Sets grid centering mode for whole column.
	 *
	 * @param columnIndex Column index.
	 * @param alignment New alignment.
	 *
	 * @throws IllegalArgumentException If [columnIndex] is out of grid range.
	 */
	fun setColumnCenterMode(columnIndex: Int, alignment: Alignment) {
		require(columnIndex in 0 until columns) {
			"Column index exceed grid bounds."
		}
		
		for (i in 0 until rows)
			centeringModes[columnIndex][i] = alignment
	}
	
	/**
	 * Sets grid centering mode for whole row.
	 *
	 * @param rowIndex Row index.
	 * @param alignment New alignment.
	 *
	 * @throws IllegalArgumentException If [rowIndex] is out of grid range.
	 */
	fun setRowCenterMode(rowIndex: Int, alignment: Alignment) {
		require(rowIndex in 0 until rows) {
			"Row index exceed grid bounds."
		}
		
		for (i in 0 until columns)
			centeringModes[i][rowIndex] = alignment
	}
	
	/**
	 * Sets grid centering mode for all cells.
	 *
	 * @param alignment New alignment.
	 */
	fun setCenterMode(alignment: Alignment) {
		for (x in 0 until columns)
			for (y in 0 until rows)
				centeringModes[x][y] = alignment
	}
	//endregion
	
	//region Get/Set values
	/**
	 * Returns whole row as [List].
	 *
	 * @param rowIndex Row index.
	 */
	fun getRow(rowIndex: Int): List = List(columns) { grid[it][rowIndex] as? T }
	
	/**
	 * Returns [List] of all rows as another [List].
	 *
	 * @return [List] of all rows.
	 */
	fun getRows(): List> = (0 until rows).map { getRow(it) }
	
	/**
	 * Returns whole column as [List].
	 *
	 * @param columnIndex Column index.
	 */
	fun getColumn(columnIndex: Int): List = grid[columnIndex].toList() as List
	
	/**
	 * Returns [List] of all columns as another [List].
	 *
	 * @return [List] of all columns.
	 */
	fun getColumns(): List> = (0 until columns).map { getColumn(it) }
	//endregion
	
	//region Get/Set column width/row height
	/**
	 * Returns preferred column width ([COLUMN_WIDTH_AUTO] for auto).
	 *
	 * @param columnIndex Column index.
	 *
	 * @throws IllegalArgumentException If [columnIndex] is out of grid range.
	 */
	fun getColumnWidth(columnIndex: Int): Double {
		require(columnIndex in columnWidths.indices) {
			"ColumnIndex out of grid range."
		}
		
		return columnWidths[columnIndex]
	}
	
	/**
	 * Sets preferred column width ([COLUMN_WIDTH_AUTO] for auto).
	 *
	 * @param columnIndex Column index.
	 * @param columnWidth New column width.
	 *
	 * @throws IllegalArgumentException if [columnIndex] is out of grid range or [columnWidth] is negative.
	 */
	fun setColumnWidth(columnIndex: Int, columnWidth: Double) {
		require(columnIndex in columnWidths.indices) {
			"ColumnIndex out of grid range."
		}
		require(columnWidth >= 0 || columnWidth == COLUMN_WIDTH_AUTO) {
			"Parameter must be positive or COLUMN_WIDTH_AUTO."
		}
		
		columnWidths[columnIndex] = columnWidth
	}
	
	/**
	 * Sets preferred column width ([COLUMN_WIDTH_AUTO] for auto) for all columns.
	 *
	 * @param columnWidths New column widths.
	 *
	 * @throws IllegalArgumentException If size of [columnWidths] does not match grid size
	 * or any columnWidth is negative.
	 */
	fun setColumnWidths(columnWidths: DoubleArray) {
		require(columnWidths.size == this.columnWidths.size) {
			"Array size does not match grid range."
		}
		require(columnWidths.all { it >= 0 || it == COLUMN_WIDTH_AUTO }) {
			"Parameter values must all be positive or COLUMN_WIDTH_AUTO."
		}
		
		this.columnWidths = columnWidths
	}
	
	/**
	 * Returns preferred row height ([ROW_HEIGHT_AUTO] for auto).
	 *
	 * @param rowIndex Row index.
	 *
	 * @throws IllegalArgumentException If [rowIndex] is out of grid range.
	 */
	fun getRowHeight(rowIndex: Int): Double {
		require(rowIndex in rowHeights.indices) {
			"ColumnIndex out of grid range."
		}
		
		return rowHeights[rowIndex]
	}
	
	/**
	 * Sets preferred row height ([ROW_HEIGHT_AUTO] for auto).
	 *
	 * @param rowIndex Row index.
	 * @param rowHeight New row height.
	 *
	 * @throws IllegalArgumentException if [rowIndex] is out of grid range or [rowHeight] is negative.
	 */
	fun setRowHeight(rowIndex: Int, rowHeight: Double) {
		require(rowIndex in rowHeights.indices) {
			"ColumnIndex out of grid range."
		}
		require(rowHeight >= 0 || rowHeight == ROW_HEIGHT_AUTO) {
			"Parameter must be positive or ROW_HEIGHT_AUTO."
		}
		
		rowHeights[rowIndex] = rowHeight
	}
	
	/**
	 * Sets preferred row height ([ROW_HEIGHT_AUTO] for auto) for all rows.
	 *
	 * @param rowHeights New row heights.
	 *
	 * @throws IllegalArgumentException If size of [rowHeights] does not match grid size
	 * or any rowHeight is negative.
	 */
	fun setRowHeights(rowHeights: DoubleArray) {
		require(rowHeights.size == this.rowHeights.size) {
			"Array size does not match grid range."
		}
		require(rowHeights.all { it >= 0 || it == ROW_HEIGHT_AUTO }) {
			"Parameter values must all be positive or ROW_HEIGHT_AUTO."
		}
		
		this.rowHeights = rowHeights
	}
	//endregion
	
	//region Grow/Trim
	/**
	 * Extends grid to given directions.
	 *
	 * @param left Amount of columns to add to the left.
	 * @param right Amount of columns to add to the right.
	 * @param top Amount of rows to add on the top.
	 * @param bottom Amount of rows to add on the bottom.
	 *
	 * @throws IllegalArgumentException If any parameter is negative.
	 */
	fun grow(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0): Boolean {
		if (left == 0 && right == 0 && top == 0 && bottom == 0)
			return false
		
		require(left >= 0 && right >= 0 && top >= 0 && bottom >= 0) {
			"All parameters must be positive."
		}
		
		val newGrid = Array(columns + left + right) { Array(rows + top + bottom) { null } }
		val newCenteringModes = Array(columns + left + right) { Array(rows + top + bottom) { Alignment.CENTER } }
		
		for (x in 0 until columns) {
			for (y in 0 until rows) {
				newGrid[x + left][y + top] = grid[x][y]
				newCenteringModes[x + left][y + top] = centeringModes[x][y]
			}
		}
		
		grid = newGrid
		centeringModes = newCenteringModes
		rows += top + bottom
		columns += left + right
		rowHeights = DoubleArray(left) { ROW_HEIGHT_AUTO } + rowHeights + DoubleArray(right) { ROW_HEIGHT_AUTO }
		columnWidths = DoubleArray(left) { COLUMN_WIDTH_AUTO } + columnWidths + DoubleArray(right) { COLUMN_WIDTH_AUTO }
		
		return true
	}
	
	/**
	 * Removes all empty outer rows and columns.
	 *
	 * @return `true` if the grid has been changes by this operation, `false` otherwise.
	 */
	fun trim(): Boolean = trimColumns() || trimRows()
	
	/**
	 * Removes all empty outer columns.
	 *
	 * @return `true` if the grid has been changes by this operation, `false` otherwise.
	 */
	fun trimColumns(): Boolean {
		val oldColumns = columns
		var firstColumn = -1
		var lastColumn = -1
		
		for (index in 0 until columns) {
			if (getColumn(index).any { it != null }) {
				firstColumn = index
				break
			}
		}
		
		//Check if array contained only null entries
		if (firstColumn == -1) {
			columns = 0
			rows = 0
			grid = Array(columns) { Array(rows) { null } }
			centeringModes = Array(columns) { Array(rows) { Alignment.CENTER } }
		} else {
			for (index in columns - 1 downTo firstColumn) {
				if (getColumn(index).any { it != null }) {
					lastColumn = index
					break
				}
			}
			
			columns = lastColumn - firstColumn + 1
			
			grid = Array(columns) { grid[it + firstColumn] }
			centeringModes = Array(columns) { centeringModes[it + firstColumn] }
			columnWidths = DoubleArray(columns) { columnWidths[it + firstColumn] }
		}
		
		return columns != oldColumns
	}
	
	/**
	 * Removes all empty outer rows.
	 *
	 * @return `true` if the grid has been changes by this operation, `false` otherwise.
	 */
	fun trimRows(): Boolean {
		val oldRows = rows
		var firstRow = -1
		var lastRow = -1
		
		for (index in 0 until rows) {
			if (getRow(index).any { it != null }) {
				firstRow = index
				break
			}
		}
		
		//Check if array contained only null entries
		if (firstRow == -1) {
			columns = 0
			rows = 0
			grid = Array(columns) { Array(rows) { null } }
			centeringModes = Array(columns) { Array(rows) { Alignment.CENTER } }
		} else {
			
			for (index in rows - 1 downTo firstRow) {
				if (getRow(index).any { it != null }) {
					lastRow = index
					break
				}
			}
			
			assert(lastRow > firstRow)
			
			rows = lastRow - firstRow + 1
			grid = Array(columns) { x -> Array(rows) { y -> grid[x][y + firstRow] } }
			centeringModes = Array(columns) { x -> Array(rows) { y -> centeringModes[x][y + firstRow] } }
			rowHeights = DoubleArray(rows) { rowHeights[it + firstRow] }
		}
		
		return rows != oldRows
	}
	//endregion
	
	//region Add/Remove columns and rows
	/**
	 * Inserts the given amount of columns at the given position.
	 *
	 * @param columnIndex Index after which the columns should be inserted.
	 * @param count Amount of columns to insert.
	 *
	 * @throws IllegalArgumentException If [columnIndex] is out of grid range or [count] is negative.
	 */
	fun addColumns(columnIndex: Int, count: Int) {
		require(columnIndex in 0..columns) {
			"Column index out of grid range."
		}
		require(count >= 0) {
			"Parameter count must be positive."
		}
		
		val newGrid = Array(columns + count) { Array(rows) { null } }
		val newCenteringModes = Array(columns + count) { Array(rows) { Alignment.CENTER } }
		
		for (x in 0 until columnIndex) {
			for (y in 0 until rows) {
				newGrid[x][y] = grid[x][y]
				newCenteringModes[x][y] = centeringModes[x][y]
			}
		}
		
		for (x in columnIndex until columns) {
			for (y in 0 until rows) {
				newGrid[x + count][y] = grid[x][y]
				newCenteringModes[x + count][y] = centeringModes[x][y]
			}
		}
		
		val newColumnWidths = DoubleArray(columns + count) { COLUMN_WIDTH_AUTO }
		columnWidths.copyInto(
			destination = newColumnWidths,
			startIndex = 0,
			endIndex = columnIndex
		)
		columnWidths.copyInto(
			destination = newColumnWidths,
			destinationOffset = columnIndex + count,
			startIndex = columnIndex,
			endIndex = columns
		)
		columnWidths = newColumnWidths
		columns += count
		grid = newGrid
		centeringModes = newCenteringModes
	}
	
	/**
	 * Removes column at given index.
	 *
	 * @param columnIndex Index of column to be deleted.
	 *
	 * @throws IllegalArgumentException If [columnIndex] is out of grid range.
	 */
	fun removeColumn(columnIndex: Int) {
		require(columnIndex in 0 until columns) {
			"Column index out of grid range."
		}
		
		if (columns == 1) {
			initEmpty()
			return
		}
		
		val newGrid = Array(columns - 1) { Array(rows) { null } }
		val newCenteringModes = Array(columns - 1) { Array(rows) { Alignment.CENTER } }
		
		for (x in 0 until columnIndex) {
			for (y in 0 until rows) {
				newGrid[x][y] = grid[x][y]
				newCenteringModes[x][y] = centeringModes[x][y]
			}
		}
		
		for (x in columnIndex + 1 until columns) {
			for (y in 0 until rows) {
				newGrid[x - 1][y] = grid[x][y]
				newCenteringModes[x - 1][y] = centeringModes[x][y]
			}
		}
		
		val newColumnWidths = DoubleArray(columns - 1) { COLUMN_WIDTH_AUTO }
		columnWidths.copyInto(
			destination = newColumnWidths,
			startIndex = 0,
			endIndex = columnIndex
		)
		columnWidths.copyInto(
			destination = newColumnWidths,
			destinationOffset = columnIndex,
			startIndex = columnIndex + 1,
			endIndex = columns
		)
		columnWidths = newColumnWidths
		columns--
		grid = newGrid
		centeringModes = newCenteringModes
	}
	
	/**
	 * Removes all empty columns.
	 */
	fun removeEmptyColumns() {
		val columnIndices = grid.indices.filter { grid[it].any { e -> e != null } }.toIntArray()
		
		if (columnIndices.isEmpty()) {
			initEmpty()
			return
		}
		
		grid = Array(columnIndices.size) { grid[columnIndices[it]] }
		centeringModes = Array(columnIndices.size) { centeringModes[columnIndices[it]] }
		columnWidths = DoubleArray(columnIndices.size) { columnWidths[columnIndices[it]] }
		columns = columnIndices.size
	}
	
	/**
	 * Inserts the given amount of rows at the given position.
	 *
	 * @param rowIndex Index after which the rows should be inserted.
	 * @param count Amount of columns to insert.
	 *
	 * @throws IllegalArgumentException If [rowIndex] is out of grid range or [count] is negative.
	 */
	fun addRows(rowIndex: Int, count: Int) {
		require(rowIndex in 0..rows) {
			"Row index out of grid range."
		}
		require(count >= 0) {
			"Parameter count must be positive."
		}
		
		val newGrid = Array(columns) { Array(rows + count) { null } }
		val newCenteringModes = Array(columns) { Array(rows + count) { Alignment.CENTER } }
		
		for (x in 0 until columns) {
			for (y in 0 until rowIndex) {
				newGrid[x][y] = grid[x][y]
				newCenteringModes[x][y] = centeringModes[x][y]
			}
		}
		
		for (x in 0 until columns) {
			for (y in rowIndex until rows) {
				newGrid[x][y + count] = grid[x][y]
				newCenteringModes[x][y + count] = centeringModes[x][y]
			}
		}
		val newRowHeights = DoubleArray(rows + count) { ROW_HEIGHT_AUTO }
		rowHeights.copyInto(
			destination = newRowHeights,
			startIndex = 0,
			endIndex = rowIndex
		)
		rowHeights.copyInto(
			destination = newRowHeights,
			destinationOffset = rowIndex + count,
			startIndex = rowIndex,
			endIndex = rows
		)
		rowHeights = newRowHeights
		rows += count
		grid = newGrid
		centeringModes = newCenteringModes
	}
	
	/**
	 * Removes row at given index.
	 *
	 * @param rowIndex Index of row to be deleted.
	 *
	 * @throws IllegalArgumentException If [rowIndex] is out of grid range.
	 */
	fun removeRow(rowIndex: Int) {
		require(rowIndex in 0 until rows) {
			"Row index out of grid range."
		}
		
		if (rows == 1) {
			initEmpty()
			return
		}
		
		val newGrid = Array(columns) { Array(rows - 1) { null } }
		val newCenteringModes = Array(columns) { Array(rows - 1) { Alignment.CENTER } }
		
		for (x in 0 until columns) {
			for (y in 0 until rowIndex) {
				newGrid[x][y] = grid[x][y]
				newCenteringModes[x][y] = centeringModes[x][y]
			}
		}
		
		for (x in 0 until columns) {
			for (y in rowIndex + 1 until rows) {
				newGrid[x][y - 1] = grid[x][y]
				newCenteringModes[x][y - 1] = centeringModes[x][y]
			}
		}
		
		val newRowHeights = DoubleArray(rows - 1) { ROW_HEIGHT_AUTO }
		rowHeights.copyInto(
			destination = newRowHeights,
			startIndex = 0,
			endIndex = rowIndex
		)
		rowHeights.copyInto(
			destination = newRowHeights,
			destinationOffset = rowIndex,
			startIndex = rowIndex + 1,
			endIndex = rows
		)
		rowHeights = newRowHeights
		rows--
		grid = newGrid
		centeringModes = newCenteringModes
	}
	
	/**
	 * Removes all empty rows.
	 */
	fun removeEmptyRows() {
		val rowIndices = (0 until rows).filter { j -> (0 until columns).any { i -> grid[i][j] != null } }.toIntArray()
		
		if (rowIndices.isEmpty()) {
			initEmpty()
			return
		}
		
		grid = Array(columns) { x -> Array(rowIndices.size) { y -> grid[x][rowIndices[y]] } }
		centeringModes = Array(columns) { x -> Array(rowIndices.size) { y -> centeringModes[x][rowIndices[y]] } }
		rowHeights = DoubleArray(rowIndices.size) { rowHeights[rowIndices[it]] }
		rows = rowIndices.size
	}
	//endregion
	
	//region Helper
	/**
	 * Creates an empty grid of size 0x0.
	 */
	private fun initEmpty() {
		grid = Array(0) { Array(0) { null } }
		centeringModes = Array(0) { Array(0) { Alignment.CENTER } }
		columnWidths = DoubleArray(0) { COLUMN_WIDTH_AUTO }
		rowHeights = DoubleArray(0) { ROW_HEIGHT_AUTO }
		columns = 0
		rows = 0
	}
	
	/**
	 * Inline forEach method only returning cells with non-null content.
	 *
	 * @see GridIterator
	 * @see GridIteratorElement
	 */
	inline fun forEachNotNull(action: (GridIteratorElement) -> Unit) {
		for (element in this.filter { it.component != null }) action(element)
	}
	
	/**
	 * Inline forEach method only returning non-null cell values.
	 *
	 * @see GridIterator
	 */
	inline fun forEachItemNotNull(action: (T) -> Unit) {
		for (element in this.mapNotNull { it.component }) action(element)
	}
	
	/**
	 * Returns a [GridIterator] for this grid.
	 *
	 * @return [GridIterator] for this grid
	 */
	override fun iterator(): Iterator> = GridIterator()
	
	/**
	 * Prints grid as "x" and "o", where "x" is a cell containing a value and "o" an empty cell.
	 */
	override fun toString(): String = "#Rows: " + rows + "\n" +
			"#Columns: " + columns + "\n" +
			grid.joinToString(separator = "\n")
			{ cols ->
				cols.joinToString(prefix = "[", postfix = "]")
				{ if (it == null) "0" else "X" }
			}
	//endregion
	
	/**
	 * An iterator over a [ComponentViewGrid]. Allows to sequentially access the elements.
	 */
	internal inner class GridIterator : Iterator> {
		/**
		 * Current row position.
		 */
		private var currRow = 0
		
		/**
		 * Current column position.
		 */
		private var currCol = 0
		
		/**
		 * Returns ``rue` if the iteration has more elements.
		 */
		override fun hasNext(): Boolean = currCol < grid.size && currRow < grid[currCol].size
		
		/**
		 * Returns the next element in the iteration.
		 */
		override fun next(): GridIteratorElement {
			if (currCol >= grid.size)
				throw NoSuchElementException()
			
			val res = GridIteratorElement(currCol, currRow, grid[currCol][currRow] as? T)
			
			currRow = ++currRow % grid[currCol].size
			
			if (currRow == 0)
				currCol++
			
			return res
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy