mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 21:28:11 +08:00
Multi-document management (create new document; tab switching) (#210)
* Multi-docs WIP * Change to number * Add new document and switch documents * Remove keybind for previous document. Change keybind for next document. * Switch documents by clicking tabs * Remove keybind for previous document. Change keybind for next document. * multi-docs * Update package-lock.json * Hook up File>New to add new document * Remove console logs and empty lines. Start new documents from 2 instead of 1. * Fix formatting
This commit is contained in:
@@ -73,7 +73,7 @@ const menuEntries: MenuListEntries = [
|
|||||||
ref: undefined,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"] },
|
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], action: async () => (await wasm).new_document() },
|
||||||
{ label: "Open…", shortcut: ["Ctrl", "O"] },
|
{ label: "Open…", shortcut: ["Ctrl", "O"] },
|
||||||
{
|
{
|
||||||
label: "Open Recent",
|
label: "Open Recent",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
|
<div class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
|
||||||
<div class="tab-group">
|
<div class="tab-group">
|
||||||
<div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel">
|
<div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel" @click="handleTabClick(tabIndex)">
|
||||||
<span>{{ tabLabel }}</span>
|
<span>{{ tabLabel }}</span>
|
||||||
<IconButton :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
|
<IconButton :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
|
||||||
</div>
|
</div>
|
||||||
@@ -144,6 +144,8 @@ import IconButton from "../widgets/buttons/IconButton.vue";
|
|||||||
import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue";
|
import PopoverButton, { PopoverButtonIcon } from "../widgets/buttons/PopoverButton.vue";
|
||||||
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
|
import { MenuDirection } from "../widgets/floating-menus/FloatingMenu.vue";
|
||||||
|
|
||||||
|
const wasm = import("../../../wasm/pkg");
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Document,
|
Document,
|
||||||
@@ -153,6 +155,12 @@ export default defineComponent({
|
|||||||
IconButton,
|
IconButton,
|
||||||
PopoverButton,
|
PopoverButton,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
async handleTabClick(tabIndex: number) {
|
||||||
|
const { select_document } = await wasm;
|
||||||
|
select_document(tabIndex);
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
tabMinWidths: { type: Boolean, default: false },
|
tabMinWidths: { type: Boolean, default: false },
|
||||||
tabCloseButtons: { type: Boolean, default: false },
|
tabCloseButtons: { type: Boolean, default: false },
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutRow class="workspace-grid-subdivision">
|
<LayoutRow class="workspace-grid-subdivision">
|
||||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 1597">
|
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 1597">
|
||||||
<Panel
|
<Panel :panelType="'Document'" :tabCloseButtons="true" :tabMinWidths="true" :tabLabels="documents" :tabActiveIndex="activeDocument" />
|
||||||
:panelType="'Document'"
|
|
||||||
:tabCloseButtons="true"
|
|
||||||
:tabMinWidths="true"
|
|
||||||
:tabLabels="['Untitled Document*', 'Document 2', 'Document 3', 'Document 4', 'Document 5', 'Document 6']"
|
|
||||||
:tabActiveIndex="0"
|
|
||||||
/>
|
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="workspace-grid-resize-gutter"></LayoutCol>
|
<LayoutCol class="workspace-grid-resize-gutter"></LayoutCol>
|
||||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 319">
|
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 319">
|
||||||
@@ -52,6 +46,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
import { ResponseType, registerResponseHandler, Response, SetActiveDocument, NewDocument } from "../../response-handler";
|
||||||
import LayoutRow from "../layout/LayoutRow.vue";
|
import LayoutRow from "../layout/LayoutRow.vue";
|
||||||
import LayoutCol from "../layout/LayoutCol.vue";
|
import LayoutCol from "../layout/LayoutCol.vue";
|
||||||
import Panel from "./Panel.vue";
|
import Panel from "./Panel.vue";
|
||||||
@@ -62,5 +57,24 @@ export default defineComponent({
|
|||||||
LayoutCol,
|
LayoutCol,
|
||||||
Panel,
|
Panel,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
registerResponseHandler(ResponseType.NewDocument, (responseData: Response) => {
|
||||||
|
const documentData = responseData as NewDocument;
|
||||||
|
if (documentData) this.documents.push(documentData.document_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
||||||
|
const documentData = responseData as SetActiveDocument;
|
||||||
|
if (documentData) this.activeDocument = documentData.document_index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeDocument: 0,
|
||||||
|
documents: ["Untitled Document"],
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export enum ResponseType {
|
|||||||
ExpandFolder = "ExpandFolder",
|
ExpandFolder = "ExpandFolder",
|
||||||
CollapseFolder = "CollapseFolder",
|
CollapseFolder = "CollapseFolder",
|
||||||
SetActiveTool = "SetActiveTool",
|
SetActiveTool = "SetActiveTool",
|
||||||
|
SetActiveDocument = "SetActiveDocument",
|
||||||
|
NewDocument = "NewDocument",
|
||||||
UpdateWorkingColors = "UpdateWorkingColors",
|
UpdateWorkingColors = "UpdateWorkingColors",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +54,10 @@ function parseResponse(responseType: string, data: any): Response {
|
|||||||
return newExpandFolder(data.ExpandFolder);
|
return newExpandFolder(data.ExpandFolder);
|
||||||
case "SetActiveTool":
|
case "SetActiveTool":
|
||||||
return newSetActiveTool(data.SetActiveTool);
|
return newSetActiveTool(data.SetActiveTool);
|
||||||
|
case "SetActiveDocument":
|
||||||
|
return newSetActiveDocument(data.SetActiveDocument);
|
||||||
|
case "NewDocument":
|
||||||
|
return newNewDocument(data.NewDocument);
|
||||||
case "UpdateCanvas":
|
case "UpdateCanvas":
|
||||||
return newUpdateCanvas(data.UpdateCanvas);
|
return newUpdateCanvas(data.UpdateCanvas);
|
||||||
case "ExportDocument":
|
case "ExportDocument":
|
||||||
@@ -95,6 +101,24 @@ function newSetActiveTool(input: any): SetActiveTool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetActiveDocument {
|
||||||
|
document_index: number;
|
||||||
|
}
|
||||||
|
function newSetActiveDocument(input: any): SetActiveDocument {
|
||||||
|
return {
|
||||||
|
document_index: input.document_index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewDocument {
|
||||||
|
document_name: string;
|
||||||
|
}
|
||||||
|
function newNewDocument(input: any): NewDocument {
|
||||||
|
return {
|
||||||
|
document_name: input.document_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateCanvas {
|
export interface UpdateCanvas {
|
||||||
document: string;
|
document: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ pub fn select_tool(tool: String) -> Result<(), JsValue> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn select_document(document: usize) -> Result<(), JsValue> {
|
||||||
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectDocument(document)).map_err(convert_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn new_document() -> Result<(), JsValue> {
|
||||||
|
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::NewDocument).map_err(convert_error))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it)
|
// TODO: When a mouse button is down that started in the viewport, this should trigger even when the mouse is outside the viewport (or even the browser window if the browser supports it)
|
||||||
/// Mouse movement within the screenspace bounds of the viewport
|
/// Mouse movement within the screenspace bounds of the viewport
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ pub fn translate_key(name: &str) -> Key {
|
|||||||
"backspace" => KeyBackspace,
|
"backspace" => KeyBackspace,
|
||||||
"alt" => KeyAlt,
|
"alt" => KeyAlt,
|
||||||
"escape" => KeyEscape,
|
"escape" => KeyEscape,
|
||||||
|
"tab" => KeyTab,
|
||||||
_ => UnknownKey,
|
_ => UnknownKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ impl Default for Document {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
document: InteralDocument::default(),
|
document: InteralDocument::default(),
|
||||||
name: String::from("Unnamed Document"),
|
name: String::from("Untitled Document"),
|
||||||
|
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Document {
|
||||||
|
pub fn with_name(name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
document: InteralDocument::default(),
|
||||||
|
name,
|
||||||
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
|
layer_data: vec![(vec![], LayerData { selected: false, expanded: true })].into_iter().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ pub enum DocumentMessage {
|
|||||||
ToggleLayerVisibility(Vec<LayerId>),
|
ToggleLayerVisibility(Vec<LayerId>),
|
||||||
ToggleLayerExpansion(Vec<LayerId>),
|
ToggleLayerExpansion(Vec<LayerId>),
|
||||||
SelectDocument(usize),
|
SelectDocument(usize),
|
||||||
|
NewDocument,
|
||||||
|
NextDocument,
|
||||||
|
PrevDocument,
|
||||||
ExportDocument,
|
ExportDocument,
|
||||||
RenderDocument,
|
RenderDocument,
|
||||||
Undo,
|
Undo,
|
||||||
@@ -85,6 +88,51 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
|
|||||||
SelectDocument(id) => {
|
SelectDocument(id) => {
|
||||||
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
|
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
|
||||||
self.active_document = id;
|
self.active_document = id;
|
||||||
|
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::UpdateCanvas {
|
||||||
|
document: self.active_document_mut().document.render_root(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NewDocument => {
|
||||||
|
self.active_document = self.documents.len();
|
||||||
|
let new_document = Document::with_name(format!("Untitled Document {}", self.active_document + 1));
|
||||||
|
self.documents.push(new_document);
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::NewDocument {
|
||||||
|
document_name: self.active_document().name.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::UpdateCanvas {
|
||||||
|
document: self.active_document_mut().document.render_root(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NextDocument => {
|
||||||
|
self.active_document = (self.active_document + 1) % self.documents.len();
|
||||||
|
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::UpdateCanvas {
|
||||||
|
document: self.active_document_mut().document.render_root(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PrevDocument => {
|
||||||
|
self.active_document = (self.active_document + self.documents.len() - 1) % self.documents.len();
|
||||||
|
responses.push_back(FrontendMessage::SetActiveDocument { document_index: self.active_document }.into());
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::UpdateCanvas {
|
||||||
|
document: self.active_document_mut().document.render_root(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ExportDocument => responses.push_back(
|
ExportDocument => responses.push_back(
|
||||||
FrontendMessage::ExportDocument {
|
FrontendMessage::ExportDocument {
|
||||||
@@ -160,9 +208,9 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
fn actions(&self) -> ActionList {
|
fn actions(&self) -> ActionList {
|
||||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||||
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, RenderDocument, ExportDocument)
|
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, RenderDocument, ExportDocument, NewDocument, NextDocument, PrevDocument)
|
||||||
} else {
|
} else {
|
||||||
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument)
|
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, NextDocument, PrevDocument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub enum FrontendMessage {
|
|||||||
CollapseFolder { path: Vec<LayerId> },
|
CollapseFolder { path: Vec<LayerId> },
|
||||||
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
|
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
|
||||||
SetActiveTool { tool_name: String },
|
SetActiveTool { tool_name: String },
|
||||||
|
SetActiveDocument { document_index: usize },
|
||||||
|
NewDocument { document_name: String },
|
||||||
UpdateCanvas { document: String },
|
UpdateCanvas { document: String },
|
||||||
ExportDocument { document: String },
|
ExportDocument { document: String },
|
||||||
EnableTextInput,
|
EnableTextInput,
|
||||||
@@ -38,6 +40,7 @@ impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
|
|||||||
CollapseFolder,
|
CollapseFolder,
|
||||||
ExpandFolder,
|
ExpandFolder,
|
||||||
SetActiveTool,
|
SetActiveTool,
|
||||||
|
NewDocument,
|
||||||
UpdateCanvas,
|
UpdateCanvas,
|
||||||
EnableTextInput,
|
EnableTextInput,
|
||||||
DisableTextInput,
|
DisableTextInput,
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ impl Default for Mapping {
|
|||||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
||||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||||
|
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyShift]},
|
||||||
|
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyShift]},
|
||||||
// Global Actions
|
// Global Actions
|
||||||
entry! {action=GlobalMessage::LogInfo, key_down=Key1},
|
entry! {action=GlobalMessage::LogInfo, key_down=Key1},
|
||||||
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
|
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ pub enum Key {
|
|||||||
KeyBackspace,
|
KeyBackspace,
|
||||||
KeyAlt,
|
KeyAlt,
|
||||||
KeyEscape,
|
KeyEscape,
|
||||||
|
KeyTab,
|
||||||
|
|
||||||
// This has to be the last element in the enum.
|
// This has to be the last element in the enum.
|
||||||
NumKeys,
|
NumKeys,
|
||||||
|
|||||||
Reference in New Issue
Block a user