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:
mfish33 2021-11-30 10:06:07 -08:00 committed by Keavon Chambers
parent fa64cfad4b
commit 5f248cd176
9 changed files with 88 additions and 52 deletions

View file

@ -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)))

View file

@ -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 => {

View file

@ -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 },

View file

@ -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>;

View file

@ -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" />

View file

@ -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];
}
});

View file

@ -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 {

View file

@ -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);
}

View file

@ -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 {