Skip to content

Commit

Permalink
Search inside a flow
Browse files Browse the repository at this point in the history
Add the capability to search for text with in a flow's objects.
- New menu items / shortcuts for find/next/previous.
- Display a find dialog inspired by the VS Code finder
- Shows total result count and current result index
- Arrows to move back and forth between items in the result set
- Zoom the camera to the currently selected item in the result set
  • Loading branch information
mehaase committed Aug 9, 2023
1 parent c3a34f7 commit 2ae57a0
Show file tree
Hide file tree
Showing 17 changed files with 682 additions and 2 deletions.
3 changes: 3 additions & 0 deletions src/attack_flow_builder/public/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"paste": "Control+V",
"delete": "Backspace",
"duplicate": "Control+D",
"find": "Control+F",
"find_next": "Control+G",
"find_previous": "Control+Shift+G",
"select_all": "Control+A"
},
"layout": {
Expand Down
19 changes: 17 additions & 2 deletions src/attack_flow_builder/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<AppHotkeyBox id="main">
<AppTitleBar id="app-title-bar"/>
<FindDialog ref="findDialog" id="find-dialog" :style="findDialogLayout" />
<div id="app-body" ref="body" :style="gridLayout">
<div class="frame center">
<BlockDiagram id="block-diagram"/>
Expand All @@ -22,7 +23,7 @@ import Configuration from "@/assets/builder.config"
// Dependencies
import { clamp } from "./assets/scripts/BlockDiagram";
import { PointerTracker } from "./assets/scripts/PointerTracker";
import { mapMutations, mapState } from 'vuex';
import { mapGetters, mapMutations, mapState } from 'vuex';
import { LoadFile, LoadSettings } from './store/Commands/AppCommands';
import { defineComponent, markRaw, ref } from 'vue';
// Components
Expand All @@ -31,6 +32,7 @@ import AppHotkeyBox from "@/components/Elements/AppHotkeyBox.vue";
import BlockDiagram from "@/components/Elements/BlockDiagram.vue";
import AppFooterBar from "@/components/Elements/AppFooterBar.vue";
import EditorSidebar from "@/components/Elements/EditorSidebar.vue";
import FindDialog from "@/components/Elements/FindDialog.vue";
const Handle = {
None : 0,
Expand Down Expand Up @@ -81,6 +83,18 @@ export default defineComponent({
return {
gridTemplateColumns: `minmax(0, 1fr) ${ r }px`
}
},
/**
* Compute the location of the find dialog
* @returns
* The current grid layout.
*/
findDialogLayout(): { right: string } {
let r = this.frameSize[Handle.Right] + 25;
return {
right: `${r}px`
}
}
},
Expand Down Expand Up @@ -193,7 +207,8 @@ export default defineComponent({
AppTitleBar,
BlockDiagram,
AppFooterBar,
EditorSidebar
EditorSidebar,
FindDialog
},
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default class Debouncer {
private timer: number | null;
private seconds: number;

/**
* Creates a {@link Debouncer}.
* @param duration
* The number of seconds that the debouncer waits before calling its target function.
*/
constructor(seconds: number) {
this.timer = null;
this.seconds = seconds;
}

/**
* Debounce a function call.
* @param fn
* The function to call. This is only called if not superseded by another call before the debounce
* duration elapses.
*/
public call(fn: TimerHandler) {
if (this.timer !== null) {
clearTimeout(this.timer);
}

this.timer = setTimeout(fn, this.seconds * 1000);
}
}
229 changes: 229 additions & 0 deletions src/attack_flow_builder/src/components/Elements/FindDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<template>
<div class="find-dialog-container" :class="{
hidden:
!isShowingFindDialog
}">
<input type="text" class="query" placeholder="Find" v-model="query" @keyup="runQuery" ref="query">
<div class="results">
<span v-if="totalResults === 0">No results</span>
<span v-if="totalResults !== 0">{{ currentResultIndex + 1 }} of {{ totalResults }}</span>
</div>
<div class="control" @pointerdown="findPrevious()" :class="{
disabled:
totalResults === 0
}">
<UpArrow />
</div>
<div class="control" @pointerdown="findNext()" :class="{
disabled:
totalResults === 0
}">
<DownArrow />
</div>
<div class="control" @pointerdown="hideFindDialog()">
<Close color="#8c8c8c" />
</div>
</div>
</template>

<script lang="ts">
import * as Store from "@/store/StoreTypes";
// Dependencies
import { defineComponent } from "vue";
import { mapGetters, mapMutations, mapState } from "vuex";
import * as App from "@/store/Commands/AppCommands";
import * as Page from "@/store/Commands/PageCommands";
import { FindResult } from "@/store/Finder";
import Debouncer from "@/assets/scripts/BlockDiagram/Utilities/Debouncer";
// Components
import Close from "@/components/Icons/Close.vue";
import DownArrow from "@/components/Icons/DownArrow.vue";
import UpArrow from "@/components/Icons/UpArrow.vue";
export default defineComponent({
name: "FindDialog",
data() {
return {
query: "",
lastQuery: "",
debouncer: new Debouncer(0.4),
totalResults: 0,
currentResultIndex: 0,
currentDiagramObject: null as any,
}
},
computed: {
...mapState("ApplicationStore", {
ctx(state: Store.ApplicationStore): Store.ApplicationStore {
return state;
},
editor(state: Store.ApplicationStore): Store.PageEditor {
return state.activePage;
}
}),
...mapGetters("ApplicationStore", ["isShowingFindDialog", "currentFindResult"]),
},
components: {
Close,
DownArrow,
UpArrow,
},
watch: {
isShowingFindDialog: {
handler(isShowingFindDialog, oldValue) {
if (isShowingFindDialog) {
this.focus();
} else {
this.blur();
}
}
},
currentFindResult: {
handler(newResult, oldResult) {
if (newResult !== null) {
this.currentResultIndex = newResult.index;
this.totalResults = newResult.totalResults;
if (this.currentDiagramObject !== newResult.diagramObject) {
this.currentDiagramObject = newResult.diagramObject;
this.execute(new Page.UnselectDescendants(this.editor.page));
this.execute(new Page.SelectObject(this.currentDiagramObject));
this.execute(new Page.MoveCameraToSelection(this.ctx, this.editor.page))
}
} else {
this.totalResults = 0;
}
}
}
},
methods: {
/**
* Application Store mutations
*/
...mapMutations("ApplicationStore", ["execute"]),
/**
* Focus the input field and highlight existing text
*/
focus() {
const queryInput = this.$refs.query as HTMLInputElement;
queryInput.focus();
queryInput.select();
},
/**
* Blur the input field.
*/
blur() {
const queryInput = this.$refs.query as HTMLInputElement;
queryInput.blur();
},
/**
* Update find results with the current query.
*/
runQuery(event: KeyboardEvent) {
if (event.key === "Escape") {
this.hideFindDialog();
} else if (this.query !== this.lastQuery) {
this.debouncer.call(() => {
this.lastQuery = this.query;
this.ctx.finder.runQuery(this.ctx.activePage, this.query);
});
}
},
/**
* Focus the next item in the result set.
*/
findNext() {
this.execute(new App.MoveToNextFindResult(this.ctx));
},
/**
* Focus the previous item in the result set.
*/
findPrevious() {
this.execute(new App.MoveToPreviousFindResult(this.ctx));
},
/**
* Hide the find dialog.
*/
hideFindDialog() {
this.execute(new App.HideFindDialog(this.ctx));
},
}
});
</script>

<style scoped>
.find-dialog-container {
position: absolute;
top: 31px;
transition: top 0.3s ease-out;
z-index: 1;
overflow: hidden;
display: flex;
align-items: center;
background-color: #242424;
border: solid 1px #303030;
color: #d9d9d9;
padding: 2px 5px;
}
.hidden {
/* visibility: hidden; */
top: -13px;
}
.query {
color: #d9d9d9;
background-color: #2e2e2e;
border: 1px solid #3d3d3d;
border-radius: 2px;
height: 20px;
width: 180px;
margin: 6px 10px 6px 6px;
padding: 2px 5px;
}
.query:focus {
outline: none;
}
.results {
white-space: nowrap;
font-size: 10pt;
margin-right: 1em;
width: 60px;
}
.control {
font-size: 14pt;
/* margin: 0; */
border-radius: 4px;
padding: 2px 5px;
}
.control * {
fill: #8c8c8c;
}
.control.disabled:hover {
background: none;
}
.control.disabled * {
fill: #3d3d3d;
}
.control:hover {
background-color: #383838;
}
.control * {
position: relative;
top: 1px;
}
</style>
20 changes: 20 additions & 0 deletions src/attack_flow_builder/src/components/Icons/Close.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<svg width="16" height="16" viewBox="0 0 384 512" :fill="color"
xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z" />
</svg>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Close",
props: {
color: {
type: String,
default: "#737373"
}
}
});
</script>
20 changes: 20 additions & 0 deletions src/attack_flow_builder/src/components/Icons/DownArrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<svg width="16" height="16" viewBox="0 0 384 512" :fill="color"
xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M169.4 470.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 370.8 224 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 306.7L54.6 265.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z" />
</svg>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DownArrow",
props: {
color: {
type: String,
default: "#737373"
}
}
});
</script>
20 changes: 20 additions & 0 deletions src/attack_flow_builder/src/components/Icons/UpArrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<svg width="16" height="16" viewBox="0 0 384 512" :fill="color"
xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z" />
</svg>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "UpArrow",
props: {
color: {
type: String,
default: "#737373"
}
}
});
</script>
Loading

0 comments on commit 2ae57a0

Please sign in to comment.