mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 21:08:18 +00:00
Display Asterisk on Unsaved Documents (#392)
* ability to mark an open document as unsaved * unsaved detection now being triggered based on layer tree height * - rust implementation of unsaved markers - upgraded eslint * updated eslint in package.json * - Renamed GetOpenDocumentsList -> UpdateOpenDocumentsList - is not -> was not * changed hash to current identifier to better reflect its meaning * resolve some merge conflicts * removed console.log statement leftover from debuging
This commit is contained in:
parent
fa64cfad4b
commit
5f248cd176
9 changed files with 88 additions and 52 deletions
|
@ -59,8 +59,9 @@ pub struct VectorManipulatorShape {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentMessageHandler {
|
||||
pub graphene_document: GrapheneDocument,
|
||||
pub document_history: Vec<DocumentSave>,
|
||||
pub document_undo_history: Vec<DocumentSave>,
|
||||
pub document_redo_history: Vec<DocumentSave>,
|
||||
pub saved_document_identifier: u64,
|
||||
pub name: String,
|
||||
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
||||
movement_handler: MovementMessageHandler,
|
||||
|
@ -72,9 +73,10 @@ impl Default for DocumentMessageHandler {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
graphene_document: GrapheneDocument::default(),
|
||||
document_history: Vec::new(),
|
||||
document_undo_history: Vec::new(),
|
||||
document_redo_history: Vec::new(),
|
||||
name: String::from("Untitled Document"),
|
||||
saved_document_identifier: 0,
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
transform_layer_handler: TransformLayerMessageHandler::default(),
|
||||
|
@ -305,8 +307,9 @@ impl DocumentMessageHandler {
|
|||
pub fn with_name(name: String) -> Self {
|
||||
Self {
|
||||
graphene_document: GrapheneDocument::default(),
|
||||
document_history: Vec::new(),
|
||||
document_undo_history: Vec::new(),
|
||||
document_redo_history: Vec::new(),
|
||||
saved_document_identifier: 0,
|
||||
name,
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
|
@ -332,23 +335,30 @@ impl DocumentMessageHandler {
|
|||
layer_data(&mut self.layer_data, path)
|
||||
}
|
||||
|
||||
pub fn backup(&mut self) {
|
||||
pub fn backup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.document_redo_history.clear();
|
||||
let new_layer_data = self
|
||||
.layer_data
|
||||
.iter()
|
||||
.filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||
.collect();
|
||||
self.document_history.push((self.graphene_document.clone_without_overlays(), new_layer_data))
|
||||
self.document_undo_history.push((self.graphene_document.clone_without_overlays(), new_layer_data));
|
||||
|
||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> Result<(), EditorError> {
|
||||
self.backup();
|
||||
self.undo()
|
||||
pub fn rollback(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> {
|
||||
self.backup(responses);
|
||||
self.undo(responses)
|
||||
// TODO: Consider if we should check if the document is saved
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Result<(), EditorError> {
|
||||
match self.document_history.pop() {
|
||||
pub fn undo(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> {
|
||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
||||
|
||||
match self.document_undo_history.pop() {
|
||||
Some((document, layer_data)) => {
|
||||
let document = std::mem::replace(&mut self.graphene_document, document);
|
||||
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
||||
|
@ -359,7 +369,10 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Result<(), EditorError> {
|
||||
pub fn redo(&mut self, responses: &mut VecDeque<Message>) -> Result<(), EditorError> {
|
||||
// Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents
|
||||
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
||||
|
||||
match self.document_redo_history.pop() {
|
||||
Some((document, layer_data)) => {
|
||||
let document = std::mem::replace(&mut self.graphene_document, document);
|
||||
|
@ -368,13 +381,26 @@ impl DocumentMessageHandler {
|
|||
.iter()
|
||||
.filter_map(|(key, value)| (!self.graphene_document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||
.collect();
|
||||
self.document_history.push((document.clone_without_overlays(), new_layer_data));
|
||||
self.document_undo_history.push((document.clone_without_overlays(), new_layer_data));
|
||||
Ok(())
|
||||
}
|
||||
None => Err(EditorError::NoTransactionInProgress),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_identifier(&self) -> u64 {
|
||||
// We can use the last state of the document to serve as the identifier to compare against
|
||||
// This is useful since when the document is empty the identifier will be 0
|
||||
self.document_undo_history
|
||||
.last()
|
||||
.map(|(graphene_document, _)| graphene_document.current_state_identifier())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn is_saved(&self) -> bool {
|
||||
self.current_identifier() == self.saved_document_identifier
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
||||
let layer = self.graphene_document.layer(&path)?;
|
||||
|
@ -421,13 +447,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
.transform_layer_handler
|
||||
.process_action(message, (&mut self.layer_data, &mut self.graphene_document, ipp), responses),
|
||||
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
|
||||
StartTransaction => self.backup(),
|
||||
StartTransaction => self.backup(responses),
|
||||
RollbackTransaction => {
|
||||
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e));
|
||||
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||
}
|
||||
AbortTransaction => {
|
||||
self.undo().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e));
|
||||
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||
}
|
||||
CommitTransaction => (),
|
||||
|
@ -455,6 +481,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
)
|
||||
}
|
||||
SaveDocument => {
|
||||
self.saved_document_identifier = self.current_identifier();
|
||||
// Update the save status of the just saved document
|
||||
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
||||
|
||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => self.name.clone(),
|
||||
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
||||
|
@ -494,13 +524,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(DocumentMessage::SetSelectedLayers(vec![new_folder_path]).into());
|
||||
}
|
||||
SetBlendModeForSelectedLayers(blend_mode) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
}
|
||||
}
|
||||
SetOpacityForSelectedLayers(opacity) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
|
@ -520,7 +550,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(ToolMessage::SelectedLayersChanged.into());
|
||||
}
|
||||
DeleteSelectedLayers => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
responses.push_front(ToolMessage::SelectedLayersChanged.into());
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
responses.push_front(DocumentOperation::DeleteLayer { path }.into());
|
||||
|
@ -533,7 +563,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
for path in self.selected_layers_sorted() {
|
||||
responses.push_back(DocumentOperation::DuplicateLayer { path }.into());
|
||||
}
|
||||
|
@ -564,8 +594,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_front(SetSelectedLayers(all_layer_paths).into());
|
||||
}
|
||||
DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()),
|
||||
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
Undo => {
|
||||
responses.push_back(SelectMessage::Abort.into());
|
||||
responses.push_back(DocumentHistoryBackward.into());
|
||||
|
@ -666,7 +696,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
let operation = DocumentOperation::TransformLayerInViewport {
|
||||
path,
|
||||
|
@ -682,7 +712,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(DocumentsMessage::PasteIntoFolder { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_position) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
let all_layer_paths = self.all_layers_sorted();
|
||||
let selected_layers = self.selected_layers_sorted();
|
||||
if let Some(pivot) = match relative_position.signum() {
|
||||
|
@ -717,7 +747,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
FlipSelectedLayers(axis) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
let scale = match axis {
|
||||
FlipAxis::X => DVec2::new(-1., 1.),
|
||||
FlipAxis::Y => DVec2::new(1., -1.),
|
||||
|
@ -739,7 +769,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
AlignSelectedLayers(axis, aggregate) => {
|
||||
self.backup();
|
||||
self.backup(responses);
|
||||
let (paths, boxes): (Vec<_>, Vec<_>) = self
|
||||
.selected_layers()
|
||||
.filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?.map(|b| (path, b)))
|
||||
|
|
|
@ -28,7 +28,7 @@ pub enum DocumentsMessage {
|
|||
NewDocument,
|
||||
OpenDocument,
|
||||
OpenDocumentFile(String, String),
|
||||
GetOpenDocumentsList,
|
||||
UpdateOpenDocumentsList,
|
||||
NextDocument,
|
||||
PrevDocument,
|
||||
}
|
||||
|
@ -55,9 +55,7 @@ impl DocumentsMessageHandler {
|
|||
|
||||
fn generate_new_document_name(&self) -> String {
|
||||
let mut doc_title_numbers = self
|
||||
.document_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.documents.get(&id))
|
||||
.ordered_document_iterator()
|
||||
.map(|doc| {
|
||||
doc.name
|
||||
.rsplit_once(DEFAULT_DOCUMENT_NAME)
|
||||
|
@ -85,7 +83,11 @@ impl DocumentsMessageHandler {
|
|||
self.documents.insert(self.document_id_counter, new_document);
|
||||
|
||||
// Send the new list of document tab names
|
||||
let open_documents = self.document_ids.iter().filter_map(|id| self.documents.get(&id).map(|doc| doc.name.clone())).collect::<Vec<String>>();
|
||||
let open_documents = self
|
||||
.document_ids
|
||||
.iter()
|
||||
.filter_map(|id| self.documents.get(&id).map(|doc| (doc.name.clone(), doc.is_saved())))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||
|
||||
|
@ -96,6 +98,11 @@ impl DocumentsMessageHandler {
|
|||
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an iterator over the open documents in order
|
||||
pub fn ordered_document_iterator(&self) -> impl Iterator<Item = &DocumentMessageHandler> {
|
||||
self.document_ids.iter().map(|id| self.documents.get(id).expect("document id was not found in the document hashmap"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DocumentsMessageHandler {
|
||||
|
@ -170,7 +177,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
};
|
||||
|
||||
// Send the new list of document tab names
|
||||
let open_documents = self.document_ids.iter().filter_map(|id| self.documents.get(&id).map(|doc| doc.name.clone())).collect();
|
||||
let open_documents = self.ordered_document_iterator().map(|doc| (doc.name.clone(), doc.is_saved())).collect();
|
||||
|
||||
// Update the list of new documents on the front end, active tab, and ensure that document renders
|
||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||
|
@ -209,9 +216,9 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
),
|
||||
}
|
||||
}
|
||||
GetOpenDocumentsList => {
|
||||
UpdateOpenDocumentsList => {
|
||||
// Send the list of document tab names
|
||||
let open_documents = self.documents.values().map(|doc| doc.name.clone()).collect();
|
||||
let open_documents = self.ordered_document_iterator().map(|doc| (doc.name.clone(), doc.is_saved())).collect();
|
||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||
}
|
||||
NextDocument => {
|
||||
|
|
|
@ -10,7 +10,7 @@ pub enum FrontendMessage {
|
|||
DisplayFolderTreeStructure { data_buffer: RawBuffer },
|
||||
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
||||
SetActiveDocument { document_index: usize },
|
||||
UpdateOpenDocumentsList { open_documents: Vec<String> },
|
||||
UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> },
|
||||
DisplayError { title: String, description: String },
|
||||
DisplayPanic { panic_info: String, title: String, description: String },
|
||||
DisplayConfirmationToCloseDocument { document_index: usize },
|
||||
|
|
|
@ -393,7 +393,6 @@ export default defineComponent({
|
|||
registerResponseHandler(ResponseType.DisplayFolderTreeStructure, (responseData: Response) => {
|
||||
const expandData = responseData as DisplayFolderTreeStructure;
|
||||
if (!expandData) return;
|
||||
console.log(expandData);
|
||||
|
||||
const path = [] as Array<bigint>;
|
||||
this.layers = [] as Array<LayerPanelEntry>;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<MenuBarInput v-if="platform !== ApplicationPlatform.Mac" />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowTitle :title="`${documents.title}${documents.unsaved ? '*' : ''} - Graphite`" />
|
||||
<WindowTitle :title="`${documents.activeDocument} - Graphite`" />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowButtonsWindows :maximized="maximized" v-if="platform === ApplicationPlatform.Windows || platform === ApplicationPlatform.Linux" />
|
||||
|
|
|
@ -17,10 +17,11 @@ import { panicProxy } from "@/utilities/panic-proxy";
|
|||
const wasm = import("@/../wasm/pkg").then(panicProxy);
|
||||
|
||||
const state = reactive({
|
||||
title: "",
|
||||
unsaved: false,
|
||||
documents: [] as Array<string>,
|
||||
documents: [] as string[],
|
||||
activeDocumentIndex: 0,
|
||||
get activeDocument() {
|
||||
return this.documents[this.activeDocumentIndex];
|
||||
},
|
||||
});
|
||||
|
||||
export async function selectDocument(tabIndex: number) {
|
||||
|
@ -83,17 +84,13 @@ export default readonly(state);
|
|||
|
||||
registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Response) => {
|
||||
const documentListData = responseData as UpdateOpenDocumentsList;
|
||||
if (documentListData) {
|
||||
state.documents = documentListData.open_documents;
|
||||
state.title = state.documents[state.activeDocumentIndex];
|
||||
}
|
||||
state.documents = documentListData.open_documents.map(({ name, isSaved }) => `${name}${isSaved ? "" : "*"}`);
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
||||
const documentData = responseData as SetActiveDocument;
|
||||
if (documentData) {
|
||||
state.activeDocumentIndex = documentData.document_index;
|
||||
state.title = state.documents[state.activeDocumentIndex];
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -119,10 +119,11 @@ export type Response =
|
|||
| DisplayConfirmationToCloseAllDocuments;
|
||||
|
||||
export interface UpdateOpenDocumentsList {
|
||||
open_documents: Array<string>;
|
||||
open_documents: { name: string; isSaved: boolean }[];
|
||||
}
|
||||
function newUpdateOpenDocumentsList(input: any): UpdateOpenDocumentsList {
|
||||
return { open_documents: input.open_documents };
|
||||
const openDocuments = input.open_documents.map((docData: [string, boolean]) => ({ name: docData[0], isSaved: docData[1] }));
|
||||
return { open_documents: openDocuments };
|
||||
}
|
||||
|
||||
export interface Color {
|
||||
|
|
|
@ -88,7 +88,7 @@ pub fn select_document(document: usize) {
|
|||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_open_documents_list() {
|
||||
let message = DocumentsMessage::GetOpenDocumentsList;
|
||||
let message = DocumentsMessage::UpdateOpenDocumentsList;
|
||||
dispatch(message);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,15 +14,17 @@ use crate::{
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Document {
|
||||
pub root: Layer,
|
||||
/// The state_identifier serves to provide a way to uniquely identify a particular state that the document is in.
|
||||
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
|
||||
#[serde(skip)]
|
||||
pub hasher: DefaultHasher,
|
||||
pub state_identifier: DefaultHasher,
|
||||
}
|
||||
|
||||
impl Default for Document {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()),
|
||||
hasher: DefaultHasher::new(),
|
||||
state_identifier: DefaultHasher::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +40,8 @@ impl Document {
|
|||
self.root.cache.clone()
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> u64 {
|
||||
self.hasher.finish()
|
||||
pub fn current_state_identifier(&self) -> u64 {
|
||||
self.state_identifier.finish()
|
||||
}
|
||||
|
||||
pub fn serialize_document(&self) -> String {
|
||||
|
@ -309,7 +311,7 @@ impl Document {
|
|||
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
|
||||
/// reaction from the frontend, responses may be returned.
|
||||
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
operation.pseudo_hash().hash(&mut self.hasher);
|
||||
operation.pseudo_hash().hash(&mut self.state_identifier);
|
||||
use DocumentResponse::*;
|
||||
|
||||
let responses = match &operation {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue