diff --git a/flags/src/com/android/launcher3/FeatureFlagsImpl.java b/flags/src/com/android/launcher3/FeatureFlagsImpl.java index 050937acc6..2dfe84d767 100644 --- a/flags/src/com/android/launcher3/FeatureFlagsImpl.java +++ b/flags/src/com/android/launcher3/FeatureFlagsImpl.java @@ -370,7 +370,7 @@ public final class FeatureFlagsImpl implements FeatureFlags { public boolean enableScalabilityForDesktopExperience() { - return false; + return true; } @Override diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index fe78058ee5..d6388a6698 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -765,6 +765,14 @@ Open in store Dock icons + %1$s (folded) + %1$s (unfolded) + Folded + Unfolded + When folded + When unfolded + Lawnchair pairs two pages together to fill the larger display + The folded value must be less than or equal to the unfolded value. It will be automatically adjusted if needed. Bottom padding Page indicator height diff --git a/lawnchair/res/xml/default_workspace_4x5.xml b/lawnchair/res/xml/default_workspace_4x5.xml index 11b4eb86e8..6e1d7bb68c 100644 --- a/lawnchair/res/xml/default_workspace_4x5.xml +++ b/lawnchair/res/xml/default_workspace_4x5.xml @@ -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"/> diff --git a/lawnchair/res/xml/default_workspace_splitdisplay_4x5.xml b/lawnchair/res/xml/default_workspace_splitdisplay_4x6.xml similarity index 95% rename from lawnchair/res/xml/default_workspace_splitdisplay_4x5.xml rename to lawnchair/res/xml/default_workspace_splitdisplay_4x6.xml index 36c1ce7137..58fc468343 100644 --- a/lawnchair/res/xml/default_workspace_splitdisplay_4x5.xml +++ b/lawnchair/res/xml/default_workspace_splitdisplay_4x6.xml @@ -33,25 +33,25 @@ launcher:packageName="com.android.vending" launcher:screen="0" launcher:x="0" - launcher:y="4"/> + launcher:y="5"/> + launcher:y="5"/> + launcher:y="5"/> + launcher:y="5"/> - \ No newline at end of file + diff --git a/lawnchair/res/xml/device_profiles.xml b/lawnchair/res/xml/device_profiles.xml index f3ad82b02b..f5a27bce73 100644 --- a/lawnchair/res/xml/device_profiles.xml +++ b/lawnchair/res/xml/device_profiles.xml @@ -51,8 +51,8 @@ + launcher:deviceCategory="tablet|desktop" > 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 diff --git a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt index af4a5ed8af..06e03dd221 100644 --- a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt +++ b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt @@ -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, +) diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt index d629493660..911f8361e7 100644 --- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt +++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt @@ -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] }, diff --git a/lawnchair/src/app/lawnchair/qsb/LawnQsbLayout.kt b/lawnchair/src/app/lawnchair/qsb/LawnQsbLayout.kt index 09a9389582..f18c5a0028 100644 --- a/lawnchair/src/app/lawnchair/qsb/LawnQsbLayout.kt +++ b/lawnchair/src/app/lawnchair/qsb/LawnQsbLayout.kt @@ -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, diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/GridOverridesPreview.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/GridOverridesPreview.kt index 6caf81e09f..40916a0248 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/GridOverridesPreview.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/GridOverridesPreview.kt @@ -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 diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/LauncherPreview.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/LauncherPreview.kt index 2b90f0109a..7a16cf5e19 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/LauncherPreview.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/LauncherPreview.kt @@ -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) { diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/controls/SliderPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/controls/SliderPreference.kt index 4922032c79..8ab2594f20 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/controls/SliderPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/controls/SliderPreference.kt @@ -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, diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt index e91220233a..3b5aa79669 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt @@ -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( diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt index 9b4cf141f8..a6454c13a8 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt @@ -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(), diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/HomeScreenGridPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/HomeScreenGridPreferences.kt index 8456d8ea93..41c0aea48e 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/HomeScreenGridPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/HomeScreenGridPreferences.kt @@ -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, + ) +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 9da9095faa..8b76a0dee5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -517,7 +517,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, List 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() diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml index 5fac66f233..2a26aed098 100644 --- a/res/xml/device_profiles.xml +++ b/res/xml/device_profiles.xml @@ -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" > - \ No newline at end of file + diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 74c16a0594..78963ca025 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -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; diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 145e81c43a..a73c263372 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -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 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; diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java index e4853c516b..1c23c2d061 100644 --- a/src/com/android/launcher3/util/DisplayController.java +++ b/src/com/android/launcher3/util/DisplayController.java @@ -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; }