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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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`, which mirrors the launcher bar's live reorder preview: a dragged loose app or whole folder becomes an empty placeholder cell (its `GridAdapter` hidden via `setHiddenKey`, keyed by `DashItem.stableKey`) and the fold-vs-reorder distinction is **spatial**, like `LauncherDragListener` — over a cell's centre (the 0.3–0.7 horizontal band) rings it (`dash_folder_drop_indicator`) for a folder create/add and freezes the gap, while over a cell's edge / the space between cells opens a reorder gap there (`custom` order only). The other icons flow around the gap as the finger moves: the listener reorders the adapter's items live (a preview over `baseItems`, the model untouched until drop) and animates each cell from its old slot to its new one via a pre-draw translate (`ACTION_DROP` commits the previewed order through `DashLayoutRepository.moveItem`). Folder-member extraction keeps the simpler **pause-to-fold** gesture (dwell to arm a fold, otherwise drop on the dash to pull the member out loose). 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`, which mirrors the launcher bar's live reorder preview: a dragged loose app or whole folder becomes an empty placeholder cell (its `GridAdapter` hidden via `setHiddenKey`, keyed by `DashItem.stableKey`) and the fold-vs-reorder distinction is **spatial**, like `LauncherDragListener` — over a cell's centre (the 0.3–0.7 horizontal band) rings it (`dash_folder_drop_indicator`) for a folder create/add and freezes the gap, while over a cell's edge / the space between cells opens a reorder gap there (`custom` order only). The other icons flow around the gap as the finger moves: the listener reorders the adapter's items live (a preview over `baseItems`, the model untouched until drop) and animates each cell from its old slot to its new one via a pre-draw translate (`ACTION_DROP` commits the previewed order through `DashLayoutRepository.moveItem`). Folder-member extraction keeps the simpler **pause-to-fold** gesture (dwell to arm a fold, otherwise drop on the dash to pull the member out loose). 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`. Starting any dash drag lowers the soft keyboard (`AppLauncherLongClickListener.hideKeyboard`), since the dash search field may have raised it — most relevant when dragging a search result, where the keyboard would otherwise cover the drop targets. When a drag lingers near the grid's top or bottom edge, `desktop/dash/DashEdgeScroller` auto-scrolls it (the platform list drag-scroll pattern: `ACTION_DRAG_LOCATION` only fires while the pointer moves, so a self-reposting Handler tick does the scrolling, accelerating the deeper into the edge zone the finger sits) so an app can be dropped onto a row that isn't currently on screen; the scroll stops on drop / exit / end. `DashGridDragListener` lazily owns one scroller per page grid and drives it from `onDrag`. Because the finger is still while auto-scroll runs, each scroll step also re-resolves the drag at the last pointer position (the row under a stationary finger changes as content scrolls), so a release without further movement acts on the cell now under the finger, not the pre-scroll one — the loose/folder path re-resolves its cached reorder/fold preview, and the member path re-resolves its armed hover/highlight (a member drop recomputes the target cell, but the fold only fires while `armedPosition` still matches, so the arm has to follow the scroll too). `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.
- **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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package be.robinj.distrohopper.desktop.dash;

import android.content.ClipData;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;

import be.robinj.distrohopper.App;
Expand Down Expand Up @@ -55,6 +57,10 @@ else if (tag instanceof DashItem.FolderItem)
*/
private void startFolderDrag (View view, String folderId)
{
// The dash search field may hold the keyboard open; a drag means the user
// is done typing, so get it out of the way of the drop targets. //
hideKeyboard (this.parent);

View source = view.isAttachedToWindow ()
? view
: this.parent.getWindow ().getDecorView ();
Expand All @@ -74,6 +80,11 @@ public static void startAppDrag (View view, App app)
{
AppManager appManager = app.getAppManager ();

// The dash search field may hold the keyboard open (e.g. dragging a
// search result); a drag means the user is done typing, so lower it so
// it doesn't cover the launcher bar or the lower dash rows. //
hideKeyboard (appManager.getContext ());

// The pressed view may have been detached by the time the long press
// fires — lens results are re-rendered as the slower lenses stream
// their results in. A drag can only start from an attached view, so
Expand Down Expand Up @@ -114,4 +125,14 @@ public static void startAppDrag (View view, App app)
appManager.startedDraggingDashApp (app);
}
}

/** Lowers the soft keyboard (raised by the dash search field), if it is up. */
private static void hideKeyboard (final HomeActivity activity)
{
final InputMethodManager imm =
(InputMethodManager) activity.getSystemService (Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.hideSoftInputFromWindow (
activity.getWindow ().getDecorView ().getWindowToken (), 0);
}
}
Loading
Loading