Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,8 @@ class QuickBuildAction(
when (tone) {
QuickBuildTone.READY -> R.drawable.ic_quick_build

// Behaviour 1: a running build shows the STANDARD build's stop button, not a
// variant of the bolt, which reads as "a build is running" to someone who does
// not already know the feature. The stop square spins inside a ring rather than
// sitting still, so the ~90 s a proxy app build takes does not read as a hang.
QuickBuildTone.BUILDING -> R.drawable.ic_quick_build_building
// Behaviour 1: the Run button's own stop icon, so both buttons say "a build is running" the same way.
QuickBuildTone.BUILDING -> R.drawable.ic_stop_daemons

// The hollow bolt: still plainly the Quick Build button, but not the filled
// "ready and fast" one. A full build during ordinary editing is normal work,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,16 +772,10 @@ open class EditorHandlerActivity :
// when not applicable, instead of the legacy grey-out used by built-in actions.
if (action.honorVisibility && !action.visible) return@forEachIndexed

action.icon?.apply {
colorFilter = action.createColorFilter(data)
alpha = if (action.enabled) 255 else 76
}

content.projectActionsToolbar.addMenuItem(
// This custom toolbar bypasses DefaultActionsRegistry's menu path, so its
// disabled-icon dim (alpha 76 there) must be mirrored here or a disabled
// action renders at full strength while refusing the tap.
icon = action.icon?.mutate()?.apply { alpha = if (action.enabled) 255 else 76 },
// This toolbar bypasses DefaultActionsRegistry's menu path, so it mirrors that
// path's tint and disabled dim here.
icon = toolbarIcon(action.icon, action.createColorFilter(data), action.enabled),
hint = getToolbarContentDescription(action, data),
onClick = { if (action.enabled) registry.executeAction(action, data) },
onLongClick = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.itsaky.androidide.activities.editor

import android.graphics.ColorFilter
import android.graphics.drawable.Drawable

/**
* Tints and dims a toolbar action's icon, mutating it first.
*
* Order matters: `LayerDrawable.mutate()` rebuilds its layers and drops a filter set before it,
* and tinting an unmutated icon writes onto state shared with every other use of the resource.
*/
internal fun toolbarIcon(
icon: Drawable?,
colorFilter: ColorFilter?,
enabled: Boolean,
): Drawable? =
icon?.mutate()?.apply {
this.colorFilter = colorFilter
alpha = if (enabled) 255 else 76
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ import org.junit.Test
*/
class QuickBuildActionPresentationTest {
@Test
fun `a running build shows a spinning stop icon, not a bolt variant`() {
// The stop square AbstractCancellableRunAction swaps in, inside a spinning ring: the
// two buttons still look like they stop the same kind of thing, and the ring answers
// the manual-QA reading of a static icon as a hung app. Any bolt variant here (the
// previous ic_quick_build_outline) fails the spec, because it did not communicate
// "a build is running" to anyone.
fun `a running build shows the Run button's stop icon, not a bolt variant`() {
// The same static ring and square AbstractCancellableRunAction swaps in, so the two buttons
// present a running build identically; any bolt variant fails the spec.
assertThat(QuickBuildAction.iconResFor(QuickBuildTone.BUILDING))
.isEqualTo(R.drawable.ic_quick_build_building)
.isEqualTo(R.drawable.ic_stop_daemons)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.itsaky.androidide.activities.editor

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

/**
* That a toolbar icon is drawn with the theme tint and the disabled dim.
*
* ADFA-6312: a layer-list icon lost its tint because it was mutated after tinting, which rebuilt
* its layers, so it drew its raw white paint on the light toolbar.
*/
@RunWith(RobolectricTestRunner::class)
class ToolbarIconTest {
private val filter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)

@Test
fun `every layer of a layered icon keeps the colour filter`() {
val icon = LayerDrawable(arrayOf(FilterLayer(), FilterLayer()))

val drawn = toolbarIcon(icon, filter, enabled = true) as LayerDrawable

for (i in 0 until drawn.numberOfLayers) {
assertThat(drawn.getDrawable(i).colorFilter).isSameInstanceAs(filter)
}
}

/**
* A layer that, like framework drawables, keeps its filter per instance and is rebuilt from
* shared state; Robolectric's legacy Paint never reports a filter back, so ColorDrawable can't.
*/
private class FilterLayer : Drawable() {
private var filter: ColorFilter? = null

override fun draw(canvas: Canvas) {}

override fun setAlpha(alpha: Int) {}

override fun setColorFilter(colorFilter: ColorFilter?) {
filter = colorFilter
}

override fun getColorFilter(): ColorFilter? = filter

@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

override fun getConstantState(): ConstantState = State

private object State : ConstantState() {
override fun newDrawable(): Drawable = FilterLayer()

override fun getChangingConfigurations(): Int = 0
}
}

@Test
fun `a disabled icon is dimmed and an enabled one is not`() {
// GradientDrawable reports alpha as set; ColorDrawable rounds 76 down to 75.
assertThat(toolbarIcon(GradientDrawable(), filter, enabled = false)!!.alpha).isEqualTo(76)
assertThat(toolbarIcon(GradientDrawable(), filter, enabled = true)!!.alpha).isEqualTo(255)
}

@Test
fun `an action without an icon gets none`() {
assertThat(toolbarIcon(null, filter, enabled = true)).isNull()
}
}
2 changes: 1 addition & 1 deletion quickbuild/docs/manual-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The button is a split button and the session's status display. Every tone has it
| Icon | Tone | Session is |
| ---------------------------------- | ------------ | ------------------------------------------------------------ |
| Solid bolt | READY | No session, or sitting on a successful build |
| Stop square spinning inside a ring | BUILDING | Provisioning, or a build running now. Tapping stops it |
| Stop square inside a ring (the Run button's stop icon) | BUILDING | Provisioning, or a build running now. Tapping stops it |
| Hollow bolt | SLOW | The next build cannot take the fast path and will be a full one. Not a failure |
| Sync arrows | RECONNECTING | The compile daemon is being respawned. Transient, resolves itself |
| Bolt with an exclamation mark | ERROR | A failure to act on - a failed build, or a daemon respawn that did not come back |
Expand Down
15 changes: 0 additions & 15 deletions resources/src/main/res/drawable/ic_quick_build_building.xml

This file was deleted.

15 changes: 0 additions & 15 deletions resources/src/main/res/drawable/ic_quick_build_building_arc.xml

This file was deleted.

13 changes: 0 additions & 13 deletions resources/src/main/res/drawable/ic_quick_build_building_stop.xml

This file was deleted.

Loading