commit 3776e85bc0e59c280075130cfd241de1f7072ba0 Author: Nyeogmi Date: Sat Jul 1 20:55:45 2023 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dcb755 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +*.kt.old + +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..942f3a2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..b1077fb --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e8d124d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..835d7ad --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "last_opened_file_path": "C:/Nyeogit/taskzap/taskzap/src/main/resources" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1688177283964 + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..58190bf --- /dev/null +++ b/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + taskzap + app.chromaticdragon + 1.0-SNAPSHOT + jar + + consoleApp + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.7.10 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + org.apache.maven.plugins + maven-compiler-plugin + + 6 + 6 + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 1.7.10 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.7.10 + + + org.jetbrains.kotlinx + kotlinx-datetime-jvm + 0.4.0 + + + + \ No newline at end of file diff --git a/src/main/kotlin/AppModel.kt b/src/main/kotlin/AppModel.kt new file mode 100644 index 0000000..2da57e7 --- /dev/null +++ b/src/main/kotlin/AppModel.kt @@ -0,0 +1,53 @@ +import kotlinx.datetime.LocalDateTime +import java.awt.image.BufferedImage +import java.util.* +import kotlin.collections.HashMap + +class AppModel { + var version: Long = 0 + private set + private var tasks: HashMap = HashMap() + + fun updateTask(task: TaskRecord): TaskRecord? { // returns the old version + version += 1 + val old = tasks[task.uuid] + tasks[task.uuid] = task + return old + } + + fun getTask(uuid: UUID): TaskRecord? { + return tasks[uuid] + } + + fun removeTask(uuid: UUID): TaskRecord? { + version += 1 + return tasks.remove(uuid) + } + + fun getTasksByBlinkAt(): ArrayList { + val list = ArrayList() + for (t in tasks) { list.add(t.value) } + list.sortBy { t -> t.blinkAt } + return list + } +} + +class TaskRecord( + val uuid: UUID, + val icon: IconRecord, + val task: String, + val link: LinkRecord?, + val createdAt: LocalDateTime, + val blinkAt: LocalDateTime, +) + +class LinkRecord( + val icon: IconRecord, + val window: Int, + val pid: Int, + val filename: String, +) + +class IconRecord(val image: BufferedImage) { + val cachedButtonArt = ButtonArt.fromArbitraryImage(image) // don't persist this, just have it always ready +} \ No newline at end of file diff --git a/src/main/kotlin/AppView.kt b/src/main/kotlin/AppView.kt new file mode 100644 index 0000000..7147808 --- /dev/null +++ b/src/main/kotlin/AppView.kt @@ -0,0 +1,125 @@ +import java.awt.* +import javax.swing.BoxLayout +import javax.swing.JDialog +import javax.swing.JPanel +import javax.swing.border.EmptyBorder + +private val GRID_SIZE = 64; +private val GRID_SIZE_INNER = 56; +private val GRID_INSET = (GRID_SIZE - GRID_SIZE_INNER) / 2 + +class AppView(model: AppModel): JDialog() { + private val viewModel = AppViewModel(model) + + private val controlPanel = buildControlPanel() + private val iconPanel = JPanel() + // TODO: Replace these with icons that have clear meaning + + init { + isAlwaysOnTop = true + isUndecorated = true + background = TRANSPARENT + bounds = Rectangle(0, 0, Toolkit.getDefaultToolkit().screenSize.width, 64) + + contentPane.layout = BorderLayout() + contentPane.add(controlPanel, BorderLayout.WEST) + contentPane.add(iconPanel, BorderLayout.CENTER) + + iconPanel.layout = FlowLayout(FlowLayout.LEFT, 0, 0) + + controlPanel.background = TRANSPARENT; + iconPanel.background = TRANSPARENT; + } + + fun touch() { + viewModel.ifDirty(contentPane) { + val ipHeight = iconPanel.height + val iconSize = ipHeight * 3/4; + val meterHeight = ipHeight - iconSize; + + iconPanel.removeAll() + + val tasks = viewModel.model.getTasksByBlinkAt() + for (task in tasks) { + iconPanel.add(TaskView(task)) + } + } + } + + private fun buildControlPanel(): JPanel { + val moveButton = ImageButton( + ButtonArt( + getImage("/move_unlit.png")!!, + getImage("/move_lit.png")!!, + getImage("/move_pressed.png")!!, + ), + null + ) + // TODO: Do something + moveButton.preferredSize = Dimension(GRID_SIZE, GRID_SIZE) + moveButton.border = EmptyBorder(GRID_INSET, GRID_INSET, GRID_INSET, GRID_INSET) + moveButton.callback = { print("test") } + + val controlPanel = JPanel() + controlPanel.layout = BoxLayout(controlPanel, BoxLayout.PAGE_AXIS) + + controlPanel.add(moveButton) + + return controlPanel + } +} + +// for now: just reexpose the underlying model +// we don't have a layer of state beyond what can be kept in the UI +class AppViewModel(val model: AppModel) { + private var cpHeight: Int = -1 + private var version: Long = -1 + + fun ifDirty(contentPane: Container, body: () -> Unit) { + if (version != model.version || contentPane.height != cpHeight) { + body() + cpHeight = contentPane.height + version = model.version + } + } +} + +class TaskView( + private val task: TaskRecord, +): JPanel() { + private var button: ImageButton + init { + layout = BorderLayout() + preferredSize = Dimension(GRID_SIZE, GRID_SIZE) + border = EmptyBorder(GRID_INSET, GRID_INSET, GRID_INSET, GRID_INSET) + background = TRANSPARENT + + button = ImageButton( + task.icon.cachedButtonArt, + null + ) + + add(button) + } + + /* + override fun paintComponent(gnull: Graphics?) { + super.paintComponent(gnull) + + val g = gnull!! + + // icon occupies top of space + g.drawImage(task.icon.image, 0, 0, iconSize, iconSize, Color(0, 0, 0, 0)) { _, _, _, _, _, _ -> false } + + // bar occupies middle 1/2 of space + val realBarHeight = meterHeight / 2 + val barY = iconSize + (meterHeight - realBarHeight)/2 + val arcSize = realBarHeight / 2 + val barX = arcSize + val realBarWidth = iconSize - arcSize * 2 + + g.color = Color(0, 255, 0, 255) + g.fillRoundRect(barX, barY, realBarWidth, realBarHeight, arcSize, arcSize) + } + */ +} \ No newline at end of file diff --git a/src/main/kotlin/ButtonArt.kt b/src/main/kotlin/ButtonArt.kt new file mode 100644 index 0000000..6f1ee37 --- /dev/null +++ b/src/main/kotlin/ButtonArt.kt @@ -0,0 +1,42 @@ +import java.awt.Graphics2D +import java.awt.image.BufferedImage +import java.awt.image.RescaleOp + +// val MAX_SIZE: Int = 256 + +// TODO: Enforce MAX_SIZE +// TODO: Cookie cutter out a roundrect + +class ButtonArt( + val base: BufferedImage, + val highlighted: BufferedImage, + val pressed: BufferedImage +) { + companion object { + fun fromArbitraryImage(base: BufferedImage): ButtonArt { + return ButtonArt( + base, + base.toHighlighted(), + base.toPressed(), + ) + } + } +} + +private fun BufferedImage.toHighlighted(): BufferedImage { + val highlighted = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + val g = highlighted.graphics as Graphics2D + val op = RescaleOp(0.75f, 128f, null) + g.drawImage(this, 0, 0, width, height, 0, 0, width, height, null) + op.filter(highlighted, highlighted) + return highlighted +} + +private fun BufferedImage.toPressed(): BufferedImage { + val pressed = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + val g = pressed.graphics as Graphics2D + val op = RescaleOp(2.0f, 0.0f, null) + g.drawImage(this, 0, 0, width, height, 0, 0, width, height, null) + op.filter(pressed, pressed) + return pressed +} diff --git a/src/main/kotlin/ImageButton.kt b/src/main/kotlin/ImageButton.kt new file mode 100644 index 0000000..49813b9 --- /dev/null +++ b/src/main/kotlin/ImageButton.kt @@ -0,0 +1,63 @@ +import java.awt.Dimension +import java.awt.Graphics +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import java.awt.image.BufferedImage +import javax.swing.JPanel + +class ImageButton( + private val art: ButtonArt, + var callback: (() -> Unit)?, +): JPanel() { + private var mouseOver = false + private var mouseDown = false + + init { + background = TRANSPARENT + preferredSize = Dimension(art.base.width, art.base.height) + this.addMouseListener(object: MouseAdapter() { + override fun mouseEntered(e: MouseEvent?) { + super.mouseEntered(e) + mouseOver = true + repaint() + } + + override fun mouseExited(e: MouseEvent?) { + super.mouseExited(e) + mouseOver = false + repaint() + } + + override fun mousePressed(e: MouseEvent?) { + super.mousePressed(e) + mouseDown = true + repaint() + } + + override fun mouseReleased(e: MouseEvent?) { + super.mouseReleased(e) + mouseDown = false + repaint() + } + + override fun mouseClicked(e: MouseEvent?) { + super.mouseClicked(e) + callback?.invoke() + } + }) + } + + override fun paintComponent(gnull: Graphics?) { + super.paintComponent(gnull) + + val g = gnull!! + val img = if (mouseDown) { art.pressed } else if (mouseOver) { art.highlighted } else { art.base } + + val insets = insets + val dx = width - (insets.left + insets.right) + val dy = height - (insets.top + insets.bottom) + + g.drawImage(img, insets.left, insets.top, dx, dy, null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..879dea2 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,71 @@ +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import java.awt.image.BufferedImage +import javax.swing.JFrame +import javax.swing.Timer +import kotlin.system.exitProcess +import kotlinx.datetime.* +import kotlinx.datetime.TimeZone +import java.awt.* +import java.io.ByteArrayInputStream +import java.util.* +import javax.imageio.ImageIO +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel + +fun main(args: Array) { + val app = App() + + app.start() +} + +class App { + private var model = AppModel() + + private var running: Boolean = false + private val worker = createWorker() + private val appView = createAppView() + + fun start() { + running = true + worker.start() + + val demoIcon = getImage("/demoIcon.png")!! + + for (i in 0..2) { + model.updateTask( + TaskRecord( + uuid = UUID.randomUUID(), + icon = IconRecord(demoIcon), + task = "FIX THE DOG", + link = null, + createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()), + blinkAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + ) + ) + } + } + + private fun createWorker(): Timer { + return Timer(33) { onFrame() } + } + + private fun createAppView(): AppView { + val av = AppView(model) + av.addWindowListener(object: WindowAdapter() { + override fun windowClosing(e: WindowEvent?) { + running = false + } + }) + + return av + } + + private fun onFrame() { + if (!running) { exitProcess(0) } + + if (running) appView.isVisible = true + appView.touch() + } +} diff --git a/src/main/kotlin/Util.kt b/src/main/kotlin/Util.kt new file mode 100644 index 0000000..9f84240 --- /dev/null +++ b/src/main/kotlin/Util.kt @@ -0,0 +1,16 @@ +import java.awt.AlphaComposite +import java.awt.Color +import java.awt.Composite +import java.awt.Graphics2D +import java.awt.image.BufferedImage +import java.awt.image.RescaleOp +import java.io.ByteArrayInputStream +import java.net.URL +import javax.imageio.ImageIO + +val TRANSPARENT: Color = Color(0, 0, 0, 0) + +fun ByteArray.toImage(): BufferedImage = ImageIO.read(ByteArrayInputStream(this)) +fun getImage(name: String): BufferedImage? = getResource(name)?.readBytes()?.toImage() +fun getResource(name: String): URL? = App::class.java.getResource(name) + diff --git a/src/main/resources/demoIcon.png b/src/main/resources/demoIcon.png new file mode 100644 index 0000000..06b1d7b Binary files /dev/null and b/src/main/resources/demoIcon.png differ diff --git a/src/main/resources/move_lit.png b/src/main/resources/move_lit.png new file mode 100644 index 0000000..a9e4932 Binary files /dev/null and b/src/main/resources/move_lit.png differ diff --git a/src/main/resources/move_pressed.png b/src/main/resources/move_pressed.png new file mode 100644 index 0000000..54a3ac7 Binary files /dev/null and b/src/main/resources/move_pressed.png differ diff --git a/src/main/resources/move_unlit.png b/src/main/resources/move_unlit.png new file mode 100644 index 0000000..0f6dfe1 Binary files /dev/null and b/src/main/resources/move_unlit.png differ