Skip to content

Commit

Permalink
feat: drag & drop (#1367)
Browse files Browse the repository at this point in the history
* Started work on drag & drop

* Simplified code

* Added todo

* Simplified code

* Support drag & drop for macOS applications

* Support drag & drop of linux applications

* Fixed test

* Add option to turn on/off drag & drop

* Added better comments
  • Loading branch information
oliverschwendener authored Feb 15, 2025
1 parent 4f99910 commit 5645d8a
Show file tree
Hide file tree
Showing 18 changed files with 81 additions and 6 deletions.
9 changes: 9 additions & 0 deletions src/common/Core/DragAndDrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Represents the information that will be used when dragging and dropping a search result item.
*/
export type DragAndDrop = {
/**
* The source file path that will be used when dragging and dropping the search result item.
*/
filePath: string;
};
6 changes: 6 additions & 0 deletions src/common/Core/SearchResultItem.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DragAndDrop } from "./DragAndDrop";
import type { Image } from "./Image";
import type { SearchResultItemAction } from "./SearchResultItemAction";

Expand Down Expand Up @@ -45,4 +46,9 @@ export type SearchResultItem = {
* Additional actions that can be invoked via the additional action menu.
*/
additionalActions?: SearchResultItemAction[];

/**
* Optional arguments for drag and drop. If given, the search result item will be draggable.
*/
dragAndDrop?: DragAndDrop;
};
1 change: 1 addition & 0 deletions src/common/Core/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./AboutUeli";
export * from "./ContextBridge";
export * from "./createEmptyInstantSearchResult";
export * from "./DragAndDrop";
export * as Extension from "./Extension";
export * from "./ExtensionInfo";
export * from "./FluentIcon";
Expand Down
16 changes: 16 additions & 0 deletions src/main/Core/DragAndDrop/DragAndDropModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DragAndDrop } from "@common/Core";
import type { UeliModuleRegistry } from "@Core/ModuleRegistry";
import { app } from "electron";

export class DragAndDropModule {
public static bootstrap(moduleRegistry: UeliModuleRegistry) {
const ipcMain = moduleRegistry.get("IpcMain");

ipcMain.on("dragStarted", async ({ sender }, { filePath }: DragAndDrop) => {
sender.startDrag({
file: filePath,
icon: await app.getFileIcon(filePath),
});
});
}
}
1 change: 1 addition & 0 deletions src/main/Core/DragAndDrop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./DragAndDropModule";
1 change: 1 addition & 0 deletions src/main/Core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./CommandlineUtility";
export * from "./DateProvider";
export * from "./Dialog";
export * from "./Dock";
export * from "./DragAndDrop";
export * from "./EnvironmentVariableProvider";
export * from "./EventEmitter";
export * from "./EventSubscriber";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class LinuxApplication implements Application {
namespace: "extension[ApplicationSearch]",
},
details: this.filePath,
dragAndDrop: { filePath: this.filePath },
id: this.getId(),
name: this.name,
image: this.image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
createCopyToClipboardAction,
createOpenFileAction,
createShowItemInFileExplorerAction,
type DragAndDrop,
type SearchResultItem,
} from "@common/Core";
import type { Image } from "@common/Core/Image";
Expand All @@ -12,6 +13,7 @@ export class WindowsApplication implements Application {
private readonly name: string,
private readonly filePath: string,
private readonly image: Image,
private readonly dragAndDrop?: DragAndDrop,
) {}

public toSearchResultItem(): SearchResultItem {
Expand All @@ -25,6 +27,7 @@ export class WindowsApplication implements Application {
id: this.getId(),
name: this.name,
image: this.image,
dragAndDrop: this.dragAndDrop,
defaultAction: createOpenFileAction({
filePath: this.filePath,
description: "Open application",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ describe(WindowsApplicationRepository, () => {
it("should return manually installed apps and windows store apps when 'include apps from windows store is enabled'", async () =>
await testGetApplications({
expected: [
new WindowsApplication("App1", "PathToApp1", { url: "PathToIcon1" }),
new WindowsApplication("App2", "PathToApp2", { url: "file://path" }),
new WindowsApplication("App1", "PathToApp1", { url: "PathToIcon1" }, { filePath: "PathToApp1" }),
new WindowsApplication("App2", "PathToApp2", { url: "file://path" }, { filePath: "PathToApp2" }),
new WindowsApplication("WindowsStoreApp1", "shell:AppsFolder\\1234", {
url: "data:image/png;base64,Logo1",
}),
Expand All @@ -100,8 +100,8 @@ describe(WindowsApplicationRepository, () => {
it("should return only manually installed apps and when 'include apps from windows store is disabled'", async () =>
await testGetApplications({
expected: [
new WindowsApplication("App1", "PathToApp1", { url: "PathToIcon1" }),
new WindowsApplication("App2", "PathToApp2", { url: "file://path" }),
new WindowsApplication("App1", "PathToApp1", { url: "PathToIcon1" }, { filePath: "PathToApp1" }),
new WindowsApplication("App2", "PathToApp2", { url: "file://path" }, { filePath: "PathToApp2" }),
],
includeWindowsStoreApps: false,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class WindowsApplicationRepository implements ApplicationRepository {
this.logger.warn(`Failed to generate icon for "${FullName}". Using generic icon instead`);
}

return new WindowsApplication(BaseName, FullName, icon ?? this.getGenericAppIcon());
return new WindowsApplication(BaseName, FullName, icon ?? this.getGenericAppIcon(), { filePath: FullName });
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class MacOsApplication implements Application {
namespace: "extension[ApplicationSearch]",
},
details: this.filePath,
dragAndDrop: { filePath: this.filePath },
id: this.getId(),
name: this.name,
image: this.image,
Expand Down
1 change: 1 addition & 0 deletions src/main/Extensions/FileSearch/FileSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class FileSearch implements Extension {
}),
description: isDirectory ? "Folder" : "File",
details: dirname(filePath),
dragAndDrop: { filePath },
id: `file-search-result:${Buffer.from(filePath).toString("base64")}`,
image: { url: filePathIconMap[filePath] },
name: basename(filePath),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class SimpleFileSearchExtension implements Extension {
description: types[filePath] === "folder" ? t("folder") : t("file"),
details: dirname(filePath),
image: images[filePath] ?? this.getDefaultIcon(),
dragAndDrop: { filePath },
defaultAction: createOpenFileAction({
filePath,
description: types[filePath] === "folder" ? t("openFolder") : t("openFile"),
Expand Down
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ if (!app.requestSingleInstanceLock()) {
Core.DialogModule.bootstrap(moduleRegistry);
Core.TerminalModule.bootstrap(moduleRegistry);
Core.ExtensionRegistryModule.bootstrap(moduleRegistry);
Core.DragAndDropModule.bootstrap(moduleRegistry);

// Extensions
Extensions.ExtensionLoader.bootstrap(moduleRegistry);
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/Core/I18n/getCoreResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
doubleClickBehavior: "Double click behavior",
selectSearchResultItem: "Select search result item",
invokeSearchResultItem: "Invoke search result item",
dragAndDrop: "Drag & Drop",
},
"de-CH": {
title: "Tastatur & Maus",
singleClickBehavior: "Einfachklick-Verhalten",
doubleClickBehavior: "Doppelklick-Verhalten",
selectSearchResultItem: "Suchergebniss anwählen",
invokeSearchResultItem: "Suchergebniss ausführen",
dragAndDrop: "Drag & Drop",
},
},
},
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/Core/Search/SearchResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const SearchResultList = ({
defaultValue: "smooth",
});

const { value: dragAndDropEnabled } = useSetting<boolean>({
key: "keyboardAndMouse.dragAndDropEnabled",
defaultValue: false,
});

return (
<div
style={{
Expand All @@ -44,6 +49,7 @@ export const SearchResultList = ({
onDoubleClick={() => onSearchResultItemDoubleClick(searchResultItem)}
scrollBehavior={scrollBehavior}
layout={layout}
dragAndDropEnabled={dragAndDropEnabled}
/>
))}
</div>
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/Core/Search/SearchResultListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type SearchResultListItemProps = {
layout: SearchResultListLayout;
searchResultItem: SearchResultItem;
scrollBehavior: ScrollBehavior;
dragAndDropEnabled: boolean;
};

export const SearchResultListItem = ({
Expand All @@ -25,6 +26,7 @@ export const SearchResultListItem = ({
searchResultItem,
scrollBehavior,
layout,
dragAndDropEnabled,
}: SearchResultListItemProps) => {
const ref = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState<boolean>(false);
Expand All @@ -50,6 +52,13 @@ export const SearchResultListItem = ({
onDoubleClick={onDoubleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
draggable={dragAndDropEnabled && searchResultItem.dragAndDrop !== undefined}
onDragStart={({ preventDefault }) => {
if (dragAndDropEnabled && searchResultItem.dragAndDrop) {
preventDefault();
window.ContextBridge.ipcRenderer.send("dragStarted", searchResultItem.dragAndDrop);
}
}}
style={{
position: "relative",
backgroundColor: isSelected ? selectedBackgroundColor : isHovered ? hoveredBackgroundColor : undefined,
Expand Down
18 changes: 17 additions & 1 deletion src/renderer/Core/Settings/Pages/KeyboardAndMouse.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSetting } from "@Core/Hooks";
import { Dropdown, Option } from "@fluentui/react-components";
import { Dropdown, Option, Switch } from "@fluentui/react-components";
import { useTranslation } from "react-i18next";
import { Setting } from "../Setting";
import { SettingGroup } from "../SettingGroup";
Expand All @@ -23,6 +23,11 @@ export const KeyboardAndMouse = () => {
invokeSearchResultItem: t("invokeSearchResultItem"),
};

const { value: dragAndDropEnabled, updateValue: setDragAndDropEnabled } = useSetting({
key: "keyboardAndMouse.dragAndDropEnabled",
defaultValue: false,
});

return (
<SettingGroupList>
<SettingGroup title="Mouse">
Expand Down Expand Up @@ -59,6 +64,17 @@ export const KeyboardAndMouse = () => {
}
/>
</SettingGroup>
<SettingGroup title="Other">
<Setting
label={t("dragAndDrop")}
control={
<Switch
checked={dragAndDropEnabled}
onChange={(_, { checked }) => setDragAndDropEnabled(checked)}
/>
}
/>
</SettingGroup>
</SettingGroupList>
);
};

0 comments on commit 5645d8a

Please sign in to comment.