feat: Improve Lawnchair grid specifications (#6583)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: TheSuperDragon <70206496+superdragonxd@users.noreply.github.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-06-28 08:37:50 +00:00
1 parent dd04bbafe7
commit 266c07c808
20 files changed
+601 -110

No files matched your search

@@ -370,7 +370,7 @@ public final class FeatureFlagsImpl implements FeatureFlags {
public boolean enableScalabilityForDesktopExperience() {
return false;
return true;
}
@Override
+8
View File
@@ -765,6 +765,14 @@
<string name="open_in_store_drop_target_label">Open in store</string>
<string name="dock_icons">Dock icons</string>
<string name="state_folded">%1$s (folded)</string>
<string name="state_unfolded">%1$s (unfolded)</string>
<string name="folded_label">Folded</string>
<string name="unfolded_label">Unfolded</string>
<string name="when_folded_label">When folded</string>
<string name="when_unfolded_label">When unfolded</string>
<string name="unfolded_grid_description">Lawnchair pairs two pages together to fill the larger display</string>
<string name="foldable_columns_error">The folded value must be less than or equal to the unfolded value. It will be automatically adjusted if needed.</string>
<string name="hotseat_bottom_space_label">Bottom padding</string>
<string name="page_indicator_height">Page indicator height</string>
+1 -1
View File
@@ -151,7 +151,7 @@
launcher:packageName="com.google.android.deskclock"
launcher:screen="0"
launcher:spanX="2"
launcher:spanY="3"
launcher:spanY="2"
launcher:x="2"
launcher:y="1"/>
@@ -33,25 +33,25 @@
launcher:packageName="com.android.vending"
launcher:screen="0"
launcher:x="0"
launcher:y="4"/>
launcher:y="5"/>
<favorite
launcher:className="com.google.android.gm.ConversationListActivityGmail"
launcher:packageName="com.google.android.gm"
launcher:screen="0"
launcher:x="1"
launcher:y="4"/>
launcher:y="5"/>
<favorite
launcher:className="com.google.android.apps.photos.home.HomeActivity"
launcher:packageName="com.google.android.apps.photos"
launcher:screen="0"
launcher:x="2"
launcher:y="4"/>
launcher:y="5"/>
<favorite
launcher:className="com.google.android.youtube.app.honeycomb.Shell$HomeActivity"
launcher:packageName="com.google.android.youtube"
launcher:screen="0"
launcher:x="3"
launcher:y="4"/>
launcher:y="5"/>
<appwidget
launcher:className="com.google.android.apps.search.assistant.verticals.snapshot.widgets.weather.WeatherWidget_Receiver"
launcher:packageName="com.google.android.googlequicksearchbox"
@@ -69,7 +69,7 @@
launcher:packageName="com.google.android.calendar"
launcher:screen="1"
launcher:spanX="4"
launcher:spanY="2"
launcher:spanY="3"
launcher:x="0"
launcher:y="3"/>
</favorites>
</favorites>
+5 -5
View File
@@ -51,8 +51,8 @@
</grid-option>
<grid-option
launcher:name="4_by_7"
launcher:numRows="7"
launcher:name="4_by_6"
launcher:numRows="6"
launcher:numColumns="4"
launcher:numFolderRows="3"
launcher:numFolderColumns="3"
@@ -130,7 +130,7 @@
launcher:dbFile="launcher_6_by_5.db"
launcher:defaultLayoutId="@xml/default_workspace_6x5"
launcher:inlineQsb="landscape"
launcher:deviceCategory="tablet" >
launcher:deviceCategory="tablet|desktop" >
<display-option
launcher:name="Tablet"
@@ -170,7 +170,7 @@
launcher:allAppsSpecsTwoPanelId="@xml/spec_foldable_all_apps_4x5_unfolded"
launcher:allAppsStyle="@style/AllAppsStyle4x5Fold"
launcher:cellStyle="@style/CellStyle4x5Fold"
launcher:defaultLayoutId="@xml/default_workspace_splitdisplay_4x5"
launcher:defaultLayoutId="@xml/default_workspace_splitdisplay_4x6"
launcher:deviceCategory="multi_display"
launcher:devicePaddingId="@xml/paddings_split_4x5"
launcher:folderSpecsId="@xml/spec_handheld_folder_4x5"
@@ -189,7 +189,7 @@
launcher:numFolderColumns="3"
launcher:numFolderRows="3"
launcher:numHotseatIcons="4"
launcher:numRows="5"
launcher:numRows="6"
launcher:workspaceCellSpecsId="@xml/spec_handheld_workspace_cell_4x5"
launcher:workspaceCellSpecsTwoPanelId="@xml/spec_foldable_workspace_cell_4x5_unfolded"
launcher:workspaceSpecsId="@xml/spec_handheld_workspace_4x5"
@@ -55,10 +55,16 @@ class DeviceProfileOverrides @Inject constructor(
prefs.hotseatColumns.set(gridInfo.numHotseatColumns)
}
fun getOverrides(defaultGrid: InvariantDeviceProfile.GridOption) = Options(
fun getOverrides(
defaultGrid: InvariantDeviceProfile.GridOption,
deviceType: Int,
previewOverrides: PreviewOverrides? = null,
) = Options(
prefs = prefs,
prefs2 = preferenceManager2,
defaultGrid = defaultGrid,
deviceType = deviceType,
previewOverrides = previewOverrides ?: PreviewOverrides(),
)
fun getTextFactors() = TextFactors(preferenceManager2)
@@ -80,6 +86,11 @@ class DeviceProfileOverrides @Inject constructor(
)
}
/** Override for any other value that's not a DBGridInfo, extends this value when needed */
data class PreviewOverrides(
val foldableDatabaseHotseatIcons: Int? = null,
)
data class Options(
val numAllAppsColumns: Int,
val numFolderRows: Int,
@@ -90,11 +101,18 @@ class DeviceProfileOverrides @Inject constructor(
val allAppsIconTextSizeFactor: Float,
val enableTaskbarOnPhone: Boolean,
// Foldable overrides (-1 means don't override)
val foldableShownHotseatIcons: Int = -1,
val foldableDatabaseHotseatIcons: Int = -1,
val foldableDatabaseAllAppsColumns: Int = -1,
) {
constructor(
prefs: PreferenceManager,
prefs2: PreferenceManager2,
defaultGrid: InvariantDeviceProfile.GridOption,
deviceType: Int,
previewOverrides: PreviewOverrides,
) : this(
numAllAppsColumns = prefs2.drawerColumns.firstCached(gridOption = defaultGrid),
numFolderRows = prefs.folderRows.get(defaultGrid),
@@ -110,6 +128,26 @@ class DeviceProfileOverrides @Inject constructor(
},
enableTaskbarOnPhone = prefs2.enableTaskbarOnPhone.firstCached(),
foldableShownHotseatIcons = if (deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY) {
val folded = prefs.hotseatColumns.get()
val unfolded = previewOverrides.foldableDatabaseHotseatIcons ?: prefs.hotseatColumnsUnfolded.get()
folded.coerceAtMost(unfolded)
} else {
-1
},
foldableDatabaseHotseatIcons = if (deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY) {
previewOverrides.foldableDatabaseHotseatIcons ?: prefs.hotseatColumnsUnfolded.get()
} else {
-1
},
foldableDatabaseAllAppsColumns = if (deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY) {
val folded = prefs2.drawerColumns.firstCached(gridOption = defaultGrid)
val unfolded = prefs2.drawerColumnsUnfolded.firstCached(gridOption = defaultGrid)
folded.coerceAtLeast(unfolded)
} else {
-1
},
)
fun applyUi(idp: InvariantDeviceProfile) {
@@ -119,6 +157,17 @@ class DeviceProfileOverrides @Inject constructor(
idp.numFolderRows[INDEX_DEFAULT] = numFolderRows
idp.numFolderColumns[INDEX_DEFAULT] = numFolderColumns
// Foldable overrides for hotseat and allapps columns
if (foldableShownHotseatIcons > 0) {
idp.numShownHotseatIcons = foldableShownHotseatIcons
}
if (foldableDatabaseHotseatIcons > 0) {
idp.numDatabaseHotseatIcons = foldableDatabaseHotseatIcons
}
if (foldableDatabaseAllAppsColumns > 0) {
idp.numDatabaseAllAppsColumns = foldableDatabaseAllAppsColumns
}
// apply icon and text size
idp.iconSize[INDEX_DEFAULT] *= iconSizeFactor
idp.iconSize[INDEX_LANDSCAPE] *= iconSizeFactor
@@ -34,6 +34,7 @@ import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.model.DeviceGridState
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.DaggerSingletonObject
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.Executors
import com.android.launcher3.util.SafeCloseable
import com.android.quickstep.RecentsModel
@@ -45,6 +46,7 @@ class PreferenceManager @Inject constructor(
) : BasePreferenceManager(context),
SafeCloseable {
private val idp get() = InvariantDeviceProfile.INSTANCE.get(context)
private val dc get() = DisplayController.INSTANCE.get(context)
private val mRecentsModel get() = RecentsModel.INSTANCE.get(context)
private val reloadIcons: () -> Unit = {
mRecentsModel.onThemeChanged()
@@ -55,6 +57,8 @@ class PreferenceManager @Inject constructor(
}
private val reloadGrid: () -> Unit = { idp.onPreferencesChanged(context) }
private val deviceType = dc.info.deviceType
private val recreate = {
LawnchairLauncher.instance?.recreateIfNotScheduled()
Unit
@@ -67,9 +71,37 @@ class PreferenceManager @Inject constructor(
val transparentIconBackground = BoolPref("prefs_transparentIconBackground", false)
val shadowBGIcons = BoolPref("pref_shadowBGIcons", true)
val addIconToHome = BoolPref("pref_add_icon_to_home", true)
val hotseatColumns = IntPref("pref_hotseatColumns", 4, reloadGrid)
val workspaceColumns = IntPref("pref_workspaceColumns", 4)
val workspaceRows = IntPref("pref_workspaceRows", 7)
private val isPhone: Boolean get() = deviceType == InvariantDeviceProfile.TYPE_PHONE
private val isTablet: Boolean get() = deviceType == InvariantDeviceProfile.TYPE_TABLET
private val isFoldable: Boolean get() = deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY
private val isDesktop: Boolean get() = deviceType == InvariantDeviceProfile.TYPE_DESKTOP
val calculatedGridSpec = when {
// This grid configuration is perfect for Phone, tested against Pixel 7,
// alternative dense configuration can be 5x5x7
isPhone -> LayoutConfig(4, 4, 6)
// This grid configuration is perfect for Tablet, tested against Pixel Tablet
isTablet -> LayoutConfig(6, 6, 5)
// This grid configuration is perfect for Foldable, tested against Pixel 10 Pro Fold
// Note: Hotseat column is 4 when folded, unfolded uses hotseatColumns + 2 or higher number
// defined in numExtendedHotseatIcons from device profile
isFoldable -> LayoutConfig(4, 4, 6, 6)
// This grid configuration is not tested against actual desktop devices,
// but tablet configuration works perfectly when displayed via emulator
isDesktop -> LayoutConfig(6, 6, 5)
// This grid configuration is the fallback for all devices type, this shouldn't be possible
else -> LayoutConfig(4, 4, 7)
}
val hotseatColumns = IntPref("pref_hotseatColumns", calculatedGridSpec.hotseatColumns, reloadGrid)
val hotseatColumnsUnfolded = IntPref("pref_hotseatColumnsUnfolded", calculatedGridSpec.hotseatColumnsUnfolded, reloadGrid)
val workspaceColumns = IntPref("pref_workspaceColumns", calculatedGridSpec.workspaceColumns)
val workspaceRows = IntPref("pref_workspaceRows", calculatedGridSpec.workspaceRows)
val workspaceIncreaseMaxGridSize = BoolPref("pref_workspace_increase_max_grid_size", false)
val folderRows = IdpIntPref("pref_folderRows", { numFolderRows[INDEX_DEFAULT] }, reloadGrid)
@@ -192,3 +224,35 @@ class PreferenceManager @Inject constructor(
@Composable
fun preferenceManager() = PreferenceManager.getInstance(LocalContext.current)
/**
* Grid layout configuration for a device's workspace.
*
* @param hotseatColumns The amount of column the dock can contain
* @param workspaceColumns The amount of column the home screen can contain
* @param workspaceRows The amount of row the home screen can contain
* @param hotseatColumnsUnfolded The amount of column the dock can contain when unfolded (foldables only)
*/
data class LayoutConfig(
/**
* Hotseat columns refer to the amount of column the dock can contain.
* For foldables, this is the folded (closed) state.
*/
val hotseatColumns: Int,
/**
* Workspace columns refer to the amount of column the home screen can contain.
*/
val workspaceColumns: Int,
/**
* Workspace rows refer to the amount of row the home screen can contain.
*/
val workspaceRows: Int,
/**
* Hotseat columns when the foldable is in unfolded (opened) state.
* For non-foldable devices, this must be equals to [hotseatColumns].
*/
val hotseatColumnsUnfolded: Int = hotseatColumns,
)
@@ -680,6 +680,12 @@ class PreferenceManager2 @Inject constructor(
onSet = { reloadHelper.reloadGrid() },
)
val drawerColumnsUnfolded = idpPreference(
key = intPreferencesKey(name = "drawer_columns_unfolded"),
defaultSelector = { numAllAppsColumns + 2 },
onSet = { reloadHelper.reloadGrid() },
)
val folderColumns = idpPreference(
key = intPreferencesKey(name = "folder_columns"),
defaultSelector = { numFolderColumns[INDEX_DEFAULT] },
@@ -145,10 +145,16 @@ class LawnQsbLayout(context: Context, attrs: AttributeSet?) : FrameLayout(contex
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val dp = activity.deviceProfile
// Unlike Phone, for Foldable/Tablet we let the original onMeasure do that instead since it
// matched what we need. It perfectly fit the QSB with the grid.
if (!dp.deviceProperties.isPhone) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
val requestedWidth = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val dp = activity.deviceProfile
val cellWidth = DeviceProfile.calculateCellWidth(
requestedWidth,
dp.cellLayoutBorderSpacePx.x,
@@ -1,7 +1,11 @@
package app.lawnchair.ui.preferences.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -10,7 +14,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.lawnchair.DeviceProfileOverrides
import app.lawnchair.preferences.preferenceManager
import com.android.launcher3.InvariantDeviceProfile
@@ -18,36 +24,88 @@ import com.android.launcher3.InvariantDeviceProfile
@Composable
fun ColumnScope.GridOverridesPreview(
modifier: Modifier = Modifier,
expandToAvailableSpace: Boolean = true,
previewOverrides: DeviceProfileOverrides.PreviewOverrides = DeviceProfileOverrides.PreviewOverrides(),
updateGridOptions: DeviceProfileOverrides.DBGridInfo.() -> DeviceProfileOverrides.DBGridInfo,
) {
val previewIdp = createPreviewIdp(updateGridOptions, previewOverrides)
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val previewAspectRatio = remember(previewIdp, isPortrait) {
val matchingProfile = previewIdp.supportedProfiles.firstOrNull { profile ->
profile.deviceProperties.isLandscape != isPortrait
} ?: previewIdp.supportedProfiles.firstOrNull { profile ->
!profile.deviceProperties.isMultiDisplay
} ?: previewIdp.supportedProfiles.firstOrNull()
val width = matchingProfile?.deviceProperties?.widthPx?.toFloat()
val height = matchingProfile?.deviceProperties?.heightPx?.toFloat()
if (width == null || height == null || height <= 0f) {
1f
} else {
width / height
}
}
WithWallpaper { wallpaper ->
DummyLauncherBox(
BoxWithConstraints(
modifier = modifier
.weight(1f)
.then(if (expandToAvailableSpace) Modifier.weight(1f) else Modifier)
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.clip(MaterialTheme.shapes.large),
contentAlignment = Alignment.Center,
) {
WallpaperPreview(
wallpaper = wallpaper,
modifier = Modifier.fillMaxSize(),
)
DummyLauncherLayout(
idp = createPreviewIdp(updateGridOptions),
modifier = Modifier.fillMaxSize(),
)
val fitByWidth = maxHeight == 0.dp || maxWidth == 0.dp || (maxWidth / maxHeight) <= previewAspectRatio
val previewModifier = if (fitByWidth) {
Modifier.requiredSize(
width = maxWidth,
height = maxWidth / previewAspectRatio,
)
} else {
Modifier.requiredSize(
width = maxHeight * previewAspectRatio,
height = maxHeight,
)
}
DummyLauncherBox(
modifier = previewModifier.clip(MaterialTheme.shapes.large),
aspectRatio = previewAspectRatio,
) {
WallpaperPreview(
wallpaper = wallpaper,
modifier = Modifier.fillMaxSize(),
)
DummyLauncherLayout(
idp = previewIdp,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
@Composable
fun createPreviewIdp(updateGridOptions: DeviceProfileOverrides.DBGridInfo.() -> DeviceProfileOverrides.DBGridInfo): InvariantDeviceProfile {
return createPreviewIdp(
updateGridOptions = updateGridOptions,
previewOverrides = DeviceProfileOverrides.PreviewOverrides(),
)
}
@Composable
fun createPreviewIdp(
updateGridOptions: DeviceProfileOverrides.DBGridInfo.() -> DeviceProfileOverrides.DBGridInfo,
previewOverrides: DeviceProfileOverrides.PreviewOverrides = DeviceProfileOverrides.PreviewOverrides(),
): InvariantDeviceProfile {
val context = LocalContext.current
val prefs = preferenceManager()
val newIdp by remember {
val newIdp by remember(previewOverrides) {
derivedStateOf {
val options = DeviceProfileOverrides.DBGridInfo(prefs)
InvariantDeviceProfile(context, updateGridOptions(options))
InvariantDeviceProfile(context, updateGridOptions(options), previewOverrides)
}
}
return newIdp
@@ -29,12 +29,13 @@ import com.android.launcher3.LauncherAppState
fun DummyLauncherBox(
modifier: Modifier = Modifier,
darkText: Boolean = wallpaperSupportsDarkText(),
aspectRatio: Float? = null,
content: @Composable BoxScope.() -> Unit,
) {
val context = LocalContext.current
val idp = remember { InvariantDeviceProfile.INSTANCE.get(context) }
val dp = idp.getDeviceProfile(context)
val ratio = dp.deviceProperties.widthPx.toFloat() / dp.deviceProperties.heightPx.toFloat()
val ratio = aspectRatio ?: (dp.deviceProperties.widthPx.toFloat() / dp.deviceProperties.heightPx.toFloat())
Box(modifier = modifier.aspectRatio(ratio, matchHeightConstraintsFirst = true)) {
LawnchairTheme(darkTheme = !darkText) {
@@ -59,6 +59,7 @@ fun SliderPreference(
step: Int,
showAsPercentage: Boolean = false,
showUnit: String = "",
enabled: Boolean = true,
) {
val transformedAdapter = rememberTransformAdapter(
adapter = adapter,
@@ -74,6 +75,7 @@ fun SliderPreference(
step = step.toFloat(),
showAsPercentage = showAsPercentage,
showUnit = showUnit,
enabled = enabled,
)
}
@@ -86,6 +88,7 @@ fun SliderPreference(
modifier: Modifier = Modifier,
showAsPercentage: Boolean = false,
showUnit: String = "",
enabled: Boolean = true,
) {
var adapterValue by adapter
@@ -100,6 +103,7 @@ fun SliderPreference(
modifier = modifier,
showAsPercentage = showAsPercentage,
showUnit = showUnit,
enabled = enabled,
)
}
@@ -113,6 +117,7 @@ private fun SliderPreference(
modifier: Modifier = Modifier,
showAsPercentage: Boolean = false,
showUnit: String = "",
enabled: Boolean = true,
) {
var sliderValue by remember { mutableFloatStateOf(value) }
val mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(LocalContext.current)
@@ -176,6 +181,7 @@ private fun SliderPreference(
.padding(top = 2.dp, bottom = 12.dp)
.padding(horizontal = 14.dp)
.height(24.dp),
enabled = enabled,
)
},
modifier = modifier,
@@ -47,10 +47,12 @@ import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference
import app.lawnchair.ui.preferences.components.controls.SliderPreference
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.controls.SwitchPreferenceWithPreview
import app.lawnchair.ui.preferences.components.controls.WarningPreference
import app.lawnchair.ui.preferences.components.layout.ExpandAndShrink
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
import app.lawnchair.ui.preferences.navigation.AppDrawerHiddenApps
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.R
object AppDrawerRoutes {
@@ -65,6 +67,7 @@ fun AppDrawerPreferences(
val prefs2 = preferenceManager2()
val context = LocalContext.current
val resources = context.resources
val isFoldable = InvariantDeviceProfile.deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY
PreferenceLayout(
label = stringResource(id = R.string.app_drawer_label),
@@ -117,13 +120,41 @@ fun AppDrawerPreferences(
}
}
PreferenceGroup(heading = stringResource(id = R.string.grid)) {
Item {
SliderPreference(
label = stringResource(id = R.string.app_drawer_columns),
adapter = prefs2.drawerColumns.getAdapter(),
step = 1,
valueRange = 3..10,
)
val drawerColumnsAdapter = prefs2.drawerColumns.getAdapter()
val drawerColumnsUnfoldedAdapter = prefs2.drawerColumnsUnfolded.getAdapter()
if (isFoldable) {
Item {
SliderPreference(
label = stringResource(id = R.string.state_folded, stringResource(id = R.string.app_drawer_columns)),
adapter = drawerColumnsAdapter,
step = 1,
valueRange = 3..10,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.state_unfolded, stringResource(id = R.string.app_drawer_columns)),
adapter = drawerColumnsUnfoldedAdapter,
step = 1,
valueRange = 3..10,
)
}
Item(
visible = drawerColumnsAdapter.state.value > drawerColumnsUnfoldedAdapter.state.value,
) {
WarningPreference(
text = stringResource(id = R.string.foldable_columns_error),
)
}
} else {
Item {
SliderPreference(
label = stringResource(id = R.string.app_drawer_columns),
adapter = drawerColumnsAdapter,
step = 1,
valueRange = 3..10,
)
}
}
Item {
SliderPreference(
@@ -44,11 +44,13 @@ import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference
import app.lawnchair.ui.preferences.components.controls.MainSwitchPreference
import app.lawnchair.ui.preferences.components.controls.SliderPreference
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.controls.WarningPreference
import app.lawnchair.ui.preferences.components.createPreviewIdp
import app.lawnchair.ui.preferences.components.layout.DividerColumn
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceGroupHeading
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.R
@Composable
@@ -137,14 +139,44 @@ fun HotseatBackgroundSettings(prefs: PreferenceManager, prefs2: PreferenceManage
@Composable
fun GridSettings(prefs: PreferenceManager, prefs2: PreferenceManager2) {
val isFoldable = InvariantDeviceProfile.deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY
val hotseatColumnsAdapter = prefs.hotseatColumns.getAdapter()
val hotseatColumnsUnfoldedAdapter = prefs.hotseatColumnsUnfolded.getAdapter()
PreferenceGroup(heading = stringResource(id = R.string.grid)) {
Item {
SliderPreference(
label = stringResource(id = R.string.dock_icons),
adapter = prefs.hotseatColumns.getAdapter(),
step = 1,
valueRange = 3..10,
)
if (isFoldable) {
Item {
SliderPreference(
label = stringResource(id = R.string.state_folded, stringResource(id = R.string.dock_icons)),
adapter = hotseatColumnsAdapter,
step = 1,
valueRange = 3..10,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.state_unfolded, stringResource(id = R.string.dock_icons)),
adapter = hotseatColumnsUnfoldedAdapter,
step = 1,
valueRange = 3..10,
)
}
Item(
visible = hotseatColumnsAdapter.state.value > hotseatColumnsUnfoldedAdapter.state.value,
) {
WarningPreference(
text = stringResource(id = R.string.foldable_columns_error),
)
}
} else {
Item {
SliderPreference(
label = stringResource(id = R.string.dock_icons),
adapter = hotseatColumnsAdapter,
step = 1,
valueRange = 3..10,
)
}
}
Item {
SliderPreference(
@@ -176,6 +208,7 @@ fun ColumnScope.DockPreferencesPreview(modifier: Modifier = Modifier) {
val adapters = listOf(
prefs2.hotseatMode.getAdapter(),
prefs.hotseatColumns.getAdapter(),
prefs.hotseatColumnsUnfolded.getAdapter(),
prefs2.themedHotseatQsb.getAdapter(),
prefs.hotseatQsbCornerRadius.getAdapter(),
prefs.hotseatQsbAlpha.getAdapter(),
@@ -1,15 +1,28 @@
package app.lawnchair.ui.preferences.destinations
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -19,15 +32,18 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.lawnchair.DeviceProfileOverrides
import app.lawnchair.preferences.asPreferenceAdapter
import app.lawnchair.preferences.getAdapter
import app.lawnchair.preferences.preferenceManager
import app.lawnchair.ui.preferences.LocalIsExpandedScreen
import app.lawnchair.ui.preferences.LocalNavController
import app.lawnchair.ui.preferences.components.GridOverridesPreview
import app.lawnchair.ui.preferences.components.controls.SliderPreference
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
import com.android.launcher3.LauncherAppState
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.R
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -35,78 +51,263 @@ import com.android.launcher3.R
fun HomeScreenGridPreferences(
modifier: Modifier = Modifier,
) {
val isExpandedScreen = LocalIsExpandedScreen.current
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val scrollState = rememberScrollState()
PreferenceLayout(
label = stringResource(id = R.string.home_screen_grid),
modifier = modifier,
isExpandedScreen = true,
scrollState = if (isPortrait) null else scrollState,
scrollState = null,
) {
val controlsScrollState = rememberScrollState()
val prefs = preferenceManager()
val columnsAdapter = prefs.workspaceColumns.getAdapter()
val rowsAdapter = prefs.workspaceRows.getAdapter()
val hotseatColumnsAdapter = prefs.hotseatColumns.getAdapter()
val hotseatColumnsUnfoldedAdapter = prefs.hotseatColumnsUnfolded.getAdapter()
val increaseMaxGridSize = prefs.workspaceIncreaseMaxGridSize.getAdapter()
val isFoldable = InvariantDeviceProfile.deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY
val originalColumns = remember { columnsAdapter.state.value }
val originalRows = remember { rowsAdapter.state.value }
val originalHotseatColumns = remember { hotseatColumnsAdapter.state.value }
val originalHotseatColumnsUnfolded = remember { hotseatColumnsUnfoldedAdapter.state.value }
val columns = rememberSaveable { mutableIntStateOf(originalColumns) }
val rows = rememberSaveable { mutableIntStateOf(originalRows) }
val hotseatColumns = rememberSaveable { mutableIntStateOf(originalHotseatColumns) }
val hotseatColumnsUnfolded = rememberSaveable {
mutableIntStateOf(originalHotseatColumnsUnfolded.coerceAtLeast(originalHotseatColumns))
}
if (isPortrait) {
GridOverridesPreview {
copy(numColumns = columns.intValue, numRows = rows.intValue)
LaunchedEffect(hotseatColumns.intValue) {
if (hotseatColumnsUnfolded.intValue < hotseatColumns.intValue) {
hotseatColumnsUnfolded.intValue = hotseatColumns.intValue
}
}
val maxGridSize = if (increaseMaxGridSize.state.value) 20 else 10
PreferenceGroup {
Item {
SliderPreference(
label = stringResource(id = R.string.columns),
adapter = columns.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.rows),
adapter = rows.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
}
val navController = LocalNavController.current
val context = LocalContext.current
val applyOverrides = {
prefs.batchEdit {
columnsAdapter.onChange(columns.intValue)
rowsAdapter.onChange(rows.intValue)
}
LauncherAppState.getIDP(context).onPreferencesChanged(context)
navController.popBackStack()
}
Box(
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.padding(horizontal = 16.dp),
.weight(1f),
) {
Button(
onClick = { applyOverrides() },
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxWidth(),
enabled = columns.intValue != originalColumns || rows.intValue != originalRows,
shapes = ButtonDefaults.shapes(),
// Don't stress over the coerce value,
// we eyeballing it till there's a much better solutions(tm) or permanent workarounds.
val settingsMinHeight = when {
// This should allow user to see the unfolded label,
// which will be their indicator that they can scroll the settings entries.
isFoldable -> (maxHeight * 0.58f).coerceAtLeast(360.dp)
// This should be enough for 3 preferences, they can't scroll beyond this.
// Should you introduce another prefs, raise the value by a little so that they
// have an indication that you can scroll the entries.
isPortrait -> (maxHeight * 0.40f).coerceAtLeast(315.dp)
// Landscape mode
else -> (maxHeight * 0.52f).coerceAtLeast(280.dp)
}
val previewMaxHeight = (maxHeight - settingsMinHeight)
.coerceAtLeast(if (isPortrait) 180.dp else 140.dp)
Column(
modifier = Modifier.fillMaxHeight(),
) {
Text(text = stringResource(id = R.string.action_apply))
GridOverridesPreview(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = previewMaxHeight)
.padding(horizontal = 16.dp, vertical = if (isPortrait) 16.dp else 12.dp),
expandToAvailableSpace = false,
updateGridOptions = {
copy(
numColumns = columns.intValue,
numRows = rows.intValue,
numHotseatColumns = hotseatColumns.intValue,
)
},
previewOverrides = if (isFoldable) {
DeviceProfileOverrides.PreviewOverrides(
foldableDatabaseHotseatIcons = hotseatColumnsUnfolded.intValue,
)
} else {
DeviceProfileOverrides.PreviewOverrides()
},
)
Column(
modifier = Modifier
.weight(1f)
.heightIn(min = settingsMinHeight),
) {
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(controlsScrollState),
) {
if (isFoldable) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = if (isExpandedScreen) Int.MAX_VALUE else 1,
) {
PreferenceGroup(heading = stringResource(id = R.string.when_folded_label)) {
Item {
SliderPreference(
label = stringResource(id = R.string.columns),
adapter = columns.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.rows),
adapter = rows.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.dock_icons),
adapter = hotseatColumns.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
}
PreferenceGroup(
heading = stringResource(id = R.string.when_unfolded_label),
) {
Item {
SliderPreference(
label = stringResource(id = R.string.dock_icons),
adapter = hotseatColumnsUnfolded.asPreferenceAdapter(),
step = 1,
valueRange = hotseatColumns.intValue..maxGridSize,
)
}
Item {
FakeExpandedGridPreference(
columns = columns.intValue * 2,
rows = rows.intValue,
description = stringResource(id = R.string.unfolded_grid_description),
)
}
}
}
} else {
PreferenceGroup {
Item {
SliderPreference(
label = stringResource(id = R.string.columns),
adapter = columns.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.rows),
adapter = rows.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
Item {
SliderPreference(
label = stringResource(id = R.string.dock_icons),
adapter = hotseatColumns.asPreferenceAdapter(),
step = 1,
valueRange = 3..maxGridSize,
)
}
}
}
}
val navController = LocalNavController.current
val context = LocalContext.current
val applyOverrides = {
prefs.batchEdit {
columnsAdapter.onChange(columns.intValue)
rowsAdapter.onChange(rows.intValue)
hotseatColumnsAdapter.onChange(hotseatColumns.intValue)
hotseatColumnsUnfoldedAdapter.onChange(hotseatColumnsUnfolded.intValue)
}
InvariantDeviceProfile.INSTANCE.get(context).onPreferencesChanged(context)
navController.popBackStack()
}
val isChanged = columns.intValue != originalColumns ||
rows.intValue != originalRows ||
hotseatColumns.intValue != originalHotseatColumns ||
hotseatColumnsUnfolded.intValue != originalHotseatColumnsUnfolded
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.padding(horizontal = 16.dp),
) {
Button(
onClick = { applyOverrides() },
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxWidth(),
enabled = isChanged,
shapes = ButtonDefaults.shapes(),
) {
Text(text = stringResource(id = R.string.action_apply))
}
}
}
}
}
}
}
@Composable
private fun FakeExpandedGridPreference(
columns: Int,
rows: Int,
description: String,
modifier: Modifier = Modifier,
) {
PreferenceTemplate(
title = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.grid),
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
)
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
LocalTextStyle provides MaterialTheme.typography.bodyLarge,
) {
Text(
text = stringResource(id = R.string.x_by_y, columns, rows),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
description = {
Text(description)
},
modifier = modifier,
applyPaddings = false,
enabled = false,
)
}
@@ -517,7 +517,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
List<SystemShortcut.Factory> shortcuts = new ArrayList(Arrays.asList(
APP_INFO, WellbeingModel.SHORTCUT_FACTORY, mHotseatPredictionController));
shortcuts.addAll(getSplitShortcuts());
if (LawnchairApp.isRecentsEnabled()) {
shortcuts.addAll(getSplitShortcuts());
}
shortcuts.add(WIDGETS);
shortcuts.add(INSTALL);
if (Flags.enableLongPressRemoveShortcut()
+2 -2
View File
@@ -174,7 +174,7 @@
launcher:devicePaddingId="@xml/paddings_6x5"
launcher:dbFile="launcher_6_by_5.db"
launcher:defaultLayoutId="@xml/default_workspace_6x5"
launcher:deviceCategory="tablet" >
launcher:deviceCategory="tablet|desktop" >
<display-option
launcher:name="Tablet"
@@ -248,4 +248,4 @@
</grid-option>
</profiles>
</profiles>
+16 -7
View File
@@ -897,8 +897,12 @@ public class DeviceProfile {
+ space;
} else if (isQsbInline) {
hotseatBarSizePx = Math.max(hotseatIconSizePx, getHotseatProfile().getQsbVisualHeight())
+ hotseatBarBottomSpacePx
+ space;
+ hotseatBarBottomSpacePx;
} else if (isQsbOnTop()) { // LC-Note: isQsbOnTop, this usually is a foldable device, not a tablet
hotseatBarSizePx = hotseatIconSizePx
+ hotseatQsbSpace
+ getHotseatProfile().getQsbVisualHeight()
+ hotseatBarBottomSpacePx;
} else {
hotseatBarSizePx = hotseatIconSizePx
+ hotseatQsbSpace
@@ -1417,7 +1421,7 @@ public class DeviceProfile {
}
var allAppLeftRightMarginMultiplier = PreferenceCacheExtensionsKt
.firstCached(preferenceManager2.getDrawerLeftRightMarginFactor());
var marginMultiplier = allAppLeftRightMarginMultiplier * (!getDeviceProperties().isTablet() ? 100 : 2);
var marginMultiplier = allAppLeftRightMarginMultiplier * (!getDeviceProperties().isTablet() ? 100 : 10);
allAppsLeftRightMargin = (int) (allAppsLeftRightMargin * marginMultiplier);
// todo fix how drawer padding values are calculated in responsive grid type
@@ -1881,8 +1885,8 @@ public class DeviceProfile {
} else {
hotseatBarPadding.left += qsbWidth;
}
} else if (isTaskbarPresent || isQsbInline) {
// Center the QSB vertically with hotseat
} else if (isQsbOnTop() || isQsbInline) {
// Keep the hotseat icons in the lower section so the QSB can sit above them.
int hotseatBarBottomPadding = getHotseatBarBottomPadding();
int hotseatBarTopPadding =
hotseatBarSizePx - hotseatBarBottomPadding - hotseatCellHeightPx;
@@ -1986,6 +1990,11 @@ public class DeviceProfile {
+ additionalQsbSpace;
}
/** LC-Note: For ID'ing tablet/foldable mode */
private boolean isQsbOnTop() {
return isTaskbarPresent || mDeviceProperties.isTwoPanels();
}
/**
* Returns the number of pixels the QSB is translated from the bottom of the screen.
*/
@@ -1993,7 +2002,7 @@ public class DeviceProfile {
if (mDeviceProperties.isPhone() && isQsbInline) {
return getHotseatBarBottomPadding()
- ((getHotseatProfile().getQsbHeight() - hotseatCellHeightPx) / 2);
} else if (isTaskbarPresent || (mDeviceProperties.isLandscape() && isQsbInline)) { // QSB on top
} else if (isQsbOnTop() || (mDeviceProperties.isLandscape() && isQsbInline)) { // LC-Note: isQsbOnTop
return hotseatBarSizePx - getHotseatProfile().getQsbHeight()
+ getHotseatProfile().getQsbShadowHeight();
} else {
@@ -2005,7 +2014,7 @@ public class DeviceProfile {
* Returns the number of pixels the hotseat is translated from the bottom of the screen.
*/
private int getHotseatBarBottomPadding() {
if (isTaskbarPresent || isQsbInline) { // QSB on top or inline
if (isQsbOnTop() || isQsbInline) { // LC-Note: isQsbOnTop
return hotseatBarBottomSpacePx - (Math.abs(hotseatCellHeightPx - iconSizePx) / 2);
} else {
return hotseatBarSizePx - hotseatCellHeightPx;
@@ -347,7 +347,16 @@ public class InvariantDeviceProfile {
this.mThemeManager = ThemeManager.INSTANCE.get(context.getApplicationContext());
this.mDisplayController = DisplayController.INSTANCE.get(context.getApplicationContext());
String gridName = DeviceProfileOverrides.INSTANCE.get(context).getGridName(dbGridInfo);
initGrid(context, gridName, dbGridInfo);
initGrid(context, gridName, dbGridInfo, null);
}
public InvariantDeviceProfile(Context context, DeviceProfileOverrides.DBGridInfo dbGridInfo,
DeviceProfileOverrides.PreviewOverrides previewOverrides) {
this.mPrefs = LauncherPrefs.get(context.getApplicationContext());
this.mThemeManager = ThemeManager.INSTANCE.get(context.getApplicationContext());
this.mDisplayController = DisplayController.INSTANCE.get(context.getApplicationContext());
String gridName = DeviceProfileOverrides.INSTANCE.get(context).getGridName(dbGridInfo);
initGrid(context, gridName, dbGridInfo, previewOverrides);
}
private String initGrid(Context context, String gridName) {
@@ -376,7 +385,7 @@ public class InvariantDeviceProfile {
}
DeviceProfileOverrides.DBGridInfo dbGridInfo = DeviceProfileOverrides.INSTANCE.get(context)
.getGridInfo();
initGrid(context, displayInfo, displayOption, dbGridInfo);
initGrid(context, displayInfo, displayOption, dbGridInfo, null);
FileLog.d(TAG, "After initGrid:"
+ "gridName:" + gridName
+ ", dbFile:" + dbFile
@@ -386,6 +395,11 @@ public class InvariantDeviceProfile {
}
private String initGrid(Context context, String gridName, DeviceProfileOverrides.DBGridInfo dbGridInfo) {
return initGrid(context, gridName, dbGridInfo, null);
}
private String initGrid(Context context, String gridName, DeviceProfileOverrides.DBGridInfo dbGridInfo,
DeviceProfileOverrides.PreviewOverrides previewOverrides) {
Info displayInfo = mDisplayController.getInfo();
List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
context,
@@ -407,7 +421,7 @@ public class InvariantDeviceProfile {
if (!displayOption.grid.name.equals(gridName)) {
mPrefs.put(GRID_NAME, displayOption.grid.name);
}
initGrid(context, displayInfo, displayOption, dbGridInfo);
initGrid(context, displayInfo, displayOption, dbGridInfo, previewOverrides);
return displayOption.grid.name;
}
@@ -427,7 +441,9 @@ public class InvariantDeviceProfile {
initGrid(context, getCurrentGridName(context));
}
private void initGrid(Context context, Info displayInfo, DisplayOption displayOption, DeviceProfileOverrides.DBGridInfo dbGridInfo) {
private void initGrid(Context context, Info displayInfo, DisplayOption displayOption,
DeviceProfileOverrides.DBGridInfo dbGridInfo,
DeviceProfileOverrides.PreviewOverrides previewOverrides) {
this.closestProfile = displayOption.grid;
enableTwoLinesInAllApps = Flags.enableTwolineToggle()
@@ -435,8 +451,8 @@ public class InvariantDeviceProfile {
&& mPrefs.get(ENABLE_TWOLINE_ALLAPPS_TOGGLE);
mLocale = context.getResources().getConfiguration().locale.toString();
DeviceProfileOverrides.Options overrideOptions = DeviceProfileOverrides.INSTANCE.get(context)
.getOverrides(displayOption.grid);
DeviceProfileOverrides.Options overrideOptions = DeviceProfileOverrides.INSTANCE.get(context)
.getOverrides(displayOption.grid, displayInfo.getDeviceType(), previewOverrides);
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
GridOption closestProfile = displayOption.grid;
numRows = dbGridInfo.getNumRows();
@@ -489,7 +505,7 @@ public class InvariantDeviceProfile {
horizontalMargin = displayOption.horizontalMargin;
numShownHotseatIcons = deviceType == TYPE_MULTI_DISPLAY
? closestProfile.numDatabaseHotseatIcons : dbGridInfo.getNumHotseatColumns();
? closestProfile.numHotseatIcons : dbGridInfo.getNumHotseatColumns();
numDatabaseHotseatIcons = deviceType == TYPE_MULTI_DISPLAY
? closestProfile.numDatabaseHotseatIcons : numShownHotseatIcons;
hotseatBarBottomSpace = displayOption.hotseatBarBottomSpace;
@@ -729,6 +729,7 @@ public class DisplayController implements DesktopVisibilityListener {
public @DeviceType int getDeviceType() {
if (mIsDesktopFormFactor) {
Log.d("LC-DisplayController", "Device has desktop profile (Desktop)");
return TYPE_DESKTOP;
}