Skip to content
Open
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
78 changes: 76 additions & 2 deletions Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.Main.pas
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ TMarkdown4DStudioFMXForm = class(TForm, IPadEditorView, IPadShell)
procedure HandleDocumentHandedOver(const FileName: string);
procedure SetViewMode(const Mode: TPadViewMode);
procedure ApplyViewMode;
function AvailableSplitWidth: Single;
procedure ApplySplitEditorWidth(const DesiredWidth: Single);
procedure HandleSplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single);
procedure HandleTocSplitterMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single);
procedure ShowFindBar;
procedure ShowReplaceBar;
procedure CloseFindBar;
Expand Down Expand Up @@ -313,6 +319,7 @@ implementation
Markdown4D.Ast.Interfaces,
Markdown4DStudio.Text,
Markdown4DStudio.Outline,
Markdown4DStudio.SplitLayout,
Markdown4DStudio.SessionSync,
Markdown4DStudio.Workspace,
Markdown4DStudio.LinkPolicy,
Expand Down Expand Up @@ -742,6 +749,7 @@ procedure TMarkdown4DStudioFMXForm.BuildTocPanel;
FTocSplitter.Parent := Self;
FTocSplitter.Align := TAlignLayout.Left;
FTocSplitter.Width := SplitterWidth;
FTocSplitter.OnMouseUp := HandleTocSplitterMouseUp;
end;

procedure TMarkdown4DStudioFMXForm.BuildEditorAndPreview;
Expand All @@ -757,6 +765,8 @@ procedure TMarkdown4DStudioFMXForm.BuildEditorAndPreview;
FMainSplitter.Parent := Self;
FMainSplitter.Align := TAlignLayout.Left;
FMainSplitter.Width := SplitterWidth;
// FMX splitters have no moved event, so a finished drag is caught here.
FMainSplitter.OnMouseUp := HandleSplitterMouseUp;

FPreview := TMarkdownViewer.Create(Self);
FPreview.Parent := Self;
Expand Down Expand Up @@ -1166,7 +1176,11 @@ procedure TMarkdown4DStudioFMXForm.Resize;
LayoutTitleBar;

if FZenActive then
UpdateZenPadding;
UpdateZenPadding
else if FViewMode = TPadViewMode.Split then
// A narrower window must take room from the editor rather than from the
// preview, which would otherwise be squeezed to nothing.
ApplySplitEditorWidth(FEditor.Width);

if (FPalette <> nil) and FPalette.Visible then
FPalette.Position.X := (ClientWidth - FPalette.Width) / 2;
Expand All @@ -1185,14 +1199,70 @@ procedure TMarkdown4DStudioFMXForm.SetViewMode(const Mode: TPadViewMode);
ApplyViewMode;
end;

function TMarkdown4DStudioFMXForm.AvailableSplitWidth: Single;
begin
// What the editor and preview actually have to share.
Result := ClientWidth - FMainSplitter.Width;

if FTocPanel.Visible then
Result := Result - FTocPanel.Width;
if FTocSplitter.Visible then
Result := Result - FTocSplitter.Width;
end;

procedure TMarkdown4DStudioFMXForm.ApplySplitEditorWidth(const DesiredWidth: Single);
begin
// Entering split view asks for the remembered width; a resize or a contents
// pane toggle asks for the width the editor already has, so that a splitter
// drag is kept and only trimmed when it no longer fits.

// The contents pane gives way first, so the two halves keep their minimum for
// as long as the window allows.
if FTocPanel.Visible then
FTocPanel.Width := TPadSplitLayout.ClampSidePanelWidth(Round(FTocPanel.Width),
Round(ClientWidth), Round(FTocSplitter.Width + FMainSplitter.Width),
MinPaneWidth, MinTocPanelWidth);

const Available = Round(AvailableSplitWidth);

// Re-stated on every width change, because a splitter told to honour a
// minimum the window cannot give lets a drag collapse the other pane.
FMainSplitter.MinSize := TPadSplitLayout.EffectiveMinPaneWidth(Available, MinPaneWidth);

FEditor.Width := TPadSplitLayout.ClampEditorWidth(Round(DesiredWidth), Available,
MinPaneWidth);
end;

procedure TMarkdown4DStudioFMXForm.HandleSplitterMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
// A drag is the user stating a preference, so remember it. It still has to
// pass the clamp, because on a narrow window the splitter cannot enforce a
// minimum the window is too small to give.
ApplySplitEditorWidth(FEditor.Width);
FSplitEditorWidth := FEditor.Width;
end;

procedure TMarkdown4DStudioFMXForm.HandleTocSplitterMouseUp(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
// Widening the contents pane takes room from the two halves, so it stops
// where they would lose their minimum. Only split view reserves room for
// two panes; the single pane modes have nothing to protect.
if FZenActive or (FViewMode <> TPadViewMode.Split) then
Exit;

ApplySplitEditorWidth(FEditor.Width);
end;

procedure TMarkdown4DStudioFMXForm.ApplyViewMode;
begin
case FViewMode of
TPadViewMode.Split:
begin
FEditor.Visible := True;
FEditor.Align := TAlignLayout.Left;
FEditor.Width := FSplitEditorWidth;
ApplySplitEditorWidth(FSplitEditorWidth);
FMainSplitter.Visible := True;
FPreview.Visible := True;
end;
Expand Down Expand Up @@ -1639,6 +1709,10 @@ procedure TMarkdown4DStudioFMXForm.ToggleTocPane;
const ShowToc = not FTocPanel.Visible;
FTocPanel.Visible := ShowToc;
FTocSplitter.Visible := ShowToc;

// Showing the contents pane takes the same room the two halves share.
if (not FZenActive) and (FViewMode = TPadViewMode.Split) then
ApplySplitEditorWidth(FEditor.Width);
end;

procedure TMarkdown4DStudioFMXForm.HandleFindClick(Sender: TObject);
Expand Down
1 change: 1 addition & 0 deletions Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.dpr
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ uses
Markdown4DStudio.Defines in '..\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Shared\Markdown4DStudio.Outline.pas',
Markdown4DStudio.SplitLayout in '..\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudioFMX.Defines in 'Markdown4DStudioFMX.Defines.pas',
Markdown4DStudio.Shell in '..\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.CommandLine in '..\Shared\Markdown4DStudio.CommandLine.pas',
Expand Down
73 changes: 71 additions & 2 deletions Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.Main.pas
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ TPadChrome = record
function BuildCommandActions: TPadCommandActions;
procedure SetViewMode(const Mode: TPadViewMode);
procedure ApplyViewMode;
function AvailableSplitWidth: Integer;
procedure ApplySplitEditorWidth(const DesiredWidth: Integer);
procedure HandleSplitterMoved(Sender: TObject);
procedure HandleTocSplitterMoved(Sender: TObject);
procedure EnforceTopBarOrder;
procedure EnforceLeftPaneOrder;
procedure ShowFindBar;
Expand Down Expand Up @@ -298,6 +302,7 @@ implementation
Markdown4DStudio.SessionSync,
Markdown4DStudio.Text,
Markdown4DStudio.Outline,
Markdown4DStudio.SplitLayout,
Markdown4DStudio.Workspace,
Markdown4DStudio.LinkPolicy,
Markdown4DStudio.SingleInstance,
Expand Down Expand Up @@ -368,6 +373,9 @@ procedure TMarkdown4DStudioVCLForm.ConfigureControls;
begin
BuildReplaceControls;
lblFindCount.Caption := EmptyFindCaption;

splMain.OnMoved := HandleSplitterMoved;
splToc.OnMoved := HandleTocSplitterMoved;
end;

procedure TMarkdown4DStudioVCLForm.BuildToolbar;
Expand Down Expand Up @@ -893,6 +901,10 @@ procedure TMarkdown4DStudioVCLForm.ToggleTocPane;
pnlToc.Visible := ShowToc;
splToc.Visible := ShowToc;

// Showing the contents pane takes the same room the two halves share.
if (not FZenActive) and (FViewMode = TPadViewMode.Split) then
ApplySplitEditorWidth(mdEditor.Width);

EnforceLeftPaneOrder;
end;

Expand Down Expand Up @@ -1716,6 +1728,59 @@ procedure TMarkdown4DStudioVCLForm.SetViewMode(const Mode: TPadViewMode);
ApplyViewMode;
end;

function TMarkdown4DStudioVCLForm.AvailableSplitWidth: Integer;
begin
// What the editor and preview actually have to share.
Result := ClientWidth - splMain.Width;

if pnlToc.Visible then
Dec(Result, pnlToc.Width);
if splToc.Visible then
Dec(Result, splToc.Width);
end;

procedure TMarkdown4DStudioVCLForm.ApplySplitEditorWidth(const DesiredWidth: Integer);
begin
// Entering split view asks for the remembered width; a resize or a contents
// pane toggle asks for the width the editor already has, so that a splitter
// drag is kept and only trimmed when it no longer fits.

// The contents pane gives way first, so the two halves keep their minimum for
// as long as the window allows.
if pnlToc.Visible then
pnlToc.Width := TPadSplitLayout.ClampSidePanelWidth(pnlToc.Width, ClientWidth,
splToc.Width + splMain.Width, MinPaneWidth, MinTocPanelWidth);

const Available = AvailableSplitWidth;

// Re-stated on every width change, because a splitter told to honour a
// minimum the window cannot give lets a drag collapse the other pane.
splMain.MinSize := TPadSplitLayout.EffectiveMinPaneWidth(Available, MinPaneWidth);

mdEditor.Width := TPadSplitLayout.ClampEditorWidth(DesiredWidth, Available,
MinPaneWidth);
end;

procedure TMarkdown4DStudioVCLForm.HandleSplitterMoved(Sender: TObject);
begin
// A drag is the user stating a preference, so remember it. It still has to
// pass the clamp, because on a narrow window the splitter cannot enforce a
// minimum the window is too small to give.
ApplySplitEditorWidth(mdEditor.Width);
FSplitEditorWidth := mdEditor.Width;
end;

procedure TMarkdown4DStudioVCLForm.HandleTocSplitterMoved(Sender: TObject);
begin
// Widening the contents pane takes room from the two halves, so it stops
// where they would lose their minimum. Only split view reserves room for
// two panes; the single pane modes have nothing to protect.
if FZenActive or (FViewMode <> TPadViewMode.Split) then
Exit;

ApplySplitEditorWidth(mdEditor.Width);
end;

procedure TMarkdown4DStudioVCLForm.ApplyViewMode;
begin
case FViewMode of
Expand All @@ -1736,7 +1801,7 @@ procedure TMarkdown4DStudioVCLForm.ApplyViewMode;
begin
mdEditor.Align := alLeft;
mdEditor.Visible := True;
mdEditor.Width := FSplitEditorWidth;
ApplySplitEditorWidth(FSplitEditorWidth);
splMain.Visible := True;
mdPreview.Visible := True;
end;
Expand Down Expand Up @@ -2058,7 +2123,11 @@ procedure TMarkdown4DStudioVCLForm.HandleResize(Sender: TObject);
LayoutTitleBar;

if FZenActive then
UpdateZenPadding;
UpdateZenPadding
else if FViewMode = TPadViewMode.Split then
// A narrower window must take room from the editor rather than from the
// preview, which would otherwise be squeezed to nothing.
ApplySplitEditorWidth(mdEditor.Width);

if FPalette <> nil then
FPalette.Left := (ClientWidth - FPalette.Width) div 2;
Expand Down
1 change: 1 addition & 0 deletions Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.dpr
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ uses
Markdown4DStudio.Defines in '..\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Shared\Markdown4DStudio.Outline.pas',
Markdown4DStudio.SplitLayout in '..\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudioVCL.Defines in 'Markdown4DStudioVCL.Defines.pas',
Markdown4DStudio.Shell in '..\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.CommandLine in '..\Shared\Markdown4DStudio.CommandLine.pas',
Expand Down
6 changes: 6 additions & 0 deletions Examples/Shared/Markdown4DStudio.Defines.pas
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ interface
SeparatorWidth = 1;
FindEditWidth = 160;
TocPanelWidth = 240;
// Neither half of the split view may be squeezed below this, whether by a
// remembered width, a window resize or a splitter drag.
MinPaneWidth = 300;
// The contents pane gives way first to protect those two, but never below
// this; past that point the two halves share what is left between them.
MinTocPanelWidth = 120;
TickIntervalMilliseconds = 100;
FindBarHeight = 32;
FindBarEditWidth = 240;
Expand Down
88 changes: 88 additions & 0 deletions Examples/Shared/Markdown4DStudio.SplitLayout.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
unit Markdown4DStudio.SplitLayout;

// Geometry for the editor/preview split, deliberately free of any framework type
// so the rule that neither pane may collapse is testable without a form and is
// shared by the VCL and FMX studios.

interface

type
TPadSplitLayout = record
public
/// <summary>
/// Editor width to apply for the window as it is now.
/// </summary>
/// <param name="SavedWidth">
/// Editor width remembered from the last time split view was active. It is
/// an absolute pixel value, so it can outlive the window size it was taken
/// at and ask for more room than there is.
/// </param>
/// <param name="AvailableWidth">
/// What the editor and the preview share, with the contents pane and every
/// splitter already subtracted.
/// </param>
/// <param name="MinPaneWidth">
/// The least either pane may become. When the available width cannot hold
/// two of these, the space is shared evenly instead.
/// </param>
class function ClampEditorWidth(const SavedWidth, AvailableWidth,
MinPaneWidth: Integer): Integer; static;

/// <summary>
/// The minimum a splitter can actually enforce for the window as it is
/// now. A splitter asked to honour a minimum the window cannot give lets a
/// drag hand the whole area to one pane, so below two minimums the answer
/// is half of what there is.
/// </summary>
class function EffectiveMinPaneWidth(const AvailableWidth,
MinPaneWidth: Integer): Integer; static;

/// <summary>
/// How wide a side panel, such as the contents pane, may be dragged before
/// it starts eating into the two halves of the split.
/// </summary>
/// <param name="TotalWidth">The whole client width.</param>
/// <param name="ReservedWidth">Width taken by the splitters themselves.</param>
/// <param name="MinSidePanelWidth">
/// How far the side panel may be asked to give way. Past that point it has
/// done all it can and the two panes share what is left between them. A
/// panel already narrower than this is never widened.
/// </param>
class function ClampSidePanelWidth(const DesiredWidth, TotalWidth,
ReservedWidth, MinPaneWidth, MinSidePanelWidth: Integer): Integer; static;
end;

implementation

uses
System.Math;

class function TPadSplitLayout.ClampEditorWidth(const SavedWidth, AvailableWidth,
MinPaneWidth: Integer): Integer;
begin
// Too narrow to honour both minimums: share what there is rather than hand the
// whole area to one pane and leave the other invisible.
if AvailableWidth <= 2 * MinPaneWidth then
Exit(Max(0, AvailableWidth div 2));

Result := EnsureRange(SavedWidth, MinPaneWidth, AvailableWidth - MinPaneWidth);
end;

class function TPadSplitLayout.EffectiveMinPaneWidth(const AvailableWidth,
MinPaneWidth: Integer): Integer;
begin
Result := Min(MinPaneWidth, Max(0, AvailableWidth div 2));
end;

class function TPadSplitLayout.ClampSidePanelWidth(const DesiredWidth, TotalWidth,
ReservedWidth, MinPaneWidth, MinSidePanelWidth: Integer): Integer;
begin
// What the side panel could take while both panes still keep their minimum.
const Allowed = Max(0, TotalWidth - ReservedWidth - 2 * MinPaneWidth);

// Never widen the panel, only ask it to give way, and never past its own
// minimum: below that the two panes share what is left instead.
Result := Min(DesiredWidth, Max(Allowed, MinSidePanelWidth));
end;

end.
2 changes: 2 additions & 0 deletions Tests/Markdown4D.Tests.dpr
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ uses
Markdown4DStudio.Defines in '..\Examples\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Examples\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Examples\Shared\Markdown4DStudio.Outline.pas',
Markdown4DStudio.SplitLayout in '..\Examples\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudio.CommandLine in '..\Examples\Shared\Markdown4DStudio.CommandLine.pas',
Markdown4DStudio.Shell in '..\Examples\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.EditorView in '..\Examples\Shared\Markdown4DStudio.EditorView.pas',
Expand All @@ -105,6 +106,7 @@ uses
Markdown4DStudio.Controller.Tests in 'Markdown4DStudio.Controller.Tests.pas',
Markdown4DStudio.HtmlExport.Tests in 'Markdown4DStudio.HtmlExport.Tests.pas',
Markdown4DStudio.LinkPolicy.Tests in 'Markdown4DStudio.LinkPolicy.Tests.pas',
Markdown4DStudio.SplitLayout.Tests in 'Markdown4DStudio.SplitLayout.Tests.pas',
Markdown4D.Text.UrlSafety.Tests in 'Markdown4D.Text.UrlSafety.Tests.pas',
Markdown4D.Viewer.ImageSettings.Tests in 'Markdown4D.Viewer.ImageSettings.Tests.pas',
StreamingMarkdown.Demo.Tests in 'StreamingMarkdown.Demo.Tests.pas',
Expand Down
Loading