diff --git a/AGENTS.md b/AGENTS.md index 98e34e6f..c2b19775 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,7 +109,7 @@ etc/ — design assets (SVG/XCF sources, desktops for uninstall). - `App`, `Application`, `AppComparatorAlphabetical` — app model classes. `App` usually wraps a PackageManager `ResolveInfo`, but can also wrap a `LauncherActivityInfo` for apps in another profile (see Profiles below), or represent DistroHopper-owned internal shortcuts that live only in the dash (currently the settings shortcut) and launch by explicit in-app intent rather than a public launcher component. Internal shortcut intents must not set `FLAG_ACTIVITY_NEW_TASK` (the target shares the home task's affinity, so it would only bring the home task to the front); the settings shortcut launches via `startActivityForResult` so `HomeActivity.onActivityResult` can handle the Customise UI result. - **Dash sort order**: `AppRepository.sort()` orders the installed list by the `preferences/AppSortOrder` chosen in settings (`APP_SORT_ORDER` = `alphabetical`|`recent`|`most_used`|`custom`, default `alphabetical`; unknown → alphabetical). `AppComparators.forOrder` builds the comparator; the two usage-based orders sort by score descending with `AppComparatorAlphabetical` as the secondary key (so a tied group stays alphabetical), and `custom` degrades to alphabetical there (the manual arrangement is applied by `DashLayoutRepository`, not by a comparator). The `custom` order is developer-gated for now (needs more testing before general availability): `PreferencesActivity` only offers it while developer mode is on and resets a stored `custom` back to alphabetical when it isn't. The usage data lives in `AppUsageStats` — per-app launch count and last-launched timestamp keyed by `App.profileScopedKey`, in its own `"app_usage"` SharedPreferences file (kept out of the main "prefs" so a launch doesn't trip its change listeners). `App.launch()` records every real launch there (internal shortcuts excluded). The order is read fresh on each `sort()`, so changing it in settings takes effect when home reloads on leaving the preferences screen. - - **Dash folders & custom order**: the dash grid renders `DashItem`s (an `AppItem` or a `FolderItem`) built by `DashLayoutRepository` from the installed list plus the persisted folders/manual order (`DashLayoutStorage`, the `"dash_layout"` SharedPreferences file, partitioned per profile). `AppManager` owns it (`getDashLayout`, `loadDashLayout` after `loadPinnedApps`, `dashLayoutChanged` to refresh; `remove` reconciles it). Folders are unnamed, hold ≥2 apps (a folder dropped to 1 dissolves), are capped at 9 (`DashLayoutRepository.MAX_FOLDER_APPS`, a 3x3 grid — see `folder/FolderGrid`), and render via `folder/FolderIconDrawable` (a mini-grid icon). `DashComparators` orders items: a folder ranks by its highest-scoring member, and in `alphabetical` mode folders group ahead of loose apps. Tapping a folder opens `FolderPopup`. Dragging within the dash is `desktop/dash/DashGridDragListener` (pause over another app to fold — the `dash_folder_drop_indicator` ring — drop on a folder to add, reorder under `custom` only, drag a folder member onto the dash to pull it out); a loose app keeps passing its `App` as the drag's local state (so the launcher still pins it), while folders/members carry a `desktop/dash/DashDragPayload`. `TrashDragListener` deletes a dropped folder (members return loose). The dash no longer auto-closes when a drag starts (`LauncherBarBinder.startedDragging` keeps it open and the BFB visible); the cross-surface drag instead closes the dash when the drag hovers the launcher or panel and re-opens it when it hovers a BFB while the dash is closed, so an app can move dash↔launcher↔desktop in one drag. `desktop/launcher/DashCrossSurfaceController` resolves the intent **by dash state**: while the dash is open, hovering the launcher (reported from `LauncherDragListener`) or the panel (`DashEdgeDragListener` on `llPanel`) closes it; while it is closed, hovering a BFB (`DashEdgeDragListener` on `lalBfb` / `tvPanelBfb`) re-opens it. A BFB only ever *opens* (**open precedence**): it sits inside its bar, so a hover registers both an open- and a close-target — counting that as a close would make the BFB a toggle and the dash would flicker across the BFB↔bar boundary, so while open the close fires only when a close-target is hovered and no BFB is, and opening drops the close-target that rode in with the BFB hover (so leaving the BFB doesn't slam it shut). Enter/exit are edge-triggered (no per-LOCATION action) so a hover resolves once without oscillating; the change is posted + debounced. Dropping while the dash is open means dropping **into** the dash: `WidgetsContainer_DragListener.dropIntoDash` removes the app from its source surface (unpin a dock pin / take a desktop app off the grid / leave a desktop folder) so it returns to just the app drawer, instead of letting the drop fall through onto the desktop behind. + - **Dash folders & custom order**: the dash grid renders `DashItem`s (an `AppItem` or a `FolderItem`) built by `DashLayoutRepository` from the installed list plus the persisted folders/manual order (`DashLayoutStorage`, the `"dash_layout"` SharedPreferences file, partitioned per profile). `AppManager` owns it (`getDashLayout`, `loadDashLayout` after `loadPinnedApps`, `dashLayoutChanged` to refresh; `remove` reconciles it). Folders are unnamed, hold ≥2 apps (a folder dropped to 1 dissolves), are capped at 9 (`DashLayoutRepository.MAX_FOLDER_APPS`, a 3x3 grid — see `folder/FolderGrid`), and render via `folder/FolderIconDrawable` (a mini-grid icon). `DashComparators` orders items: a folder ranks by its highest-scoring member, and in `alphabetical` mode folders group ahead of loose apps. Tapping a folder opens `FolderPopup`. Dragging within the dash is `desktop/dash/DashGridDragListener` (pause over another app to fold — the `dash_folder_drop_indicator` ring — drop on a folder to add, reorder under `custom` only, drag a folder member onto the dash to pull it out); a loose app keeps passing its `App` as the drag's local state (so the launcher still pins it), while folders/members carry a `desktop/dash/DashDragPayload`. `TrashDragListener` deletes a dropped folder (members return loose). Dragging an app *icon* from the dash (loose or launcher-pinned; also dash search results) shows the **app-info target** (`lalAppInfo`, `desktop/launcher/AppInfoDragListener`; its icon is a per-theme `launcher_appinfo_image` applied by `ThemeApplier`, like the trash/preferences icons — a layout-level `custom:icon` does nothing, `AppLauncher` only reads `applauncher_icon`) in the trash's place — the dash always shows every installed app, so there is nothing to delete; dropping the icon there opens the system's App info screen (`App.openAppInfo()`, via LauncherApps for other-profile apps). The dash long-click listener marks the drag via `LauncherBarBinder.dragStartedFromDashApp()`, sticky until `stoppedDragging`; dash folders/folder members keep the trash (delete / leave folder). The dash no longer auto-closes when a drag starts (`LauncherBarBinder.startedDragging` keeps it open and the BFB visible); the cross-surface drag instead closes the dash when the drag hovers the launcher or panel and re-opens it when it hovers a BFB while the dash is closed, so an app can move dash↔launcher↔desktop in one drag. `desktop/launcher/DashCrossSurfaceController` resolves the intent **by dash state**: while the dash is open, hovering the launcher (reported from `LauncherDragListener`) or the panel (`DashEdgeDragListener` on `llPanel`) closes it; while it is closed, hovering a BFB (`DashEdgeDragListener` on `lalBfb` / `tvPanelBfb`) re-opens it. A BFB only ever *opens* (**open precedence**): it sits inside its bar, so a hover registers both an open- and a close-target — counting that as a close would make the BFB a toggle and the dash would flicker across the BFB↔bar boundary, so while open the close fires only when a close-target is hovered and no BFB is, and opening drops the close-target that rode in with the BFB hover (so leaving the BFB doesn't slam it shut). Enter/exit are edge-triggered (no per-LOCATION action) so a hover resolves once without oscillating; the change is posted + debounced. Dropping while the dash is open means dropping **into** the dash: `WidgetsContainer_DragListener.dropIntoDash` removes the app from its source surface (unpin a dock pin / take a desktop app off the grid / leave a desktop folder) so it returns to just the app drawer, instead of letting the drop fall through onto the desktop behind. - **Launcher folders**: `LauncherLayoutRepository` (+ `LauncherLayoutStorage`, the `"launcher_layout"` file) layers folders over the **per-desktop** pinned apps, the counterpart to the dash's `DashLayoutRepository`. It stores only folder **membership** — the bar's order is the pinned order itself (`AppRepository.pinnedOn`), with a folder rendered (once) at its first member's position — so reordering the bar reorders the pinned model (`AppRepository.reorderPinned`, fed by `LauncherBarBinder.flattenBarKeys`) and the floating launcher service / persistence stay in step. `AppManager` owns it (`getLauncherLayout`, `loadLauncherLayout` after `loadPinnedApps`, `launcherLayoutChanged`; `remove` reconciles it). The bar renders `LauncherItem`s (app or `LauncherFolderView` mini-grid); `LauncherBarBinder.refreshPinnedView`/`buildMorph` are item-based and the per-desktop morph (`LauncherMorph`, now generic) fades folders in/out (a folder id is unique to one desktop). Drag uses the existing placeholder reorder generalised to views (`draggedPinnedItemOver`), with `AppLauncherDragListener` adding a dwell-to-fold (`foldDraggedOnto`: create/add, toast when full) and folder drags carrying a `LauncherDragPayload`; `TrashDragListener` **deletes a launcher folder and unpins its members** (unlike the dash, where they return loose). Tapping a launcher folder opens the shared `FolderPopup` (with a `LauncherDragPayload.FolderMemberDrag` payload); long-pressing a member there drags it **out** of the folder, behaving exactly like dragging the pin itself — `LauncherDragListener` opens a placeholder via `LauncherBarBinder.startedDraggingLauncherFolderMember` so it reorders/folds normally, and because a folder only *groups* already-pinned apps the app is **not** re-pinned but **ungrouped** (`removeFromFolder`) on a committing drop (cancel leaves it in the folder; `flattenBarKeysExtracting` keeps the dragged occurrence at the drop slot). `FolderPopup` (shared with the dash) is an **in-activity overlay** — added to the activity's `content`, NOT a `PopupWindow` — so the extract drag originates in the same window as its drop targets and carries its local state to them (a cross-window / `DRAG_FLAG_GLOBAL` drag arrives with a *null* local state, which the listeners need); this mirrors `DesktopFolderOverlay`. A launcher folder member can also be dropped on the **desktop** (`WidgetsContainer_DragListener.Drag.LauncherFolderMember`): it is placed there, then ungrouped and unpinned off the bar, so it leaves the launcher — like dropping a dock pin on the desktop. - **Desktop folders** (apps only): a folder is a 2×2 `widgets/DesktopFolderView` on the 8×8 grid whose apps are packed 1×1 on the 3×3 `folder/FolderGrid` for storage (`widgets/DesktopFolderLayout` + `DesktopFolderCell`, `withApp` fit-or-null, ≥2 apps, ≤9; persisted in the shared `"desktop_layout"` file via `DesktopLayoutStorage`, alongside the desktop widgets and pinned apps). The folder view itself is unnamed (the `FolderIconDrawable` mini-grid, no label), but the opened overlay shows each member app's label (it inflates the shared `widget_dash_applauncher` icon-over-label cell, like the dash/launcher popups — not a bare icon). `widgets/DesktopFolderHost` mirrors `DesktopAppHost` (create from two desktop apps, two `addApp` overloads — a loose `DesktopAppView` removed off the grid, or an `App` coming straight from another surface — both toast-on-full, deleteFolder removes apps, moveTo, restore/persist, per-page, uninstall reconcile) and is the third desktop owner in `home/Desktops`; it `restore()`s after widgets + desktop apps so its 2×2 avoids occupied cells, and `WidgetsContainer.collectOccupied` includes folder cells (+ `findViewAtCell` for hit-testing). Tapping a folder opens `widgets/DesktopFolderOverlay` — an **in-activity** overlay (not a PopupWindow) so the extract drag originates in the activity window and carries its local state to the drop targets. The overlay lays its apps out with the adaptive `FolderGrid.columns`/`rows` mapping (NOT the stored packed col/row), the **same** layout the `FolderIconDrawable` preview uses, so an opened folder matches its icon (4 apps read as a 2×2, not a row-major 3+1). All three folder popovers share `folder/FolderOverlay` for their chrome: a dim, GPU-blurred (`RenderEffect`, minSdk 31, no wallpaper bitmap) backdrop with the grid opened **centred over the tapped icon** (clamped on-screen) by a scale+fade animation that grows from it. The desktop's loose-XOR-in-folder invariant is also enforced on restore (a folder member that is *also* a loose desktop app is dropped — loose wins — via `DesktopAppHost.viewForKey`) and live (a cross-surface drop that lands an app loose on the desktop calls `DesktopFolderHost.dropFromFolders`). Drag wiring is in `widgets/WidgetsContainer_DragListener` (`Drag.DesktopFolder` reposition; dwell a desktop app over another app/folder folds — app→app creates **at the target's cell** (the stationary app dragged onto), app→folder adds; an **incoming** app — a `Drag.IncomingApp` from the dash/search or off the launcher bar — dwelt over a folder adds straight into it via `addApp(folderId, app)`, the drop ring showing as for any fold, then the launcher origin is cleared like the loose-drop path); `TrashDragListener` deletes a desktop folder (its apps gone). Long-pressing a member in the overlay extracts it (a `DesktopFolderMemberDrag`): dropped on the desktop it lands at the drop cell (`DesktopFolderHost.removeMember` clears/shrinks the folder *before* placing the extracted app, so the still-present 2×2 can't bump it and a dissolved folder's other member — returned near the folder's old cell — can't take the drop spot), dropped on the trash it is deleted (`deleteMember`). - **Profiles (work profile support)**: `Profiles` wraps the profile diff --git a/app/src/main/java/be/robinj/distrohopper/App.java b/app/src/main/java/be/robinj/distrohopper/App.java index f55de9b7..21795245 100644 --- a/app/src/main/java/be/robinj/distrohopper/App.java +++ b/app/src/main/java/be/robinj/distrohopper/App.java @@ -218,6 +218,36 @@ else if (this.user != null) { } } + /** + * Opens the system's App info screen for this app. Apps in another profile + * need LauncherApps (a regular Settings intent would show the personal + * profile's copy); personal-profile apps use the plain details intent so + * this also works for packages without a launcher activity entry. + */ + public void openAppInfo () + { + try { + if (this.user != null) { + final LauncherApps launcherApps = + (LauncherApps) this.context.getSystemService (Context.LAUNCHER_APPS_SERVICE); + launcherApps.startAppDetailsActivity ( + new ComponentName (this.packageName, this.activityName), + this.user, null, null); + } else { + final Intent intent = new Intent ( + android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + android.net.Uri.fromParts ("package", this.packageName, null)); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + this.context.startActivity (intent); + } + } catch (final Exception ex) { + final String errorMessage = format ("Failed to open app info for %s: %s.", + this.packageName, ex.getClass ().getSimpleName ()); + Log.getInstance ().e ("App", errorMessage); + Toast.makeText (this.context, errorMessage, Toast.LENGTH_SHORT).show (); + } + } + // Track app launches for the usage-based dash sort orders, recorded only // after the start call is accepted so a failed launch (e.g. a stale package // entry, a disabled activity, or a locked work profile) can't push a diff --git a/app/src/main/java/be/robinj/distrohopper/HomeActivity.java b/app/src/main/java/be/robinj/distrohopper/HomeActivity.java index ef049e45..427042a7 100644 --- a/app/src/main/java/be/robinj/distrohopper/HomeActivity.java +++ b/app/src/main/java/be/robinj/distrohopper/HomeActivity.java @@ -191,6 +191,7 @@ protected void onCreate (Bundle savedInstanceState) final be.robinj.distrohopper.desktop.launcher.AppLauncher lalBfb = this.viewFinder.get(llBfbSpinnerWrapper, R.id.lalBfb); final be.robinj.distrohopper.desktop.launcher.AppLauncher lalPreferences = this.viewFinder.get(llLauncher, R.id.lalPreferences); final be.robinj.distrohopper.desktop.launcher.AppLauncher lalTrash = this.viewFinder.get(llLauncher, R.id.lalTrash); + final be.robinj.distrohopper.desktop.launcher.AppLauncher lalAppInfo = this.viewFinder.get(llLauncher, R.id.lalAppInfo); this.llDash = this.viewFinder.get(llLauncherAndDashContainer, R.id.llDash); final Wallpaper wpWallpaper = this.viewFinder.get(R.id.wpWallpaper); final LinearLayout llPanel = this.viewFinder.get(R.id.llPanel); @@ -290,6 +291,7 @@ else if (HomeActivity.this.dash.isOpen ()) lalSpinner.init (); lalPreferences.init (); lalTrash.init (); + lalAppInfo.init (); // Process panel user preferences // Themes should probably handle this? // final Resources res = this.getResources (); @@ -360,6 +362,9 @@ public void endTransition(LayoutTransition t, ViewGroup container, // Attached here rather than once apps are loaded: widgets are draggable // (and droppable on the trash) as soon as they are restored below // lalTrash.setOnDragListener (new TrashDragListener (this)); + // Shown in the trash's place while an app icon is dragged from the dash: + // dropping it there opens the system's App info screen for it // + lalAppInfo.setOnDragListener (new be.robinj.distrohopper.desktop.launcher.AppInfoDragListener (this)); // Cross-surface drag: hovering the launcher or panel closes the dash to // reveal the desktop, while hovering either BFB (the launcher's, or the diff --git a/app/src/main/java/be/robinj/distrohopper/desktop/dash/AppLauncherLongClickListener.java b/app/src/main/java/be/robinj/distrohopper/desktop/dash/AppLauncherLongClickListener.java index 383fd3bd..318e2877 100644 --- a/app/src/main/java/be/robinj/distrohopper/desktop/dash/AppLauncherLongClickListener.java +++ b/app/src/main/java/be/robinj/distrohopper/desktop/dash/AppLauncherLongClickListener.java @@ -101,7 +101,13 @@ public static void startAppDrag (View view, App app) // without an active drag would leave the bar stuck, as no // ACTION_DRAG_ENDED will ever restore it // if (source.startDragAndDrop (data, new View.DragShadowBuilder (view), payload, 0)) + { + // A dash app icon: the bar shows the "app info" drop target in + // the trash's place (there is nothing to delete — the dash + // always shows every installed app) // + LauncherBarBinder.dragStartedFromDashApp (); appManager.startedDraggingPinnedApp (app); + } } else { @@ -111,7 +117,10 @@ public static void startAppDrag (View view, App app) ClipData data = ClipData.newPlainText ("dash", "dash"); if (source.startDragAndDrop (data, new View.DragShadowBuilder (view), app, 0)) + { + LauncherBarBinder.dragStartedFromDashApp (); appManager.startedDraggingDashApp (app); + } } } } diff --git a/app/src/main/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListener.java b/app/src/main/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListener.java new file mode 100644 index 00000000..32c1873a --- /dev/null +++ b/app/src/main/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListener.java @@ -0,0 +1,78 @@ +package be.robinj.distrohopper.desktop.launcher; + +import android.graphics.Color; +import android.view.DragEvent; +import android.view.View; +import android.view.ViewGroup; + +import be.robinj.distrohopper.App; +import be.robinj.distrohopper.ExceptionHandler; +import be.robinj.distrohopper.HomeActivity; +import be.robinj.distrohopper.home.LauncherBarBinder; + +/** + * The launcher bar's "app info" drop target, shown in the trash's place while + * an app icon is dragged from the dash (where the trash would have nothing to + * delete — the dash always shows every installed app). Dropping the icon here + * opens the system's App info screen for it. + */ +public class AppInfoDragListener implements ViewGroup.OnDragListener +{ + private final HomeActivity activity; + private int colour = -1; + + public AppInfoDragListener (final HomeActivity activity) + { + this.activity = activity; + } + + @Override + public boolean onDrag (View view, DragEvent event) + { + try + { + AppLauncher lalAppInfo = (AppLauncher) view; + if (this.colour == -1) + this.colour = lalAppInfo.getColour (); + + switch (event.getAction ()) + { + case DragEvent.ACTION_DRAG_ENTERED: + lalAppInfo.setColour (Color.rgb (40, 120, 255)); + break; + case DragEvent.ACTION_DROP: // Falls through // + final App app = draggedApp (event); + if (app != null) + app.openAppInfo (); + + LauncherBarBinder.stoppedDragging (this.activity); + case DragEvent.ACTION_DRAG_EXITED: + lalAppInfo.setColour (this.colour); + break; + } + } + catch (Exception ex) + { + ExceptionHandler exh = new ExceptionHandler (ex); + exh.show (this.activity); + } + + return true; + } + + /** + * The app behind a dash icon drag: a plain {@link App} (not pinned yet) or a + * {@link LauncherDragPayload.PinnedAppDrag} (already on the launcher bar). + * Anything else (folders, widgets, ...) never shows this target; ignore it. + */ + private static App draggedApp (final DragEvent event) + { + final Object localState = event.getLocalState (); + if (localState instanceof App) + return (App) localState; + else if (localState instanceof LauncherDragPayload.PinnedAppDrag) + return ((LauncherDragPayload.PinnedAppDrag) localState).getApp (); + + return null; + } +} diff --git a/app/src/main/java/be/robinj/distrohopper/home/HomeStateBinder.kt b/app/src/main/java/be/robinj/distrohopper/home/HomeStateBinder.kt index 0e72cabf..8a27ddcf 100644 --- a/app/src/main/java/be/robinj/distrohopper/home/HomeStateBinder.kt +++ b/app/src/main/java/be/robinj/distrohopper/home/HomeStateBinder.kt @@ -110,6 +110,7 @@ object HomeStateBinder { (llLauncherRunningApps.getChildAt(i) as AppLauncher).init() viewFinder.get(R.id.lalTrash).init() + viewFinder.get(R.id.lalAppInfo).init() viewFinder.get(R.id.lalPreferences).init() // The slot size changed, so the whole-slot scroll clip must re-measure. diff --git a/app/src/main/java/be/robinj/distrohopper/home/LauncherBarBinder.kt b/app/src/main/java/be/robinj/distrohopper/home/LauncherBarBinder.kt index 905295db..97feb426 100644 --- a/app/src/main/java/be/robinj/distrohopper/home/LauncherBarBinder.kt +++ b/app/src/main/java/be/robinj/distrohopper/home/LauncherBarBinder.kt @@ -752,6 +752,24 @@ class LauncherBarBinder(private val appManager: AppManager) { * with just the activity. */ + /** + * Whether the current drag shows the "app info" target in the trash's + * place. Set (by the dash's long-click listener) when an app icon is + * picked up from the dash — the dash always shows every installed app, + * so the trash would have nothing to delete there; opening the system's + * App info screen is the useful drop instead. Dash *folders* keep the + * trash (dropping one there deletes the folder). Sticky for the drag's + * lifetime — mid-drag [startedDragging] refreshers (e.g. hovering the + * launcher bar) re-apply it — and cleared by [stoppedDragging]. + */ + private var dragShowsAppInfo = false + + /** Marks the drag that is starting as a dash app-icon drag (see [dragShowsAppInfo]). */ + @JvmStatic + fun dragStartedFromDashApp() { + dragShowsAppInfo = true + } + @JvmStatic fun startedDragging(activity: HomeActivity) { val viewFinder = activity.viewFinder @@ -759,6 +777,7 @@ class LauncherBarBinder(private val appManager: AppManager) { val lalBfb = viewFinder.get(llLauncher, R.id.lalBfb) val lalPreferences = viewFinder.get(llLauncher, R.id.lalPreferences) val lalTrash = viewFinder.get(llLauncher, R.id.lalTrash) + val lalAppInfo = viewFinder.get(llLauncher, R.id.lalAppInfo) // The BFB stays visible during the drag: it is the "re-open the dash" // target for the cross-surface drag (hover it to bring the dash back), @@ -768,7 +787,8 @@ class LauncherBarBinder(private val appManager: AppManager) { // the dash to reveal the desktop — see Bfb/Launcher drag listeners. lalBfb.visibility = View.VISIBLE lalPreferences.visibility = View.GONE - lalTrash.visibility = View.VISIBLE + lalTrash.visibility = if (dragShowsAppInfo) View.GONE else View.VISIBLE + lalAppInfo.visibility = if (dragShowsAppInfo) View.VISIBLE else View.GONE viewFinder.get(llLauncher, R.id.llLauncherPinnedApps).alpha = 0.9F @@ -777,11 +797,14 @@ class LauncherBarBinder(private val appManager: AppManager) { @JvmStatic fun stoppedDragging(activity: HomeActivity) { + dragShowsAppInfo = false + val viewFinder = activity.viewFinder val llLauncher = viewFinder.get(R.id.llLauncher) val lalBfb = viewFinder.get(llLauncher, R.id.lalBfb) val lalPreferences = viewFinder.get(llLauncher, R.id.lalPreferences) val lalTrash = viewFinder.get(llLauncher, R.id.lalTrash) + val lalAppInfo = viewFinder.get(llLauncher, R.id.lalAppInfo) val theme = DependencyContainer.of(activity).themeManager.current val lalPreferences_location = theme.lalPreferences_getLocation( @@ -790,6 +813,7 @@ class LauncherBarBinder(private val appManager: AppManager) { lalPreferences.visibility = if (lalPreferences_location == Location.NONE) View.GONE else View.VISIBLE lalTrash.visibility = View.GONE + lalAppInfo.visibility = View.GONE viewFinder.get(llLauncher, R.id.llLauncherPinnedApps).alpha = 1.0F diff --git a/app/src/main/java/be/robinj/distrohopper/home/ThemeApplier.kt b/app/src/main/java/be/robinj/distrohopper/home/ThemeApplier.kt index b8899af4..5ce2d08e 100644 --- a/app/src/main/java/be/robinj/distrohopper/home/ThemeApplier.kt +++ b/app/src/main/java/be/robinj/distrohopper/home/ThemeApplier.kt @@ -40,6 +40,7 @@ class ThemeApplier( val lalBfb = this.viewFinder.get(llBfbSpinnerWrapper, R.id.lalBfb) val lalPreferences = this.viewFinder.get(llLauncher, R.id.lalPreferences) val lalTrash = this.viewFinder.get(llLauncher, R.id.lalTrash) + val lalAppInfo = this.viewFinder.get(llLauncher, R.id.lalAppInfo) val llDash = this.viewFinder.get(R.id.llDash) val llDashCustomise = this.viewFinder.get(llDash, R.id.llDashCustomise) val imgDashBackgroundGradient = this.viewFinder.get(llDash, R.id.imgDashBackgroundGradient) @@ -59,6 +60,7 @@ class ThemeApplier( imgDashBackgroundGradient.setImageResource(this.theme.dash_background_gradient) lalPreferences.setIcon(res.getDrawable(this.theme.launcher_preferences_image, null)) lalTrash.setIcon(res.getDrawable(this.theme.launcher_trash_image, null)) + lalAppInfo.setIcon(res.getDrawable(this.theme.launcher_appinfo_image, null)) val llPanel_layoutParams = llPanel.layoutParams as RelativeLayout.LayoutParams llPanel_layoutParams.height = res.getDimension(this.theme.panel_height).toInt() diff --git a/app/src/main/java/be/robinj/distrohopper/home/WallpaperColourApplier.kt b/app/src/main/java/be/robinj/distrohopper/home/WallpaperColourApplier.kt index 678b64e0..71f38240 100644 --- a/app/src/main/java/be/robinj/distrohopper/home/WallpaperColourApplier.kt +++ b/app/src/main/java/be/robinj/distrohopper/home/WallpaperColourApplier.kt @@ -44,6 +44,7 @@ class WallpaperColourApplier( this.viewFinder.get(R.id.lalPreferences).colour = colour this.viewFinder.get(R.id.lalSpinner).colour = colour this.viewFinder.get(R.id.lalTrash).colour = colour + this.viewFinder.get(R.id.lalAppInfo).colour = colour } val launcherBackgroundResources = res.obtainTypedArray(this.theme.launcher_background) diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Budgie.java b/app/src/main/java/be/robinj/distrohopper/theme/Budgie.java index 29c77d61..45f53f53 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Budgie.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Budgie.java @@ -35,6 +35,7 @@ public Budgie () this.launcher_preferences_image = R.drawable.theme_budgie_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_budgie_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_budgie_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_budgie_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_budgie_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_budgie_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_budgie_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Cinnamon.java b/app/src/main/java/be/robinj/distrohopper/theme/Cinnamon.java index 86bfe770..d76cc41f 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Cinnamon.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Cinnamon.java @@ -35,6 +35,7 @@ public Cinnamon() this.launcher_preferences_image = R.drawable.theme_cinnamon_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_cinnamon_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_cinnamon_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_cinnamon_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_cinnamon_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_cinnamon_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_cinnamon_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Cosmic.java b/app/src/main/java/be/robinj/distrohopper/theme/Cosmic.java index d1e57f79..b2c2b440 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Cosmic.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Cosmic.java @@ -36,6 +36,7 @@ public Cosmic() this.launcher_preferences_image = R.drawable.theme_cosmic_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_cosmic_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_cosmic_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_cosmic_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_cosmic_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_cosmic_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_cosmic_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Default.java b/app/src/main/java/be/robinj/distrohopper/theme/Default.java index 6a0269cd..cbf7388f 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Default.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Default.java @@ -35,6 +35,7 @@ public Default () this.launcher_preferences_image = R.drawable.theme_default_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_default_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_default_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_default_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_default_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_default_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_default_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Elementary.java b/app/src/main/java/be/robinj/distrohopper/theme/Elementary.java index 183da512..aa9418ec 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Elementary.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Elementary.java @@ -35,6 +35,7 @@ public Elementary () this.launcher_preferences_image = R.drawable.theme_elementary_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_elementary_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_elementary_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_elementary_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_elementary_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_elementary_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_elementary_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Gnome.java b/app/src/main/java/be/robinj/distrohopper/theme/Gnome.java index 740338d5..46de65a5 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Gnome.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Gnome.java @@ -35,6 +35,7 @@ public Gnome () this.launcher_preferences_image = R.drawable.theme_gnome_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_gnome_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_gnome_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_gnome_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_gnome_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_gnome_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_gnome_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Mate.java b/app/src/main/java/be/robinj/distrohopper/theme/Mate.java index 884322a7..e13ed758 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Mate.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Mate.java @@ -36,6 +36,7 @@ public Mate() this.launcher_preferences_image = R.drawable.theme_mate_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_mate_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_mate_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_mate_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_mate_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_mate_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_mate_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Plasma.java b/app/src/main/java/be/robinj/distrohopper/theme/Plasma.java index 1412b5e8..ca2b56e4 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Plasma.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Plasma.java @@ -35,6 +35,7 @@ public Plasma() this.launcher_preferences_image = R.drawable.theme_plasma_launcher_preferences_image; this.launcher_preferences_location_when_panel_hidden = R.integer.theme_plasma_launcher_preferences_location_when_panel_hidden; this.launcher_trash_image = R.drawable.theme_plasma_launcher_trash_image; + this.launcher_appinfo_image = R.drawable.theme_plasma_launcher_appinfo_image; this.launcher_applauncher_backgroundcolour_dynamic = R.bool.theme_plasma_launcher_applauncher_backgroundcolour_dynamic; this.launcher_applauncher_backgroundcolour = R.color.theme_plasma_launcher_applauncher_backgroundcolour; this.launcher_applauncher_backgroundcolour_opacity = R.integer.theme_plasma_launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/java/be/robinj/distrohopper/theme/Theme.java b/app/src/main/java/be/robinj/distrohopper/theme/Theme.java index 83d7929d..3dcd7969 100644 --- a/app/src/main/java/be/robinj/distrohopper/theme/Theme.java +++ b/app/src/main/java/be/robinj/distrohopper/theme/Theme.java @@ -63,6 +63,7 @@ public abstract class Theme public int launcher_preferences_image; public int launcher_preferences_location_when_panel_hidden; public int launcher_trash_image; + public int launcher_appinfo_image; public int launcher_applauncher_backgroundcolour_dynamic; public int launcher_applauncher_backgroundcolour; public int launcher_applauncher_backgroundcolour_opacity; diff --git a/app/src/main/res/drawable/launcher_appinfo.xml b/app/src/main/res/drawable/launcher_appinfo.xml new file mode 100644 index 00000000..8d9c94c6 --- /dev/null +++ b/app/src/main/res/drawable/launcher_appinfo.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 9b1a3b73..40f07270 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -219,6 +219,17 @@ custom:applauncher_special="false" custom:icon="@drawable/launcher_trash" /> + + + diff --git a/app/src/main/res/values/theme_budgie.xml b/app/src/main/res/values/theme_budgie.xml index 545bfd0e..bffdf3f3 100644 --- a/app/src/main/res/values/theme_budgie.xml +++ b/app/src/main/res/values/theme_budgie.xml @@ -45,6 +45,7 @@ @drawable/theme_budgie_res_launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_cinnamon.xml b/app/src/main/res/values/theme_cinnamon.xml index 71a98502..ee1c88f9 100644 --- a/app/src/main/res/values/theme_cinnamon.xml +++ b/app/src/main/res/values/theme_cinnamon.xml @@ -41,6 +41,7 @@ @drawable/theme_cinnamon_res_launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_cosmic.xml b/app/src/main/res/values/theme_cosmic.xml index c40ef4da..eb806732 100644 --- a/app/src/main/res/values/theme_cosmic.xml +++ b/app/src/main/res/values/theme_cosmic.xml @@ -42,6 +42,7 @@ @drawable/theme_cosmic_res_launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_default.xml b/app/src/main/res/values/theme_default.xml index 9cbdff1b..0ad7dc2b 100644 --- a/app/src/main/res/values/theme_default.xml +++ b/app/src/main/res/values/theme_default.xml @@ -40,6 +40,7 @@ @drawable/launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo true @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_elementary.xml b/app/src/main/res/values/theme_elementary.xml index 9ed8edca..bd47c8fe 100644 --- a/app/src/main/res/values/theme_elementary.xml +++ b/app/src/main/res/values/theme_elementary.xml @@ -41,6 +41,7 @@ @drawable/theme_elementary_res_launcher_preferences @integer/position_right @drawable/theme_elementary_res_launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_gnome.xml b/app/src/main/res/values/theme_gnome.xml index d8a5f01d..f335a758 100644 --- a/app/src/main/res/values/theme_gnome.xml +++ b/app/src/main/res/values/theme_gnome.xml @@ -42,6 +42,7 @@ @drawable/launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_mate.xml b/app/src/main/res/values/theme_mate.xml index a2a03aa0..e51a4548 100644 --- a/app/src/main/res/values/theme_mate.xml +++ b/app/src/main/res/values/theme_mate.xml @@ -41,6 +41,7 @@ @drawable/theme_mate_res_launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/main/res/values/theme_plasma.xml b/app/src/main/res/values/theme_plasma.xml index d5c3aed5..2b6102aa 100644 --- a/app/src/main/res/values/theme_plasma.xml +++ b/app/src/main/res/values/theme_plasma.xml @@ -41,6 +41,7 @@ @drawable/theme_plasma_res_launcher_preferences @integer/position_none @drawable/launcher_trash + @drawable/launcher_appinfo false @android:color/transparent 204 diff --git a/app/src/test/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListenerTest.kt b/app/src/test/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListenerTest.kt new file mode 100644 index 00000000..30c6a60a --- /dev/null +++ b/app/src/test/java/be/robinj/distrohopper/desktop/launcher/AppInfoDragListenerTest.kt @@ -0,0 +1,99 @@ +package be.robinj.distrohopper.desktop.launcher + +import android.graphics.Color +import android.provider.Settings +import android.view.DragEvent +import android.view.View +import androidx.test.core.app.ActivityScenario +import be.robinj.distrohopper.ActivityTestSupport +import be.robinj.distrohopper.DragEvents +import be.robinj.distrohopper.HomeActivity +import be.robinj.distrohopper.R +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows +import org.robolectric.annotation.LooperMode + +@RunWith(RobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.LEGACY) +class AppInfoDragListenerTest { + private lateinit var scenario: ActivityScenario + + @Before fun setUp() { scenario = ActivityTestSupport.launchHome() } + @After fun tearDown() { scenario.close() } + + @Test fun theAppInfoTargetGetsItsThemedIcon() { + // The icon is applied by ThemeApplier from the theme's launcher_appinfo_image + // (a layout-level custom:icon attribute is dead — see AppLauncher's styleable); + // regression test for the target rendering as a blank tile. + scenario.onActivity { activity -> + val lalAppInfo = activity.findViewById(R.id.lalAppInfo) + + assertNotNull(lalAppInfo.icon) + assertNotNull(lalAppInfo + .findViewById(R.id.imgIcon).drawable) + } + } + + @Test fun enteringTheAppInfoTargetHighlightsItAndExitingRestoresIt() { + scenario.onActivity { activity -> + val lalAppInfo = activity.findViewById(R.id.lalAppInfo) + val originalColour = lalAppInfo.colour + val listener = AppInfoDragListener(activity) + + listener.onDrag(lalAppInfo, DragEvents.obtain(DragEvent.ACTION_DRAG_ENTERED)) + assertEquals(Color.rgb(40, 120, 255), lalAppInfo.colour) + + listener.onDrag(lalAppInfo, DragEvents.obtain(DragEvent.ACTION_DRAG_EXITED)) + assertEquals(originalColour, lalAppInfo.colour) + } + } + + @Test fun droppingADashAppOpensItsAppInfoScreenAndLeavesDragMode() { + scenario.onActivity { activity -> + val app = activity.appManager.findAppsByPackageName("com.example.alpha").first() + val lalAppInfo = activity.findViewById(R.id.lalAppInfo) + val listener = AppInfoDragListener(activity) + + listener.onDrag(lalAppInfo, + DragEvents.obtain(DragEvent.ACTION_DROP, localState = app)) + + val started = Shadows.shadowOf(activity).nextStartedActivity + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, started.action) + assertEquals("package:com.example.alpha", started.dataString) + assertEquals(View.GONE, lalAppInfo.visibility) + } + } + + @Test fun droppingAPinnedDashAppOpensItsAppInfoScreen() { + scenario.onActivity { activity -> + val app = activity.appManager.findAppsByPackageName("com.example.beta").first() + val listener = AppInfoDragListener(activity) + + listener.onDrag(activity.findViewById(R.id.lalAppInfo), + DragEvents.obtain(DragEvent.ACTION_DROP, + localState = LauncherDragPayload.PinnedAppDrag(app))) + + val started = Shadows.shadowOf(activity).nextStartedActivity + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, started.action) + assertEquals("package:com.example.beta", started.dataString) + } + } + + @Test fun droppingSomethingElseOpensNothing() { + scenario.onActivity { activity -> + val listener = AppInfoDragListener(activity) + + listener.onDrag(activity.findViewById(R.id.lalAppInfo), + DragEvents.obtain(DragEvent.ACTION_DROP, localState = Any())) + + assertNull(Shadows.shadowOf(activity).nextStartedActivity) + } + } +} diff --git a/app/src/test/java/be/robinj/distrohopper/home/LauncherBarBinderTest.kt b/app/src/test/java/be/robinj/distrohopper/home/LauncherBarBinderTest.kt index fd3c3517..b4a90f93 100644 --- a/app/src/test/java/be/robinj/distrohopper/home/LauncherBarBinderTest.kt +++ b/app/src/test/java/be/robinj/distrohopper/home/LauncherBarBinderTest.kt @@ -143,6 +143,26 @@ class LauncherBarBinderTest { } } + @Test fun draggingADashAppIconShowsAppInfoInsteadOfTheTrash() { + scenario.onActivity { activity -> + val binder = LauncherBarBinder(activity.appManager) + + LauncherBarBinder.dragStartedFromDashApp() + binder.startedDraggingPinnedApp() + + assertEquals(View.GONE, activity.findViewById(R.id.lalTrash).visibility) + assertEquals(View.VISIBLE, activity.findViewById(R.id.lalAppInfo).visibility) + + // Stopping the drag clears the flag: the next drag shows the trash again. + binder.stoppedDraggingPinnedApp() + assertEquals(View.GONE, activity.findViewById(R.id.lalAppInfo).visibility) + binder.startedDraggingPinnedApp() + assertEquals(View.VISIBLE, activity.findViewById(R.id.lalTrash).visibility) + assertEquals(View.GONE, activity.findViewById(R.id.lalAppInfo).visibility) + binder.stoppedDraggingPinnedApp() + } + } + @Test fun stoppingADragRestoresTheLauncherBar() { scenario.onActivity { activity -> val binder = LauncherBarBinder(activity.appManager)