Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/user-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ not undoable.
| Tool | Task |
|---|---|
| [Camera & navigation](camera-navigation.md) | Orbit, zoom, and pan the viewport around its pivot |
| [Keyboard shortcuts](shortcuts.md) | Every binding, generated from the editor's own table |
| [Editing attributes](attributes.md) | Scrub numeric fields by dragging their name; drop assets into slots |
| [Library](library.md) | Drag ready-made roads, intersections, and props into the scene |
| [Create Road](create-road.md) | Lay a new clothoid road through waypoints with a lane template |
Expand Down
63 changes: 63 additions & 0 deletions docs/user-guide/shortcuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Keyboard shortcuts

*Every keyboard binding in the RoadMaker editor, in one place.*

Nothing here is only reachable by keyboard: each of these is also a menu or
toolbar entry, and the keys are accelerators for what you can already point at.
Mouse and trackpad navigation — orbiting, panning, zooming, and the modifier
chords — live in [Camera & navigation](camera-navigation.md).

Keys are written in the cross-platform spelling. Where a platform differs, the
**Notes** column says so: `Ctrl` is `⌘ Command` on macOS for the File and Edit
conventions below.

> The tables below are **generated** from the editor's shortcut table
> (`editor/src/app/shortcut_registry.cpp`), which is also what the editor binds
> from. A CI test compares this page against that table, so a binding cannot
> change without this page changing with it. Edit the table, not the tables
> below.

## File

| Action | Shortcut | Notes |
|---|---|---|
| New scene | `Ctrl+N` | ⌘N on macOS |
| Open an OpenDRIVE (.xodr) file | `Ctrl+O` | ⌘O on macOS |
| Save | `Ctrl+S` | ⌘S on macOS |
| Save as… | `Ctrl+Shift+S` | ⇧⌘S on macOS |
| Quit | `Ctrl+Q` | ⌘Q on macOS, where the platform menu owns it |

## Edit

| Action | Shortcut | Notes |
|---|---|---|
| Undo | `Ctrl+Z` | ⌘Z on macOS |
| Redo | `Ctrl+Shift+Z` | Ctrl+Y also works on Windows; ⇧⌘Z on macOS |

## Tools

| Action | Shortcut | Notes |
|---|---|---|
| Select/Move tool | `Q` | |
| Move tool | `M` | |
| Create Road tool | `C` | |
| Edit Nodes tool | `N` | |
| Lane Profile tool | `L` | |
| Elevation tool | `E` | |
| Create Junction tool | `J` | |
| Split tool | `K` | |
| Delete tool | `X` | |

## View

| Action | Shortcut | Notes |
|---|---|---|
| Frame the selection (the whole scene when nothing is selected) | `F` | |
| Frame on the point under the cursor (keeps the zoom) | `V` | |
| Perspective projection | `P` | |
| Orthographic projection | `O` | |
| Look from the north | `Num+8` or `8` | Numpad; the top-row digit is the alternate |
| Look from the south | `Num+2` or `2` | Numpad; the top-row digit is the alternate |
| Look from the west | `Num+4` or `4` | Numpad; the top-row digit is the alternate |
| Look from the east | `Num+6` or `6` | Numpad; the top-row digit is the alternate |
| Top-down view, north up | `Num+5` or `5` | Numpad; the top-row digit is the alternate |
2 changes: 2 additions & 0 deletions editor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ qt_standard_project_setup()
# editor tests can link the same objects and run headless (offscreen QPA).
add_library(roadmaker_editor_lib STATIC
src/app/actions.cpp
src/app/shortcut_registry.cpp
src/app/context_menu.cpp
src/app/crash_handler.cpp
src/app/icons.cpp
Expand All @@ -53,6 +54,7 @@ add_library(roadmaker_editor_lib STATIC
src/document/scene_tree_model.cpp
src/document/selection_model.cpp
src/panels/diagnostics_panel.cpp
src/panels/editor2d_host.cpp
src/panels/library_panel.cpp
src/panels/profile_panel.cpp
src/panels/properties_panel.cpp
Expand Down
69 changes: 32 additions & 37 deletions editor/src/app/actions.cpp
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
#include "app/actions.hpp"

#include "app/icons.hpp"
#include "app/shortcut_registry.hpp"

namespace roadmaker::editor {

Actions::Actions(QUndoStack& undo_stack, QObject* parent) : QObject(parent) {
// iconText carries the short label the labeled toolbar renders under the
// icon (ToolButtonTextUnderIcon); the menu keeps the full text.
new_file = new QAction(tr("&New"), this);
new_file->setShortcut(QKeySequence::New);
new_file->setShortcuts(shortcuts::sequences(shortcuts::Id::NewScene));
new_file->setIconText(tr("New"));
new_file->setToolTip(tr("New scene — start an empty road network"));

open = new QAction(tr("&Open…"), this);
open->setShortcut(QKeySequence::Open);
open->setShortcuts(shortcuts::sequences(shortcuts::Id::Open));
open->setIconText(tr("Open"));
open->setToolTip(tr("Open an OpenDRIVE (.xodr) file"));

save = new QAction(tr("&Save"), this);
save->setShortcut(QKeySequence::Save);
save->setShortcuts(shortcuts::sequences(shortcuts::Id::Save));
save->setIconText(tr("Save"));
save->setToolTip(tr("Save the scene as OpenDRIVE"));

save_as = new QAction(tr("Save &As…"), this);
save_as->setShortcut(QKeySequence::SaveAs);
save_as->setShortcuts(shortcuts::sequences(shortcuts::Id::SaveAs));

export_glb = new QAction(tr("&Export glTF…"), this);
export_glb->setEnabled(false); // enabled once a file is loaded
Expand All @@ -36,83 +37,83 @@ Actions::Actions(QUndoStack& undo_stack, QObject* parent) : QObject(parent) {
#endif

quit = new QAction(tr("&Quit"), this);
quit->setShortcut(QKeySequence::Quit);
quit->setShortcuts(shortcuts::sequences(shortcuts::Id::Quit));
quit->setMenuRole(QAction::QuitRole);

undo = undo_stack.createUndoAction(this, tr("&Undo"));
undo->setShortcut(QKeySequence::Undo);
undo->setShortcuts(shortcuts::sequences(shortcuts::Id::Undo));
redo = undo_stack.createRedoAction(this, tr("&Redo"));
redo->setShortcut(QKeySequence::Redo);
redo->setShortcuts(shortcuts::sequences(shortcuts::Id::Redo));

tool_group = new QActionGroup(this);
tool_select = new QAction(tr("&Select/Move"), this);
tool_select->setCheckable(true);
tool_select->setChecked(true); // the default tool
// Q, not V: V is frame-on-cursor (GW-1 step 10). Rebound in p1-s2.
tool_select->setShortcut(Qt::Key_Q);
tool_select->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolSelect));
tool_select->setIconText(tr("Select"));
tool_select->setToolTip(tr("Select/Move — click picks, drag spans a rubber band, "
"drag a node handle moves it (Q)"));
tool_group->addAction(tool_select);

tool_move = new QAction(tr("&Move"), this);
tool_move->setCheckable(true);
tool_move->setShortcut(Qt::Key_M);
tool_move->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolMove));
tool_move->setIconText(tr("Move"));
tool_move->setToolTip(tr("Move — hover shows the 4-arrow cursor; drag a road or a prop to "
"move it, or click to select and transform it (M)"));
tool_group->addAction(tool_move);

tool_create_road = new QAction(tr("&Create Road"), this);
tool_create_road->setCheckable(true);
tool_create_road->setShortcut(Qt::Key_C);
tool_create_road->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolCreateRoad));
tool_create_road->setIconText(tr("Road"));
tool_create_road->setToolTip(tr("Create Road — click places waypoints, Enter or double-click "
"creates the road, Esc cancels (C)"));
tool_group->addAction(tool_create_road);

tool_edit_nodes = new QAction(tr("Edit &Nodes"), this);
tool_edit_nodes->setCheckable(true);
tool_edit_nodes->setShortcut(Qt::Key_N);
tool_edit_nodes->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolEditNodes));
tool_edit_nodes->setIconText(tr("Nodes"));
tool_edit_nodes->setToolTip(tr("Edit Nodes — drag a node to move it, click a midpoint "
"marker to insert, Delete removes the active node (N)"));
tool_group->addAction(tool_edit_nodes);

tool_lane_profile = new QAction(tr("&Lane Profile"), this);
tool_lane_profile->setCheckable(true);
tool_lane_profile->setShortcut(Qt::Key_L);
tool_lane_profile->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolLaneProfile));
tool_lane_profile->setIconText(tr("Lanes"));
tool_lane_profile->setToolTip(tr("Lane Profile — click a lane, then edit its type, width, and "
"road mark in the Properties panel (L)"));
tool_group->addAction(tool_lane_profile);

tool_elevation = new QAction(tr("&Elevation"), this);
tool_elevation->setCheckable(true);
tool_elevation->setShortcut(Qt::Key_E);
tool_elevation->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolElevation));
tool_elevation->setIconText(tr("Elevation"));
tool_elevation->setToolTip(tr("Elevation — click a road node, then set its height in the "
"Properties panel; the grade re-fits smoothly (E)"));
tool_group->addAction(tool_elevation);

tool_create_junction = new QAction(tr("Create &Junction"), this);
tool_create_junction->setCheckable(true);
tool_create_junction->setShortcut(Qt::Key_J);
tool_create_junction->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolCreateJunction));
tool_create_junction->setIconText(tr("Junction"));
tool_create_junction->setToolTip(tr("Create Junction — click 2+ road ends, or 1 end + a road "
"body to tee into it; Enter generates, Esc cancels (J)"));
tool_group->addAction(tool_create_junction);

tool_split = new QAction(tr("&Split"), this);
tool_split->setCheckable(true);
tool_split->setShortcut(Qt::Key_K);
tool_split->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolSplit));
tool_split->setIconText(tr("Split"));
tool_split->setToolTip(tr("Split — click a road to cut it in two at the marker (K)"));
tool_group->addAction(tool_split);

tool_delete = new QAction(tr("&Delete"), this);
tool_delete->setCheckable(true);
tool_delete->setShortcut(Qt::Key_X);
tool_delete->setShortcuts(shortcuts::sequences(shortcuts::Id::ToolDelete));
tool_delete->setIconText(tr("Delete"));
tool_delete->setToolTip(tr("Delete — click a road to delete it, undo restores (X)"));
tool_group->addAction(tool_delete);
Expand Down Expand Up @@ -144,59 +145,53 @@ Actions::Actions(QUndoStack& undo_stack, QObject* parent) : QObject(parent) {
add_from_library->setToolTip(tr("Open the Library panel to drag in roads, intersections, "
"and props"));
frame_selection = new QAction(tr("&Frame Selection"), this);
frame_selection->setShortcut(Qt::Key_F);
frame_selection->setShortcuts(shortcuts::sequences(shortcuts::Id::FrameSelection));
frame_selection->setIconText(tr("Frame"));
frame_selection->setToolTip(tr("Frame the selection — the whole scene when "
"nothing is selected (F)"));
frame_cursor = new QAction(tr("Frame Under &Cursor"), this);
frame_cursor->setShortcut(Qt::Key_V);
frame_cursor->setShortcuts(shortcuts::sequences(shortcuts::Id::FrameCursor));
frame_cursor->setIconText(tr("Frame Cursor"));
frame_cursor->setToolTip(tr("Move the pivot to the point under the cursor, keeping the "
"zoom (V)"));

// Projection (GW-1 step 11). Exclusive: the view is one or the other.
projection_group = new QActionGroup(this);
view_perspective = new QAction(tr("&Perspective"), this);
view_perspective->setShortcut(Qt::Key_P);
view_perspective->setShortcuts(shortcuts::sequences(shortcuts::Id::ViewPerspective));
view_perspective->setCheckable(true);
view_perspective->setChecked(true); // the startup projection
view_perspective->setToolTip(tr("Perspective projection (P)"));
view_orthographic = new QAction(tr("&Orthographic"), this);
view_orthographic->setShortcut(Qt::Key_O);
view_orthographic->setShortcuts(shortcuts::sequences(shortcuts::Id::ViewOrthographic));
view_orthographic->setCheckable(true);
view_orthographic->setToolTip(tr("Orthographic projection — parallel, no foreshortening (O)"));
projection_group->addAction(view_perspective);
projection_group->addAction(view_orthographic);

// Cardinal views (GW-1 steps 12-13). The numpad digits are the primary
// binding; the top-row digits are the alternate for keyboards without one.
const auto make_cardinal =
[this](const QString& text, const QString& tip, Qt::Key numpad, Qt::Key digit) {
auto* action = new QAction(text, this);
action->setShortcuts({QKeySequence(Qt::KeypadModifier | numpad), QKeySequence(digit)});
action->setToolTip(tip);
return action;
};
const auto make_cardinal = [this](const QString& text, const QString& tip, shortcuts::Id id) {
auto* action = new QAction(text, this);
action->setShortcuts(shortcuts::sequences(id)); // numpad + the top-row alternate
action->setToolTip(tip);
return action;
};
view_north = make_cardinal(tr("Look from &North"),
tr("Look from the north, southward (numpad 8, or 8)"),
Qt::Key_8,
Qt::Key_8);
shortcuts::Id::ViewNorth);
view_south = make_cardinal(tr("Look from &South"),
tr("Look from the south, northward (numpad 2, or 2)"),
Qt::Key_2,
Qt::Key_2);
shortcuts::Id::ViewSouth);
view_west = make_cardinal(tr("Look from &West"),
tr("Look from the west, eastward (numpad 4, or 4)"),
Qt::Key_4,
Qt::Key_4);
shortcuts::Id::ViewWest);
view_east = make_cardinal(tr("Look from &East"),
tr("Look from the east, westward (numpad 6, or 6)"),
Qt::Key_6,
Qt::Key_6);
shortcuts::Id::ViewEast);
view_top = make_cardinal(tr("&Top-Down"),
tr("Plan view from directly above, north up (numpad 5, or 5)"),
Qt::Key_5,
Qt::Key_5);
shortcuts::Id::ViewTop);
merge_roads = new QAction(tr("&Merge Roads"), this);
merge_roads->setIconText(tr("Merge"));
merge_roads->setEnabled(false); // enabled only for a mergeable 2-road selection
Expand Down
53 changes: 43 additions & 10 deletions editor/src/app/main_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
#include "document/library_drop.hpp"
#include "document/library_manifest.hpp"
#include "panels/diagnostics_panel.hpp"
#include "panels/editor2d_host.hpp"
#include "panels/library_panel.hpp"
#include "panels/profile_panel.hpp"
#include "panels/properties_panel.hpp"
#include "panels/scene_tree_panel.hpp"
#include "tools/create_junction_tool.hpp"
Expand Down Expand Up @@ -284,11 +284,12 @@ MainWindow::MainWindow(QWidget* parent, bool restore_saved_layout)
tool_manager_.register_tool(ToolId::Elevation, std::move(elevation_tool));
connect(actions_->tool_elevation, &QAction::triggered, this, [this] {
tool_manager_.set_active(ToolId::Elevation);
// Surface the Profile dock (vertical-profile handles + the overpass Cross
// Surface the 2D Editor pane (vertical-profile handles + the overpass Cross
// Over/Under controls) when the elevation workflow starts — otherwise it
// hides behind a View-menu toggle (discoverability rule, product-parity.md).
profile_dock_->show();
profile_dock_->raise();
editor2d_dock_->show();
editor2d_dock_->raise();
editor2d_host_->raise_relevant_page();
});
// The Properties panel edits the node the Elevation tool has made active.
properties_panel_->set_elevation_tool(elevation_tool_);
Expand Down Expand Up @@ -419,11 +420,22 @@ void MainWindow::build_docks() {
properties_dock_->widget()->setMinimumWidth(300);
addDockWidget(Qt::RightDockWidgetArea, properties_dock_);

profile_dock_ = new QDockWidget(tr("Profile"), this);
profile_dock_->setObjectName(QStringLiteral("dock.profile"));
profile_dock_->setWidget(new ProfilePanel(document_, selection_, profile_dock_));
addDockWidget(Qt::BottomDockWidgetArea, profile_dock_);
profile_dock_->hide(); // opt-in via the View menu — vertical design is occasional
// The 2D Editor pane hosts the flat, per-entity editors. The vertical
// profile is the first page; the cross-section and Signal Phase Editor plug
// in later (GW-4 step 4, p4-s5).
//
// NOT "dock.profile": restoreState() matches saved geometry by objectName, so
// reusing the old name would drop this dock into the position a stale layout
// remembers for a different widget. The new name is simply unknown to old
// settings, which restoreState ignores — the default placement below wins.
editor2d_dock_ = new QDockWidget(tr("2D Editor"), this);
editor2d_dock_->setObjectName(QStringLiteral("dock.editor2d"));
editor2d_host_ = new Editor2DHost(selection_, editor2d_dock_);
editor2d_host_->register_page(
std::make_unique<ProfileEditorPage>(document_, selection_, editor2d_host_));
editor2d_dock_->setWidget(editor2d_host_);
addDockWidget(Qt::BottomDockWidgetArea, editor2d_dock_);
editor2d_dock_->hide(); // opt-in via the View menu — 2D editing is occasional

diagnostics_dock_ = new QDockWidget(tr("Diagnostics"), this);
diagnostics_dock_->setObjectName(QStringLiteral("dock.diagnostics"));
Expand Down Expand Up @@ -471,7 +483,7 @@ void MainWindow::build_menus() {
view_menu->addAction(library_dock_->toggleViewAction());
view_menu->addAction(properties_dock_->toggleViewAction());
view_menu->addAction(diagnostics_dock_->toggleViewAction());
view_menu->addAction(profile_dock_->toggleViewAction());
view_menu->addAction(editor2d_dock_->toggleViewAction());
view_menu->addSeparator();
auto* textured_action = new QAction(tr("&Textured Rendering"), this);
textured_action->setCheckable(true);
Expand Down Expand Up @@ -598,6 +610,27 @@ void MainWindow::update_tool_options() {

void MainWindow::build_status_bar() {
statusBar()->addWidget(status_hover_, 1);
// PERMANENT, not a normal widget: showMessage() hides the normal indications
// while a transient message is up, and the instruction must survive that —
// it answers "what can I do with this tool", which stays true while results
// and refusals come and go. Being permanent also lays it out clear of the
// message, so the two can never paint over each other.
status_instruction_ = new QLabel(this);
status_instruction_->setObjectName(QStringLiteral("status_instruction"));
statusBar()->addPermanentWidget(status_instruction_);
// Follow the active tool: its instruction() is the ONE source for "what does
// this tool do", shown both here and as the viewport corner hint (issue #103
// — during an interaction the user's eyes are on the viewport). Tools used to
// emit the same sentence transiently on activate(); they no longer do, so the
// transient channel carries only results and state-dependent guidance.
const auto show_instruction = [this] {
const Tool* tool = tool_manager_.active();
const QString text = tool == nullptr ? QString() : tool->instruction();
status_instruction_->setText(text);
viewport_->set_hint(text);
};
connect(&tool_manager_, &ToolManager::active_changed, this, show_instruction);
show_instruction(); // the startup tool
statusBar()->addPermanentWidget(status_entities_);
auto* version_label =
new QLabel(tr("kernel %1")
Expand Down
Loading
Loading