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