mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 08:48:11 +08:00
Migrate dialogs to Rust and add a New File dialog (#623)
* Migrate coming soon and about dialog to Rust * Migrate confirm close and close all * Migrate dialog error * Improve keyboard navigation throughout UI * Cleanup and fix panic dialog * Reduce css spacing to better match old dialogs * Add new document modal * Fix crash when generating default name * Populate rust about graphite data on startup * Code review changes * Move one more :focus CSS rule into App.vue * Add a dialog message and move dialogs * Split out keyboard input navigation from this branch * Improvements including simplifying panic dialog code Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
7729b6219e
commit
f177f63217
@@ -0,0 +1,45 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Provides metadata about the build environment.
|
||||||
|
///
|
||||||
|
/// This data is viewable in the editor via the [`crate::dialog::AboutGraphite`] dialog.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BuildMetadata {
|
||||||
|
pub release: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub hash: String,
|
||||||
|
pub branch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BuildMetadata {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
release: "unknown".to_string(),
|
||||||
|
timestamp: "unknown".to_string(),
|
||||||
|
hash: "unknown".to_string(),
|
||||||
|
branch: "unknown".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuildMetadata {
|
||||||
|
pub fn release_series(&self) -> String {
|
||||||
|
format!("Release Series: {}", self.release)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_info(&self) -> String {
|
||||||
|
format!("{}\n{}\n{}", self.commit_timestamp(), self.commit_hash(), self.commit_branch())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_timestamp(&self) -> String {
|
||||||
|
format!("Date: {}", self.timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_hash(&self) -> String {
|
||||||
|
format!("Hash: {}", self.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_branch(&self) -> String {
|
||||||
|
format!("Branch: {}", self.branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,20 @@ use crate::viewport_tools::tool_message_handler::ToolMessageHandler;
|
|||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use super::BuildMetadata;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Dispatcher {
|
pub struct Dispatcher {
|
||||||
message_queue: VecDeque<Message>,
|
message_queue: VecDeque<Message>,
|
||||||
pub responses: Vec<FrontendMessage>,
|
pub responses: Vec<FrontendMessage>,
|
||||||
message_handlers: DispatcherMessageHandlers,
|
message_handlers: DispatcherMessageHandlers,
|
||||||
|
build_metadata: BuildMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct DispatcherMessageHandlers {
|
struct DispatcherMessageHandlers {
|
||||||
|
dialog_message_handler: DialogMessageHandler,
|
||||||
global_message_handler: GlobalMessageHandler,
|
global_message_handler: GlobalMessageHandler,
|
||||||
input_mapper_message_handler: InputMapperMessageHandler,
|
input_mapper_message_handler: InputMapperMessageHandler,
|
||||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||||
@@ -31,6 +35,9 @@ struct DispatcherMessageHandlers {
|
|||||||
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Rerender))),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Rerender))),
|
||||||
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Artboard(
|
||||||
|
ArtboardMessageDiscriminant::RenderArtboards,
|
||||||
|
))),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayer),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayDocumentLayerTreeStructure),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::DisplayDocumentLayerTreeStructure),
|
||||||
@@ -63,6 +70,11 @@ impl Dispatcher {
|
|||||||
match message {
|
match message {
|
||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
NoOp => {}
|
NoOp => {}
|
||||||
|
Dialog(message) => {
|
||||||
|
self.message_handlers
|
||||||
|
.dialog_message_handler
|
||||||
|
.process_action(message, (&self.build_metadata, &self.message_handlers.portfolio_message_handler), &mut self.message_queue);
|
||||||
|
}
|
||||||
Frontend(message) => {
|
Frontend(message) => {
|
||||||
// Image and font loading should be immediately handled
|
// Image and font loading should be immediately handled
|
||||||
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message {
|
if let FrontendMessage::UpdateImageData { .. } | FrontendMessage::TriggerFontLoad { .. } = message {
|
||||||
@@ -101,6 +113,9 @@ impl Dispatcher {
|
|||||||
&mut self.message_queue,
|
&mut self.message_queue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[remain::unsorted]
|
||||||
|
PopulateBuildMetadata { new } => self.build_metadata = new,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +123,7 @@ impl Dispatcher {
|
|||||||
pub fn collect_actions(&self) -> ActionList {
|
pub fn collect_actions(&self) -> ActionList {
|
||||||
// TODO: Reduce the number of heap allocations
|
// TODO: Reduce the number of heap allocations
|
||||||
let mut list = Vec::new();
|
let mut list = Vec::new();
|
||||||
|
list.extend(self.message_handlers.dialog_message_handler.actions());
|
||||||
list.extend(self.message_handlers.input_preprocessor_message_handler.actions());
|
list.extend(self.message_handlers.input_preprocessor_message_handler.actions());
|
||||||
list.extend(self.message_handlers.input_mapper_message_handler.actions());
|
list.extend(self.message_handlers.input_mapper_message_handler.actions());
|
||||||
list.extend(self.message_handlers.global_message_handler.actions());
|
list.extend(self.message_handlers.global_message_handler.actions());
|
||||||
@@ -434,18 +450,22 @@ mod test {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for response in responses {
|
for response in responses {
|
||||||
if let FrontendMessage::DisplayDialogError { title, description } = response {
|
if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response {
|
||||||
println!();
|
if let crate::layout::widgets::LayoutRow::Row { widgets } = &layout[0] {
|
||||||
println!("-------------------------------------------------");
|
if let crate::layout::widgets::Widget::TextLabel(crate::layout::widgets::TextLabel { value, .. }) = &widgets[0].widget {
|
||||||
println!("Failed test due to receiving a DisplayDialogError while loading the graphite sample file!");
|
println!();
|
||||||
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
println!("-------------------------------------------------");
|
||||||
println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file");
|
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
|
||||||
println!("DisplayDialogError details:");
|
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
||||||
println!("Title: {}", title);
|
println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file.");
|
||||||
println!("description: {}", description);
|
println!("DisplayDialogError details:");
|
||||||
println!("-------------------------------------------------");
|
println!();
|
||||||
println!();
|
println!("Description: {}", value);
|
||||||
panic!()
|
println!("-------------------------------------------------");
|
||||||
|
println!();
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::BuildMetadata;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
use graphite_proc_macros::*;
|
use graphite_proc_macros::*;
|
||||||
@@ -23,6 +24,8 @@ pub enum Message {
|
|||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
NoOp,
|
NoOp,
|
||||||
#[child]
|
#[child]
|
||||||
|
Dialog(DialogMessage),
|
||||||
|
#[child]
|
||||||
Frontend(FrontendMessage),
|
Frontend(FrontendMessage),
|
||||||
#[child]
|
#[child]
|
||||||
Global(GlobalMessage),
|
Global(GlobalMessage),
|
||||||
@@ -36,6 +39,9 @@ pub enum Message {
|
|||||||
Portfolio(PortfolioMessage),
|
Portfolio(PortfolioMessage),
|
||||||
#[child]
|
#[child]
|
||||||
Tool(ToolMessage),
|
Tool(ToolMessage),
|
||||||
|
|
||||||
|
#[remain::unsorted]
|
||||||
|
PopulateBuildMetadata { new: BuildMetadata },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
mod build_metadata;
|
||||||
pub mod dispatcher;
|
pub mod dispatcher;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod message_handler;
|
pub mod message_handler;
|
||||||
|
|
||||||
pub use crate::communication::dispatcher::*;
|
pub use crate::communication::dispatcher::*;
|
||||||
pub use crate::input::InputPreprocessorMessageHandler;
|
pub use crate::input::InputPreprocessorMessageHandler;
|
||||||
|
pub use build_metadata::BuildMetadata;
|
||||||
|
|
||||||
use rand_chacha::rand_core::{RngCore, SeedableRng};
|
use rand_chacha::rand_core::{RngCore, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use crate::message_prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::NewDocumentDialogUpdate;
|
||||||
|
|
||||||
|
#[remain::sorted]
|
||||||
|
#[impl_message(Message, Dialog)]
|
||||||
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum DialogMessage {
|
||||||
|
#[remain::unsorted]
|
||||||
|
#[child]
|
||||||
|
NewDocumentDialog(NewDocumentDialogUpdate),
|
||||||
|
|
||||||
|
CloseAllDocumentsWithConfirmation,
|
||||||
|
CloseDialogAndThen {
|
||||||
|
followup: Box<Message>,
|
||||||
|
},
|
||||||
|
DisplayDialogError {
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
RequestAboutGraphiteDialog,
|
||||||
|
RequestComingSoonDialog {
|
||||||
|
issue: Option<i32>,
|
||||||
|
},
|
||||||
|
RequestNewDocumentDialog,
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use crate::communication::BuildMetadata;
|
||||||
|
use crate::document::PortfolioMessageHandler;
|
||||||
|
use crate::layout::{layout_message::LayoutTarget, widgets::PropertyHolder};
|
||||||
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DialogMessageHandler {
|
||||||
|
new_document_dialog: NewDocument,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageHandler<DialogMessage, (&BuildMetadata, &PortfolioMessageHandler)> for DialogMessageHandler {
|
||||||
|
#[remain::check]
|
||||||
|
fn process_action(&mut self, message: DialogMessage, (build_metadata, portfolio): (&BuildMetadata, &PortfolioMessageHandler), responses: &mut VecDeque<Message>) {
|
||||||
|
#[remain::sorted]
|
||||||
|
match message {
|
||||||
|
#[remain::unsorted]
|
||||||
|
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_action(message, (), responses),
|
||||||
|
|
||||||
|
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||||
|
let dialog = dialogs::CloseAllDocuments;
|
||||||
|
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "Copy".to_string() }.into());
|
||||||
|
}
|
||||||
|
DialogMessage::CloseDialogAndThen { followup } => {
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialogDismiss.into());
|
||||||
|
responses.push_back(*followup);
|
||||||
|
}
|
||||||
|
DialogMessage::DisplayDialogError { title, description } => {
|
||||||
|
let dialog = dialogs::Error { title, description };
|
||||||
|
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||||
|
}
|
||||||
|
DialogMessage::RequestAboutGraphiteDialog => {
|
||||||
|
let about_graphite = AboutGraphite {
|
||||||
|
build_metadata: build_metadata.clone(),
|
||||||
|
};
|
||||||
|
about_graphite.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() }.into());
|
||||||
|
}
|
||||||
|
DialogMessage::RequestComingSoonDialog { issue } => {
|
||||||
|
let coming_soon = ComingSoon { issue };
|
||||||
|
coming_soon.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||||
|
}
|
||||||
|
DialogMessage::RequestNewDocumentDialog => {
|
||||||
|
self.new_document_dialog = NewDocument {
|
||||||
|
name: portfolio.generate_new_document_name(),
|
||||||
|
infinite: true,
|
||||||
|
dimensions: glam::UVec2::new(1920, 1080),
|
||||||
|
};
|
||||||
|
self.new_document_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,CloseAllDocumentsWithConfirmation);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::{communication::BuildMetadata, layout::widgets::*, message_prelude::FrontendMessage};
|
||||||
|
|
||||||
|
/// A dialog for displaying information on [`BuildMetadata`] viewable via `help -> about graphite` in the menu bar.
|
||||||
|
pub struct AboutGraphite {
|
||||||
|
pub build_metadata: BuildMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for AboutGraphite {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let links = [
|
||||||
|
("Website", "https://graphite.rs"),
|
||||||
|
("Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"),
|
||||||
|
("License", "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt"),
|
||||||
|
("Third-Party Licenses", "/third-party-licenses.txt"),
|
||||||
|
];
|
||||||
|
let link_widgets = links
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, url)| {
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: label.to_string(),
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Graphite".to_string(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: self.build_metadata.release_series(),
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: self.build_metadata.commit_info(),
|
||||||
|
multiline: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row { widgets: link_widgets },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::layout::widgets::*;
|
||||||
|
use crate::message_prelude::{DialogMessage, FrontendMessage, PortfolioMessage};
|
||||||
|
|
||||||
|
/// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar.
|
||||||
|
pub struct CloseAllDocuments;
|
||||||
|
|
||||||
|
impl PropertyHolder for CloseAllDocuments {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let button_widgets = vec![
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Discard All".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| {
|
||||||
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
followup: Box::new(PortfolioMessage::CloseAllDocuments.into()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Cancel".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Close all documents?".to_string(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Unsaved work will be lost!".to_string(),
|
||||||
|
multiline: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row { widgets: button_widgets },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::layout::widgets::*;
|
||||||
|
use crate::message_prelude::{DialogMessage, DocumentMessage, FrontendMessage, PortfolioMessage};
|
||||||
|
|
||||||
|
/// A dialog for confirming the closing a document with unsaved changes.
|
||||||
|
pub struct CloseDocument {
|
||||||
|
pub document_name: String,
|
||||||
|
pub document_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for CloseDocument {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let document_id = self.document_id;
|
||||||
|
|
||||||
|
let button_widgets = vec![
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Save".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
emphasized: true,
|
||||||
|
on_update: WidgetCallback::new(|_| {
|
||||||
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
followup: Box::new(DocumentMessage::SaveDocument.into()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Discard".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(move |_| {
|
||||||
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
followup: Box::new(PortfolioMessage::CloseDocument { document_id }.into()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Cancel".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Save changes before closing?".to_string(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: self.document_name.clone(),
|
||||||
|
multiline: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row { widgets: button_widgets },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
|
||||||
|
|
||||||
|
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
|
||||||
|
pub struct ComingSoon {
|
||||||
|
pub issue: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for ComingSoon {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let mut details = "This feature is not implemented yet".to_string();
|
||||||
|
let mut buttons = vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "OK".to_string(),
|
||||||
|
emphasized: true,
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||||
|
..Default::default()
|
||||||
|
}))];
|
||||||
|
if let Some(issue) = self.issue {
|
||||||
|
details += &format!("— but you can help add it!\nSee issue #{issue} on GitHub.");
|
||||||
|
buttons.push(WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: format!("Issue #{issue}"),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(move |_| {
|
||||||
|
FrontendMessage::TriggerVisitLink {
|
||||||
|
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Coming soon".to_string(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: details,
|
||||||
|
multiline: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row { widgets: buttons },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
|
||||||
|
|
||||||
|
/// A dialog to notify users of a non-fatal error.
|
||||||
|
pub struct Error {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for Error {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: self.title.clone(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: self.description.clone(),
|
||||||
|
multiline: true,
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
widgets: vec![WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "OK".to_string(),
|
||||||
|
emphasized: true,
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||||
|
..Default::default()
|
||||||
|
}))],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mod about_dialog;
|
||||||
|
mod close_all_documents_dialog;
|
||||||
|
mod close_document_dialog;
|
||||||
|
mod coming_soon_dialog;
|
||||||
|
mod error_dialog;
|
||||||
|
mod new_document_dialog;
|
||||||
|
|
||||||
|
pub use about_dialog::AboutGraphite;
|
||||||
|
pub use close_all_documents_dialog::CloseAllDocuments;
|
||||||
|
pub use close_document_dialog::CloseDocument;
|
||||||
|
pub use coming_soon_dialog::ComingSoon;
|
||||||
|
pub use error_dialog::Error;
|
||||||
|
pub use new_document_dialog::{NewDocument, NewDocumentDialogUpdate, NewDocumentDialogUpdateDiscriminant};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
|
use crate::layout::widgets::*;
|
||||||
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use glam::UVec2;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A dialog to allow users to set some initial options about a new document.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct NewDocument {
|
||||||
|
pub name: String,
|
||||||
|
pub infinite: bool,
|
||||||
|
pub dimensions: UVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyHolder for NewDocument {
|
||||||
|
fn properties(&self) -> WidgetLayout {
|
||||||
|
let title = vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "New document".into(),
|
||||||
|
bold: true,
|
||||||
|
..Default::default()
|
||||||
|
}))];
|
||||||
|
|
||||||
|
let name = vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Name".into(),
|
||||||
|
table_align: true,
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
|
value: self.name.clone(),
|
||||||
|
on_update: WidgetCallback::new(|text_input: &TextInput| NewDocumentDialogUpdate::Name(text_input.value.clone()).into()),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
let infinite = vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Infinite Canvas".into(),
|
||||||
|
table_align: true,
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||||
|
checked: self.infinite,
|
||||||
|
icon: "Checkmark".to_string(),
|
||||||
|
on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| NewDocumentDialogUpdate::Infinite(checkbox_input.checked).into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
let scale = vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Dimensions".into(),
|
||||||
|
table_align: true,
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Unrelated,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
|
value: self.dimensions.x as f64,
|
||||||
|
label: "W".into(),
|
||||||
|
unit: " px".into(),
|
||||||
|
disabled: self.infinite,
|
||||||
|
is_integer: true,
|
||||||
|
min: Some(0.),
|
||||||
|
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsX(number_input.value).into()),
|
||||||
|
..NumberInput::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
|
value: self.dimensions.y as f64,
|
||||||
|
label: "H".into(),
|
||||||
|
unit: " px".into(),
|
||||||
|
disabled: self.infinite,
|
||||||
|
is_integer: true,
|
||||||
|
min: Some(0.),
|
||||||
|
on_update: WidgetCallback::new(|number_input: &NumberInput| NewDocumentDialogUpdate::DimensionsY(number_input.value).into()),
|
||||||
|
..NumberInput::default()
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
let button_widgets = vec![
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "OK".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
emphasized: true,
|
||||||
|
on_update: WidgetCallback::new(|_| {
|
||||||
|
DialogMessage::CloseDialogAndThen {
|
||||||
|
followup: Box::new(NewDocumentDialogUpdate::Submit.into()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||||
|
label: "Cancel".to_string(),
|
||||||
|
min_width: 96,
|
||||||
|
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
WidgetLayout::new(vec![
|
||||||
|
LayoutRow::Row { widgets: title },
|
||||||
|
LayoutRow::Row { widgets: name },
|
||||||
|
LayoutRow::Row { widgets: infinite },
|
||||||
|
LayoutRow::Row { widgets: scale },
|
||||||
|
LayoutRow::Row { widgets: button_widgets },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[impl_message(Message, DialogMessage, NewDocumentDialog)]
|
||||||
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum NewDocumentDialogUpdate {
|
||||||
|
Name(String),
|
||||||
|
Infinite(bool),
|
||||||
|
DimensionsX(f64),
|
||||||
|
DimensionsY(f64),
|
||||||
|
|
||||||
|
Submit,
|
||||||
|
BufferArtboard,
|
||||||
|
AddArtboard,
|
||||||
|
FitCanvas,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageHandler<NewDocumentDialogUpdate, ()> for NewDocument {
|
||||||
|
fn process_action(&mut self, action: NewDocumentDialogUpdate, _data: (), responses: &mut VecDeque<Message>) {
|
||||||
|
match action {
|
||||||
|
NewDocumentDialogUpdate::Name(name) => self.name = name,
|
||||||
|
NewDocumentDialogUpdate::Infinite(infinite) => self.infinite = infinite,
|
||||||
|
NewDocumentDialogUpdate::DimensionsX(x) => self.dimensions.x = x as u32,
|
||||||
|
NewDocumentDialogUpdate::DimensionsY(y) => self.dimensions.y = y as u32,
|
||||||
|
|
||||||
|
NewDocumentDialogUpdate::Submit => {
|
||||||
|
responses.push_back(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }.into());
|
||||||
|
|
||||||
|
responses.push_back(NewDocumentDialogUpdate::BufferArtboard.into());
|
||||||
|
}
|
||||||
|
NewDocumentDialogUpdate::BufferArtboard => {
|
||||||
|
if !self.infinite {
|
||||||
|
responses.push_back(NewDocumentDialogUpdate::AddArtboard.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NewDocumentDialogUpdate::AddArtboard => {
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::AddArtboard {
|
||||||
|
id: None,
|
||||||
|
position: (0., 0.),
|
||||||
|
size: (self.dimensions.x as f64, self.dimensions.y as f64),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
responses.push_back(NewDocumentDialogUpdate::FitCanvas.into());
|
||||||
|
}
|
||||||
|
NewDocumentDialogUpdate::FitCanvas => {
|
||||||
|
responses.push_back(DocumentMessage::ZoomCanvasToFitAll.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_actions! {NewDocumentDialogUpdate;}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! Handles dialogs/modals/popups that appear as boxes in the centre of the editor.
|
||||||
|
//!
|
||||||
|
//! Dialogs are represented as structs that implement the [`crate::layout::widgets::PropertyHolder`] trait.
|
||||||
|
//!
|
||||||
|
//! To open a dialog, call the function `register_properties` on the dialog struct with `responses` and the `LayoutTarget::DialogDetails`
|
||||||
|
//! and then you can open the dialog with [`crate::message_prelude::FrontendMessage::DisplayDialog`]
|
||||||
|
|
||||||
|
mod dialog_message;
|
||||||
|
mod dialog_message_handler;
|
||||||
|
mod dialogs;
|
||||||
|
|
||||||
|
pub mod messages {
|
||||||
|
pub use super::dialog_message::{DialogMessage, DialogMessageDiscriminant};
|
||||||
|
pub use super::dialog_message_handler::DialogMessageHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use dialogs::*;
|
||||||
@@ -509,7 +509,7 @@ impl DocumentMessageHandler {
|
|||||||
|
|
||||||
pub fn load_default_font(&self, responses: &mut VecDeque<Message>) {
|
pub fn load_default_font(&self, responses: &mut VecDeque<Message>) {
|
||||||
if !self.graphene_document.font_cache.has_default() {
|
if !self.graphene_document.font_cache.has_default() {
|
||||||
responses.push_back(FrontendMessage::TriggerDefaultFontLoad.into())
|
responses.push_back(FrontendMessage::TriggerFontLoadDefault.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,7 +536,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||||||
checked: true,
|
checked: true,
|
||||||
icon: "Grid".into(),
|
icon: "Grid".into(),
|
||||||
tooltip: "Grid".into(),
|
tooltip: "Grid".into(),
|
||||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(318) }.into()),
|
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()),
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||||
title: "Grid".into(),
|
title: "Grid".into(),
|
||||||
@@ -581,7 +581,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||||||
value: "pixels".into(),
|
value: "pixels".into(),
|
||||||
icon: "ViewModePixels".into(),
|
icon: "ViewModePixels".into(),
|
||||||
tooltip: "View Mode: Pixels".into(),
|
tooltip: "View Mode: Pixels".into(),
|
||||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(320) }.into()),
|
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()),
|
||||||
..RadioEntryData::default()
|
..RadioEntryData::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ pub enum PortfolioMessage {
|
|||||||
},
|
},
|
||||||
CloseActiveDocumentWithConfirmation,
|
CloseActiveDocumentWithConfirmation,
|
||||||
CloseAllDocuments,
|
CloseAllDocuments,
|
||||||
CloseAllDocumentsWithConfirmation,
|
|
||||||
CloseDocument {
|
CloseDocument {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
},
|
},
|
||||||
@@ -35,6 +34,9 @@ pub enum PortfolioMessage {
|
|||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
},
|
},
|
||||||
NewDocument,
|
NewDocument,
|
||||||
|
NewDocumentWithName {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
NextDocument,
|
NextDocument,
|
||||||
OpenDocument,
|
OpenDocument,
|
||||||
OpenDocumentFile {
|
OpenDocumentFile {
|
||||||
@@ -59,11 +61,10 @@ pub enum PortfolioMessage {
|
|||||||
data: String,
|
data: String,
|
||||||
},
|
},
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
RequestAboutGraphiteDialog,
|
|
||||||
SelectDocument {
|
SelectDocument {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
},
|
},
|
||||||
SetActiveDcoument {
|
SetActiveDocument {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
},
|
},
|
||||||
UpdateDocumentBar,
|
UpdateDocumentBar,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::frontend::utility_types::FrontendDocumentDetails;
|
|||||||
use crate::input::InputPreprocessorMessageHandler;
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
use crate::layout::layout_message::LayoutTarget;
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
use crate::layout::widgets::PropertyHolder;
|
use crate::layout::widgets::PropertyHolder;
|
||||||
use crate::message_prelude::*;
|
use crate::{dialog, message_prelude::*};
|
||||||
|
|
||||||
use graphene::Operation as DocumentOperation;
|
use graphene::Operation as DocumentOperation;
|
||||||
|
|
||||||
@@ -29,14 +29,13 @@ impl PortfolioMessageHandler {
|
|||||||
self.documents.get_mut(&self.active_document_id).unwrap()
|
self.documents.get_mut(&self.active_document_id).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_new_document_name(&self) -> String {
|
pub fn generate_new_document_name(&self) -> String {
|
||||||
let mut doc_title_numbers = self
|
let mut doc_title_numbers = self
|
||||||
.ordered_document_iterator()
|
.ordered_document_iterator()
|
||||||
.map(|doc| {
|
.filter_map(|doc| {
|
||||||
doc.name
|
doc.name
|
||||||
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
||||||
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
|
.map(|(prefix, number)| (prefix.is_empty()).then(|| number.trim().parse::<isize>().ok()).flatten().unwrap_or(1))
|
||||||
.unwrap()
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<isize>>();
|
.collect::<Vec<isize>>();
|
||||||
|
|
||||||
@@ -168,9 +167,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
// Create a new blank document
|
// Create a new blank document
|
||||||
responses.push_back(NewDocument.into());
|
responses.push_back(NewDocument.into());
|
||||||
}
|
}
|
||||||
CloseAllDocumentsWithConfirmation => {
|
|
||||||
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
|
|
||||||
}
|
|
||||||
CloseDocument { document_id } => {
|
CloseDocument { document_id } => {
|
||||||
let document_index = self.document_index(document_id);
|
let document_index = self.document_index(document_id);
|
||||||
self.documents.remove(&document_id);
|
self.documents.remove(&document_id);
|
||||||
@@ -222,7 +218,13 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||||
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
|
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
|
||||||
} else {
|
} else {
|
||||||
responses.push_back(FrontendMessage::DisplayConfirmationToCloseDocument { document_id }.into());
|
let dialog = dialog::CloseDocument {
|
||||||
|
document_name: target_document.name.clone(),
|
||||||
|
document_id,
|
||||||
|
};
|
||||||
|
dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||||
|
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||||
|
|
||||||
// Select the document being closed
|
// Select the document being closed
|
||||||
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
|
responses.push_back(PortfolioMessage::SelectDocument { document_id }.into());
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,12 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||||
self.load_document(new_document, document_id, false, responses);
|
self.load_document(new_document, document_id, false, responses);
|
||||||
}
|
}
|
||||||
|
NewDocumentWithName { name } => {
|
||||||
|
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||||
|
let document_id = generate_uuid();
|
||||||
|
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||||
|
self.load_document(new_document, document_id, false, responses);
|
||||||
|
}
|
||||||
NextDocument => {
|
NextDocument => {
|
||||||
let current_index = self.document_index(self.active_document_id);
|
let current_index = self.document_index(self.active_document_id);
|
||||||
let next_index = (current_index + 1) % self.document_ids.len();
|
let next_index = (current_index + 1) % self.document_ids.len();
|
||||||
@@ -303,7 +311,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
self.load_document(document, document_id, true, responses);
|
self.load_document(document, document_id, true, responses);
|
||||||
}
|
}
|
||||||
Err(e) => responses.push_back(
|
Err(e) => responses.push_back(
|
||||||
FrontendMessage::DisplayDialogError {
|
DialogMessage::DisplayDialogError {
|
||||||
title: "Failed to open document".to_string(),
|
title: "Failed to open document".to_string(),
|
||||||
description: e.to_string(),
|
description: e.to_string(),
|
||||||
}
|
}
|
||||||
@@ -408,16 +416,14 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
let prev_id = self.document_ids[prev_index];
|
let prev_id = self.document_ids[prev_index];
|
||||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
|
responses.push_back(PortfolioMessage::SelectDocument { document_id: prev_id }.into());
|
||||||
}
|
}
|
||||||
RequestAboutGraphiteDialog => {
|
|
||||||
responses.push_back(FrontendMessage::DisplayDialogAboutGraphite.into());
|
|
||||||
}
|
|
||||||
SelectDocument { document_id } => {
|
SelectDocument { document_id } => {
|
||||||
let active_document = self.active_document();
|
let active_document = self.active_document();
|
||||||
if !active_document.is_saved() {
|
if !active_document.is_saved() {
|
||||||
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into());
|
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into());
|
||||||
}
|
}
|
||||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||||
responses.push_back(SetActiveDcoument { document_id }.into());
|
responses.push_back(SetActiveDocument { document_id }.into());
|
||||||
|
|
||||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
||||||
responses.push_back(RenderDocument.into());
|
responses.push_back(RenderDocument.into());
|
||||||
@@ -428,7 +434,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
responses.push_back(PortfolioMessage::UpdateDocumentBar.into());
|
||||||
}
|
}
|
||||||
SetActiveDcoument { document_id } => {
|
SetActiveDocument { document_id } => {
|
||||||
self.active_document_id = document_id;
|
self.active_document_id = document_id;
|
||||||
}
|
}
|
||||||
UpdateDocumentBar => {
|
UpdateDocumentBar => {
|
||||||
@@ -457,7 +463,6 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
|||||||
let mut common = actions!(PortfolioMessageDiscriminant;
|
let mut common = actions!(PortfolioMessageDiscriminant;
|
||||||
NewDocument,
|
NewDocument,
|
||||||
CloseActiveDocumentWithConfirmation,
|
CloseActiveDocumentWithConfirmation,
|
||||||
CloseAllDocumentsWithConfirmation,
|
|
||||||
CloseAllDocuments,
|
CloseAllDocuments,
|
||||||
NextDocument,
|
NextDocument,
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
|
|||||||
@@ -13,32 +13,31 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
|
||||||
pub enum FrontendMessage {
|
pub enum FrontendMessage {
|
||||||
// Display prefix: make the frontend show something, like a dialog
|
// Display prefix: make the frontend show something, like a dialog
|
||||||
DisplayConfirmationToCloseAllDocuments,
|
DisplayDialog { icon: String },
|
||||||
DisplayConfirmationToCloseDocument { document_id: u64 },
|
DisplayDialogDismiss,
|
||||||
DisplayDialogAboutGraphite,
|
|
||||||
DisplayDialogComingSoon { issue: Option<i32> },
|
|
||||||
DisplayDialogError { title: String, description: String },
|
|
||||||
DisplayDialogPanic { panic_info: String, title: String, description: String },
|
DisplayDialogPanic { panic_info: String, title: String, description: String },
|
||||||
DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer },
|
DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer },
|
||||||
DisplayEditableTextbox { text: String, line_width: Option<f64>, font_size: f64, color: Color },
|
DisplayEditableTextbox { text: String, line_width: Option<f64>, font_size: f64, color: Color },
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
|
|
||||||
// Trigger prefix: cause a browser API to do something
|
// Trigger prefix: cause a browser API to do something
|
||||||
TriggerDefaultFontLoad,
|
|
||||||
TriggerFileDownload { document: String, name: String },
|
TriggerFileDownload { document: String, name: String },
|
||||||
TriggerFileUpload,
|
TriggerFileUpload,
|
||||||
TriggerFontLoad { font: String },
|
TriggerFontLoad { font: String },
|
||||||
|
TriggerFontLoadDefault,
|
||||||
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
TriggerIndexedDbRemoveDocument { document_id: u64 },
|
||||||
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
TriggerIndexedDbWriteDocument { document: String, details: FrontendDocumentDetails, version: String },
|
||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
TriggerTextCopy { copy_text: String },
|
TriggerTextCopy { copy_text: String },
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
|
TriggerVisitLink { url: String },
|
||||||
|
|
||||||
// Update prefix: give the frontend a new value or state for it to use
|
// Update prefix: give the frontend a new value or state for it to use
|
||||||
UpdateActiveDocument { document_id: u64 },
|
UpdateActiveDocument { document_id: u64 },
|
||||||
UpdateActiveTool { tool_name: String },
|
UpdateActiveTool { tool_name: String },
|
||||||
UpdateCanvasRotation { angle_radians: f64 },
|
UpdateCanvasRotation { angle_radians: f64 },
|
||||||
UpdateCanvasZoom { factor: f64 },
|
UpdateCanvasZoom { factor: f64 },
|
||||||
|
UpdateDialogDetails { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
UpdateDocumentArtboards { svg: String },
|
UpdateDocumentArtboards { svg: String },
|
||||||
UpdateDocumentArtwork { svg: String },
|
UpdateDocumentArtwork { svg: String },
|
||||||
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
|
UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout },
|
||||||
|
|||||||
@@ -185,10 +185,10 @@ impl Default for Mapping {
|
|||||||
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp},
|
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp},
|
||||||
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown},
|
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown},
|
||||||
// Portfolio actions
|
// Portfolio actions
|
||||||
entry! {action=PortfolioMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
|
entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
|
||||||
entry! {action=PortfolioMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
entry! {action=DialogMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
|
||||||
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]},
|
||||||
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub enum LayoutMessage {
|
|||||||
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)]
|
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LayoutTarget {
|
pub enum LayoutTarget {
|
||||||
|
DialogDetails,
|
||||||
DocumentBar,
|
DocumentBar,
|
||||||
PropertiesOptionsPanel,
|
PropertiesOptionsPanel,
|
||||||
PropertiesSectionsPanel,
|
PropertiesSectionsPanel,
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ impl LayoutMessageHandler {
|
|||||||
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
|
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) {
|
||||||
let widget_layout = &self.layouts[layout_target as usize];
|
let widget_layout = &self.layouts[layout_target as usize];
|
||||||
let message = match layout_target {
|
let message = match layout_target {
|
||||||
|
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
|
||||||
|
layout_target,
|
||||||
|
layout: widget_layout.layout.clone(),
|
||||||
|
},
|
||||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
@@ -31,6 +35,7 @@ impl LayoutMessageHandler {
|
|||||||
layout_target,
|
layout_target,
|
||||||
layout: widget_layout.layout.clone(),
|
layout: widget_layout.layout.clone(),
|
||||||
},
|
},
|
||||||
|
|
||||||
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
||||||
};
|
};
|
||||||
responses.push_back(message.into());
|
responses.push_back(message.into());
|
||||||
@@ -38,8 +43,10 @@ impl LayoutMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
||||||
|
#[remain::check]
|
||||||
fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) {
|
fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) {
|
||||||
use LayoutMessage::*;
|
use LayoutMessage::*;
|
||||||
|
#[remain::sorted]
|
||||||
match action {
|
match action {
|
||||||
SendLayout { layout, layout_target } => {
|
SendLayout { layout, layout_target } => {
|
||||||
self.layouts[layout_target as usize] = layout;
|
self.layouts[layout_target as usize] = layout;
|
||||||
@@ -49,52 +56,12 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
UpdateLayout { layout_target, widget_id, value } => {
|
UpdateLayout { layout_target, widget_id, value } => {
|
||||||
let layout = &mut self.layouts[layout_target as usize];
|
let layout = &mut self.layouts[layout_target as usize];
|
||||||
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id).expect("Received invalid widget_id from the frontend");
|
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id).expect("Received invalid widget_id from the frontend");
|
||||||
|
#[remain::sorted]
|
||||||
match &mut widget_holder.widget {
|
match &mut widget_holder.widget {
|
||||||
Widget::NumberInput(number_input) => match value {
|
Widget::CheckboxInput(checkbox_input) => {
|
||||||
Value::Number(num) => {
|
let update_value = value.as_bool().expect("CheckboxInput update was not of type: bool");
|
||||||
let update_value = num.as_f64().unwrap();
|
checkbox_input.checked = update_value;
|
||||||
number_input.value = update_value;
|
let callback_message = (checkbox_input.on_update.callback)(checkbox_input);
|
||||||
let callback_message = (number_input.on_update.callback)(number_input);
|
|
||||||
responses.push_back(callback_message);
|
|
||||||
}
|
|
||||||
Value::String(str) => match str.as_str() {
|
|
||||||
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
|
|
||||||
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
|
|
||||||
_ => {
|
|
||||||
panic!("Invalid string found when updating `NumberInput`")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => panic!("Invalid type found when updating `NumberInput`"),
|
|
||||||
},
|
|
||||||
Widget::Separator(_) => {}
|
|
||||||
Widget::IconButton(icon_button) => {
|
|
||||||
let callback_message = (icon_button.on_update.callback)(icon_button);
|
|
||||||
responses.push_back(callback_message);
|
|
||||||
}
|
|
||||||
Widget::IconLabel(_) => {}
|
|
||||||
Widget::PopoverButton(_) => {}
|
|
||||||
Widget::OptionalInput(optional_input) => {
|
|
||||||
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
|
||||||
optional_input.checked = update_value;
|
|
||||||
let callback_message = (optional_input.on_update.callback)(optional_input);
|
|
||||||
responses.push_back(callback_message);
|
|
||||||
}
|
|
||||||
Widget::RadioInput(radio_input) => {
|
|
||||||
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
|
||||||
radio_input.selected_index = update_value as u32;
|
|
||||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
|
||||||
responses.push_back(callback_message);
|
|
||||||
}
|
|
||||||
Widget::TextInput(text_input) => {
|
|
||||||
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
|
||||||
text_input.value = update_value.into();
|
|
||||||
let callback_message = (text_input.on_update.callback)(text_input);
|
|
||||||
responses.push_back(callback_message);
|
|
||||||
}
|
|
||||||
Widget::TextAreaInput(text_area_input) => {
|
|
||||||
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
|
||||||
text_area_input.value = update_value.into();
|
|
||||||
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
Widget::ColorInput(color_input) => {
|
Widget::ColorInput(color_input) => {
|
||||||
@@ -121,6 +88,57 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
let callback_message = (font_input.on_update.callback)(font_input);
|
let callback_message = (font_input.on_update.callback)(font_input);
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
|
Widget::IconButton(icon_button) => {
|
||||||
|
let callback_message = (icon_button.on_update.callback)(icon_button);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Widget::IconLabel(_) => {}
|
||||||
|
Widget::NumberInput(number_input) => match value {
|
||||||
|
Value::Number(num) => {
|
||||||
|
let update_value = num.as_f64().unwrap();
|
||||||
|
number_input.value = update_value;
|
||||||
|
let callback_message = (number_input.on_update.callback)(number_input);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Value::String(str) => match str.as_str() {
|
||||||
|
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
|
||||||
|
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
|
||||||
|
_ => {
|
||||||
|
panic!("Invalid string found when updating `NumberInput`")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => panic!("Invalid type found when updating `NumberInput`"),
|
||||||
|
},
|
||||||
|
Widget::OptionalInput(optional_input) => {
|
||||||
|
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
||||||
|
optional_input.checked = update_value;
|
||||||
|
let callback_message = (optional_input.on_update.callback)(optional_input);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Widget::PopoverButton(_) => {}
|
||||||
|
Widget::RadioInput(radio_input) => {
|
||||||
|
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
||||||
|
radio_input.selected_index = update_value as u32;
|
||||||
|
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Widget::Separator(_) => {}
|
||||||
|
Widget::TextAreaInput(text_area_input) => {
|
||||||
|
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
||||||
|
text_area_input.value = update_value.into();
|
||||||
|
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Widget::TextButton(text_button) => {
|
||||||
|
let callback_message = (text_button.on_update.callback)(text_button);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
|
Widget::TextInput(text_input) => {
|
||||||
|
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
||||||
|
text_input.value = update_value.into();
|
||||||
|
let callback_message = (text_input.on_update.callback)(text_input);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
Widget::TextLabel(_) => {}
|
Widget::TextLabel(_) => {}
|
||||||
};
|
};
|
||||||
self.send_layout(layout_target, responses);
|
self.send_layout(layout_target, responses);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ impl<T> Default for WidgetCallback<T> {
|
|||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Widget {
|
pub enum Widget {
|
||||||
|
CheckboxInput(CheckboxInput),
|
||||||
ColorInput(ColorInput),
|
ColorInput(ColorInput),
|
||||||
FontInput(FontInput),
|
FontInput(FontInput),
|
||||||
IconButton(IconButton),
|
IconButton(IconButton),
|
||||||
@@ -160,6 +161,7 @@ pub enum Widget {
|
|||||||
RadioInput(RadioInput),
|
RadioInput(RadioInput),
|
||||||
Separator(Separator),
|
Separator(Separator),
|
||||||
TextAreaInput(TextAreaInput),
|
TextAreaInput(TextAreaInput),
|
||||||
|
TextButton(TextButton),
|
||||||
TextInput(TextInput),
|
TextInput(TextInput),
|
||||||
TextLabel(TextLabel),
|
TextLabel(TextLabel),
|
||||||
}
|
}
|
||||||
@@ -191,6 +193,7 @@ pub struct NumberInput {
|
|||||||
#[serde(rename = "displayDecimalPlaces")]
|
#[serde(rename = "displayDecimalPlaces")]
|
||||||
#[derivative(Default(value = "3"))]
|
#[derivative(Default(value = "3"))]
|
||||||
pub display_decimal_places: u32,
|
pub display_decimal_places: u32,
|
||||||
|
pub disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||||
@@ -288,6 +291,20 @@ pub struct IconButton {
|
|||||||
pub on_update: WidgetCallback<IconButton>,
|
pub on_update: WidgetCallback<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
|
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
|
||||||
|
pub struct TextButton {
|
||||||
|
pub label: String,
|
||||||
|
pub emphasized: bool,
|
||||||
|
pub disabled: bool,
|
||||||
|
pub min_width: u32,
|
||||||
|
pub gap_after: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_update: WidgetCallback<TextButton>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
#[derivative(Debug, PartialEq)]
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct OptionalInput {
|
pub struct OptionalInput {
|
||||||
@@ -300,6 +317,20 @@ pub struct OptionalInput {
|
|||||||
pub on_update: WidgetCallback<OptionalInput>,
|
pub on_update: WidgetCallback<OptionalInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
|
pub struct CheckboxInput {
|
||||||
|
pub checked: bool,
|
||||||
|
pub icon: String,
|
||||||
|
#[serde(rename = "outlineStyle")]
|
||||||
|
pub outline_style: bool,
|
||||||
|
#[serde(rename = "title")]
|
||||||
|
pub tooltip: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_update: WidgetCallback<CheckboxInput>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||||
#[derivative(Debug, PartialEq)]
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct PopoverButton {
|
pub struct PopoverButton {
|
||||||
@@ -342,4 +373,7 @@ pub struct TextLabel {
|
|||||||
pub value: String,
|
pub value: String,
|
||||||
pub bold: bool,
|
pub bold: bool,
|
||||||
pub italic: bool,
|
pub italic: bool,
|
||||||
|
pub multiline: bool,
|
||||||
|
#[serde(rename = "tableAlign")]
|
||||||
|
pub table_align: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod communication;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
|
pub mod dialog;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
pub mod global;
|
pub mod global;
|
||||||
@@ -59,6 +60,7 @@ pub mod message_prelude {
|
|||||||
pub use crate::document::clipboards::Clipboard;
|
pub use crate::document::clipboards::Clipboard;
|
||||||
pub use crate::LayerId;
|
pub use crate::LayerId;
|
||||||
|
|
||||||
|
pub use crate::dialog::messages::*;
|
||||||
pub use crate::document::{ArtboardMessage, ArtboardMessageDiscriminant};
|
pub use crate::document::{ArtboardMessage, ArtboardMessageDiscriminant};
|
||||||
pub use crate::document::{DocumentMessage, DocumentMessageDiscriminant};
|
pub use crate::document::{DocumentMessage, DocumentMessageDiscriminant};
|
||||||
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Initialize other stateful Vue systems
|
// Initialize other stateful Vue systems
|
||||||
const dialog = createDialogState(editor);
|
const dialog = createDialogState(editor);
|
||||||
const documents = createDocumentsState(editor, dialog);
|
const documents = createDocumentsState(editor);
|
||||||
const fullscreen = createFullscreenState();
|
const fullscreen = createFullscreenState();
|
||||||
initErrorHandling(editor, dialog);
|
initErrorHandling(editor, dialog);
|
||||||
createAutoSaveManager(editor, documents);
|
createAutoSaveManager(editor, documents);
|
||||||
|
|||||||
@@ -288,7 +288,8 @@ import {
|
|||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
TriggerFontLoad,
|
TriggerFontLoad,
|
||||||
TriggerDefaultFontLoad,
|
TriggerFontLoadDefault,
|
||||||
|
TriggerVisitLink,
|
||||||
} from "@/dispatcher/js-messages";
|
} from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
import { textInputCleanup } from "@/lifetime/input";
|
import { textInputCleanup } from "@/lifetime/input";
|
||||||
@@ -466,7 +467,10 @@ export default defineComponent({
|
|||||||
const responseBuffer = await response.arrayBuffer();
|
const responseBuffer = await response.arrayBuffer();
|
||||||
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(responseBuffer), false);
|
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(responseBuffer), false);
|
||||||
});
|
});
|
||||||
this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont);
|
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoadDefault, loadDefaultFont);
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(TriggerVisitLink, async (triggerOpenLink) => {
|
||||||
|
window.open(triggerOpenLink.url, "_blank");
|
||||||
|
});
|
||||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
|
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
|
||||||
// If the Clipboard API is supported in the browser, copy text to the clipboard
|
// If the Clipboard API is supported in the browser, copy text to the clipboard
|
||||||
navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
|
navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
|
||||||
@@ -520,10 +524,31 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gets metadat populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
||||||
|
const loadBuildMetadata = (): void => {
|
||||||
|
const release = process.env.VUE_APP_RELEASE_SERIES;
|
||||||
|
let timestamp = "";
|
||||||
|
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 8);
|
||||||
|
const branch = process.env.VUE_APP_COMMIT_BRANCH;
|
||||||
|
{
|
||||||
|
const date = new Date(process.env.VUE_APP_COMMIT_DATE || "");
|
||||||
|
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||||
|
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||||
|
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
||||||
|
.formatToParts(new Date())
|
||||||
|
.find((part) => part.type === "timeZoneName");
|
||||||
|
const timezoneNameString = timezoneName?.value;
|
||||||
|
timestamp = `${dateString} ${timeString} ${timezoneNameString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.instance.populate_build_metadata(release || "", timestamp, hash, branch || "");
|
||||||
|
};
|
||||||
|
|
||||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||||
// Get initial Document Bar
|
// Get initial Document Bar
|
||||||
this.editor.instance.init_document_bar();
|
this.editor.instance.init_document_bar();
|
||||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||||
|
loadBuildMetadata();
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
const documentModeEntries: SectionsOfMenuListEntries = [
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
:data-index="index"
|
:data-index="index"
|
||||||
:draggable="draggable"
|
:draggable="draggable"
|
||||||
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
|
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
|
||||||
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`"
|
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null"
|
||||||
>
|
>
|
||||||
<LayoutRow class="layer-type-icon">
|
<LayoutRow class="layer-type-icon">
|
||||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
|
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutCol class="properties">
|
<LayoutCol class="properties">
|
||||||
<LayoutRow class="options-bar">
|
<LayoutRow class="options-bar">
|
||||||
<WidgetLayout :layout="propertiesOptionsLayout"></WidgetLayout>
|
<WidgetLayout :layout="propertiesOptionsLayout" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="sections" :scrollableY="true">
|
<LayoutRow class="sections" :scrollableY="true">
|
||||||
<WidgetLayout :layout="propertiesSectionsLayout"></WidgetLayout>
|
<WidgetLayout :layout="propertiesSectionsLayout" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
<div class="widget-row">
|
<div class="widget-row">
|
||||||
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
||||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||||
<PopoverButton v-if="component.kind === 'PopoverButton'">
|
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||||
<h3>{{ component.props.title }}</h3>
|
|
||||||
<p>{{ component.props.text }}</p>
|
|
||||||
</PopoverButton>
|
|
||||||
<NumberInput
|
|
||||||
v-if="component.kind === 'NumberInput'"
|
|
||||||
v-bind="component.props"
|
|
||||||
@update:value="(value: number) => updateLayout(component.widget_id, value)"
|
|
||||||
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
|
||||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
|
||||||
/>
|
|
||||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
|
||||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
|
||||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
<FontInput
|
<FontInput
|
||||||
v-if="component.kind === 'FontInput'"
|
v-if="component.kind === 'FontInput'"
|
||||||
@@ -22,26 +10,43 @@
|
|||||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
||||||
/>
|
/>
|
||||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||||
|
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
||||||
|
<NumberInput
|
||||||
|
v-if="component.kind === 'NumberInput'"
|
||||||
|
v-bind="component.props"
|
||||||
|
@update:value="(value: number) => updateLayout(component.widget_id, value)"
|
||||||
|
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
||||||
|
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||||
|
/>
|
||||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||||
|
<PopoverButton v-if="component.kind === 'PopoverButton'">
|
||||||
|
<h3>{{ component.props.title }}</h3>
|
||||||
|
<p>{{ component.props.text }}</p>
|
||||||
|
</PopoverButton>
|
||||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||||
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
||||||
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="component.props">{{ component.props.value }}</TextLabel>
|
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
<TextButton v-if="component.kind === 'TextButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||||
|
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
|
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ component.props.value }}</TextLabel>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.widget-row {
|
.widget-row {
|
||||||
min-height: 32px;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
--widget-height: 24px;
|
--widget-height: 24px;
|
||||||
min-height: var(--widget-height);
|
|
||||||
line-height: var(--widget-height);
|
|
||||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||||
|
min-height: var(--widget-height);
|
||||||
|
|
||||||
|
&:not(.multiline) {
|
||||||
|
line-height: var(--widget-height);
|
||||||
|
}
|
||||||
|
|
||||||
&.icon-label.size-12 {
|
&.icon-label.size-12 {
|
||||||
--widget-height: 12px;
|
--widget-height: 12px;
|
||||||
@@ -61,6 +66,8 @@ import { WidgetRow } from "@/dispatcher/js-messages";
|
|||||||
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||||
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
|
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||||
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||||
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||||
@@ -82,10 +89,16 @@ export default defineComponent({
|
|||||||
updateLayout(widgetId: BigInt, value: unknown) {
|
updateLayout(widgetId: BigInt, value: unknown) {
|
||||||
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
||||||
},
|
},
|
||||||
|
withoutValue(props: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const { value: _, ...rest } = props;
|
||||||
|
return rest;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Separator,
|
Separator,
|
||||||
PopoverButton,
|
PopoverButton,
|
||||||
|
TextButton,
|
||||||
|
CheckboxInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="text-button" :class="{ emphasized, disabled }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" @click="(e: MouseEvent) => action(e)">
|
<button
|
||||||
|
class="text-button"
|
||||||
|
:class="{ emphasized, disabled }"
|
||||||
|
:data-emphasized="emphasized || null"
|
||||||
|
:data-disabled="disabled || null"
|
||||||
|
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||||
|
@click="(e: MouseEvent) => action(e)"
|
||||||
|
>
|
||||||
<TextLabel>{{ label }}</TextLabel>
|
<TextLabel>{{ label }}</TextLabel>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<FloatingMenu class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
<FloatingMenu class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
||||||
<LayoutRow>
|
<LayoutRow ref="main">
|
||||||
<LayoutCol class="icon-column">
|
<LayoutCol class="icon-column">
|
||||||
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
|
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||||
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
|
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
<LayoutCol class="main-column">
|
<LayoutCol class="main-column">
|
||||||
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
|
<WidgetLayout v-if="dialog.state.widgets.layout.length > 0" :layout="dialog.state.widgets" class="details" />
|
||||||
<TextLabel class="details">{{ dialog.state.details }}</TextLabel>
|
<LayoutRow v-if="dialog.state.jsCallbackBasedButtons?.length > 0" class="panic-buttons-row">
|
||||||
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0">
|
<TextButton v-for="(button, index) in dialog.state.jsCallbackBasedButtons" :key="index" :action="() => button.callback?.()" v-bind="button.props" />
|
||||||
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback?.()" v-bind="button.props" />
|
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
@@ -49,21 +48,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-column {
|
.main-column {
|
||||||
.heading {
|
margin: -4px 0;
|
||||||
user-select: text;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
max-width: 400px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons-row {
|
.panic-buttons-row {
|
||||||
margin-top: 16px;
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +73,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|||||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inject: ["dialog"],
|
inject: ["dialog"],
|
||||||
@@ -86,8 +82,8 @@ export default defineComponent({
|
|||||||
LayoutCol,
|
LayoutCol,
|
||||||
FloatingMenu,
|
FloatingMenu,
|
||||||
IconLabel,
|
IconLabel,
|
||||||
TextLabel,
|
|
||||||
TextButton,
|
TextButton,
|
||||||
|
WidgetLayout,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dismiss() {
|
dismiss() {
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ const MenuList = defineComponent({
|
|||||||
|
|
||||||
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
|
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
|
||||||
|
|
||||||
|
if (!floatingMenu) return;
|
||||||
|
|
||||||
// Save open/closed state before forcing open, if necessary, for measurement
|
// Save open/closed state before forcing open, if necessary, for measurement
|
||||||
const initiallyOpen = floatingMenu.isOpen();
|
const initiallyOpen = floatingMenu.isOpen();
|
||||||
if (!initiallyOpen) floatingMenu.setOpen();
|
if (!initiallyOpen) floatingMenu.setOpen();
|
||||||
@@ -265,7 +267,9 @@ const MenuList = defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER };
|
return {
|
||||||
|
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
FloatingMenu,
|
FloatingMenu,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutRow class="checkbox-input" :class="{ 'outline-style': outlineStyle }">
|
<LayoutRow class="checkbox-input" :class="{ 'outline-style': outlineStyle }">
|
||||||
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @input="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
|
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @change="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
|
||||||
<label :for="`checkbox-input-${id}`">
|
<label :for="`checkbox-input-${id}`">
|
||||||
<LayoutRow class="checkbox-box">
|
<LayoutRow class="checkbox-box">
|
||||||
<IconLabel :icon="icon" />
|
<IconLabel :icon="icon" />
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.checkbox-input {
|
.checkbox-input {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
.checkbox-box {
|
.checkbox-box {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutRow class="dropdown-input">
|
<LayoutRow class="font-input">
|
||||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
||||||
<span>{{ activeEntry.label }}</span>
|
<span>{{ activeEntry.label }}</span>
|
||||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.dropdown-input {
|
.font-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.dropdown-box {
|
.dropdown-box {
|
||||||
|
|||||||
@@ -65,15 +65,7 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
|||||||
ref: undefined,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: (): void => editor.instance.new_document() },
|
{ label: "New…", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: (): void => editor.instance.request_new_document_dialog() },
|
||||||
{
|
|
||||||
label: "New 1920x1080",
|
|
||||||
icon: "File",
|
|
||||||
action: (): void => {
|
|
||||||
editor.instance.new_document();
|
|
||||||
editor.instance.create_artboard_and_fit_to_viewport(0, 0, 1920, 1080);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: (): void => editor.instance.open_document() },
|
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: (): void => editor.instance.open_document() },
|
||||||
{
|
{
|
||||||
label: "Open Recent",
|
label: "Open Recent",
|
||||||
@@ -168,7 +160,12 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
|||||||
label: "Help",
|
label: "Help",
|
||||||
ref: undefined,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[{ label: "About Graphite", action: async (): Promise<void> => editor.instance.request_about_graphite_dialog() }],
|
[
|
||||||
|
{
|
||||||
|
label: "About Graphite",
|
||||||
|
action: async (): Promise<void> => editor.instance.request_about_graphite_dialog(),
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
|
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
|
||||||
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
|
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="text-label" :class="{ bold, italic }">
|
<span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.text-label {
|
.text-label {
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.bold {
|
&.bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -16,6 +16,16 @@
|
|||||||
&.italic {
|
&.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.multiline {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.table-align {
|
||||||
|
flex: 0 0 30%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -26,6 +36,8 @@ export default defineComponent({
|
|||||||
props: {
|
props: {
|
||||||
bold: { type: Boolean as PropType<boolean>, default: false },
|
bold: { type: Boolean as PropType<boolean>, default: false },
|
||||||
italic: { type: Boolean as PropType<boolean>, default: false },
|
italic: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tableAlign: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
multiline: { type: Boolean as PropType<boolean>, default: false },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<MenuBarInput v-if="platform !== 'Mac'" />
|
<MenuBarInput v-if="platform !== 'Mac'" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="header-part">
|
<LayoutRow class="header-part">
|
||||||
<WindowTitle :title="`${activeDocumentDisplayName} - Graphite`" />
|
<WindowTitle :text="`${activeDocumentDisplayName} - Graphite`" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="header-part">
|
<LayoutRow class="header-part">
|
||||||
<WindowButtonsWindows :maximized="maximized" v-if="platform === 'Windows' || platform === 'Linux'" />
|
<WindowButtonsWindows :maximized="maximized" v-if="platform === 'Windows' || platform === 'Linux'" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutRow class="window-title">
|
<LayoutRow class="window-title">
|
||||||
<span>{{ title }}</span>
|
<span>{{ text }}</span>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
title: { type: String as PropType<string>, required: true },
|
text: { type: String as PropType<string>, required: true },
|
||||||
},
|
},
|
||||||
components: { LayoutRow },
|
components: { LayoutRow },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { Transform, Type } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
|
|
||||||
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
|
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
|
||||||
|
import { IconName } from "@/utilities/icons";
|
||||||
|
|
||||||
export class JsMessage {
|
export class JsMessage {
|
||||||
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
|
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
|
||||||
@@ -143,12 +144,6 @@ export class UpdateActiveDocument extends JsMessage {
|
|||||||
readonly document_id!: BigInt;
|
readonly document_id!: BigInt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayDialogError extends JsMessage {
|
|
||||||
readonly title!: string;
|
|
||||||
|
|
||||||
readonly description!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DisplayDialogPanic extends JsMessage {
|
export class DisplayDialogPanic extends JsMessage {
|
||||||
readonly panic_info!: string;
|
readonly panic_info!: string;
|
||||||
|
|
||||||
@@ -157,14 +152,10 @@ export class DisplayDialogPanic extends JsMessage {
|
|||||||
readonly description!: string;
|
readonly description!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayConfirmationToCloseDocument extends JsMessage {
|
export class DisplayDialog extends JsMessage {
|
||||||
readonly document_id!: BigInt;
|
readonly icon!: IconName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayConfirmationToCloseAllDocuments extends JsMessage {}
|
|
||||||
|
|
||||||
export class DisplayDialogAboutGraphite extends JsMessage {}
|
|
||||||
|
|
||||||
export class UpdateDocumentArtwork extends JsMessage {
|
export class UpdateDocumentArtwork extends JsMessage {
|
||||||
readonly svg!: string;
|
readonly svg!: string;
|
||||||
}
|
}
|
||||||
@@ -388,7 +379,9 @@ export class IndexedDbDocumentDetails extends DocumentDetails {
|
|||||||
id!: string;
|
id!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerDefaultFontLoad extends JsMessage {}
|
export class TriggerFontLoadDefault extends JsMessage {}
|
||||||
|
|
||||||
|
export class DisplayDialogDismiss extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||||
document!: string;
|
document!: string;
|
||||||
@@ -409,9 +402,13 @@ export class TriggerFontLoad extends JsMessage {
|
|||||||
font!: string;
|
font!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TriggerVisitLink extends JsMessage {
|
||||||
|
url!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WidgetLayout {
|
export interface WidgetLayout {
|
||||||
layout_target: unknown;
|
|
||||||
layout: LayoutRow[];
|
layout: LayoutRow[];
|
||||||
|
layout_target: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultWidgetLayout(): WidgetLayout {
|
export function defaultWidgetLayout(): WidgetLayout {
|
||||||
@@ -434,18 +431,20 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetKind =
|
export type WidgetKind =
|
||||||
| "NumberInput"
|
| "CheckboxInput"
|
||||||
| "Separator"
|
|
||||||
| "IconButton"
|
|
||||||
| "PopoverButton"
|
|
||||||
| "OptionalInput"
|
|
||||||
| "RadioInput"
|
|
||||||
| "TextInput"
|
|
||||||
| "TextAreaInput"
|
|
||||||
| "TextLabel"
|
|
||||||
| "IconLabel"
|
|
||||||
| "ColorInput"
|
| "ColorInput"
|
||||||
| "FontInput";
|
| "FontInput"
|
||||||
|
| "IconButton"
|
||||||
|
| "IconLabel"
|
||||||
|
| "NumberInput"
|
||||||
|
| "OptionalInput"
|
||||||
|
| "PopoverButton"
|
||||||
|
| "RadioInput"
|
||||||
|
| "Separator"
|
||||||
|
| "TextAreaInput"
|
||||||
|
| "TextButton"
|
||||||
|
| "TextInput"
|
||||||
|
| "TextLabel";
|
||||||
|
|
||||||
export interface Widget {
|
export interface Widget {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
@@ -454,6 +453,13 @@ export interface Widget {
|
|||||||
props: any;
|
props: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
||||||
|
layout_target!: unknown;
|
||||||
|
|
||||||
|
@Transform(({ value }) => createWidgetLayout(value))
|
||||||
|
layout!: LayoutRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||||
layout_target!: unknown;
|
layout_target!: unknown;
|
||||||
|
|
||||||
@@ -512,10 +518,6 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayDialogComingSoon extends JsMessage {
|
|
||||||
issue: number | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TriggerTextCommit extends JsMessage {}
|
export class TriggerTextCommit extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerTextCopy extends JsMessage {
|
export class TriggerTextCopy extends JsMessage {
|
||||||
@@ -530,17 +532,14 @@ type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInst
|
|||||||
type MessageMaker = typeof JsMessage | JSMessageFactory;
|
type MessageMaker = typeof JsMessage | JSMessageFactory;
|
||||||
|
|
||||||
export const messageMakers: Record<string, MessageMaker> = {
|
export const messageMakers: Record<string, MessageMaker> = {
|
||||||
DisplayConfirmationToCloseAllDocuments,
|
DisplayDialog,
|
||||||
DisplayConfirmationToCloseDocument,
|
|
||||||
DisplayDialogAboutGraphite,
|
|
||||||
DisplayDialogComingSoon,
|
|
||||||
DisplayDialogError,
|
|
||||||
DisplayDialogPanic,
|
DisplayDialogPanic,
|
||||||
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
|
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
|
||||||
DisplayEditableTextbox,
|
DisplayEditableTextbox,
|
||||||
UpdateImageData,
|
UpdateImageData,
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
TriggerDefaultFontLoad,
|
TriggerFontLoadDefault,
|
||||||
|
DisplayDialogDismiss,
|
||||||
TriggerFileDownload,
|
TriggerFileDownload,
|
||||||
TriggerFileUpload,
|
TriggerFileUpload,
|
||||||
TriggerIndexedDbRemoveDocument,
|
TriggerIndexedDbRemoveDocument,
|
||||||
@@ -549,10 +548,12 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
TriggerTextCommit,
|
TriggerTextCommit,
|
||||||
TriggerTextCopy,
|
TriggerTextCopy,
|
||||||
TriggerViewportResize,
|
TriggerViewportResize,
|
||||||
|
TriggerVisitLink,
|
||||||
UpdateActiveDocument,
|
UpdateActiveDocument,
|
||||||
UpdateActiveTool,
|
UpdateActiveTool,
|
||||||
UpdateCanvasRotation,
|
UpdateCanvasRotation,
|
||||||
UpdateCanvasZoom,
|
UpdateCanvasZoom,
|
||||||
|
UpdateDialogDetails,
|
||||||
UpdateDocumentArtboards,
|
UpdateDocumentArtboards,
|
||||||
UpdateDocumentArtwork,
|
UpdateDocumentArtwork,
|
||||||
UpdateDocumentBarLayout,
|
UpdateDocumentBarLayout,
|
||||||
|
|||||||
@@ -1,54 +1,73 @@
|
|||||||
import { DisplayDialogError, DisplayDialogPanic } from "@/dispatcher/js-messages";
|
import { DisplayDialogPanic, WidgetLayout } from "@/dispatcher/js-messages";
|
||||||
import { DialogState } from "@/state/dialog";
|
import { DialogState } from "@/state/dialog";
|
||||||
import { EditorState } from "@/state/wasm-loader";
|
import { EditorState } from "@/state/wasm-loader";
|
||||||
import { stripIndents } from "@/utilities/strip-indents";
|
import { stripIndents } from "@/utilities/strip-indents";
|
||||||
import { TextButtonWidget } from "@/utilities/widgets";
|
import { TextButtonWidget } from "@/utilities/widgets";
|
||||||
|
|
||||||
export function initErrorHandling(editor: EditorState, dialogState: DialogState): void {
|
export function initErrorHandling(editor: EditorState, dialogState: DialogState): void {
|
||||||
// Graphite error dialog
|
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayDialogError, (displayDialogError) => {
|
|
||||||
const okButton: TextButtonWidget = {
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => dialogState.dismissDialog(),
|
|
||||||
props: { label: "OK", emphasized: true, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const buttons = [okButton];
|
|
||||||
|
|
||||||
dialogState.createDialog("Warning", displayDialogError.title, displayDialogError.description, buttons);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Code panic dialog and console error
|
// Code panic dialog and console error
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayDialogPanic, (displayDialogPanic) => {
|
editor.dispatcher.subscribeJsMessage(DisplayDialogPanic, (displayDialogPanic) => {
|
||||||
// `Error.stackTraceLimit` is only available in V8/Chromium
|
// `Error.stackTraceLimit` is only available in V8/Chromium
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(Error as any).stackTraceLimit = Infinity;
|
(Error as any).stackTraceLimit = Infinity;
|
||||||
const stackTrace = new Error().stack || "";
|
const stackTrace = new Error().stack || "";
|
||||||
const panicDetails = `${displayDialogPanic.panic_info}\n\n${stackTrace}`;
|
const panicDetails = `${displayDialogPanic.panic_info}${stackTrace ? `\n\n${stackTrace}` : ""}`;
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(panicDetails);
|
console.error(panicDetails);
|
||||||
|
|
||||||
const reloadButton: TextButtonWidget = {
|
preparePanicDialog(dialogState, displayDialogPanic.title, displayDialogPanic.description, panicDetails);
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => window.location.reload(),
|
|
||||||
props: { label: "Reload", emphasized: true, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const copyErrorLogButton: TextButtonWidget = {
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => navigator.clipboard.writeText(panicDetails),
|
|
||||||
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const reportOnGithubButton: TextButtonWidget = {
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
|
||||||
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const buttons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
|
||||||
|
|
||||||
dialogState.createDialog("Warning", displayDialogPanic.title, displayDialogPanic.description, buttons);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preparePanicDialog(dialogState: DialogState, title: string, details: string, panicDetails: string): void {
|
||||||
|
const widgets: WidgetLayout = {
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
kind: "TextLabel",
|
||||||
|
props: { value: title, bold: true },
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
widget_id: 0n,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
kind: "TextLabel",
|
||||||
|
props: { value: details, multiline: true },
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
widget_id: 0n,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
layout_target: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadButton: TextButtonWidget = {
|
||||||
|
kind: "TextButton",
|
||||||
|
callback: async () => window.location.reload(),
|
||||||
|
props: { label: "Reload", emphasized: true, minWidth: 96 },
|
||||||
|
};
|
||||||
|
const copyErrorLogButton: TextButtonWidget = {
|
||||||
|
kind: "TextButton",
|
||||||
|
callback: async () => navigator.clipboard.writeText(panicDetails),
|
||||||
|
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||||
|
};
|
||||||
|
const reportOnGithubButton: TextButtonWidget = {
|
||||||
|
kind: "TextButton",
|
||||||
|
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
||||||
|
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||||
|
};
|
||||||
|
const jsCallbackBasedButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
||||||
|
|
||||||
|
dialogState.createPanicDialog(widgets, jsCallbackBasedButtons);
|
||||||
|
}
|
||||||
|
|
||||||
function githubUrl(panicDetails: string): string {
|
function githubUrl(panicDetails: string): string {
|
||||||
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
|||||||
if (e.ctrlKey && e.shiftKey && key === "i") return false;
|
if (e.ctrlKey && e.shiftKey && key === "i") return false;
|
||||||
if (e.ctrlKey && e.shiftKey && key === "j") return false;
|
if (e.ctrlKey && e.shiftKey && key === "j") return false;
|
||||||
|
|
||||||
|
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
|
||||||
|
const inCanvas = e.target instanceof Element && e.target.closest("[data-canvas]");
|
||||||
|
if (!inCanvas && (key === "tab" || key === "enter")) return false;
|
||||||
|
|
||||||
// Redirect to the backend
|
// Redirect to the backend
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -87,12 +91,6 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
|||||||
|
|
||||||
if (dialog.dialogIsVisible()) {
|
if (dialog.dialogIsVisible()) {
|
||||||
if (key === "escape") dialog.dismissDialog();
|
if (key === "escape") dialog.dismissDialog();
|
||||||
if (key === "enter") {
|
|
||||||
dialog.submitDialog();
|
|
||||||
|
|
||||||
// Prevent the Enter key from acting like a click on the last clicked button, which might reopen the dialog
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,6 +111,13 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
|||||||
const onPointerMove = (e: PointerEvent): void => {
|
const onPointerMove = (e: PointerEvent): void => {
|
||||||
if (!e.buttons) viewportPointerInteractionOngoing = false;
|
if (!e.buttons) viewportPointerInteractionOngoing = false;
|
||||||
|
|
||||||
|
// Don't redirect pointer movement to the backend if there's no ongoing interaction and it's over a floating menu on top of the canvas
|
||||||
|
// TODO: A better approach is to pass along a boolean to the backend's input preprocessor so it can know if it's being occluded by the GUI.
|
||||||
|
// TODO: This would allow it to properly decide to act on removing hover focus from something that was hovered in the canvas before moving over the GUI.
|
||||||
|
// TODO: Further explanation: https://github.com/GraphiteEditor/Graphite/pull/623#discussion_r866436197
|
||||||
|
const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]");
|
||||||
|
if (!viewportPointerInteractionOngoing && inFloatingMenu) return;
|
||||||
|
|
||||||
const modifiers = makeModifiersBitfield(e);
|
const modifiers = makeModifiersBitfield(e);
|
||||||
editor.instance.on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers);
|
editor.instance.on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { reactive, readonly } from "vue";
|
import { reactive, readonly } from "vue";
|
||||||
|
|
||||||
import { DisplayDialogAboutGraphite, DisplayDialogComingSoon } from "@/dispatcher/js-messages";
|
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, WidgetLayout } from "@/dispatcher/js-messages";
|
||||||
import { EditorState } from "@/state/wasm-loader";
|
import { EditorState } from "@/state/wasm-loader";
|
||||||
import { IconName } from "@/utilities/icons";
|
import { IconName } from "@/utilities/icons";
|
||||||
import { stripIndents } from "@/utilities/strip-indents";
|
|
||||||
import { TextButtonWidget } from "@/utilities/widgets";
|
import { TextButtonWidget } from "@/utilities/widgets";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
@@ -11,103 +10,47 @@ export function createDialogState(editor: EditorState) {
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
icon: "" as IconName,
|
icon: "" as IconName,
|
||||||
heading: "",
|
widgets: defaultWidgetLayout(),
|
||||||
details: "",
|
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
|
||||||
buttons: [] as TextButtonWidget[],
|
jsCallbackBasedButtons: undefined as undefined | TextButtonWidget[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createDialog = (icon: IconName, heading: string, details: string, buttons: TextButtonWidget[]): void => {
|
// Creates a panic dialog from JS.
|
||||||
|
// Normal dialogs are created in the Rust backend, however for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
|
||||||
|
const createPanicDialog = (widgets: WidgetLayout, jsCallbackBasedButtons: TextButtonWidget[]): void => {
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
state.icon = icon;
|
state.icon = "Warning";
|
||||||
state.heading = heading;
|
state.widgets = widgets;
|
||||||
state.details = details;
|
state.jsCallbackBasedButtons = jsCallbackBasedButtons;
|
||||||
state.buttons = buttons;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismissDialog = (): void => {
|
const dismissDialog = (): void => {
|
||||||
state.visible = false;
|
state.visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitDialog = (): void => {
|
|
||||||
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
|
|
||||||
firstEmphasizedButton?.callback?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogIsVisible = (): boolean => state.visible;
|
const dialogIsVisible = (): boolean => state.visible;
|
||||||
|
|
||||||
const comingSoon = (issueNumber?: number): void => {
|
const comingSoon = (issueNumber?: number): void => {
|
||||||
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
|
editor.instance.request_coming_soon_dialog(issueNumber);
|
||||||
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
|
|
||||||
|
|
||||||
const okButton: TextButtonWidget = {
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => dismissDialog(),
|
|
||||||
props: { label: "OK", emphasized: true, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const issueButton: TextButtonWidget = {
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
|
|
||||||
props: { label: `Issue #${issueNumber}`, minWidth: 96 },
|
|
||||||
};
|
|
||||||
const buttons = issueNumber ? [okButton, issueButton] : [okButton];
|
|
||||||
|
|
||||||
createDialog("Warning", "Coming soon", details, buttons);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAboutHandler = (): void => {
|
|
||||||
const date = new Date(process.env.VUE_APP_COMMIT_DATE || "");
|
|
||||||
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
||||||
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
||||||
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
|
||||||
.formatToParts(new Date())
|
|
||||||
.find((part) => part.type === "timeZoneName");
|
|
||||||
const timezoneNameString = timezoneName?.value;
|
|
||||||
|
|
||||||
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12);
|
|
||||||
|
|
||||||
const details = stripIndents`
|
|
||||||
Release Series: ${process.env.VUE_APP_RELEASE_SERIES}
|
|
||||||
|
|
||||||
Date: ${dateString} ${timeString} ${timezoneNameString}
|
|
||||||
Hash: ${hash}
|
|
||||||
Branch: ${process.env.VUE_APP_COMMIT_BRANCH}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const buttons: TextButtonWidget[] = [
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): unknown => window.open("https://graphite.rs", "_blank"),
|
|
||||||
props: { label: "Website", emphasized: false, minWidth: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/graphs/contributors", "_blank"),
|
|
||||||
props: { label: "Credits", emphasized: false, minWidth: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): unknown => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank"),
|
|
||||||
props: { label: "License", emphasized: false, minWidth: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): unknown => window.open("/third-party-licenses.txt", "_blank"),
|
|
||||||
props: { label: "Third-Party Licenses", emphasized: false, minWidth: 0 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
createDialog("GraphiteLogo", "Graphite", details, buttons);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run on creation
|
// Run on creation
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayDialogAboutGraphite, () => onAboutHandler());
|
editor.dispatcher.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayDialogComingSoon, (displayDialogComingSoon) => comingSoon(displayDialogComingSoon.issue));
|
state.visible = true;
|
||||||
|
state.icon = displayDialog.icon;
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatcher.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);
|
||||||
|
|
||||||
|
editor.dispatcher.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
|
||||||
|
state.widgets = updateDialogDetails;
|
||||||
|
state.jsCallbackBasedButtons = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state: readonly(state),
|
state: readonly(state),
|
||||||
createDialog,
|
createPanicDialog,
|
||||||
dismissDialog,
|
dismissDialog,
|
||||||
submitDialog,
|
|
||||||
dialogIsVisible,
|
dialogIsVisible,
|
||||||
comingSoon,
|
comingSoon,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,80 +1,18 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { reactive, readonly } from "vue";
|
import { reactive, readonly } from "vue";
|
||||||
|
|
||||||
import {
|
import { TriggerFileDownload, FrontendDocumentDetails, TriggerFileUpload, UpdateActiveDocument, UpdateOpenDocumentsList } from "@/dispatcher/js-messages";
|
||||||
DisplayConfirmationToCloseAllDocuments,
|
|
||||||
DisplayConfirmationToCloseDocument,
|
|
||||||
TriggerFileDownload,
|
|
||||||
FrontendDocumentDetails,
|
|
||||||
TriggerFileUpload,
|
|
||||||
UpdateActiveDocument,
|
|
||||||
UpdateOpenDocumentsList,
|
|
||||||
} from "@/dispatcher/js-messages";
|
|
||||||
import { DialogState } from "@/state/dialog";
|
|
||||||
import { EditorState } from "@/state/wasm-loader";
|
import { EditorState } from "@/state/wasm-loader";
|
||||||
import { download, upload } from "@/utilities/files";
|
import { download, upload } from "@/utilities/files";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function createDocumentsState(editor: EditorState, dialogState: DialogState) {
|
export function createDocumentsState(editor: EditorState) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
unsaved: false,
|
unsaved: false,
|
||||||
documents: [] as FrontendDocumentDetails[],
|
documents: [] as FrontendDocumentDetails[],
|
||||||
activeDocumentIndex: 0,
|
activeDocumentIndex: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeDocumentWithConfirmation = async (documentId: BigInt): Promise<void> => {
|
|
||||||
// Assume we receive a correct document_id
|
|
||||||
const targetDocument = state.documents.find((doc) => doc.id === documentId) as FrontendDocumentDetails;
|
|
||||||
const tabLabel = targetDocument.displayName;
|
|
||||||
|
|
||||||
// Show the close confirmation prompt
|
|
||||||
dialogState.createDialog("File", "Save changes before closing?", tabLabel, [
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async (): Promise<void> => {
|
|
||||||
editor.instance.save_document();
|
|
||||||
dialogState.dismissDialog();
|
|
||||||
},
|
|
||||||
props: { label: "Save", emphasized: true, minWidth: 96 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async (): Promise<void> => {
|
|
||||||
editor.instance.close_document(targetDocument.id);
|
|
||||||
dialogState.dismissDialog();
|
|
||||||
},
|
|
||||||
props: { label: "Discard", minWidth: 96 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: async (): Promise<void> => {
|
|
||||||
dialogState.dismissDialog();
|
|
||||||
},
|
|
||||||
props: { label: "Cancel", minWidth: 96 },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAllDocumentsWithConfirmation = (): void => {
|
|
||||||
dialogState.createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): void => {
|
|
||||||
editor.instance.close_all_documents();
|
|
||||||
dialogState.dismissDialog();
|
|
||||||
},
|
|
||||||
props: { label: "Discard All", minWidth: 96 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "TextButton",
|
|
||||||
callback: (): void => {
|
|
||||||
dialogState.dismissDialog();
|
|
||||||
},
|
|
||||||
props: { label: "Cancel", minWidth: 96 },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up message subscriptions on creation
|
// Set up message subscriptions on creation
|
||||||
editor.dispatcher.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => {
|
editor.dispatcher.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => {
|
||||||
state.documents = updateOpenDocumentList.open_documents;
|
state.documents = updateOpenDocumentList.open_documents;
|
||||||
@@ -86,14 +24,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
|
|||||||
state.activeDocumentIndex = activeId;
|
state.activeDocumentIndex = activeId;
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseDocument, (displayConfirmationToCloseDocument) => {
|
|
||||||
closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => {
|
|
||||||
closeAllDocumentsWithConfirmation();
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatcher.subscribeJsMessage(TriggerFileUpload, async () => {
|
editor.dispatcher.subscribeJsMessage(TriggerFileUpload, async () => {
|
||||||
const extension = editor.rawWasm.file_save_suffix();
|
const extension = editor.rawWasm.file_save_suffix();
|
||||||
const data = await upload(extension);
|
const data = await upload(extension);
|
||||||
@@ -110,7 +40,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
state: readonly(state),
|
state: readonly(state),
|
||||||
closeAllDocumentsWithConfirmation,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export type DocumentsState = ReturnType<typeof createDocumentsState>;
|
export type DocumentsState = ReturnType<typeof createDocumentsState>;
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ export function getWasmInstance(): WasmInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateEditorStateType = {
|
type CreateEditorStateType = {
|
||||||
/// Allows subscribing to messages from the WASM backend
|
// Allows subscribing to messages from the WASM backend
|
||||||
rawWasm: WasmInstance;
|
rawWasm: WasmInstance;
|
||||||
/// Bindings to WASM wrapper declarations (generated by wasm-bindgen)
|
// Bindings to WASM wrapper declarations (generated by wasm-bindgen)
|
||||||
dispatcher: ReturnType<typeof createJsDispatcher>;
|
dispatcher: ReturnType<typeof createJsDispatcher>;
|
||||||
/// WASM wrapper's exported functions (generated by wasm-bindgen)
|
// WASM wrapper's exported functions (generated by wasm-bindgen)
|
||||||
instance: RustEditorInstance;
|
instance: RustEditorInstance;
|
||||||
};
|
};
|
||||||
export function createEditorState(): CreateEditorStateType {
|
export function createEditorState(): CreateEditorStateType {
|
||||||
|
|||||||
+15
-16
@@ -150,8 +150,8 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_document(&self) {
|
pub fn request_new_document_dialog(&self) {
|
||||||
let message = PortfolioMessage::NewDocument;
|
let message = DialogMessage::RequestNewDocumentDialog;
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +212,23 @@ impl JsEditorHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_all_documents_with_confirmation(&self) {
|
pub fn close_all_documents_with_confirmation(&self) {
|
||||||
let message = PortfolioMessage::CloseAllDocumentsWithConfirmation;
|
let message = DialogMessage::CloseAllDocumentsWithConfirmation;
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn populate_build_metadata(&self, release: String, timestamp: String, hash: String, branch: String) {
|
||||||
|
let new = editor::communication::BuildMetadata { release, timestamp, hash, branch };
|
||||||
|
let message = Message::PopulateBuildMetadata { new };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_about_graphite_dialog(&self) {
|
pub fn request_about_graphite_dialog(&self) {
|
||||||
let message = PortfolioMessage::RequestAboutGraphiteDialog;
|
let message = DialogMessage::RequestAboutGraphiteDialog;
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_coming_soon_dialog(&self, issue: Option<i32>) {
|
||||||
|
let message = DialogMessage::RequestComingSoonDialog { issue };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,18 +554,6 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an artboard at a specified point with a width and height
|
|
||||||
pub fn create_artboard_and_fit_to_viewport(&self, pos_x: f64, pos_y: f64, width: f64, height: f64) {
|
|
||||||
let message = ArtboardMessage::AddArtboard {
|
|
||||||
id: None,
|
|
||||||
position: (pos_x, pos_y),
|
|
||||||
size: (width, height),
|
|
||||||
};
|
|
||||||
self.dispatch(message);
|
|
||||||
let message = DocumentMessage::ZoomCanvasToFitAll;
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||||
pub fn init_document_bar(&self) {
|
pub fn init_document_bar(&self) {
|
||||||
let message = PortfolioMessage::UpdateDocumentBar;
|
let message = PortfolioMessage::UpdateDocumentBar;
|
||||||
|
|||||||
Reference in New Issue
Block a user