Merge pull request #654 from TriliumNext/feature/adjustable_launcher_bar_position

Add support for horizontal launcher bar
This commit is contained in:
Elian Doran 2024-11-23 14:40:30 +02:00 committed by GitHub
commit 68fd954a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 418 additions and 183 deletions

View File

@ -88,8 +88,11 @@ export default class Component {
if (fun) { if (fun) {
return this.callMethod(fun, data); return this.callMethod(fun, data);
} } else {
else { if (!this.parent) {
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
}
return this.parent.triggerCommand(name, data); return this.parent.triggerCommand(name, data);
} }
} }

View File

@ -83,6 +83,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js"; import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) { constructor(customWidgets) {
@ -92,112 +93,120 @@ export default class DesktopLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
appContext.noteTreeWidget = new NoteTreeWidget(); appContext.noteTreeWidget = new NoteTreeWidget();
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext) .setParent(appContext)
.child(new FlexContainer("column") .optChild(launcherPaneIsHorizontal, new FlexContainer('row')
.id("launcher-pane") .child(new TabRowWidget().class("full-width"))
.css("width", "53px") .child(new TitleBarButtonsWidget())
.child(new GlobalMenuWidget()) .css('height', '40px')
.child(new LauncherContainer()) .css('background-color', 'var(--launcher-pane-background-color)')
.child(new LeftPaneToggleWidget()) .setParent(appContext)
) )
.child(new LeftPaneContainer() .optChild(launcherPaneIsHorizontal, launcherPane)
.child(new QuickSearchWidget()) .child(new FlexContainer('row')
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('column')
.id('rest-pane')
.css("flex-grow", "1") .css("flex-grow", "1")
.child(new FlexContainer('row') .optChild(!launcherPaneIsHorizontal, launcherPane)
.child(new TabRowWidget()) .child(new LeftPaneContainer()
.child(new TitleBarButtonsWidget()) .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.css('height', '40px') .child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
) )
.child(new FlexContainer('row') .child(new FlexContainer('column')
.filling() .id('rest-pane')
.collapsible() .css("flex-grow", "1")
.child(new FlexContainer('column') .optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget())
.css('height', '40px')
)
.child(new FlexContainer('row')
.filling() .filling()
.collapsible() .collapsible()
.id('center-pane') .child(new FlexContainer('column')
.child(new SplitNoteContainer(() => .filling()
new NoteWrapperWidget() .collapsible()
.child(new FlexContainer('row').class('title-row') .id('center-pane')
.css("height", "50px") .child(new SplitNoteContainer(() =>
.css("min-height", "50px") new NoteWrapperWidget()
.css('align-items', "center") .child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }') .css("height", "50px")
.child(new NoteIconWidget()) .css("min-height", "50px")
.child(new NoteTitleWidget()) .css('align-items', "center")
.child(new SpacerWidget(0, 1)) .cssBlock('.title-row > * { margin: 5px; }')
.child(new MovePaneButton(true)) .child(new NoteIconWidget())
.child(new MovePaneButton(false)) .child(new NoteTitleWidget())
.child(new ClosePaneButton()) .child(new SpacerWidget(0, 1))
.child(new CreatePaneButton()) .child(new MovePaneButton(true))
) .child(new MovePaneButton(false))
.child( .child(new ClosePaneButton())
new RibbonContainer() .child(new CreatePaneButton())
// the order of the widgets matter. Some of these want to "activate" themselves )
// when visible. When this happens to multiple of them, the first one "wins". .child(
// promoted attributes should always win. new RibbonContainer()
.ribbon(new ClassicEditorToolbar()) // the order of the widgets matter. Some of these want to "activate" themselves
.ribbon(new PromotedAttributesWidget()) // when visible. When this happens to multiple of them, the first one "wins".
.ribbon(new ScriptExecutorWidget()) // promoted attributes should always win.
.ribbon(new SearchDefinitionWidget()) .ribbon(new ClassicEditorToolbar())
.ribbon(new EditedNotesWidget()) .ribbon(new PromotedAttributesWidget())
.ribbon(new BookPropertiesWidget()) .ribbon(new ScriptExecutorWidget())
.ribbon(new NotePropertiesWidget()) .ribbon(new SearchDefinitionWidget())
.ribbon(new FilePropertiesWidget()) .ribbon(new EditedNotesWidget())
.ribbon(new ImagePropertiesWidget()) .ribbon(new BookPropertiesWidget())
.ribbon(new BasicPropertiesWidget()) .ribbon(new NotePropertiesWidget())
.ribbon(new OwnedAttributeListWidget()) .ribbon(new FilePropertiesWidget())
.ribbon(new InheritedAttributesWidget()) .ribbon(new ImagePropertiesWidget())
.ribbon(new NotePathsWidget()) .ribbon(new BasicPropertiesWidget())
.ribbon(new NoteMapRibbonWidget()) .ribbon(new OwnedAttributeListWidget())
.ribbon(new SimilarNotesWidget()) .ribbon(new InheritedAttributesWidget())
.ribbon(new NoteInfoWidget()) .ribbon(new NotePathsWidget())
.button(new RevisionsButton()) .ribbon(new NoteMapRibbonWidget())
.button(new NoteActionsWidget()) .ribbon(new SimilarNotesWidget())
) .ribbon(new NoteInfoWidget())
.child(new SharedInfoWidget()) .button(new RevisionsButton())
.child(new WatchedFileUpdateStatusWidget()) .button(new NoteActionsWidget())
.child(new FloatingButtons() )
.child(new EditButton()) .child(new SharedInfoWidget())
.child(new ShowTocWidgetButton()) .child(new WatchedFileUpdateStatusWidget())
.child(new ShowHighlightsListWidgetButton()) .child(new FloatingButtons()
.child(new CodeButtonsWidget()) .child(new EditButton())
.child(new RelationMapButtons()) .child(new ShowTocWidgetButton())
.child(new CopyImageReferenceButton()) .child(new ShowHighlightsListWidgetButton())
.child(new SvgExportButton()) .child(new CodeButtonsWidget())
.child(new BacklinksWidget()) .child(new RelationMapButtons())
.child(new HideFloatingButtonsButton()) .child(new CopyImageReferenceButton())
) .child(new SvgExportButton())
.child(new MermaidWidget()) .child(new BacklinksWidget())
.child( .child(new HideFloatingButtonsButton())
new ScrollingContainer() )
.filling() .child(new MermaidWidget())
.child(new SqlTableSchemasWidget()) .child(
.child(new NoteDetailWidget()) new ScrollingContainer()
.child(new NoteListWidget()) .filling()
.child(new SearchResultWidget()) .child(new SqlTableSchemasWidget())
.child(new SqlResultWidget()) .child(new NoteDetailWidget())
.child(new ScrollPaddingWidget()) .child(new NoteListWidget())
) .child(new SearchResultWidget())
.child(new ApiLogWidget()) .child(new SqlResultWidget())
.child(new FindWidget()) .child(new ScrollPaddingWidget())
.child( )
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC .child(new ApiLogWidget())
...this.customWidgets.get('note-detail-pane') .child(new FindWidget())
) .child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
...this.customWidgets.get('note-detail-pane')
)
)
) )
.child(...this.customWidgets.get('center-pane'))
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
) )
.child(...this.customWidgets.get('center-pane'))
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
) )
) )
) )
@ -225,4 +234,27 @@ export default class DesktopLayout {
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()); .child(new PromptDialog());
} }
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LeftPaneToggleWidget(true))
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true))
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")
.class("vertical")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false))
.child(new LeftPaneToggleWidget(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
} }

View File

@ -24,6 +24,7 @@ import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js"; import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@ -112,15 +113,12 @@ span.fancytree-expander {
export default class MobileLayout { export default class MobileLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext) .setParent(appContext)
.cssBlock(MOBILE_CSS) .cssBlock(MOBILE_CSS)
.child(new FlexContainer("column") .child(this.#buildLauncherPane(launcherPaneIsHorizontal))
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
)
.child(new FlexContainer("row") .child(new FlexContainer("row")
.filling() .filling()
.child(new ScreenContainer("tree", 'column') .child(new ScreenContainer("tree", 'column')
@ -140,12 +138,14 @@ export default class MobileLayout {
.child(new FlexContainer('row').contentSized() .child(new FlexContainer('row').contentSized()
.css('font-size', 'larger') .css('font-size', 'larger')
.css('align-items', 'center') .css('align-items', 'center')
.child(new MobileDetailMenuWidget().contentSized()) .optChild(!launcherPaneIsHorizontal, new MobileDetailMenuWidget(false).contentSized())
.child(new NoteTitleWidget() .child(new NoteTitleWidget()
.contentSized() .contentSized()
.css("position: relative;") .css("position: relative;")
.css("top: 5px;") .css("top: 5px;")
.optCss(launcherPaneIsHorizontal, "padding-left", "0.5em")
) )
.optChild(launcherPaneIsHorizontal, new MobileDetailMenuWidget(true).contentSized())
.child(new CloseDetailButtonWidget().contentSized())) .child(new CloseDetailButtonWidget().contentSized()))
.child(new SharedInfoWidget()) .child(new SharedInfoWidget())
.child(new FloatingButtons() .child(new FloatingButtons()
@ -174,4 +174,25 @@ export default class MobileLayout {
.child(new ConfirmDialog()) .child(new ConfirmDialog())
); );
} }
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true));
} else {
launcherPane = new FlexContainer("column")
.class("vertical")
.css("width", "53px")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
} }

View File

@ -40,6 +40,21 @@ class BasicWidget extends Component {
return this; return this;
} }
/**
* Conditionally adds the given components as children to this component.
*
* @param {boolean} condition whether to add the components.
* @param {...any} components the components to be added as children to this component provided the condition is truthy.
* @returns self for chaining.
*/
optChild(condition, ...components) {
if (condition) {
return this.child(...components);
} else {
return this;
}
}
id(id) { id(id) {
this.attrs.id = id; this.attrs.id = id;
return this; return this;
@ -50,11 +65,34 @@ class BasicWidget extends Component {
return this; return this;
} }
/**
* Sets the CSS attribute of the given name to the given value.
*
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
css(name, value) { css(name, value) {
this.attrs.style += `${name}: ${value};`; this.attrs.style += `${name}: ${value};`;
return this; return this;
} }
/**
* Sets the CSS attribute of the given name to the given value, but only if the condition provided is truthy.
*
* @param {boolean} condition `true` in order to apply the CSS, `false` to ignore it.
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
optCss(condition, name, value) {
if (condition) {
return this.css(name, value);
}
return this;
}
contentSized() { contentSized() {
this.css("contain", "none"); this.css("contain", "none");

View File

@ -23,7 +23,11 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.tooltip = new bootstrap.Tooltip(this.$widget, { this.tooltip = new bootstrap.Tooltip(this.$widget, {
html: true, title: () => this.getTitle(), trigger: 'hover' html: true,
title: () => this.getTitle(),
trigger: 'hover',
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
}) })
if (this.settings.onContextMenu) { if (this.settings.onContextMenu) {
@ -36,8 +40,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
}); });
} }
this.$widget.attr("data-placement", this.settings.titlePlacement);
super.doRender(); super.doRender();
} }

View File

@ -5,7 +5,7 @@ import UpdateAvailableWidget from "./update_available.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
const TPL = ` const TPL = `
<div class="dropdown global-menu dropend"> <div class="dropdown global-menu">
<style> <style>
.global-menu { .global-menu {
width: 53px; width: 53px;
@ -107,22 +107,6 @@ const TPL = `
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" <button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action global-menu-button"> aria-expanded="false" class="icon-action global-menu-button">
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>
<div class="global-menu-button-update-available"></div> <div class="global-menu-button-update-available"></div>
</button> </button>
@ -235,7 +219,7 @@ const TPL = `
${t('global_menu.options')} ${t('global_menu.options')}
</li> </li>
<div class="dropdown-divider"></div> <div class="dropdown-divider desktop-only"></div>
<li class="dropdown-item show-help-button" data-trigger-command="showHelp"> <li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-help-circle"></span> <span class="bx bx-help-circle"></span>
@ -265,18 +249,46 @@ const TPL = `
`; `;
export default class GlobalMenuWidget extends BasicWidget { export default class GlobalMenuWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.updateAvailableWidget = new UpdateAvailableWidget(); this.updateAvailableWidget = new UpdateAvailableWidget();
this.isHorizontalLayout = isHorizontalLayout;
} }
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); if (!this.isHorizontalLayout) {
this.$widget.addClass("dropend");
}
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" }); const $globalMenuButton = this.$widget.find(".global-menu-button")
if (!this.isHorizontalLayout) {
$globalMenuButton.prepend($(`\
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>`));
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
} else {
$globalMenuButton.toggleClass("bx bx-menu");
}
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"), {
alignment: "bottom"
});
this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog")); this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
@ -300,7 +312,7 @@ export default class GlobalMenuWidget extends BasicWidget {
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass('dropdown-toggle')) { if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass('dropdown-toggle')) {
e.stopPropagation(); e.stopPropagation();
} }
}) })
this.$widget.find(".global-menu-button-update-available").append( this.$widget.find(".global-menu-button-update-available").append(
this.updateAvailableWidget.render() this.updateAvailableWidget.render()
@ -316,10 +328,14 @@ export default class GlobalMenuWidget extends BasicWidget {
this.$zoomState = this.$widget.find(".zoom-state"); this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => { this.$widget.on('show.bs.dropdown', () => {
this.updateZoomState(); this.updateZoomState();
this.tooltip.hide(); if (this.tooltip) {
this.tooltip.disable(); this.tooltip.hide();
this.tooltip.disable();
}
}); });
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable()); if (this.tooltip) {
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
}
this.$widget.find(".zoom-buttons").on("click", this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change // delay to wait for the actual zoom change

View File

@ -4,7 +4,7 @@ import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget { export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.class("launcher-button"); this.class("launcher-button");
@ -20,6 +20,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
this.settings.command = () => options.is('leftPaneVisible') this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane" ? "hideLeftPane"
: "showLeftPane"; : "showLeftPane";
if (isHorizontalLayout) {
this.settings.titlePlacement = "bottom";
}
} }
refreshIcon() { refreshIcon() {

View File

@ -2,13 +2,7 @@ import BasicWidget from "../basic_widget.js";
const TPL = ` const TPL = `
<div class="dropdown right-dropdown-widget dropend"> <div class="dropdown right-dropdown-widget dropend">
<style> <button type="button" data-bs-toggle="dropdown"
.right-dropdown-widget {
height: 53px;
}
</style>
<button type="button" data-bs-toggle="dropdown" data-placement="right"
aria-haspopup="true" aria-expanded="false" aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button> class="bx right-dropdown-button launcher-button"></button>
@ -25,6 +19,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.iconClass = iconClass; this.iconClass = iconClass;
this.title = title; this.title = title;
this.dropdownTpl = dropdownTpl; this.dropdownTpl = dropdownTpl;
this.settings = {
titlePlacement: "right"
};
} }
doRender() { doRender() {
@ -33,7 +31,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title); this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new bootstrap.Tooltip(this.$tooltip); this.tooltip = new bootstrap.Tooltip(this.$tooltip, {
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
});
this.$widget.find(".right-dropdown-button") this.$widget.find(".right-dropdown-button")
.addClass(this.iconClass) .addClass(this.iconClass)

View File

@ -10,12 +10,14 @@ import CommandButtonWidget from "../buttons/command_button.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js"; import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
export default class LauncherWidget extends BasicWidget { export default class LauncherWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.innerWidget = null; this.innerWidget = null;
this.isHorizontalLayout = isHorizontalLayout;
} }
isEnabled() { isEnabled() {
@ -63,6 +65,9 @@ export default class LauncherWidget extends BasicWidget {
} }
this.child(this.innerWidget); this.child(this.innerWidget);
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true; return true;
} }
@ -86,29 +91,31 @@ export default class LauncherWidget extends BasicWidget {
initBuiltinWidget(note) { initBuiltinWidget(note) {
const builtinWidget = note.getLabelValue("builtinWidget"); const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
if (builtinWidget === 'calendar') { case "calendar":
return new CalendarWidget(note.title, note.getIcon()); return new CalendarWidget(note.title, note.getIcon());
} else if (builtinWidget === 'spacer') { case "spacer":
// || has to be inside since 0 is a valid value // || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100"); const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor); return new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') { case "bookmarks":
return new BookmarkButtons(); return new BookmarkButtons();
} else if (builtinWidget === 'protectedSession') { case "protectedSession":
return new ProtectedSessionStatusWidget(); return new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') { case "syncStatus":
return new SyncStatusWidget(); return new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') { case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory"); return new HistoryNavigationButton(note, "backInNoteHistory");
} else if (builtinWidget === 'forwardInHistoryButton') { case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory"); return new HistoryNavigationButton(note, "forwardInNoteHistory");
} else if (builtinWidget === 'todayInJournal') { case "todayInJournal":
return new TodayLauncher(note); return new TodayLauncher(note);
} else { case "quickSearch":
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); return new QuickSearchLauncherWidget(this.isHorizontalLayout);
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
} }
} }
} }

View File

@ -4,12 +4,13 @@ import appContext from "../../components/app_context.js";
import LauncherWidget from "./launcher.js"; import LauncherWidget from "./launcher.js";
export default class LauncherContainer extends FlexContainer { export default class LauncherContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('column'); super(isHorizontalLayout ? "row" : "column");
this.id('launcher-container'); this.id('launcher-container');
this.css('height', '100%'); this.css(isHorizontalLayout ? "width" : 'height', '100%');
this.filling(); this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load(); this.load();
} }
@ -29,7 +30,7 @@ export default class LauncherContainer extends FlexContainer {
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) { for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try { try {
const launcherWidget = new LauncherWidget(); const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote); const success = await launcherWidget.initLauncher(launcherNote);
if (success) { if (success) {

View File

@ -1,8 +1,8 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer { export default class RootContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('row'); super(isHorizontalLayout ? "column" : "row");
this.id('root-widget'); this.id('root-widget');
this.css('height', '100%'); this.css('height', '100%');

View File

@ -6,12 +6,20 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
const TPL = `<button type="button" class="action-button bx bx-menu" style="padding-top: 10px;"></button>`; const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
class MobileDetailMenuWidget extends BasicWidget { class MobileDetailMenuWidget extends BasicWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
this.$widget.on("click", async e => { this.$widget.on("click", async e => {
const note = appContext.tabManager.getActiveContextNote(); const note = appContext.tabManager.getActiveContextNote();

View File

@ -0,0 +1,33 @@
import utils from "../services/utils.js";
import QuickSearchWidget from "./quick_search.js";
/**
* Similar to the {@link QuickSearchWidget} but meant to be included inside the launcher bar.
*
* <p>
* Adds specific tweaks such as:
*
* - Hiding the widget on mobile.
*/
export default class QuickSearchLauncherWidget extends QuickSearchWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
if (!this.isHorizontalLayout) {
// The quick search widget is added somewhere else on the vertical layout.
return false;
}
if (utils.isMobile()) {
// The widget takes too much spaces to be included in the mobile layout.
return false;
}
return super.isEnabled();
}
}

View File

@ -55,6 +55,10 @@ const TAB_ROW_TPL = `
background: var(--main-background-color); background: var(--main-background-color);
overflow: hidden; overflow: hidden;
} }
.tab-row-widget.full-width {
background: var(--launcher-pane-background-color);
}
.tab-row-widget * { .tab-row-widget * {
box-sizing: inherit; box-sizing: inherit;

View File

@ -5,6 +5,26 @@ import { t } from "../../../../services/i18n.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4>${t("theme.layout")}</h4>
<div class="form-group row">
<div>
<label>
<input type="radio" name="layout-orientation" value="vertical" />
<strong>${t("theme.layout-vertical-title")}</strong>
- ${t("theme.layout-vertical-description")}
</label>
</div>
<div>
<label>
<input type="radio" name="layout-orientation" value="horizontal" />
<strong>${t("theme.layout-horizontal-title")}</strong>
- ${t("theme.layout-horizontal-description")}
</label>
</div>
</div>
<h4>${t("theme.title")}</h4> <h4>${t("theme.title")}</h4>
<div class="form-group row"> <div class="form-group row">
@ -19,7 +39,7 @@ const TPL = `
${t("theme.override_theme_fonts_label")} ${t("theme.override_theme_fonts_label")}
</label> </label>
</div> </div>
</div> </div>
</div>`; </div>`;
export default class ThemeOptions extends OptionsWidget { export default class ThemeOptions extends OptionsWidget {
@ -27,6 +47,11 @@ export default class ThemeOptions extends OptionsWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select"); this.$themeSelect = this.$widget.find(".theme-select");
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts"); this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
this.$layoutOrientation = this.$widget.find(`input[name="layout-orientation"]`).on("change", async () => {
const newLayoutOrientation = this.$widget.find(`input[name="layout-orientation"]:checked`).val();
await this.updateOption("layoutOrientation", newLayoutOrientation);
utils.reloadFrontendApp("layout orientation change");
});
this.$themeSelect.on('change', async () => { this.$themeSelect.on('change', async () => {
const newTheme = this.$themeSelect.val(); const newTheme = this.$themeSelect.val();
@ -57,5 +82,8 @@ export default class ThemeOptions extends OptionsWidget {
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts); this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`)
.prop("checked", "true");
} }
} }

View File

@ -40,6 +40,10 @@ body {
font-size: var(--main-font-size); font-size: var(--main-font-size);
} }
body.mobile .desktop-only {
display: none !important;
}
a { a {
text-decoration: none; text-decoration: none;
} }
@ -1022,6 +1026,18 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
overflow: auto; overflow: auto;
} }
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
left: calc(-100% + 10px);
}
#launcher-pane.horizontal .right-dropdown-widget {
width: 53px;
}
#launcher-pane.vertical .right-dropdown-widget {
height: 53px;
}
/* rotate caret on hover */ /* rotate caret on hover */
.dropdown-menu > li > a:hover:after { .dropdown-menu > li > a:hover:after {
text-decoration: underline; text-decoration: underline;
@ -1123,9 +1139,21 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
cursor: pointer; cursor: pointer;
border: none; border: none;
color: var(--launcher-pane-text-color); color: var(--launcher-pane-text-color);
background-color: var(--launcher-pane-background-color); background-color: var(--launcher-pane-background-color);
height: 53px; }
#launcher-pane.vertical .launcher-button {
width: 100%; width: 100%;
height: 53px;
}
#launcher-pane.horizontal .launcher-button {
width: 53px;
height: 100%;
}
#launcher-pane.horizontal .quick-search {
width: 350px;
} }
#launcher-pane .icon-action:hover { #launcher-pane .icon-action:hover {

View File

@ -1061,7 +1061,12 @@
"theme_label": "Theme", "theme_label": "Theme",
"override_theme_fonts_label": "Override theme fonts", "override_theme_fonts_label": "Override theme fonts",
"light_theme": "Light", "light_theme": "Light",
"dark_theme": "Dark" "dark_theme": "Dark",
"layout": "Layout",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"layout-vertical-description": "launcher bar is on the left (default)",
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
}, },
"zoom_factor": { "zoom_factor": {
"title": "Zoom Factor (desktop build only)", "title": "Zoom Factor (desktop build only)",

View File

@ -66,7 +66,8 @@ const ALLOWED_OPTIONS = new Set([
'editedNotesOpenInRibbon', 'editedNotesOpenInRibbon',
'locale', 'locale',
'firstDayOfWeek', 'firstDayOfWeek',
'textNoteEditorType' 'textNoteEditorType',
'layoutOrientation'
]); ]);
function getOptions() { function getOptions() {

View File

@ -34,7 +34,7 @@ interface Item {
baseSize?: string; baseSize?: string;
growthFactor?: string; growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap"; targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar"; builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
command?: keyof typeof Command; command?: keyof typeof Command;
} }
@ -240,6 +240,7 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
{ id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' }, { id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
{ id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' }, { id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
{ id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" }, { id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
{ id: '_lbQuickSearch', title: "Quick Search", type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' }, { id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
{ id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }, { id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
{ id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' } { id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }

View File

@ -134,7 +134,9 @@ const defaultOptions: DefaultOption[] = [
{ name: "codeBlockWordWrap", value: "false", isSynced: true }, { name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration // Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true } { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "layoutOrientation", value: "vertical", isSynced: false }
]; ];
/** /**