Skip to content

Commit

Permalink
Implement action shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
Exidex committed Mar 26, 2024
1 parent e6ff621 commit db91d65
Show file tree
Hide file tree
Showing 23 changed files with 776 additions and 37 deletions.
14 changes: 14 additions & 0 deletions dev_plugin/gauntlet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ description = """
A reasonably long detail description that tries to tell something usefull
"""

[[entrypoint.actions]]
id = 'testAction1'
description = "test action description 1"
shortcut = { key = 'a', kind = 'main'}

[[entrypoint.actions]]
id = 'testAction2'
description = "test action description 2"
shortcut = { key = 'B', kind = 'main'}

[[entrypoint.preferences]]
name = 'testBool'
Expand Down Expand Up @@ -72,6 +81,11 @@ path = 'src/form-view.tsx'
type = 'view'
description = ''

[[entrypoint.actions]]
id = 'testAction'
description = "test action description in form"
shortcut = { key = ':', kind = 'main'}

[[entrypoint]]
id = 'inline-view'
name = 'Inline view'
Expand Down
6 changes: 4 additions & 2 deletions dev_plugin/src/detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export default function DetailView(): ReactElement {
const { testBool } = usePluginPreferences<{ testBool: boolean }>();
const entrypointPreferences = useEntrypointPreferences<DetailViewEntrypointConfig>();

const PORT = Deno.env.get("RUST_LOG");
console.log("RUST_LOG:", PORT);
const env = Deno.env.get("RUST_LOG");
console.log("RUST_LOG:", env);

const logoData = assetDataSync("logo.png");

Expand All @@ -69,6 +69,7 @@ export default function DetailView(): ReactElement {
}}
/>
<Action
id="testAction1"
title={"action 2.2"}
onAction={() => {
console.log("ActionTest 2.2")
Expand All @@ -77,6 +78,7 @@ export default function DetailView(): ReactElement {
</ActionPanel.Section>
<ActionPanel.Section>
<Action
id="testAction2"
title={"action 3"}
onAction={() => {
console.log("ActionTest 3")
Expand Down
27 changes: 25 additions & 2 deletions dev_plugin/src/form-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactElement, useState } from 'react';
import { Form } from "@project-gauntlet/api/components";
import { Action, ActionPanel, Form } from "@project-gauntlet/api/components";

export default function FormView(): ReactElement {

Expand All @@ -9,7 +9,30 @@ export default function FormView(): ReactElement {
const [selected, setSelected] = useState<string | undefined>("default_selected_item");

return (
<Form>
<Form
actions={
<ActionPanel title={"action panel"}>
<Action
title={"action 1"}
onAction={() => {
console.log("ActionTest Form 1")
}}
/>
<Action
title={"action 2"}
onAction={() => {
console.log("ActionTest Form 2")
}}
/>
<Action
id="testAction"
title={"action 3"}
onAction={() => {
console.log("ActionTest Form 3")
}}
/>
</ActionPanel>
}>
{/* uncontrolled */}
<Form.TextField
label={"Text Field"}
Expand Down
4 changes: 3 additions & 1 deletion js/api/src/gen/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare global {
namespace JSX {
interface IntrinsicElements {
["gauntlet:action"]: {
id?: string;
title: string;
onAction: () => void;
};
Expand Down Expand Up @@ -172,11 +173,12 @@ export type ElementComponent<Comp extends FC<any>> = Element<Comp> | EmptyNode |
export type StringComponent = StringNode | EmptyNode | Iterable<StringComponent>;
export type StringOrElementComponent<Comp extends FC<any>> = StringNode | EmptyNode | Element<Comp> | Iterable<StringOrElementComponent<Comp>>;
export interface ActionProps {
id?: string;
title: string;
onAction: () => void;
}
export const Action: FC<ActionProps> = (props: ActionProps): ReactNode => {
return <gauntlet:action title={props.title} onAction={props.onAction}/>;
return <gauntlet:action id={props.id} title={props.title} onAction={props.onAction}/>;
};
export interface ActionPanelSectionProps {
children?: ElementComponent<typeof Action>;
Expand Down
45 changes: 44 additions & 1 deletion js/core/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ function findWidgetWithId(widget: UiWidget, widgetId: number): UiWidget | undefi
return undefined;
}

function findAllActionHandlers(widget: UiWidget): { id: string, onAction: () => void }[] {
if (widget.widgetType === "gauntlet:action") {
const id = widget.widgetProperties["id"];
const onAction = widget.widgetProperties["onAction"];
if (!!id && !!onAction) {
return [{ id, onAction }]
} else {
return []
}
}

let result: { id: string, onAction: () => void }[] = []
for (let widgetChild of widget.widgetChildren) {
const actionHandler = findAllActionHandlers(widgetChild);

result.push(...actionHandler)
}

return result;
}

function handleEvent(event: ViewEvent) {
InternalApi.op_log_trace("plugin_event_handler", `Handling view event: ${Deno.inspect(event)}`);
InternalApi.op_log_trace("plugin_event_handler", `Root widget: ${Deno.inspect(latestRootUiWidget)}`);
Expand Down Expand Up @@ -67,6 +88,20 @@ function handleEvent(event: ViewEvent) {
}
}

async function handleKeyboardEvent(event: NotReactsKeyboardEvent) {
InternalApi.op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`);
if (latestRootUiWidget) {
const actionHandlers = findAllActionHandlers(latestRootUiWidget);

const id = await InternalApi.fetch_action_id_for_shortcut(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta);

const actionHandler = actionHandlers.find(value => value.id === id);

if (actionHandler) {
actionHandler.onAction()
}
}
}

async function runLoop() {
while (true) {
Expand All @@ -78,7 +113,15 @@ async function runLoop() {
try {
handleEvent(pluginEvent)
} catch (e) {
console.error("Error occurred when receiving event to handle", e)
console.error("Error occurred when receiving view event to handle", e)
}
break;
}
case "KeyboardEvent": {
try {
await handleKeyboardEvent(pluginEvent)
} catch (e) {
console.error("Error occurred when receiving keyboard event to handle", e)
}
break;
}
Expand Down
8 changes: 6 additions & 2 deletions js/react_renderer/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class GauntletContextValue {
return this._navStack[this._navStack.length - 1]
}

entrypointId() {
return this._entrypointId!!
}

rerender = (component: ReactNode) => {
this._rerender!!(component)
};
Expand All @@ -72,7 +76,7 @@ class GauntletContextValue {
};

entrypointPreferences = () => {
return InternalApi.get_entrypoint_preferences(this._entrypointId!!)
return InternalApi.get_entrypoint_preferences(this.entrypointId())
}

pluginPreferences = () => {
Expand Down Expand Up @@ -318,7 +322,7 @@ export const createHostConfig = (): HostConfig<
replaceContainerChildren(container: RootUiWidget, newChildren: ChildSet): void {
InternalApi.op_log_trace("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren)}`)
container.widgetChildren = newChildren
InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), container)
InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), gauntletContextValue.entrypointId(), container)
},

cloneHiddenInstance(
Expand Down
17 changes: 15 additions & 2 deletions js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface DenoCore {
ops: InternalApi
}

type PluginEvent = ViewEvent | RunCommand | RunGeneratedCommand | OpenView | PluginCommand | OpenInlineView | ReloadSearchIndex
type PluginEvent = ViewEvent | NotReactsKeyboardEvent | RunCommand | RunGeneratedCommand | OpenView | PluginCommand | OpenInlineView | ReloadSearchIndex
type RenderLocation = "InlineView" | "View"

type ViewEvent = {
Expand All @@ -16,6 +16,17 @@ type ViewEvent = {
eventArguments: PropertyValue[]
}

// naming to avoid collision
type NotReactsKeyboardEvent = {
type: "KeyboardEvent"
entrypointId: string
key: string
modifierShift: boolean
modifierControl: boolean
modifierAlt: boolean
modifierMeta: boolean
}

type OpenView = {
type: "OpenView"
frontend: string
Expand Down Expand Up @@ -89,7 +100,9 @@ interface InternalApi {

load_search_index(searchItems: AdditionalSearchItem[]): Promise<void>;

op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, container: UiWidget): void;
op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, entrypoint_id: string, container: UiWidget): void;

fetch_action_id_for_shortcut(entrypointId: string, key: string, modifierShift: boolean, modifierControl: boolean, modifierAlt: boolean, modifierMeta: boolean): Promise<string>;
}

// component model types
Expand Down
1 change: 1 addition & 0 deletions rust/client/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub enum NativeUiRequestData {
},
ReplaceView {
plugin_id: PluginId,
entrypoint_id: EntrypointId,
render_location: RenderLocation,
top_level_view: bool,
container: NativeUiWidget,
Expand Down
4 changes: 3 additions & 1 deletion rust/client/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use tonic::{Request, Response, Status};

use common::model::{PluginId, RenderLocation};
use common::model::{EntrypointId, PluginId, RenderLocation};
use common::rpc::{RpcClearInlineViewRequest, RpcClearInlineViewResponse, RpcRenderLocation, RpcReplaceViewRequest, RpcReplaceViewResponse, RpcShowWindowRequest, RpcShowWindowResponse};
use common::rpc::rpc_frontend_server::RpcFrontend;
use utils::channel::RequestSender;
Expand All @@ -16,6 +16,7 @@ impl RpcFrontend for RpcFrontendServerImpl {
async fn replace_view(&self, request: Request<RpcReplaceViewRequest>) -> Result<Response<RpcReplaceViewResponse>, Status> {
let request = request.into_inner();
let plugin_id = request.plugin_id;
let entrypoint_id = request.entrypoint_id;
let container = request.container.ok_or(Status::invalid_argument("container"))?;
let top_level_view = request.top_level_view;
let render_location = request.render_location;
Expand All @@ -33,6 +34,7 @@ impl RpcFrontend for RpcFrontendServerImpl {

let data = NativeUiRequestData::ReplaceView {
plugin_id: PluginId::from_string(plugin_id),
entrypoint_id: EntrypointId::from_string(entrypoint_id),
render_location,
top_level_view,
container
Expand Down
16 changes: 12 additions & 4 deletions rust/client/src/ui/client_context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use common::model::{PluginId, RenderLocation};
use common::model::{EntrypointId, PluginId, RenderLocation};

use crate::model::{NativeUiViewEvent, NativeUiWidget};
use crate::ui::widget::ComponentWidgetEvent;
Expand Down Expand Up @@ -47,10 +47,18 @@ impl ClientContext {
&mut self.view
}

pub fn replace_view(&mut self, render_location: RenderLocation, container: NativeUiWidget, plugin_id: &PluginId) {
pub fn get_view_plugin_id(&self) -> PluginId {
self.view.get_plugin_id()
}

pub fn get_view_entrypoint_id(&self) -> EntrypointId {
self.view.get_entrypoint_id()
}

pub fn replace_view(&mut self, render_location: RenderLocation, container: NativeUiWidget, plugin_id: &PluginId, entrypoint_id: &EntrypointId) {
match render_location {
RenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container),
RenderLocation::View => self.get_mut_view_container().replace_view(container)
RenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container, plugin_id, entrypoint_id),
RenderLocation::View => self.get_mut_view_container().replace_view(container, plugin_id, entrypoint_id)
}
}

Expand Down
57 changes: 52 additions & 5 deletions rust/client/src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::{Arc, RwLock as StdRwLock};
use global_hotkey::{GlobalHotKeyManager, HotKeyState};

use global_hotkey::{GlobalHotKeyManager, HotKeyState};
use iced::{Command, Event, event, executor, font, futures, keyboard, Length, Padding, Settings, Size, Subscription, subscription, window};
use iced::futures::channel::mpsc::Sender;
use iced::futures::SinkExt;
Expand All @@ -18,7 +18,7 @@ use tonic::transport::Server;

use client_context::ClientContext;
use common::model::{EntrypointId, PluginId, PropertyValue, RenderLocation};
use common::rpc::{BackendClient, RpcEntrypointTypeSearchResult, RpcEventRenderView, RpcEventRunCommand, RpcEventRunGeneratedCommand, RpcEventViewEvent, RpcRequestRunCommandRequest, RpcRequestRunGeneratedCommandRequest, RpcRequestViewRenderRequest, RpcSearchRequest, RpcSendViewEventRequest, RpcUiPropertyValue, RpcUiWidgetId};
use common::rpc::{BackendClient, RpcEntrypointTypeSearchResult, RpcEventKeyboardEvent, RpcEventRenderView, RpcEventRunCommand, RpcEventRunGeneratedCommand, RpcEventViewEvent, RpcRequestRunCommandRequest, RpcRequestRunGeneratedCommandRequest, RpcRequestViewRenderRequest, RpcSearchRequest, RpcSendKeyboardEventRequest, RpcSendViewEventRequest, RpcUiPropertyValue, RpcUiWidgetId};
use common::rpc::rpc_backend_client::RpcBackendClient;
use common::rpc::rpc_frontend_server::RpcFrontendServer;
use common::rpc::rpc_ui_property_value::Value;
Expand Down Expand Up @@ -244,12 +244,59 @@ impl Application for AppModel {
}
}
AppMsg::IcedEvent(Event::Keyboard(event)) => {
let mut backend_client = self.backend_client.clone();

match event {
keyboard::Event::KeyPressed { key, .. } => {
keyboard::Event::KeyPressed { key, modifiers, .. } => {
match key {
Key::Named(Named::ArrowUp) => iced::widget::focus_previous(),
Key::Named(Named::ArrowDown) => iced::widget::focus_next(),
Key::Named(Named::Escape) => self.previous_view(),
Key::Character(char) => {
if let Some(_) = self.view_data {
let (plugin_id, entrypoint_id) = {
let client_context = self.client_context.read().expect("lock is poisoned");
(client_context.get_view_plugin_id(), client_context.get_view_entrypoint_id())
};

println!("key pressed: {:?}. shift: {:?} control: {:?} alt: {:?} meta: {:?}", char, modifiers.shift(), modifiers.control(), modifiers.alt(), modifiers.logo());

match char.as_ref() {
// only stuff that is present on 60% keyboard
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "-" | "=" |
"!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" |
"a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" |
"A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" |
"," | "." | "/" | "[" | "]" | ";" | "'" | "\\" |
"<" | ">" | "?" | "{" | "}" | ":" | "\"" | "|" => {
Command::perform(async move {
let event = RpcEventKeyboardEvent {
entrypoint_id: entrypoint_id.to_string(),
key: char.to_string(),
modifier_shift: modifiers.shift(),
modifier_control: modifiers.control(),
modifier_alt: modifiers.alt(),
modifier_meta: modifiers.logo(),
};

let request = RpcSendKeyboardEventRequest {
plugin_id: plugin_id.to_string(),
event: Some(event),
};

backend_client.send_keyboard_event(Request::new(request))
.await
.unwrap();
}, |_| AppMsg::Noop)
}
_ => {
Command::none()
}
}
} else {
Command::none()
}
}
_ => Command::none()
}
}
Expand Down Expand Up @@ -593,8 +640,8 @@ async fn request_loop(
let mut client_context = client_context.write().expect("lock is poisoned");

match request_data {
NativeUiRequestData::ReplaceView { plugin_id, render_location, top_level_view, container } => {
client_context.replace_view(render_location, container, &plugin_id);
NativeUiRequestData::ReplaceView { plugin_id, entrypoint_id, render_location, top_level_view, container } => {
client_context.replace_view(render_location, container, &plugin_id, &entrypoint_id);

app_msg = AppMsg::SetTopLevelView(top_level_view);

Expand Down
Loading

0 comments on commit db91d65

Please sign in to comment.