Graph: pan with left or right, open with the other

This commit is contained in:
sarah committed 2026-10-09 09:51:38 +02:00
1 parent d97f82beb2
commit cc7d624a57
8 files changed
+50 -62

No files matched your search

+3 -2
View File
@@ -49,9 +49,10 @@ DefaultSettings: dict[str, Any] = {
# live preview). "Separate": rendered View and raw Edit are two modes.
"MarkdownEditing": "Live",
# Graph view: directories as dots too (off: only files and the references
# between them), and the mouse button that pans ("Left", "Middle", "Right").
# between them), and the mouse button that pans ("Left" or "Right"; the
# other one opens a dot).
"GraphShowDirectories": True,
"GraphPanButton": "Middle",
"GraphPanButton": "Right",
}
DefaultThemesDirectory = ApplicationDirectory / "DefaultThemes"
+3 -2
View File
@@ -168,9 +168,10 @@ class AppConfig:
# in place (like Obsidian's live preview). Only affects Markdown.
MarkdownEditing: str = "Live"
# Graph view: directories as dots too (off: only files and the references
# between them), and the mouse button that pans ("Left", "Middle", "Right").
# between them), and the mouse button that pans ("Left" or "Right"; the
# other one opens a dot).
GraphShowDirectories: bool = True
GraphPanButton: str = "Middle"
GraphPanButton: str = "Right"
# Double-click on a file: True opens it in SaraView's own viewer, False
# hands it to the system's default program. The menu always offers the other.
OpenFilesInApp: bool = True
+16 -31
View File
@@ -5,8 +5,8 @@ colours by depth, files White), joined to its parent directory and to every file
On a server the graph comes from /api/graph; locally BuildLocalGraph makes
the same one with the same reference rules as Backend/Graph.py. The layout is
the web view's force simulation (same constants), in numpy, drawn as it
settles. Scroll to zoom, pan with the button picked in Settings > Graph,
hover a dot to light up its links, click it to open it."""
settles. Scroll to zoom, hover a dot to light up its links; one mouse button
pans and the other opens a dot (Settings > Graph)."""
import math
import re
@@ -238,9 +238,8 @@ def FilterGraph(Graph: dict, ShowDirectories: bool) -> dict:
class GraphView(ft.Container):
"""Pan and zoom are done here rather than with an InteractiveViewer, so
the pan button can be picked (Settings > Graph). Flet reports drags for
the left and right buttons only, so Middle works like middle-click
autoscroll: click it to grab the graph, move to pan, click again to let go."""
the buttons can be picked (Settings > Graph): one mouse button pans, the
other opens the dot it clicks."""
def __init__(self, TheApp: App, Root: str, DirectoryPath: str,
OnOpenNode: Callable[[dict], None], OnClose: Callable[[], None]):
@@ -248,7 +247,9 @@ class GraphView(ft.Container):
self.Root = Root
self.DirectoryPath = DirectoryPath
self.OnOpenNode = OnOpenNode
self.PanButton = TheApp.Config.GraphPanButton
# Anything but "Left" (including an old "Middle") pans with Right.
self.PanButton = "Left" if TheApp.Config.GraphPanButton == "Left" else "Right"
self.OpenButton = "Right" if self.PanButton == "Left" else "Left"
self.ShowDirectories = TheApp.Config.GraphShowDirectories
self.Layout: Optional[Layout] = None
self.ScopePath = ""
@@ -264,7 +265,6 @@ class GraphView(ft.Container):
self.PanX = 0.0
self.PanY = 0.0
self.DragFrom: Optional[tuple[float, float]] = None
self.Grabbed = False # Middle: grabbed by a middle click
self.ScopePicker = ft.Dropdown(
value="Root",
@@ -288,21 +288,21 @@ class GraphView(ft.Container):
# Hidden (not just its text) once the graph is up: even empty, it sits
# over the graph and would catch every click.
self.StatusBox = ft.Container(self.Status, alignment=ft.Alignment.CENTER)
Hint = {"Left": "Drag to pan", "Right": "Right-drag to pan",
"Middle": "Middle-click to grab and pan, again to let go"}.get(self.PanButton, "")
Hint = ("Drag to pan, right-click a dot to open it" if self.PanButton == "Left"
else "Right-drag to pan, click a dot to open it")
self.Surface = ft.GestureDetector(
content=ft.Container(self.Plane, expand=True, clip_behavior=ft.ClipBehavior.HARD_EDGE, bgcolor=Palette.Base),
on_tap_up=self._OnTap,
on_tap_up=lambda e: self._OnClick(e, "Left"),
on_secondary_tap_up=lambda e: self._OnClick(e, "Right"),
on_hover=self._OnHover,
on_exit=lambda e: self._SetHovered(None),
on_scroll=self._OnScroll,
on_pan_start=lambda e: self._StartDrag(e, "Left"),
on_pan_down=lambda e: self._StartDrag(e, "Left"), # down, not start: start comes after the drag slop
on_pan_update=lambda e: self._Drag(e, "Left"),
on_pan_end=lambda e: self._EndDrag(),
on_right_pan_start=lambda e: self._StartDrag(e, "Right"),
on_right_pan_update=lambda e: self._Drag(e, "Right"),
on_right_pan_end=lambda e: self._EndDrag(),
on_tertiary_tap_up=self._OnMiddleClick,
hover_interval=20,
drag_interval=16,
expand=True,
@@ -317,7 +317,7 @@ class GraphView(ft.Container):
], spacing=12),
ft.Divider(height=1, color=Palette.Surface1),
ft.Stack([self.Surface, self.StatusBox], expand=True),
ft.Text(f"{Hint}, scroll to zoom, click a dot to open it.", size=11, color=Palette.Overlay0),
ft.Text(f"{Hint}, scroll to zoom.", size=11, color=Palette.Overlay0),
], spacing=8, expand=True),
expand=True, bgcolor=Palette.Base, padding=12,
)
@@ -488,29 +488,15 @@ class GraphView(ft.Container):
def _OnHover(self, Event) -> None:
X, Y = Event.local_position.x, Event.local_position.y
if self.Grabbed and self.DragFrom is not None:
self._PanBy(X - self.DragFrom[0], Y - self.DragFrom[1])
self.DragFrom = (X, Y)
return
self._SetHovered(self._NodeAt(X, Y))
def _OnTap(self, Event) -> None:
if self.Grabbed: # a left click also lets go
self.Grabbed = False
self.DragFrom = None
def _OnClick(self, Event, Button: str) -> None:
if Button != self.OpenButton:
return
Number = self._NodeAt(Event.local_position.x, Event.local_position.y)
if Number is not None:
self.OnOpenNode(self.Layout.Nodes[Number])
def _OnMiddleClick(self, Event) -> None:
if self.PanButton != "Middle":
return
self.Grabbed = not self.Grabbed
self.DragFrom = (Event.local_position.x, Event.local_position.y) if self.Grabbed else None
self.Surface.mouse_cursor = ft.MouseCursor.MOVE if self.Grabbed else ft.MouseCursor.BASIC
self.Surface.update()
def _StartDrag(self, Event, Button: str) -> None:
if Button == self.PanButton:
self.DragFrom = (Event.local_position.x, Event.local_position.y)
@@ -523,8 +509,7 @@ class GraphView(ft.Container):
self.DragFrom = (X, Y)
def _EndDrag(self) -> None:
if not self.Grabbed:
self.DragFrom = None
self.DragFrom = None
def _OnScroll(self, Event) -> None:
"""Zooms around the pointer."""
+4 -6
View File
@@ -122,11 +122,10 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
value=Config.GraphShowDirectories)
PanButtonPicker = ft.RadioGroup(
ft.Column([
ft.Radio(value="Left", label="Left"),
ft.Radio(value="Middle", label="Middle (click to grab, move, click again to let go)"),
ft.Radio(value="Right", label="Right"),
ft.Radio(value="Right", label="Right (left-click opens a dot)"),
ft.Radio(value="Left", label="Left (right-click opens a dot)"),
], spacing=0),
value=Config.GraphPanButton,
value="Left" if Config.GraphPanButton == "Left" else "Right",
)
# Connection.
@@ -224,8 +223,7 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
ft.Divider(height=1, color=Palette.Surface1),
ft.Text("Pan with mouse button"),
PanButtonPicker,
_Note("Scroll zooms. Flet can't follow a held middle button, so Middle works like "
"middle-click autoscroll."),
_Note("Scroll zooms. The other button opens the dot you click."),
], spacing=12, scroll=ft.ScrollMode.AUTO),
"Connection": ft.Column([
_Note("Browse a directory on this computer, or connect to a SaraView server."),
+1 -1
View File
@@ -383,7 +383,7 @@ export default function App() {
RootName={ActiveRoot}
DirectoryPath={CurrentDirectoryPath}
ShowDirectories={Settings ? Settings.GraphShowDirectories : true}
PanButton={Settings ? Settings.GraphPanButton : "Middle"}
PanButton={Settings ? Settings.GraphPanButton : "Right"}
OnOpenNode={HandleOpenGraphNode}
OnClose={() => SetIsGraphOpen(false)}
/>
+13 -12
View File
@@ -8,8 +8,9 @@ import { FetchGraph } from "../Api.js";
// it (Backend/Graph.py). With directories turned off (Settings > Graph) only
// the files and the references between them are left.
// The layout is a small force simulation: dots push each other apart and
// links pull them together until it settles. Pan with the button picked in
// Settings > Graph, scroll to zoom, drag a dot to move it, click one to open it.
// links pull them together until it settles. One mouse button pans (Settings
// > Graph), the other opens the dot it clicks or moves it when dragged.
// Scroll zooms.
const ChildLength = 50;
const ReferenceLength = 80;
@@ -19,7 +20,6 @@ const LabelZoom = 0.7; // below this, only hovered labels are drawn
const ClickSlop = 4; // pixels a click may wander and still count as a click
// Directories by depth, like the headings: h1 Purple, h2 Blue, ...
const DepthRoles = ["Purple", "Blue", "Green", "Yellow", "Orange", "Red"];
const ButtonNumbers = { Left: 0, Middle: 1, Right: 2 };
function ReadRole(Name) {
return getComputedStyle(document.documentElement).getPropertyValue(`--${Name}`).trim();
@@ -191,7 +191,9 @@ export default function GraphPane({ RootName, DirectoryPath, ShowDirectories, Pa
const Depth = (Node.Id ? Node.Id.split("/").length : 0) - BaseDepth;
Node.Colour = Node.IsDirectory ? DepthColours[Depth % DepthColours.length] : Colours.File;
}
const PanNumber = ButtonNumbers[PanButton] ?? 1;
// Anything but "Left" (including an old "Middle") pans with Right.
const PanNumber = PanButton === "Left" ? 0 : 2;
const OpenNumber = PanNumber === 0 ? 2 : 0;
const State = {
Zoom: 1,
@@ -313,11 +315,9 @@ export default function GraphPane({ RootName, DirectoryPath, ShowDirectories, Pa
const Node = NodeAt(X, Y);
State.Moved = false;
State.DownAt = [X, Y];
if (Event.button !== 0 || Event.button === PanNumber) {
Event.preventDefault(); // no middle-click autoscroll or right-click menu
}
if (Event.button === 0 && Node) {
// Left on a dot: a click opens it, a drag moves it.
Event.preventDefault();
if (Event.button === OpenNumber && Node) {
// The open button on a dot: a click opens it, a drag moves it.
State.Dragging = Node;
Node.Pinned = true;
} else if (Event.button === PanNumber) {
@@ -365,7 +365,7 @@ export default function GraphPane({ RootName, DirectoryPath, ShowDirectories, Pa
}
function OnContextMenu(Event) {
if (PanNumber === 2) Event.preventDefault();
Event.preventDefault(); // right-click pans or opens here, never the browser menu
}
function OnLeave() {
@@ -432,8 +432,9 @@ export default function GraphPane({ RootName, DirectoryPath, ShowDirectories, Pa
{!Graph && !Error && <div className="PreviewEmpty">Building the graph…</div>}
{Graph && <canvas ref={CanvasRef} className="GraphCanvas" />}
<div className="GraphHint">
{PanButton === "Left" ? "Drag" : `${PanButton}-drag`} the background to pan, scroll to zoom, drag a dot to move it,
click one to open it.
{PanButton === "Left"
? "Drag to pan, right-click a dot to open it (right-drag moves it), scroll to zoom."
: "Right-drag to pan, click a dot to open it (drag moves it), scroll to zoom."}
</div>
</div>
);
+6 -4
View File
@@ -412,10 +412,12 @@ export default function SettingsPanel({
</div>
<div className="SettingsRow">
<label className="SettingsLabel">Pan with mouse button</label>
<select value={Settings.GraphPanButton} onChange={(Event) => UpdateField("GraphPanButton", Event.target.value)}>
<option value="Left">Left</option>
<option value="Middle">Middle</option>
<option value="Right">Right</option>
<select
value={Settings.GraphPanButton === "Left" ? "Left" : "Right"}
onChange={(Event) => UpdateField("GraphPanButton", Event.target.value)}
>
<option value="Right">Right (left-click opens a dot)</option>
<option value="Left">Left (right-click opens a dot)</option>
</select>
</div>
</div>
+4 -4
View File
@@ -302,10 +302,10 @@ the current directory, as dots like Obsidian's graph: directories in the
heading colours by depth (purple, blue, green, ...), files white, each
joined to its parent directory and to the files that reference it (Markdown
links and images, `[[wiki links]]` and portals). Hover a dot to light up its
links, click it to open it, scroll to zoom. Settings > Graph picks the mouse
button that pans (Middle by default) and can hide directories, leaving only
files and the links between them. In the client, Flet can't follow a held
middle button, so Middle there is click to grab, move, click to let go.
links, scroll to zoom. One mouse button pans and the other opens the dot
you click: right-drag pans and left-click opens by default, or the other way
round in Settings > Graph, which can also hide directories, leaving only
files and the links between them.
Rename takes a `/` to move, like the web view (`../Archive/Notes.md`).
Shortcuts: Enter opens, Backspace goes up, F2 renames, Delete deletes,