diff --git a/src/settings/TPMSettingsTab.tsx b/src/settings/TPMSettingsTab.tsx index 52e49f7..47e5c9d 100644 --- a/src/settings/TPMSettingsTab.tsx +++ b/src/settings/TPMSettingsTab.tsx @@ -2,7 +2,14 @@ import {App, PluginSettingTab, Setting, ValueComponent} from "obsidian"; import TPMPlugin from "../main"; import {createRoot, Root} from "react-dom/client"; import React, {useEffect, useState} from "react"; -import {getSettings, setSettingsValueAndSave, SettingName, TPM_DEFAULT_SETTINGS, usePluginSettings} from "./settings"; +import { + getSettings, + ModifierKeyOnClick, + setSettingsValueAndSave, + SettingName, + TPM_DEFAULT_SETTINGS, + usePluginSettings +} from "./settings"; import {SerializedType} from "./SerializedType"; import {isTagNameValid} from "../data-model/markdown-parse"; import {HStack, VStack} from "../ui/react-view/view-template/h-stack"; @@ -128,6 +135,20 @@ export class TPMSettingsTab extends PluginSettingTab { ) + new Setting(containerEl).setName("Always open task in new tab modify key") + .setDesc("If the key is pressed when clicking the task, always open in a new tab. None for never apply modifier keys.") + .addDropdown(dropdown => { + dropdown.addOptions({ + [ModifierKeyOnClick.None]: "None", + [ModifierKeyOnClick.Alt]: "Alt (Option)", + [ModifierKeyOnClick.MetaOrCtrl]: "Ctrl (Cmd)", + [ModifierKeyOnClick.Shift]: "Shift", + }).setValue(this.plugin.settings.always_open_task_in_new_tab_modify_key) + .onChange(async (value) => { + await setSettingsValueAndSave(this.plugin, "always_open_task_in_new_tab_modify_key", value) + }) + + }) } setValueAndSave(settingName: SettingName) { diff --git a/src/settings/settings.tsx b/src/settings/settings.tsx index c413902..fe76796 100644 --- a/src/settings/settings.tsx +++ b/src/settings/settings.tsx @@ -48,6 +48,14 @@ export enum TaskPriority { Low = 4, } +export enum ModifierKeyOnClick { + None = "None", // never open a new tab + MetaOrCtrl = "MetaOrCtrl", // meta is command on mac, windows key on windows + // Ctrl = "Ctrl", // ctrl on windows. not detected on mac + Alt = "Alt", // alt on windows, option on mac + Shift = "Shift", // shift +} + function getDefaultPriority() { return Math.floor(maxPriorityTags / 2); // 1 -> medium's index } @@ -87,7 +95,7 @@ export interface TPMSettings { priority_tags: SerializedType[], // 0.5.0, from high to low, should add the prefix `tpm/tag/` search_opened_tabs_before_navigating_tasks: boolean, // 0.6.0, if true, look for the existing tabs first, if not found, open a new tab; if false, always open in current tab open_new_tab_if_task_tab_not_found: boolean, // 0.6.0, if true, when the task file is not found in existing tabs, open a new tab; if false, open in current editor, - always_open_task_in_new_tab_modify_key: SerializedType, // 0.6.0. Key stroke enum. If click the task with this key pressed, always open a new tab. + always_open_task_in_new_tab_modify_key: ModifierKeyOnClick, // 0.6.0. Key stroke enum. If click the task with this key pressed, always open a new tab. cached_help_page_tutorial_tldr: SerializedType, } @@ -115,7 +123,7 @@ export const TPM_DEFAULT_SETTINGS: Partial = { priority_tags: ["hi", "med_hi", "med", "med_lo", "lo"] as SerializedType[], // 0.5.0 search_opened_tabs_before_navigating_tasks: true, // 0.6.0 open_new_tab_if_task_tab_not_found: true, // 0.6.0 - always_open_task_in_new_tab_modify_key: "Meta", // TODO + always_open_task_in_new_tab_modify_key: ModifierKeyOnClick.MetaOrCtrl, // 0.6.0 cached_help_page_tutorial_tldr: false, } export type SettingName = keyof TPMSettings; @@ -170,3 +178,20 @@ export function usePluginSettings(name: SettingName): }, [name, plugin, handler]); return [value, setValueAndSave]; } + + +export function getForceNewTabOnClick(plugin: OdaPmToolPlugin, event: React.MouseEvent) { + let forceNewTab = false; + switch (plugin.settings.always_open_task_in_new_tab_modify_key) { + case ModifierKeyOnClick.MetaOrCtrl: + forceNewTab = event.metaKey || event.ctrlKey; + break; + case ModifierKeyOnClick.Alt: + forceNewTab = event.altKey; + break; + case ModifierKeyOnClick.Shift: + forceNewTab = event.shiftKey; + break; + } + return forceNewTab; +} diff --git a/src/ui/react-view/event-handling/general-mouse-event-handler.tsx b/src/ui/react-view/event-handling/general-mouse-event-handler.tsx new file mode 100644 index 0000000..8b86895 --- /dev/null +++ b/src/ui/react-view/event-handling/general-mouse-event-handler.tsx @@ -0,0 +1,5 @@ +import {MouseEvent} from "react"; + +// dom has its own MouseEvent, but we want to use React's MouseEvent +// to satisfy react's generic type MouseEventHandler while ignore the generic type T +export type GeneralMouseEventHandler = ((e: MouseEvent) => void) | (() => void) diff --git a/src/ui/react-view/project-view.tsx b/src/ui/react-view/project-view.tsx index 2592253..f36a11b 100644 --- a/src/ui/react-view/project-view.tsx +++ b/src/ui/react-view/project-view.tsx @@ -1,11 +1,12 @@ import {OdaPmProject, OdaPmProjectDefinition} from "../../data-model/OdaPmProject"; -import React, {useContext} from "react"; +import React, {MouseEvent, useContext} from "react"; import {PluginContext} from "../obsidian/manage-page-view"; import {ClickableIconView, InternalLinkView} from "./view-template/icon-view"; import {iconViewAsAWholeStyle} from "./style-def"; import {openProjectPrecisely} from "../../utils/io-util"; import {VStack} from "./view-template/h-stack"; import {HoveringPopup, usePopup} from "./view-template/hovering-popup"; +import {getForceNewTabOnClick} from "../../settings/settings"; export const IconName_Project = "folder"; @@ -18,8 +19,9 @@ function ProjectLinkView(props: { onContentClicked={openProject} content={}/> - function openProject() { - openProjectPrecisely(props.project, props.def, plugin.app.workspace); + function openProject(e: MouseEvent) { + const forceNewTab = getForceNewTabOnClick(plugin, e); + openProjectPrecisely(props.project, props.def, plugin.app.workspace, forceNewTab); } } diff --git a/src/ui/react-view/task-table-view.tsx b/src/ui/react-view/task-table-view.tsx index 62a0038..5433974 100644 --- a/src/ui/react-view/task-table-view.tsx +++ b/src/ui/react-view/task-table-view.tsx @@ -2,9 +2,10 @@ import {OdaPmTask, setTaskPriority} from "../../data-model/OdaPmTask"; import OdaPmToolPlugin from "../../main"; import {I_OdaPmStep, I_OdaPmWorkflow, TaskStatus_checked, TaskStatus_unchecked} from "../../data-model/workflow-def"; import {openTaskPrecisely, rewriteTask} from "../../utils/io-util"; -import React, {ReactElement, useContext, useEffect, useState} from "react"; +import React, {MouseEvent, ReactElement, useContext, useEffect, useState} from "react"; import {PluginContext} from "../obsidian/manage-page-view"; import { + getForceNewTabOnClick, getSettings, setSettingsValueAndSave, TableSortBy, @@ -120,18 +121,16 @@ export const OdaTaskSummaryCell = ({oTask, taskFirstColumn, showCheckBox, showPr {showCheckBox ? { - devLog("[taskview] ExternalControlledCheckbox Clicked") - openThisTask(); - }} + onContentClicked={openThisTask} externalControl={oTask.stepCompleted()} /> : checkBoxContent} ; - function openThisTask() { - devLog("[taskview] Open this task") - openTaskPrecisely(workspace, oTask.boundTask); + function openThisTask(event: MouseEvent) { + const forceNewTab = getForceNewTabOnClick(plugin, event); + devLog(`[taskview] Open this task alt: ${event.altKey} shift: ${event.shiftKey} ctrl: ${event.ctrlKey} meta: ${event.metaKey} forceNewTab: ${forceNewTab}`) + openTaskPrecisely(workspace, oTask.boundTask, forceNewTab); } diff --git a/src/ui/react-view/view-template/checkbox.tsx b/src/ui/react-view/view-template/checkbox.tsx index 161e178..f158529 100644 --- a/src/ui/react-view/view-template/checkbox.tsx +++ b/src/ui/react-view/view-template/checkbox.tsx @@ -1,6 +1,7 @@ import React, {JSX, useState} from "react"; import {I_Stylable} from "./icon-view"; import {IRenderable} from "../../common/i-renderable"; +import {GeneralMouseEventHandler} from "../event-handling/general-mouse-event-handler"; /** * A checkbox that is totally controlled by its parent. @@ -15,7 +16,7 @@ export const ExternalControlledCheckbox = ({externalControl, onChange, onContent { externalControl: boolean, onChange: () => void, - onContentClicked?: () => void, + onContentClicked?: GeneralMouseEventHandler, content?: IRenderable, } & I_Stylable) => { diff --git a/src/ui/react-view/view-template/icon-view.tsx b/src/ui/react-view/view-template/icon-view.tsx index 74683a8..9cf08fd 100644 --- a/src/ui/react-view/view-template/icon-view.tsx +++ b/src/ui/react-view/view-template/icon-view.tsx @@ -3,6 +3,7 @@ import React from "react"; import {getIcon} from "obsidian"; import {HtmlStringComponent} from "./html-string-component"; import {I_InteractableId} from "../props-typing/i-interactable-id"; +import {GeneralMouseEventHandler} from "../event-handling/general-mouse-event-handler"; export const CssClass_Link = "cm-underline"; export const obsidianIconTopOffset = 4; @@ -33,8 +34,8 @@ export function ObsidianIconView({iconName, style}: { iconName: string } & I_Sty */ export function InternalLinkView({content, onIconClicked, onContentClicked, style}: { content: IRenderable, - onIconClicked?: () => void, - onContentClicked?: () => void, + onIconClicked?: GeneralMouseEventHandler, + onContentClicked?: GeneralMouseEventHandler, } & I_Stylable) { return void; - onContentClicked?: () => void; + onIconClicked?: GeneralMouseEventHandler + onContentClicked?: GeneralMouseEventHandler clickable?: boolean; } diff --git a/src/ui/react-view/workflow-filter.tsx b/src/ui/react-view/workflow-filter.tsx index 75f7e98..62e7561 100644 --- a/src/ui/react-view/workflow-filter.tsx +++ b/src/ui/react-view/workflow-filter.tsx @@ -1,7 +1,7 @@ // region Legend import {HStack} from "./view-template/h-stack"; import {I_OdaPmWorkflow, Workflow_Type_Enum_Array, WorkflowType} from "../../data-model/workflow-def"; -import React, {useContext} from "react"; +import React, {MouseEvent, useContext} from "react"; import {PluginContext} from "../obsidian/manage-page-view"; import {ExternalControlledCheckbox} from "./view-template/checkbox"; import {I_Stylable, InternalLinkView} from "./view-template/icon-view"; @@ -14,6 +14,7 @@ import {taskCheckBoxMargin} from "./task-table-view"; import {ExternalToggleView} from "./view-template/toggle-view"; import {OptionValueType, SearchableDropdown} from "./view-template/searchable-dropdown"; import {devLog} from "../../utils/env-util"; +import {getForceNewTabOnClick} from "../../settings/settings"; /** * Accept children as a HStack with a unified style @@ -200,11 +201,15 @@ export const ClickableWorkflowView = ({workflow, displayNames, setDisplayNames, {showWorkflowIcon ? getIconByWorkflow(workflow) : null} } - onIconClicked={() => - // Go to workflow def - openTaskPrecisely(plugin.app.workspace, workflow.boundTask)} + onIconClicked={openThisWorkflow} onContentClicked={tickCheckbox}/> ; + + function openThisWorkflow(e: MouseEvent) { + const forceNewTab = getForceNewTabOnClick(plugin, e); + return openTaskPrecisely(plugin.app.workspace, workflow.boundTask, forceNewTab); + } + return {showCheckBox ? ]*(\d+\.|\d+\)|\*|-|\+)\s*(\[.{0,1}\])?\s*( // endregion -function openFileAtStart(workspace: Workspace, path: string) { - workspace.openLinkText(path, path, false, { - state: { - active: true - }, - eState: { - line: 0, - cursor: { - from: {line: 0, ch: 0}, - to: {line: 1, ch: 0}, - }, - }, - }); - -} - -// if we use workspace.openLinkText, a task without a block id will be opened with its section -export async function openTaskPrecisely(workspace: Workspace, task: STask) { - devLog(`openTaskPrecisely ${workspace.getLeavesOfType("markdown").length} tabs`) +async function getOpenInNewTab(forceNewTab: boolean, workspace: Workspace, path: string) { const settings = getSettings(); let foundTabWithFile = false; - if (settings?.search_opened_tabs_before_navigating_tasks) { + if ( + !forceNewTab && settings?.search_opened_tabs_before_navigating_tasks + ) { const mdLeaf = workspace.getLeavesOfType("markdown").find((k: WorkspaceLeaf) => { const mdView = k.view as MarkdownView; // mdView can have no file (ie the file is not saved to disk) - return mdView.file?.path === task.path; + return mdView.file?.path === path; }) if (mdLeaf) { // If found, set the active leaf to this view @@ -86,18 +70,44 @@ export async function openTaskPrecisely(workspace: Workspace, task: STask) { } - let openInNewLeaf = false; - if (!settings?.search_opened_tabs_before_navigating_tasks) { - // if we don't search opened tabs, we always open in current tab - openInNewLeaf = false; - } else { - if (foundTabWithFile) { + let openInNewLeaf = forceNewTab; + if (!forceNewTab) { + // if we do not force new tab, we need to check if we should open in new tab + if (!settings?.search_opened_tabs_before_navigating_tasks) { + // if we don't search opened tabs, we always open in current tab openInNewLeaf = false; } else { - openInNewLeaf = settings?.open_new_tab_if_task_tab_not_found ?? false; + if (foundTabWithFile) { + openInNewLeaf = false; + } else { + openInNewLeaf = settings?.open_new_tab_if_task_tab_not_found ?? false; + } } } - + return openInNewLeaf; +} + +async function openFileAtStart(workspace: Workspace, path: string, forceNewTab = false) { + const openInNewLeaf = await getOpenInNewTab(forceNewTab, workspace, path); + workspace.openLinkText(path, path, openInNewLeaf, { + state: { + active: true + }, + eState: { + line: 0, + cursor: { + from: {line: 0, ch: 0}, + to: {line: 1, ch: 0}, + }, + }, + }); +} + + +// if we use workspace.openLinkText, a task without a block id will be opened with its section +export async function openTaskPrecisely(workspace: Workspace, task: STask, forceNewTab = false) { + devLog(`[taskview] openTaskPrecisely ${workspace.getLeavesOfType("markdown").length} tabs. ForceNewTab ${forceNewTab}`) + const openInNewLeaf = await getOpenInNewTab(forceNewTab, workspace, task.path); // Copy from dataview. See TaskItem. // highlight cursor devLog(`[OpenTask] Task ${task.path} openInNewLeaf ${openInNewLeaf}`) @@ -117,16 +127,16 @@ export async function openTaskPrecisely(workspace: Workspace, task: STask) { ); } -export function openProjectPrecisely(project: OdaPmProject, defType: OdaPmProjectDefinition, workspace: Workspace) { +export function openProjectPrecisely(project: OdaPmProject, defType: OdaPmProjectDefinition, workspace: Workspace, forceNewTab = false) { switch (defType.type) { case "folder": // Project_FolderProject_Frontmatter case "file": // Project_FileProject_Frontmatter - openFileAtStart(workspace, defType.page?.path) + openFileAtStart(workspace, defType.page?.path, forceNewTab) console.log(defType.page?.path) break; case "tag_override": // task or wf override console.log(`openProjectPrecisely: tag_override. ${defType.taskable} ${defType.path}`) - openTaskPrecisely(workspace, defType.taskable?.boundTask) + openTaskPrecisely(workspace, defType.taskable?.boundTask, forceNewTab) break; } }