Skip to content

Commit

Permalink
widgets context menu (#1955)
Browse files Browse the repository at this point in the history
  • Loading branch information
sawka authored Feb 12, 2025
1 parent 4880531 commit 932376d
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 7 deletions.
141 changes: 141 additions & 0 deletions aiprompts/contextmenu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Context Menu Quick Reference

This guide provides a quick overview of how to create and display a context menu using our system.

---

## ContextMenuItem Type

Define each menu item using the `ContextMenuItem` type:

```ts
type ContextMenuItem = {
label?: string;
type?: "separator" | "normal" | "submenu" | "checkbox" | "radio";
role?: string; // Electron role (optional)
click?: () => void; // Callback for item selection (not needed if role is set)
submenu?: ContextMenuItem[]; // For nested menus
checked?: boolean; // For checkbox or radio items
visible?: boolean;
enabled?: boolean;
sublabel?: string;
};
```

---

## Import and Show the Menu

Import the context menu module:

```ts
import { ContextMenuModel } from "@/app/store/contextmenu";
```

To display the context menu, call:

```ts
ContextMenuModel.showContextMenu(menu, event);
```

- **menu**: An array of `ContextMenuItem`.
- **event**: The mouse event that triggered the context menu (typically from an onContextMenu handler).

---

## Basic Example

A simple context menu with a separator:

```ts
const menu: ContextMenuItem[] = [
{
label: "New File",
click: () => {
/* create a new file */
},
},
{
label: "New Folder",
click: () => {
/* create a new folder */
},
},
{ type: "separator" },
{
label: "Rename",
click: () => {
/* rename item */
},
},
];

ContextMenuModel.showContextMenu(menu, e);
```

---

## Example with Submenu and Checkboxes

Toggle settings using a submenu with checkbox items:

```ts
const isClearOnStart = true; // Example setting

const menu: ContextMenuItem[] = [
{
label: "Clear Output On Restart",
submenu: [
{
label: "On",
type: "checkbox",
checked: isClearOnStart,
click: () => {
// Set the config to enable clear on restart
},
},
{
label: "Off",
type: "checkbox",
checked: !isClearOnStart,
click: () => {
// Set the config to disable clear on restart
},
},
],
},
];

ContextMenuModel.showContextMenu(menu, e);
```

---

## Editing a Config File Example

Open a configuration file (e.g., `widgets.json`) in preview mode:

```ts
{
label: "Edit widgets.json",
click: () => {
fireAndForget(async () => {
const path = `${getApi().getConfigDir()}/widgets.json`;
const blockDef: BlockDef = {
meta: { view: "preview", file: path },
};
await createBlock(blockDef, false, true);
});
},
}
```

---

## Summary

- **Menu Definition**: Use the `ContextMenuItem` type.
- **Actions**: Use `click` for actions; use `submenu` for nested options.
- **Separators**: Use `type: "separator"` to group items.
- **Toggles**: Use `type: "checkbox"` or `"radio"` with the `checked` property.
- **Displaying**: Use `ContextMenuModel.showContextMenu(menu, event)` to render the menu.
50 changes: 50 additions & 0 deletions aiprompts/getsetconfigvar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Setting and Reading Config Variables

This document provides a quick reference for updating and reading configuration values in our system.

---

## Setting a Config Variable

To update a configuration, use the `RpcApi.SetConfigCommand` function. The command takes an object with a key/value pair where the key is the config variable and the value is the new setting.

**Example:**

```ts
await RpcApi.SetConfigCommand(TabRpcClient, { "web:defaulturl": url });
```

In this example, `"web:defaulturl"` is the key and `url` is the new value. Use this approach for any config key.

---

## Reading a Config Value

To read a configuration value, retrieve the corresponding atom using `getSettingsKeyAtom` and then use `globalStore.get` to access its current value. getSettingsKeyAtom returns a jotai Atom.

**Example:**

```ts
const configAtom = getSettingsKeyAtom("app:defaultnewblock");
const configValue = globalStore.get(configAtom) ?? "default value";
```

Here, `"app:defaultnewblock"` is the config key and `"default value"` serves as a fallback if the key isn't set.

Inside of a react componet we should not use globalStore, instead we use useSettingsKeyAtom (this is just a jotai useAtomValue call wrapped around the getSettingsKeyAtom call)

```tsx
const configValue = useSettingsKeyAtom("app:defaultnewblock") ?? "default value";
```

---

## Relevant Imports

```ts
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getSettingsKeyAtom, useSettingsKeyAtom, globalStore } from "@/app/store/global";
```

Keep this guide handy for a quick reference when working with configuration values.
64 changes: 57 additions & 7 deletions frontend/app/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
import { ErrorBoundary } from "@/app/element/errorboundary";
import { CenteredDiv } from "@/app/element/quickelems";
import { ModalsRenderer } from "@/app/modals/modalsrenderer";
import { NotificationPopover } from "@/app/notification/notificationpopover";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { TabBar } from "@/app/tab/tabbar";
import { TabContent } from "@/app/tab/tabcontent";
import { atoms, createBlock, isDev } from "@/store/global";
import { isBlank, makeIconClass } from "@/util/util";
import { atoms, createBlock, getApi, isDev } from "@/store/global";
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
import clsx from "clsx";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { NotificationPopover } from "../notification/notificationpopover";

import clsx from "clsx";

const iconRegex = /^[a-z0-9-]+$/;

Expand Down Expand Up @@ -56,12 +58,60 @@ const Widgets = memo(() => {
};
const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true;
const widgets = sortByDisplayOrder(fullConfig?.widgets);

const handleWidgetsBarContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
const menu: ContextMenuItem[] = [
{
label: "Edit widgets.json",
click: () => {
fireAndForget(async () => {
const path = `${getApi().getConfigDir()}/widgets.json`;
const blockDef: BlockDef = {
meta: { view: "preview", file: path },
};
await createBlock(blockDef, false, true);
});
},
},
{
label: "Show Help Widgets",
submenu: [
{
label: "On",
type: "checkbox",
checked: showHelp,
click: () => {
fireAndForget(async () => {
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": true });
});
},
},
{
label: "Off",
type: "checkbox",
checked: !showHelp,
click: () => {
fireAndForget(async () => {
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": false });
});
},
},
],
},
];
ContextMenuModel.showContextMenu(menu, e);
};

return (
<div className="flex flex-col w-12 overflow-hidden py-1 -ml-1 select-none">
<div
className="flex flex-col w-12 overflow-hidden py-1 -ml-1 select-none"
onContextMenu={handleWidgetsBarContextMenu}
>
{widgets?.map((data, idx) => <Widget key={`widget-${idx}`} widget={data} />)}
<div className="flex-grow" />
{showHelp ? (
<>
<div className="flex-grow" />
<Widget key="tips" widget={tipsWidget} />
<Widget key="help" widget={helpWidget} />
</>
Expand Down

0 comments on commit 932376d

Please sign in to comment.