mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 00:38:12 +08:00
Dialog redesign and content revamp (#1409)
* Revamp the content and design of dialogs * Add the Licenses dialog
This commit is contained in:
+10
-25
@@ -37,32 +37,17 @@ pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE");
|
||||
pub const GRAPHITE_GIT_COMMIT_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH");
|
||||
pub const GRAPHITE_GIT_COMMIT_BRANCH: &str = env!("GRAPHITE_GIT_COMMIT_BRANCH");
|
||||
|
||||
pub fn release_series() -> String {
|
||||
format!("Release Series: {}", GRAPHITE_RELEASE_SERIES)
|
||||
}
|
||||
|
||||
pub fn commit_info() -> String {
|
||||
format!("{}\n{}\n{}", commit_timestamp(), commit_hash(), commit_branch())
|
||||
}
|
||||
|
||||
pub fn commit_info_localized(localized_commit_date: &str) -> String {
|
||||
format!("{}\n{}\n{}", commit_timestamp_localized(localized_commit_date), commit_hash(), commit_branch())
|
||||
}
|
||||
|
||||
pub fn commit_timestamp() -> String {
|
||||
format!("Date: {}", GRAPHITE_GIT_COMMIT_DATE)
|
||||
}
|
||||
|
||||
pub fn commit_timestamp_localized(localized_commit_date: &str) -> String {
|
||||
format!("Date: {}", localized_commit_date)
|
||||
}
|
||||
|
||||
pub fn commit_hash() -> String {
|
||||
format!("Hash: {}", &GRAPHITE_GIT_COMMIT_HASH[..8])
|
||||
}
|
||||
|
||||
pub fn commit_branch() -> String {
|
||||
format!("Branch: {}", GRAPHITE_GIT_COMMIT_BRANCH)
|
||||
format!(
|
||||
"Release Series: {}\n\
|
||||
Branch: {}\n\
|
||||
Hash: {}\n\
|
||||
{}",
|
||||
GRAPHITE_RELEASE_SERIES,
|
||||
GRAPHITE_GIT_COMMIT_BRANCH,
|
||||
&GRAPHITE_GIT_COMMIT_HASH[..8],
|
||||
localized_commit_date
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -553,15 +553,18 @@ mod test {
|
||||
let print_problem_to_terminal_on_failure = |value: &String| {
|
||||
println!();
|
||||
println!("-------------------------------------------------");
|
||||
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file.");
|
||||
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
|
||||
println!("After bumping this version number, update the documents in `/demo-artwork` by editing their JSON to");
|
||||
println!("ensure they remain compatible with both the bumped version number and the serialization format change.");
|
||||
println!("Failed test due to receiving a DisplayDialogError while loading a Graphite demo file.");
|
||||
println!();
|
||||
println!("That probably means the document serialization format changed. In that case, you need to bump the constant value");
|
||||
println!("`GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`, then update the documents in `/demo-artwork` by editing");
|
||||
println!("their JSON to ensure they remain compatible with both the bumped version number and the serialization format changes.");
|
||||
println!();
|
||||
println!("DisplayDialogError details:");
|
||||
println!();
|
||||
println!("Description: {}", value);
|
||||
println!("-------------------------------------------------");
|
||||
println!();
|
||||
|
||||
panic!()
|
||||
};
|
||||
|
||||
@@ -581,7 +584,7 @@ mod test {
|
||||
|
||||
for response in responses {
|
||||
// Check for the existence of the file format incompatibility warning dialog after opening the test file
|
||||
if let FrontendMessage::UpdateDialogDetails { layout_target: _, diff } = response {
|
||||
if let FrontendMessage::UpdateDialogColumn1 { layout_target: _, diff } = response {
|
||||
if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value {
|
||||
if let LayoutGroup::Row { widgets } = &sub_layout[0] {
|
||||
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
|
||||
|
||||
@@ -29,12 +29,16 @@ pub enum DialogMessage {
|
||||
RequestAboutGraphiteDialog,
|
||||
RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||
localized_commit_date: String,
|
||||
localized_commit_year: String,
|
||||
},
|
||||
RequestComingSoonDialog {
|
||||
issue: Option<i32>,
|
||||
},
|
||||
RequestDemoArtworkDialog,
|
||||
RequestExportDialog,
|
||||
RequestLicensesDialogWithLocalizedCommitDate {
|
||||
localized_commit_year: String,
|
||||
},
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog};
|
||||
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -16,48 +16,54 @@ impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ()),
|
||||
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, portfolio),
|
||||
#[remain::unsorted]
|
||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
|
||||
#[remain::unsorted]
|
||||
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, preferences),
|
||||
|
||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||
let dialog = simple_dialogs::CloseAllDocumentsDialog;
|
||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "Copy".to_string() });
|
||||
let dialog = simple_dialogs::CloseAllDocumentsDialog {
|
||||
unsaved_document_names: portfolio.unsaved_document_names(),
|
||||
};
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::CloseDialogAndThen { followups } => {
|
||||
responses.add(FrontendMessage::DisplayDialogDismiss);
|
||||
for message in followups.into_iter() {
|
||||
responses.add(message);
|
||||
}
|
||||
|
||||
// This come after followups, so that the followups (which can cause the dialog to open) happen first, then we close it afterwards.
|
||||
// If it comes before, the dialog reopens (and appears to not close at all).
|
||||
responses.add(FrontendMessage::DisplayDialogDismiss);
|
||||
}
|
||||
DialogMessage::DisplayDialogError { title, description } => {
|
||||
let dialog = simple_dialogs::ErrorDialog { title, description };
|
||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() });
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestAboutGraphiteDialog => {
|
||||
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
||||
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
|
||||
});
|
||||
}
|
||||
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date } => {
|
||||
let about_graphite = AboutGraphiteDialog { localized_commit_date };
|
||||
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||
localized_commit_date,
|
||||
localized_commit_year,
|
||||
} => {
|
||||
let dialog = AboutGraphiteDialog {
|
||||
localized_commit_date,
|
||||
localized_commit_year,
|
||||
};
|
||||
|
||||
about_graphite.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "GraphiteLogo".to_string() });
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestComingSoonDialog { issue } => {
|
||||
let coming_soon = ComingSoonDialog { issue };
|
||||
coming_soon.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "Warning".to_string() });
|
||||
let dialog = ComingSoonDialog { issue };
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestDemoArtworkDialog => {
|
||||
let demo_artwork_dialog = DemoArtworkDialog;
|
||||
demo_artwork_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "Image".to_string() });
|
||||
let dialog = DemoArtworkDialog;
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestExportDialog => {
|
||||
if let Some(document) = portfolio.active_document() {
|
||||
@@ -83,29 +89,30 @@ impl MessageHandler<DialogMessage, (&PortfolioMessageHandler, &PreferencesMessag
|
||||
.collect();
|
||||
|
||||
self.export_dialog = ExportDialogMessageHandler {
|
||||
file_name: document.name.clone(),
|
||||
scale_factor: 1.,
|
||||
artboards,
|
||||
has_selection: document.selected_layers().next().is_some(),
|
||||
..Default::default()
|
||||
};
|
||||
self.export_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
||||
self.export_dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
}
|
||||
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
|
||||
let dialog = LicensesDialog { localized_commit_year };
|
||||
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
infinite: false,
|
||||
dimensions: glam::UVec2::new(1920, 1080),
|
||||
};
|
||||
self.new_document_dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
||||
self.new_document_dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestPreferencesDialog => {
|
||||
self.preferences_dialog = PreferencesDialogMessageHandler {};
|
||||
self.preferences_dialog.send_layout(responses, LayoutTarget::DialogDetails, preferences);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "Settings".to_string() });
|
||||
self.preferences_dialog.send_dialog_to_frontend(responses, preferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
|
||||
#[impl_message(Message, DialogMessage, ExportDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ExportDialogMessage {
|
||||
FileName(String),
|
||||
FileType(FileType),
|
||||
ScaleFactor(f64),
|
||||
TransparentBackground(bool),
|
||||
|
||||
@@ -7,7 +7,6 @@ use document_legacy::LayerId;
|
||||
/// A dialog to allow users to customize their file export.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExportDialogMessageHandler {
|
||||
pub file_name: String,
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
@@ -16,17 +15,16 @@ pub struct ExportDialogMessageHandler {
|
||||
pub has_selection: bool,
|
||||
}
|
||||
|
||||
impl MessageHandler<ExportDialogMessage, ()> for ExportDialogMessageHandler {
|
||||
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, _data: ()) {
|
||||
impl MessageHandler<ExportDialogMessage, &PortfolioMessageHandler> for ExportDialogMessageHandler {
|
||||
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, portfolio: &PortfolioMessageHandler) {
|
||||
match message {
|
||||
ExportDialogMessage::FileName(name) => self.file_name = name,
|
||||
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
|
||||
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
||||
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
||||
|
||||
ExportDialogMessage::Submit => responses.add_front(DocumentMessage::ExportDocument {
|
||||
file_name: self.file_name.clone(),
|
||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
@@ -34,33 +32,60 @@ impl MessageHandler<ExportDialogMessage, ()> for ExportDialogMessageHandler {
|
||||
}),
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
self.send_dialog_to_frontend(responses);
|
||||
}
|
||||
|
||||
advertise_actions! {ExportDialogUpdate;}
|
||||
}
|
||||
|
||||
impl LayoutHolder for ExportDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let file_name = vec![
|
||||
TextLabel::new("File Name").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(&self.file_name)
|
||||
.on_update(|text_input: &TextInput| ExportDialogMessage::FileName(text_input.value.clone()).into())
|
||||
impl DialogLayoutHolder for ExportDialogMessageHandler {
|
||||
const ICON: &'static str = "File";
|
||||
const TITLE: &'static str = "Export";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("Export")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![ExportDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for ExportDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
|
||||
.into_iter()
|
||||
.map(|(val, name)| RadioEntryData::new(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
|
||||
.collect();
|
||||
|
||||
let export_type = vec![
|
||||
TextLabel::new("File Type").table_align(true).widget_holder(),
|
||||
TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(entries).selected_index(self.file_type as u32).widget_holder(),
|
||||
];
|
||||
|
||||
let resolution = vec![
|
||||
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(self.scale_factor))
|
||||
.unit("")
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.disabled(self.file_type == FileType::Svg)
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into())
|
||||
.min_width(200)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string(), false));
|
||||
let mut export_area_options = vec![
|
||||
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
|
||||
@@ -74,13 +99,13 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
.collect()];
|
||||
|
||||
let export_area = vec![
|
||||
TextLabel::new("Bounds").table_align(true).widget_holder(),
|
||||
TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
let transparent_background = vec![
|
||||
TextLabel::new("Transparency").table_align(true).widget_holder(),
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(self.transparent_background)
|
||||
.disabled(self.file_type == FileType::Jpg)
|
||||
@@ -88,42 +113,11 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let resolution = vec![
|
||||
TextLabel::new("Scale Factor").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(self.scale_factor))
|
||||
.unit(" ")
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.disabled(self.file_type == FileType::Svg)
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into())
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let button_widgets = vec![
|
||||
TextButton::new("Export")
|
||||
.min_width(96)
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![ExportDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Export").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: file_name },
|
||||
LayoutGroup::Row { widgets: export_type },
|
||||
LayoutGroup::Row { widgets: resolution },
|
||||
LayoutGroup::Row { widgets: export_area },
|
||||
LayoutGroup::Row { widgets: transparent_background },
|
||||
LayoutGroup::Row { widgets: button_widgets },
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Handles modal dialogs that appear as floating menus in the center of the editor window.
|
||||
//!
|
||||
//! Dialogs are represented as structs that implement the `LayoutHolder` trait.
|
||||
//! Dialogs are represented as structs that implement the `DialogLayoutHolder` trait.
|
||||
//!
|
||||
//! To open a dialog, call the function `register_properties` on the dialog struct with `responses` and the `LayoutTarget::DialogDetails` enum variant.
|
||||
//! To open a dialog, call the function `send_dialog_to_frontend()` on the dialog struct.
|
||||
//! Then dialog can be opened by sending the `FrontendMessage::DisplayDialog` message;
|
||||
|
||||
mod dialog_message;
|
||||
|
||||
+27
-22
@@ -42,26 +42,47 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
|
||||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
self.send_dialog_to_frontend(responses);
|
||||
}
|
||||
|
||||
advertise_actions! {NewDocumentDialogUpdate;}
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
const ICON: &'static str = "File";
|
||||
const TITLE: &'static str = "New Document";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![NewDocumentDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let title = vec![TextLabel::new("New Document").bold(true).widget_holder()];
|
||||
|
||||
let name = vec![
|
||||
TextLabel::new("Name").table_align(true).widget_holder(),
|
||||
TextLabel::new("Name").table_align(true).min_width(90).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(&self.name)
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into())
|
||||
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let infinite = vec![
|
||||
TextLabel::new("Infinite Canvas").table_align(true).widget_holder(),
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(self.infinite)
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
|
||||
@@ -69,7 +90,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
];
|
||||
|
||||
let scale = vec![
|
||||
TextLabel::new("Dimensions").table_align(true).widget_holder(),
|
||||
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(self.dimensions.x as f64))
|
||||
.label("W")
|
||||
@@ -94,26 +115,10 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let button_widgets = vec![
|
||||
TextButton::new("OK")
|
||||
.min_width(96)
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![NewDocumentDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row { widgets: title },
|
||||
LayoutGroup::Row { widgets: name },
|
||||
LayoutGroup::Row { widgets: infinite },
|
||||
LayoutGroup::Row { widgets: scale },
|
||||
LayoutGroup::Row { widgets: button_widgets },
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
+50
-23
@@ -11,19 +11,18 @@ impl MessageHandler<PreferencesDialogMessage, &PreferencesMessageHandler> for Pr
|
||||
PreferencesDialogMessage::Confirm => {}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::DialogDetails, preferences);
|
||||
self.send_dialog_to_frontend(responses, preferences);
|
||||
}
|
||||
|
||||
advertise_actions! {PreferencesDialogUpdate;}
|
||||
}
|
||||
|
||||
// This doesn't actually implement the `DialogLayoutHolder` trait like the other dialog message handlers.
|
||||
// That's because we need to give `send_layout` the `preferences` argument, which is not part of the trait.
|
||||
// However, it's important to keep the methods in sync with those from the trait for consistency.
|
||||
impl PreferencesDialogMessageHandler {
|
||||
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout(preferences),
|
||||
layout_target,
|
||||
})
|
||||
}
|
||||
const ICON: &'static str = "Settings";
|
||||
const TITLE: &'static str = "Editor Preferences";
|
||||
|
||||
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
|
||||
let zoom_with_scroll = vec![
|
||||
@@ -64,9 +63,32 @@ impl PreferencesDialogMessageHandler {
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let button_widgets = vec![
|
||||
TextButton::new("Ok")
|
||||
.min_width(96)
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row { widgets: zoom_with_scroll },
|
||||
LayoutGroup::Row { widgets: imaginate_server_hostname },
|
||||
LayoutGroup::Row { widgets: imaginate_refresh_frequency },
|
||||
]))
|
||||
}
|
||||
pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout(preferences),
|
||||
layout_target,
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
Layout::default()
|
||||
}
|
||||
fn send_layout_column_2(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout_column_2(),
|
||||
layout_target,
|
||||
});
|
||||
}
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
@@ -75,20 +97,25 @@ impl PreferencesDialogMessageHandler {
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Reset to Defaults")
|
||||
.min_width(96)
|
||||
.on_update(|_| PreferencesMessage::ResetToDefaults.into())
|
||||
.widget_holder(),
|
||||
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Editor Preferences").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: zoom_with_scroll },
|
||||
LayoutGroup::Row { widgets: imaginate_server_hostname },
|
||||
LayoutGroup::Row { widgets: imaginate_refresh_frequency },
|
||||
LayoutGroup::Row { widgets: button_widgets },
|
||||
]))
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
fn send_layout_buttons(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout_buttons(),
|
||||
layout_target,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn send_dialog_to_frontend(&self, responses: &mut VecDeque<Message>, preferences: &PreferencesMessageHandler) {
|
||||
self.send_layout(responses, LayoutTarget::DialogColumn1, preferences);
|
||||
self.send_layout_column_2(responses, LayoutTarget::DialogColumn2);
|
||||
self.send_layout_buttons(responses, LayoutTarget::DialogButtons);
|
||||
responses.add(FrontendMessage::DisplayDialog {
|
||||
icon: Self::ICON.into(),
|
||||
title: Self::TITLE.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,71 @@
|
||||
use crate::application::{commit_info_localized, release_series};
|
||||
use crate::application::commit_info_localized;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
/// A dialog for displaying information on [BuildMetadata] viewable via *Help* > *About Graphite* in the menu bar.
|
||||
pub struct AboutGraphiteDialog {
|
||||
pub localized_commit_date: String,
|
||||
pub localized_commit_year: String,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for AboutGraphiteDialog {
|
||||
const ICON: &'static str = "GraphiteLogo";
|
||||
const TITLE: &'static str = "About Graphite";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
let links = [
|
||||
("Website", "Website", "https://graphite.rs"),
|
||||
("Volunteer", "Volunteer", "https://graphite.rs/volunteer/"),
|
||||
("Credits", "Credits", "https://github.com/GraphiteEditor/Graphite/graphs/contributors"),
|
||||
];
|
||||
let mut widgets = links
|
||||
.into_iter()
|
||||
.map(|(icon, label, url)| {
|
||||
TextButton::new(label)
|
||||
.icon(Some(icon.into()))
|
||||
.no_background(true)
|
||||
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
|
||||
.widget_holder()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Cloning here and below seems to be necessary to appease the borrow checker, as far as I can tell.
|
||||
let localized_commit_year = self.localized_commit_year.clone();
|
||||
widgets.push(
|
||||
TextButton::new("Licenses")
|
||||
.icon(Some("License".into()))
|
||||
.no_background(true)
|
||||
.on_update(move |_| {
|
||||
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate {
|
||||
localized_commit_year: localized_commit_year.clone(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
);
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for AboutGraphiteDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
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)| TextButton::new(label).on_update(|_| FrontendMessage::TriggerVisitLink { url: url.to_string() }.into()).widget_holder())
|
||||
.collect();
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Graphite".to_string()).bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(release_series()).widget_holder()],
|
||||
widgets: vec![TextLabel::new("About this release").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: link_widgets },
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_holder()],
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,43 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
/// A dialog for confirming the closing of all documents viewable via `file -> close all` in the menu bar.
|
||||
pub struct CloseAllDocumentsDialog;
|
||||
pub struct CloseAllDocumentsDialog {
|
||||
pub unsaved_document_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for CloseAllDocumentsDialog {
|
||||
const ICON: &'static str = "Warning";
|
||||
const TITLE: &'static str = "Closing All Documents";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("Discard All")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for CloseAllDocumentsDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let discard = TextButton::new("Discard All")
|
||||
.min_width(96)
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let cancel = TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder();
|
||||
let unsaved_list = "• ".to_string() + &self.unsaved_document_names.join("\n• ");
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Close all documents?").multiline(true).widget_holder()],
|
||||
widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Unsaved work will be lost!").multiline(true).widget_holder()],
|
||||
widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: vec![discard, cancel] },
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ pub struct CloseDocumentDialog {
|
||||
pub document_id: u64,
|
||||
}
|
||||
|
||||
impl LayoutHolder for CloseDocumentDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let document_id = self.document_id;
|
||||
impl DialogLayoutHolder for CloseDocumentDialog {
|
||||
const ICON: &'static str = "Warning";
|
||||
const TITLE: &'static str = "Closing Document";
|
||||
|
||||
let button_widgets = vec![
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let document_id = self.document_id;
|
||||
let widgets = vec![
|
||||
TextButton::new("Save")
|
||||
.min_width(96)
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
@@ -24,7 +25,6 @@ impl LayoutHolder for CloseDocumentDialog {
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Discard")
|
||||
.min_width(96)
|
||||
.on_update(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
@@ -32,17 +32,22 @@ impl LayoutHolder for CloseDocumentDialog {
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextButton::new("Cancel").min_width(96).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for CloseDocumentDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Save changes before closing?").bold(true).widget_holder()],
|
||||
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(&self.document_name).multiline(true).widget_holder()],
|
||||
widgets: vec![TextLabel::new(format!("\"{}\" has unsaved changes", self.document_name)).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: button_widgets },
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
/// A dialog to notify users of an unfinished issue, optionally with an issue number.
|
||||
pub struct ComingSoonDialog {
|
||||
pub issue: Option<i32>,
|
||||
}
|
||||
|
||||
impl LayoutHolder for ComingSoonDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut details = "This feature is not implemented yet".to_string();
|
||||
impl DialogLayoutHolder for ComingSoonDialog {
|
||||
const ICON: &'static str = "Delay";
|
||||
const TITLE: &'static str = "Coming Soon";
|
||||
|
||||
let mut buttons = vec![TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.min_width(96)
|
||||
.on_update(|_| FrontendMessage::DisplayDialogDismiss.into())
|
||||
.widget_holder()];
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for ComingSoonDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let header = vec![TextLabel::new("You've stumbled upon a placeholder").bold(true).widget_holder()];
|
||||
let row1 = vec![TextLabel::new("This feature is not implemented yet.").widget_holder()];
|
||||
|
||||
let mut rows = vec![LayoutGroup::Row { widgets: header }, LayoutGroup::Row { widgets: row1 }];
|
||||
|
||||
if let Some(issue) = self.issue {
|
||||
let _ = write!(details, "— but you can help add it!\nSee issue #{issue} on GitHub.");
|
||||
buttons.push(
|
||||
TextButton::new(format!("Issue #{issue}"))
|
||||
.min_width(96)
|
||||
.on_update(move |_| {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
);
|
||||
let row2 = vec![TextLabel::new("But you can help build it! Visit its issue:").widget_holder()];
|
||||
let row3 = vec![TextButton::new(format!("GitHub Issue #{issue}"))
|
||||
.icon(Some("Website".into()))
|
||||
.no_background(true)
|
||||
.on_update(move |_| {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: format!("https://github.com/GraphiteEditor/Graphite/issues/{issue}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()];
|
||||
|
||||
rows.push(LayoutGroup::Row { widgets: row2 });
|
||||
rows.push(LayoutGroup::Row { widgets: row3 });
|
||||
}
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Coming soon").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(details).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: buttons },
|
||||
]))
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(rows))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,38 @@ use crate::messages::prelude::*;
|
||||
/// A dialog to let the user browse a gallery of demo artwork that can be opened.
|
||||
pub struct DemoArtworkDialog;
|
||||
|
||||
const ARTWORK: [(&str, &str, &str); 2] = [
|
||||
(
|
||||
"Valley of Spires",
|
||||
"ThumbnailValleyOfSpires",
|
||||
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/valley-of-spires.graphite",
|
||||
),
|
||||
(
|
||||
"Just a Potted Cactus",
|
||||
"ThumbnailJustAPottedCactus",
|
||||
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/just-a-potted-cactus.graphite",
|
||||
),
|
||||
];
|
||||
|
||||
impl DialogLayoutHolder for DemoArtworkDialog {
|
||||
const ICON: &'static str = "Image";
|
||||
const TITLE: &'static str = "Demo Artwork";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for DemoArtworkDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let artwork = [
|
||||
(
|
||||
"Valley of Spires",
|
||||
"ThumbnailValleyOfSpires",
|
||||
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/valley-of-spires.graphite",
|
||||
),
|
||||
(
|
||||
"Just a Potted Cactus",
|
||||
"ThumbnailJustAPottedCactus",
|
||||
"https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/just-a-potted-cactus.graphite",
|
||||
),
|
||||
];
|
||||
|
||||
let image_widgets = artwork
|
||||
let images = ARTWORK
|
||||
.into_iter()
|
||||
.map(|(_, thumbnail, _)| ImageLabel::new(thumbnail.to_string()).width(Some("256px".into())).widget_holder())
|
||||
.collect();
|
||||
|
||||
let button_widgets = artwork
|
||||
let buttons = ARTWORK
|
||||
.into_iter()
|
||||
.map(|(label, _, url)| {
|
||||
TextButton::new(label)
|
||||
@@ -39,12 +50,6 @@ impl LayoutHolder for DemoArtworkDialog {
|
||||
})
|
||||
.collect();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Demo Artwork".to_string()).bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row { widgets: image_widgets },
|
||||
LayoutGroup::Row { widgets: button_widgets },
|
||||
]))
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ pub struct ErrorDialog {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for ErrorDialog {
|
||||
const ICON: &'static str = "Warning";
|
||||
const TITLE: &'static str = "Error";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for ErrorDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
@@ -16,13 +27,6 @@ impl LayoutHolder for ErrorDialog {
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(&self.description).multiline(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.min_width(96)
|
||||
.on_update(|_| FrontendMessage::DisplayDialogDismiss.into())
|
||||
.widget_holder()],
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub struct LicensesDialog {
|
||||
pub localized_commit_year: String,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for LicensesDialog {
|
||||
const ICON: &'static str = "License12px";
|
||||
const TITLE: &'static str = "Licenses";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md";
|
||||
let links = [
|
||||
("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"),
|
||||
("IconsGrid", "Graphite Icons", icons_license_link),
|
||||
("License", "Graphite License", "https://graphite.rs/license/"),
|
||||
("License", "Other Licenses", "/third-party-licenses.txt"),
|
||||
];
|
||||
let widgets = links
|
||||
.into_iter()
|
||||
.map(|(icon, label, url)| {
|
||||
TextButton::new(label)
|
||||
.icon(Some(icon.into()))
|
||||
.no_background(true)
|
||||
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
|
||||
.widget_holder()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for LicensesDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let description = concat!(
|
||||
"The Graphite logo and brand identity are copyright © [YEAR]\nGraphite Labs, LLC. See \"Graphite Logo\" for usage policy.",
|
||||
"\n\n",
|
||||
"The Graphite editor's icons and design assets are copyright\n© [YEAR] Graphite Labs, LLC. See \"Graphite Icons\" for details.",
|
||||
"\n\n",
|
||||
"Graphite code is copyright © [YEAR] Graphite contributors\nand is made available under the Apache 2.0 license. See\n\"Graphite License\" for details.",
|
||||
"\n\n",
|
||||
"Graphite is distributed with third-party open source code\ndependencies. See \"Other Licenses\" for details.",
|
||||
)
|
||||
.replace("[YEAR]", &self.localized_commit_year);
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(description).multiline(true).widget_holder()],
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod close_document_dialog;
|
||||
mod coming_soon_dialog;
|
||||
mod demo_artwork_dialog;
|
||||
mod error_dialog;
|
||||
mod licenses_dialog;
|
||||
|
||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
||||
@@ -11,3 +12,4 @@ pub use close_document_dialog::CloseDocumentDialog;
|
||||
pub use coming_soon_dialog::ComingSoonDialog;
|
||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||
pub use error_dialog::ErrorDialog;
|
||||
pub use licenses_dialog::LicensesDialog;
|
||||
|
||||
@@ -18,14 +18,13 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum FrontendMessage {
|
||||
// Display prefix: make the frontend show something, like a dialog
|
||||
DisplayDialog {
|
||||
title: String,
|
||||
icon: String,
|
||||
},
|
||||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic {
|
||||
#[serde(rename = "panicInfo")]
|
||||
panic_info: String,
|
||||
header: String,
|
||||
description: String,
|
||||
},
|
||||
DisplayEditableTextbox {
|
||||
text: String,
|
||||
@@ -122,7 +121,17 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: u64,
|
||||
},
|
||||
UpdateDialogDetails {
|
||||
UpdateDialogButtons {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDialogColumn1 {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDialogColumn2 {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
|
||||
@@ -289,7 +289,9 @@ impl LayoutMessageHandler {
|
||||
|
||||
#[remain::sorted]
|
||||
let message = match layout_target {
|
||||
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { layout_target, diff },
|
||||
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
|
||||
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
|
||||
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
|
||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
|
||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
||||
LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff },
|
||||
|
||||
@@ -15,8 +15,12 @@ use std::sync::Arc;
|
||||
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize, specta::Type)]
|
||||
#[repr(u8)]
|
||||
pub enum LayoutTarget {
|
||||
/// Contains the contents of the dialog, including the title and action buttons. Must be shown with the `FrontendMessage::DisplayDialog` message.
|
||||
DialogDetails,
|
||||
/// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message.
|
||||
DialogButtons,
|
||||
/// Contains the contents of the dialog's primary column. Must be shown with the `FrontendMessage::DisplayDialog` message.
|
||||
DialogColumn1,
|
||||
/// Contains the contents of the dialog's secondary column (often blank). Must be shown with the `FrontendMessage::DisplayDialog` message.
|
||||
DialogColumn2,
|
||||
/// Contains the widgets located directly above the canvas to the right, for example the zoom in and out buttons.
|
||||
DocumentBar,
|
||||
/// Contains the dropdown for design / select / guide mode found on the top left of the canvas.
|
||||
@@ -56,6 +60,40 @@ pub trait LayoutHolder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Structs implementing this hold the layout (like [`LayoutHolder`]) for dialog content, but it also requires defining the dialog's title, icon, and action buttons.
|
||||
pub trait DialogLayoutHolder: LayoutHolder {
|
||||
const ICON: &'static str;
|
||||
const TITLE: &'static str;
|
||||
|
||||
fn layout_buttons(&self) -> Layout;
|
||||
fn send_layout_buttons(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout_buttons(),
|
||||
layout_target,
|
||||
});
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
Layout::default()
|
||||
}
|
||||
fn send_layout_column_2(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.layout_column_2(),
|
||||
layout_target,
|
||||
});
|
||||
}
|
||||
|
||||
fn send_dialog_to_frontend(&self, responses: &mut VecDeque<Message>) {
|
||||
self.send_layout(responses, LayoutTarget::DialogColumn1);
|
||||
self.send_layout_column_2(responses, LayoutTarget::DialogColumn2);
|
||||
self.send_layout_buttons(responses, LayoutTarget::DialogButtons);
|
||||
responses.add(FrontendMessage::DisplayDialog {
|
||||
icon: Self::ICON.into(),
|
||||
title: Self::TITLE.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a choice of layout type. The chosen layout contains an arrangement of widgets mounted somewhere specific in the frontend.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)]
|
||||
pub enum Layout {
|
||||
|
||||
@@ -84,6 +84,9 @@ pub struct TextButton {
|
||||
|
||||
pub icon: Option<String>,
|
||||
|
||||
#[serde(rename = "noBackground")]
|
||||
pub no_background: bool,
|
||||
|
||||
pub emphasized: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
|
||||
@@ -140,8 +140,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||
document_name: target_document.name.clone(),
|
||||
document_id,
|
||||
};
|
||||
dialog.send_layout(responses, LayoutTarget::DialogDetails);
|
||||
responses.add(FrontendMessage::DisplayDialog { icon: "File".to_string() });
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
|
||||
// Select the document being closed
|
||||
responses.add(PortfolioMessage::SelectDocument { document_id });
|
||||
@@ -598,6 +597,10 @@ impl PortfolioMessageHandler {
|
||||
self.active_document_id
|
||||
}
|
||||
|
||||
pub fn unsaved_document_names(&self) -> Vec<String> {
|
||||
self.documents.values().filter(|document| !document.is_saved()).map(|document| document.name.clone()).collect()
|
||||
}
|
||||
|
||||
pub fn generate_new_document_name(&self) -> String {
|
||||
let mut doc_title_numbers = self
|
||||
.ordered_document_iterator()
|
||||
|
||||
@@ -80,39 +80,40 @@ impl ToolMetadata for PathTool {
|
||||
|
||||
impl LayoutHolder for PathTool {
|
||||
fn layout(&self) -> Layout {
|
||||
if let Some(SingleSelectedPoint { coordinates: DVec2 { x, y }, .. }) = self.tool_data.single_selected_point {
|
||||
let x_location = NumberInput::new(Some(x))
|
||||
.unit(" px")
|
||||
.label("X")
|
||||
.min_width(120)
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
let new_x = number_input.value.unwrap_or(x);
|
||||
PathToolMessage::SelectedPointXChanged { new_x }.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let coordinates = self.tool_data.single_selected_point.as_ref().map(|point| point.coordinates);
|
||||
let (x, y) = coordinates.map(|point| (Some(point.x), Some(point.y))).unwrap_or((None, None));
|
||||
|
||||
let y_location = NumberInput::new(Some(y))
|
||||
.unit(" px")
|
||||
.label("Y")
|
||||
.min_width(120)
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
let new_y = number_input.value.unwrap_or(y);
|
||||
PathToolMessage::SelectedPointYChanged { new_y }.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let x_location = NumberInput::new(x)
|
||||
.unit(" px")
|
||||
.label("X")
|
||||
.min_width(120)
|
||||
.disabled(x.is_none())
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
let new_x = number_input.value.unwrap_or(x.unwrap());
|
||||
PathToolMessage::SelectedPointXChanged { new_x }.into()
|
||||
})
|
||||
.widget_holder();
|
||||
|
||||
let seperator = Separator::new(SeparatorType::Unrelated).widget_holder();
|
||||
let y_location = NumberInput::new(y)
|
||||
.unit(" px")
|
||||
.label("Y")
|
||||
.min_width(120)
|
||||
.disabled(y.is_none())
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
let new_y = number_input.value.unwrap_or(y.unwrap());
|
||||
PathToolMessage::SelectedPointYChanged { new_y }.into()
|
||||
})
|
||||
.widget_holder();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![x_location, seperator, y_location],
|
||||
}]))
|
||||
} else {
|
||||
Layout::WidgetLayout(WidgetLayout::default())
|
||||
}
|
||||
let seperator = Separator::new(SeparatorType::Related).widget_holder();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![x_location, seperator, y_location],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,8 @@ impl LayoutHolder for SelectTool {
|
||||
|
||||
let selected_layers_count = self.tool_data.selected_layers_count;
|
||||
let deactivate_alignment = selected_layers_count < 2;
|
||||
let deactivate_boolean_ops = selected_layers_count < 2;
|
||||
let deactivate_flip = selected_layers_count < 1;
|
||||
let deactivate_pivot = selected_layers_count < 1;
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
@@ -207,41 +209,48 @@ impl LayoutHolder for SelectTool {
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Align", "Coming soon").widget_holder(),
|
||||
PopoverButton::new("Align", "Coming soon").disabled(deactivate_alignment).widget_holder(),
|
||||
Separator::new(SeparatorType::Section).widget_holder(),
|
||||
IconButton::new("FlipHorizontal", 24)
|
||||
.tooltip("Flip Horizontal")
|
||||
.disabled(deactivate_flip)
|
||||
.on_update(|_| SelectToolMessage::FlipHorizontal.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("FlipVertical", 24)
|
||||
.tooltip("Flip Vertical")
|
||||
.disabled(deactivate_flip)
|
||||
.on_update(|_| SelectToolMessage::FlipVertical.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Flip", "Coming soon").widget_holder(),
|
||||
PopoverButton::new("Flip", "Coming soon").disabled(deactivate_flip).widget_holder(),
|
||||
Separator::new(SeparatorType::Section).widget_holder(),
|
||||
IconButton::new("BooleanUnion", 24)
|
||||
.tooltip("Boolean Union (coming soon)")
|
||||
.tooltip("Coming Soon: Boolean Union")
|
||||
.disabled(deactivate_boolean_ops)
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("BooleanSubtractFront", 24)
|
||||
.tooltip("Boolean Subtract Front (coming soon)")
|
||||
.tooltip("Coming Soon: Boolean Subtract Front")
|
||||
.disabled(deactivate_boolean_ops)
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("BooleanSubtractBack", 24)
|
||||
.tooltip("Boolean Subtract Back (coming soon)")
|
||||
.tooltip("Coming Soon: Boolean Subtract Back")
|
||||
.disabled(deactivate_boolean_ops)
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("BooleanIntersect", 24)
|
||||
.tooltip("Boolean Intersect (coming soon)")
|
||||
.tooltip("Coming Soon: Boolean Intersect")
|
||||
.disabled(deactivate_boolean_ops)
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("BooleanDifference", 24)
|
||||
.tooltip("Boolean Difference (coming soon)")
|
||||
.tooltip("Coming Soon: Boolean Difference")
|
||||
.disabled(deactivate_boolean_ops)
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Boolean", "Coming soon").widget_holder(),
|
||||
PopoverButton::new("Boolean Operations", "Coming soon").disabled(deactivate_boolean_ops).widget_holder(),
|
||||
],
|
||||
}]))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user