Add focus document toggle (#3672)

This commit is contained in:
Timon 2026-01-23 16:28:02 +01:00 committed by GitHub
parent 2be7790d4d
commit 7a5790744f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 58 deletions

View File

@ -232,6 +232,7 @@ impl Dispatcher {
Message::MenuBar(message) => {
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
menu_bar_message_handler.focus_document = self.message_handlers.portfolio_message_handler.focus_document;
menu_bar_message_handler.data_panel_open = self.message_handlers.portfolio_message_handler.data_panel_open;
menu_bar_message_handler.layers_panel_open = self.message_handlers.portfolio_message_handler.layers_panel_open;
menu_bar_message_handler.properties_panel_open = self.message_handlers.portfolio_message_handler.properties_panel_open;

View File

@ -334,7 +334,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: true }),
entry!(KeyDown(KeyC); modifiers=[Alt, Shift], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: false }),
entry!(KeyDownNoRepeat(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath),
// TODO: Change to KeyDownNoRepeat when https://github.com/GraphiteEditor/Graphite/issues/2266 is resolved
entry!(KeyDown(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath),
//
// DocumentMessage
entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle),
@ -448,6 +449,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
entry!(KeyDown(Enter); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleFocusDocument),
//
// DialogMessage
entry!(KeyDown(KeyE); modifiers=[Accel], action_dispatch=DialogMessage::RequestExportDialog),

View File

@ -21,6 +21,7 @@ pub struct MenuBarMessageHandler {
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
pub focus_document: bool,
}
#[message_handler_data]
@ -628,23 +629,31 @@ impl LayoutHolder for MenuBarMessageHandler {
.icon("FullscreenEnter")
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Fullscreen))
.on_commit(|_| AppWindowMessage::Fullscreen.into()),
MenuListEntry::new("Focus Document")
.label("Focus Document")
.icon(if self.focus_document { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleFocusDocument))
.on_commit(|_| PortfolioMessage::ToggleFocusDocument.into()),
],
vec![
MenuListEntry::new("Properties")
.label("Properties")
.icon(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen))
.on_commit(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()),
.on_commit(|_| PortfolioMessage::TogglePropertiesPanelOpen.into())
.disabled(self.focus_document),
MenuListEntry::new("Layers")
.label("Layers")
.icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen))
.on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
.on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into())
.disabled(self.focus_document),
MenuListEntry::new("Data")
.label("Data")
.icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleDataPanelOpen))
.on_commit(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
.on_commit(|_| PortfolioMessage::ToggleDataPanelOpen.into())
.disabled(self.focus_document),
],
])
.widget_instance(),

View File

@ -316,8 +316,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}
DocumentMessage::ClearLayersPanel => {
// Send an empty layer list
let data_buffer: RawBuffer = Self::default().serialize_root();
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
if layers_panel_open {
let data_buffer: RawBuffer = Self::default().serialize_root();
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
}
// Clear the control bar
responses.add(LayoutMessage::SendLayout {

View File

@ -149,6 +149,7 @@ pub enum PortfolioMessage {
ignore_hash: bool,
},
ToggleResetNodesToDefinitionsOnOpen,
ToggleFocusDocument,
ToggleDataPanelOpen,
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,

View File

@ -60,11 +60,12 @@ pub struct PortfolioMessageHandler {
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
pub reset_node_definitions_on_open: bool,
pub data_panel_open: bool,
#[derivative(Default(value = "true"))]
pub layers_panel_open: bool,
pub focus_document: bool,
#[derivative(Default(value = "true"))]
pub properties_panel_open: bool,
#[derivative(Default(value = "true"))]
pub layers_panel_open: bool,
pub data_panel_open: bool,
}
#[message_handler_data]
@ -94,9 +95,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
viewport,
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
data_panel_open: self.data_panel_open && !self.focus_document,
layers_panel_open: self.layers_panel_open && !self.focus_document,
properties_panel_open: self.properties_panel_open && !self.focus_document,
};
document.process_message(message, responses, document_inputs)
}
@ -158,9 +159,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
viewport,
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
data_panel_open: self.data_panel_open && !self.focus_document,
layers_panel_open: self.layers_panel_open && !self.focus_document,
properties_panel_open: self.properties_panel_open && !self.focus_document,
};
document.process_message(message, responses, document_inputs)
}
@ -452,7 +453,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
}
self.load_document(new_document, document_id, self.layers_panel_open, responses, false);
self.load_document(new_document, document_id, responses, false);
responses.add(PortfolioMessage::SelectDocument { document_id });
}
PortfolioMessage::NextDocument => {
@ -655,7 +656,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
// Load the document into the portfolio so it opens in the editor
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
self.load_document(document, document_id, responses, to_front);
if select_after_open {
responses.add(PortfolioMessage::SelectDocument { document_id });
@ -1150,7 +1151,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let node_to_inspect = self.node_to_inspect();
let Some(document) = self.documents.get_mut(&document_id) else {
log::error!("Tried to render non-existent document");
log::error!("Tried to render non-existent document {:?}", document_id);
return;
};
@ -1177,7 +1178,101 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
Ok(message) => responses.add_front(message),
}
}
PortfolioMessage::ToggleFocusDocument => {
self.focus_document = !self.focus_document;
responses.add(MenuBarMessage::SendLayout);
if self.focus_document {
if self.properties_panel_open {
responses.add(PropertiesPanelMessage::Clear);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: false });
}
if self.layers_panel_open {
responses.add(DocumentMessage::ClearLayersPanel);
responses.add(FrontendMessage::UpdateLayersPanelState { open: false });
}
if self.data_panel_open {
responses.add(DataPanelMessage::ClearLayout);
responses.add(FrontendMessage::UpdateDataPanelState { open: false });
}
} else {
if self.properties_panel_open {
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: true });
}
if self.layers_panel_open {
responses.add(FrontendMessage::UpdateLayersPanelState { open: true });
}
if self.data_panel_open {
responses.add(FrontendMessage::UpdateDataPanelState { open: true });
}
// Run the graph to grab the data
if self.properties_panel_open || self.layers_panel_open || self.data_panel_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
if self.properties_panel_open {
responses.add(PropertiesPanelMessage::Refresh);
}
if self.layers_panel_open && self.active_document_id.is_some() {
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
}
}
}
PortfolioMessage::TogglePropertiesPanelOpen => {
if self.focus_document {
return;
}
self.properties_panel_open = !self.properties_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.properties_panel_open {
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(PropertiesPanelMessage::Refresh);
} else {
responses.add(PropertiesPanelMessage::Clear);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
}
}
PortfolioMessage::ToggleLayersPanelOpen => {
if self.focus_document {
return;
}
self.layers_panel_open = !self.layers_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.layers_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
if self.active_document_id.is_some() {
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
}
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
responses.add(DocumentMessage::ClearLayersPanel);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
}
}
PortfolioMessage::ToggleDataPanelOpen => {
if self.focus_document {
return;
}
self.data_panel_open = !self.data_panel_open;
responses.add(MenuBarMessage::SendLayout);
@ -1195,40 +1290,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
}
}
PortfolioMessage::TogglePropertiesPanelOpen => {
self.properties_panel_open = !self.properties_panel_open;
responses.add(MenuBarMessage::SendLayout);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
// Run the graph to grab the data
if self.properties_panel_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(PropertiesPanelMessage::Refresh);
}
PortfolioMessage::ToggleLayersPanelOpen => {
self.layers_panel_open = !self.layers_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.layers_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
responses.add(DocumentMessage::ClearLayersPanel);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
}
}
PortfolioMessage::ToggleRulers => {
if let Some(document) = self.active_document_mut() {
document.rulers_visible = !document.rulers_visible;
@ -1280,7 +1341,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
fn actions(&self) -> ActionList {
let mut common = actions!(PortfolioMessageDiscriminant;
Open,
ToggleDataPanelOpen,
ToggleFocusDocument,
);
// Extend with actions that require an active document
@ -1305,6 +1366,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
}
// Extend with actions that are disabled when focusing the document
if !self.focus_document {
common.extend(actions!(PortfolioMessageDiscriminant;
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,
ToggleDataPanelOpen,
));
}
common
}
}
@ -1385,14 +1455,14 @@ impl PortfolioMessageHandler {
}
}
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front {
self.document_ids.push_front(document_id);
} else {
self.document_ids.push_back(document_id);
}
new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
new_document.update_layers_panel_control_bar_widgets(self.layers_panel_open && !self.focus_document, responses);
new_document.update_layers_panel_bottom_bar_widgets(self.layers_panel_open && !self.focus_document, responses);
self.documents.insert(document_id, new_document);
@ -1439,7 +1509,7 @@ impl PortfolioMessageHandler {
/// Get the ID of the selected node that should be used as the current source for the Data panel.
pub fn node_to_inspect(&self) -> Option<NodeId> {
// Skip if the Data panel is not open
if !self.data_panel_open {
if !self.data_panel_open || self.focus_document {
return None;
}

View File

@ -93,7 +93,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
potentiallyRestoreCanvasFocus(e);
if (!canvasFocused && !targetIsTextField(e.target || undefined) && ["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key)) return false;
if (
!canvasFocused &&
!targetIsTextField(e.target || undefined) &&
["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key) &&
!(e.ctrlKey || e.metaKey || e.altKey)
)
return false;
// Don't redirect if a MenuList is open
if (window.document.querySelector("[data-floating-menu-content]")) return false;