mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 18:01:45 +08:00
Find best window to open second-instance files and directories in (#1054)
* Files via command-line are opened in the best window * Don't show FSW changed notification while saving * Fixed source-code mode setting and remove focus/typewritter option * Simplify ignore list * Fix invalid dialog parameter * Fix invalid dialog parameter (2) * Use async message box dialog * Update documentation * few changes * Check timer before calling clearTimeout * Improve switch style * Fix style
This commit is contained in:
parent
164e9a1d87
commit
e6e652713a
@ -11,16 +11,16 @@ const getEnvironmentDefinitions = function () {
|
||||
} catch(_) {
|
||||
// Ignore error if we build without git.
|
||||
}
|
||||
const isOfficialRelease = !!process.env.MARKTEXT_IS_OFFICIAL_RELEASE
|
||||
const versionSuffix = isOfficialRelease ? '' : ` (${shortHash})`
|
||||
|
||||
const isStableRelease = !!process.env.MARKTEXT_IS_STABLE
|
||||
const versionSuffix = isStableRelease ? '' : ` (${shortHash})`
|
||||
return {
|
||||
'global.MARKTEXT_GIT_SHORT_HASH': JSON.stringify(shortHash),
|
||||
'global.MARKTEXT_GIT_HASH': JSON.stringify(fullHash),
|
||||
|
||||
'global.MARKTEXT_VERSION': JSON.stringify(version),
|
||||
'global.MARKTEXT_VERSION_STRING': JSON.stringify(`v${version}${versionSuffix}`),
|
||||
'global.MARKTEXT_IS_OFFICIAL_RELEASE': JSON.stringify(isOfficialRelease)
|
||||
'global.MARKTEXT_IS_STABLE': JSON.stringify(isStableRelease)
|
||||
}
|
||||
}
|
||||
|
||||
|
18
.github/CHANGELOG.md
vendored
18
.github/CHANGELOG.md
vendored
@ -4,18 +4,28 @@
|
||||
|
||||
- `preference.md` is deprecated and no longer supported. Please use the GUI or edit `preferences.json` manually.
|
||||
- Removed portable Windows executable. NSIS installer can now be used to install per-user or machine wide.
|
||||
- Changed `viewToggleFullScreen` and `windowCloseWindow` key bindings to `windowToggleFullScreen` and `fileCloseWindow`.
|
||||
- Removed `viewChangeFont` key binding.
|
||||
- Mark Text is now single-instance application on Linux and Windows too.
|
||||
|
||||
**:cactus:Feature**
|
||||
|
||||
- GUI settings (#1028)
|
||||
- The cursor jump to the end of format or to the next brackets when press `tab`(#976)
|
||||
- Tab drag & drop inside the window
|
||||
- Scrollable tabs
|
||||
- Support to replace the root folder in a window
|
||||
- Second-instance files and directories via command-line are opened in the best window
|
||||
- Mark Text can use a default directory that is automatically opened during startup (#711)
|
||||
- New CLI flags: `--disable-gpu` and `-n,--new-window`
|
||||
|
||||
**:butterfly:Optimization**
|
||||
|
||||
- Rewrite `select all` when press `CtrlOrCmd + A` (#937)
|
||||
- Set the cursor at the end of `#` in header when press arrow down to jump to the next paragraph.(#978)
|
||||
- Improved startup time
|
||||
- Replace empty untitled tabs (#830)
|
||||
- Editor window is shown immediately while loading
|
||||
|
||||
**:beetle:Bug fix**
|
||||
|
||||
@ -23,6 +33,14 @@
|
||||
- Fixed some bugs after press `backspace` (#934, #938)
|
||||
- Change `inline math` vertical align to `top` (#977)
|
||||
- Prevent to open the same file twice, instead select the existing tab (#878)
|
||||
- Fixed some minor filesystem watcher issues
|
||||
- Fixed rename filesystem watcher bug which lead to multiple issues because the parent directory was watched after deleting a file on Linux using `rename`
|
||||
- Fixed incorrect file content after a watched file was edited externally (#1043)
|
||||
|
||||
**:warning:Breaking Development Changes:**
|
||||
|
||||
- Environment variable `MARKTEXT_IS_OFFICIAL_RELEASE` is now `MARKTEXT_IS_STABLE`
|
||||
- Renamed npm script `build:dir` to `build:bin`
|
||||
|
||||
### 0.14.0
|
||||
|
||||
|
@ -7,11 +7,11 @@ matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode9.2
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1 MARKTEXT_IS_OFFICIAL_RELEASE=1 MARKTEXT_EXIT_ON_ERROR=1
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1 MARKTEXT_IS_STABLE=1 MARKTEXT_EXIT_ON_ERROR=1
|
||||
compiler: clang
|
||||
- os: linux
|
||||
dist: trusty
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1 MARKTEXT_IS_OFFICIAL_RELEASE=1 MARKTEXT_EXIT_ON_ERROR=1 DISPLAY=:99.0
|
||||
env: CC=clang CXX=clang++ npm_config_clang=1 MARKTEXT_IS_STABLE=1 MARKTEXT_EXIT_ON_ERROR=1 DISPLAY=:99.0
|
||||
compiler: clang
|
||||
|
||||
cache:
|
||||
|
@ -14,7 +14,7 @@ branches:
|
||||
skip_tags: true
|
||||
|
||||
environment:
|
||||
MARKTEXT_IS_OFFICIAL_RELEASE: 1
|
||||
MARKTEXT_IS_STABLE: 1
|
||||
MARKTEXT_EXIT_ON_ERROR: 1
|
||||
GH_TOKEN:
|
||||
secure: Ki5AJWygDYhzMJxl0b0rDx3bhAYmar2aPdwVHiai9IigqsvZpWHLeI3qpTiiaOWL
|
||||
|
15
doc/ENVIRONMENT.md
Normal file
15
doc/ENVIRONMENT.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Environment
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------- | ----------------------------------------------------------- |
|
||||
| `MARKTEXT_DEBUG` | Enable debug mode. |
|
||||
| `MARKTEXT_DEBUG_KEYBOARD` | Print more keyboard information when debug mode is enabled. |
|
||||
| `MARKTEXT_ERROR_INTERACTION` | Never show the error dialog to report bugs. |
|
||||
|
||||
## Development
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------ |
|
||||
| `MARKTEXT_EXIT_ON_ERROR` | Exit on the first error or exception that occurs. |
|
||||
| `MARKTEXT_DEV_HIDE_BROWSER_ANALYZER` | Don't show the dependency analyzer. |
|
||||
| `MARKTEXT_IS_STABLE` | **Please don't use this!** Used to identify stable releases. |
|
10
docs/CLI.md
10
docs/CLI.md
@ -1,20 +1,22 @@
|
||||
# Command Line Interface
|
||||
|
||||
```
|
||||
Usage: marktext [commands] [path]
|
||||
Usage: marktext [commands] [path ...]
|
||||
|
||||
Available commands:
|
||||
|
||||
--debug Enable debug mode
|
||||
--safe Disable plugins and other user configuration
|
||||
--dump-keyboard-layout Dump keyboard information
|
||||
--user-data-dir Change the user data directory
|
||||
--disable-gpu Disable GPU hardware acceleration
|
||||
-v, --verbose Be verbose
|
||||
--version Print version information
|
||||
--help Print this help message
|
||||
-h, --help Print this help message
|
||||
```
|
||||
|
||||
`marktext` should point to your installation of Mark Text. The exact location will vary from platform to platform. Since I'm on macOS, I created convenient alias for the version of Mark Text that I have installed.
|
||||
`marktext` should point to your installation of Mark Text. The exact location will vary from platform to platform. On macOS, you can create a convenient alias like:
|
||||
|
||||
```sh
|
||||
alias marktext="/Applications/Mark\ Text.app/Contents/MacOS/Mark\ Text"
|
||||
```
|
||||
|
||||
|
@ -33,9 +33,26 @@ Here is an example:
|
||||
**Mark Text menu (macOS only):**
|
||||
|
||||
| Id | Description |
|
||||
| -------------- | --------------------------------------- |
|
||||
| ----------------- | --------------------------------------- |
|
||||
| `mtHide` | Hide Mark Text |
|
||||
| `mtHideOthers` | Hide all other windows except Mark Text |
|
||||
| `filePreferences` | Open settings window |
|
||||
| `fileQuit` | Quit Mark Text |
|
||||
|
||||
**File menu:**
|
||||
|
||||
| Id | Description |
|
||||
|:----------------- | ----------------------------------------- |
|
||||
| `fileNewFile` | New file |
|
||||
| `fileNewTab` | New tab |
|
||||
| `fileOpenFile` | Open markdown file |
|
||||
| `fileOpenFolder` | Open folder |
|
||||
| `fileSave` | Save |
|
||||
| `fileSaveAs` | Save as... |
|
||||
| `filePreferences` | Open settings window (Linux/Windows only) |
|
||||
| `fileCloseTab` | Close tab |
|
||||
| `fileCloseWindow` | Close window |
|
||||
| `fileQuit` | Quit Mark Text (Linux/Windows only) |
|
||||
|
||||
**Edit menu:**
|
||||
|
||||
@ -100,16 +117,14 @@ Here is an example:
|
||||
**Window menu:**
|
||||
|
||||
| Id | Description |
|
||||
| ------------------- | ------------------- |
|
||||
| ------------------------ | ---------------------- |
|
||||
| `windowMinimize` | Minimize the window |
|
||||
| `windowCloseWindow` | Close the window |
|
||||
| `windowToggleFullScreen` | Toggle fullscreen mode |
|
||||
|
||||
**View menu:**
|
||||
|
||||
| Id | Description |
|
||||
| ----------------------------- | ---------------------------------------- |
|
||||
| `viewToggleFullScreen` | Toggle fullscreen mode |
|
||||
| `viewChangeFont` | Open font dialog |
|
||||
| `viewSourceCodeMode` | Switch to source code mode |
|
||||
| `viewTypewriterMode` | Enable typewriter mode |
|
||||
| `viewFocusMode` | Enable focus mode |
|
||||
@ -117,4 +132,3 @@ Here is an example:
|
||||
| `viewToggleTabBar` | Toggle tabbar |
|
||||
| `viewDevToggleDeveloperTools` | Toggle developer tools (debug mode only) |
|
||||
| `viewDevReload` | Reload window (debug mode only) |
|
||||
|
||||
|
@ -3,14 +3,16 @@
|
||||
#### General
|
||||
|
||||
| Key | Type | Default Value | Description |
|
||||
| -------------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| autoSave | Boolean | ture | Automatically save the content being edited. option value: true, false |
|
||||
| autoSaveDelay | Number | 3000 | The delay in milliseconds after a changed file is saved automatically? 3000 ~10000 |
|
||||
| titleBarStyle | String | csd | The title bar style. the native option will result in a standard gray opaque title bar. `csd` (macOS only), `custom`, `native` |
|
||||
| ---------------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| autoSave | Boolean | false | Automatically save the content being edited. option value: true, false |
|
||||
| autoSaveDelay | Number | 5000 | The delay in milliseconds after a changed file is saved automatically? 3000 ~10000 |
|
||||
| titleBarStyle | String | custom | The title bar style on Linux and Window: `custom` or `native` |
|
||||
| openFilesInNewWindow | Boolean | false | true, false |
|
||||
| openFolderInNewWindow | Boolean | false | true, false |
|
||||
| aidou | Boolean | true | Enable aidou. Optional value: true, false |
|
||||
| fileSortBy | String | modified | Sort files in opened folder by `created` time, modified time and title. |
|
||||
| startUp | String | lastState | The action after Mark Text startup, open the last edited content, open the specified folder or blank page, optional value: `lasteState`, `folder`, `blank` |
|
||||
| fileSortBy | String | created | Sort files in opened folder by `created` time, modified time and title. |
|
||||
| startUpAction | String | lastState | The action after Mark Text startup, open the last edited content, open the specified folder or blank page, optional value: `lasteState`, `folder`, `blank` |
|
||||
| defaultDirectoryToOpen | String | `""` | The path that should be opened if `startUpAction=folder`. |
|
||||
| language | String | en | The language Mark Text use. |
|
||||
|
||||
#### Editor
|
||||
@ -33,7 +35,7 @@
|
||||
#### Markdown
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| preferLooseListItem | Boolean | true | The preferred list type. |
|
||||
| bulletListMarker | String | `-` | The preferred marker used in bullet list, optional value: `-`, `*` `+` |
|
||||
| orderListDelimiter | String | `.` | The preferred delimiter used in order list, optional value: `.` `)` |
|
||||
@ -41,8 +43,18 @@
|
||||
| tabSize | Number | 4 | The number of spaces a tab is equal to |
|
||||
| listIndentation | String | 1 | The list indentation of sub list items or paragraphs, optional value `dfm`, `tab` or number 1~4 |
|
||||
|
||||
#### View
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ----------------------------- | ------- | ------- | -------------------------------------------------- |
|
||||
| sideBarVisibility<sup>*</sup> | Boolean | false | Controls the visibility of the side bar. |
|
||||
| tabBarVisibility<sup>*</sup> | Boolean | false | Controls the visibility of the tabs. |
|
||||
| sourceCodeModeEnabled* | Boolean | false | Controls the visibility of the source-code editor. |
|
||||
|
||||
\*: These options are default/fallback values that are used if not session is loaded and are overwritten by the menu entries.
|
||||
|
||||
#### Theme
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ----- | ------ | ------- | -------------------------------------------------------------- |
|
||||
| theme | String | light | `dark` `graphite` `material-dark` `one-dark` `light` `ulysses` |
|
||||
| ----- | ------ | ------- | --------------------------------------------------------------------- |
|
||||
| theme | String | light | `dark`, `graphite`, `material-dark`, `one-dark`, `light` or `ulysses` |
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- Create a release candidate
|
||||
- Create branch `release-v%version%`
|
||||
- Set environment variable `MARKTEXT_IS_OFFICIAL_RELEASE` to `1` (default on AppVeyor and Travis CI)
|
||||
- Set environment variable `MARKTEXT_IS_STABLE` to `1` (default on AppVeyor and Travis CI)
|
||||
- Ensure [changelog](https://github.com/marktext/marktext/blob/master/.github/CHANGELOG.md) is up-to-date
|
||||
- Bump version in `package.json` and changelog
|
||||
- Update all `README.md` files
|
||||
|
@ -11,7 +11,7 @@
|
||||
"release:mac": "node .electron-vue/build.js && electron-builder build --mac --publish always",
|
||||
"release:win": "node .electron-vue/build.js && electron-builder build --win --publish always",
|
||||
"build": "node .electron-vue/build.js && electron-builder",
|
||||
"build:dir": "node .electron-vue/build.js && electron-builder --dir",
|
||||
"build:bin": "node .electron-vue/build.js && electron-builder --dir",
|
||||
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
|
||||
"build:dev": "node .electron-vue/build.js",
|
||||
"dev": "node .electron-vue/dev-runner.js",
|
||||
@ -189,7 +189,6 @@
|
||||
"prismjs": "^1.16.0",
|
||||
"snabbdom": "^0.7.3",
|
||||
"snabbdom-to-html": "^5.1.1",
|
||||
"snapsvg": "^0.5.1",
|
||||
"source-map-support": "^0.5.12",
|
||||
"turndown": "^5.0.3",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
@ -226,7 +225,6 @@
|
||||
"dotenv": "^8.0.0",
|
||||
"electron": "^5.0.1",
|
||||
"electron-builder": "^20.40.2",
|
||||
"electron-debug": "^3.0.0",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"electron-rebuild": "^1.8.4",
|
||||
"electron-updater": "^4.0.6",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,3 +9,9 @@ Categories=Office;TextEditor;Utility;
|
||||
MimeType=text/markdown;
|
||||
Keywords=marktext;
|
||||
StartupWMClass=marktext
|
||||
Actions=NewWindow;
|
||||
|
||||
[Desktop Action NewWindow]
|
||||
Name=New Window
|
||||
Exec=marktext --new-window %F
|
||||
Icon=marktext
|
||||
|
@ -5,7 +5,6 @@
|
||||
<title>Mark Text</title>
|
||||
<style>
|
||||
html, body {
|
||||
background: #ffffff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@ -18,7 +17,12 @@
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Initial drag area (for Linux and Windows). -->
|
||||
<div id="init-drag-region" style="position: absolute;-webkit-app-region: drag;left: 0;right: 0;top: 0;height: 25px;"></div>
|
||||
|
||||
<!-- Vue app -->
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Set `__static` path to static files in production -->
|
||||
<script>
|
||||
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
|
@ -1,19 +1,20 @@
|
||||
import path from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import log from 'electron-log'
|
||||
import { exec } from 'child_process'
|
||||
import { app, ipcMain, systemPreferences, clipboard } from 'electron'
|
||||
import dayjs from 'dayjs'
|
||||
import log from 'electron-log'
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron'
|
||||
import { isLinux, isOsx } from '../config'
|
||||
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath } from '../filesystem'
|
||||
import parseArgs from '../cli/parser'
|
||||
import { isChildOfDirectory } from '../filesystem'
|
||||
import { normalizeMarkdownPath } from '../filesystem/markdown'
|
||||
import { getMenuItemById } from '../menu'
|
||||
import { selectTheme } from '../menu/actions/theme'
|
||||
import { dockMenu } from '../menu/templates'
|
||||
import { watchers } from '../utils/imagePathAutoComplement'
|
||||
import { WindowType } from '../windows/base'
|
||||
import EditorWindow from '../windows/editor'
|
||||
import SettingWindow from '../windows/setting'
|
||||
import { WindowType } from './windowManager'
|
||||
// import ShortcutCapture from 'shortcut-capture'
|
||||
|
||||
class App {
|
||||
|
||||
@ -42,6 +43,39 @@ class App {
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features', 'true')
|
||||
}
|
||||
|
||||
app.on('second-instance', (event, argv, workingDirectory) => {
|
||||
const { _openFilesCache, _windowManager } = this
|
||||
const args = parseArgs(argv.slice(1))
|
||||
|
||||
const buf = []
|
||||
for (const pathname of args._) {
|
||||
// Ignore all unknown flags
|
||||
if (pathname.startsWith('--')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const info = normalizeMarkdownPath(path.resolve(workingDirectory, pathname))
|
||||
if (info) {
|
||||
buf.push(info)
|
||||
}
|
||||
}
|
||||
|
||||
if (args['--new-window']) {
|
||||
this._openPathList(buf, true)
|
||||
return
|
||||
}
|
||||
|
||||
_openFilesCache.push(...buf)
|
||||
if (_openFilesCache.length) {
|
||||
this._openFilesToOpen()
|
||||
} else {
|
||||
const activeWindow = _windowManager.getActiveWindow()
|
||||
if (activeWindow) {
|
||||
activeWindow.bringToFront()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on('open-file', this.openFile) // macOS only
|
||||
|
||||
app.on('ready', this.ready)
|
||||
@ -89,7 +123,9 @@ class App {
|
||||
}
|
||||
|
||||
ready = () => {
|
||||
const { _args: args } = this
|
||||
const { _args: args, _openFilesCache } = this
|
||||
const { preferences } = this._accessor
|
||||
|
||||
if (!isOsx && args._.length) {
|
||||
for (const pathname of args._) {
|
||||
// Ignore all unknown flags
|
||||
@ -97,13 +133,21 @@ class App {
|
||||
continue
|
||||
}
|
||||
|
||||
const info = this.normalizePath(pathname)
|
||||
const info = normalizeMarkdownPath(pathname)
|
||||
if (info) {
|
||||
this._openFilesCache.push(info)
|
||||
_openFilesCache.push(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { startUpAction, defaultDirectoryToOpen } = preferences.getAll()
|
||||
if (startUpAction === 'folder' && defaultDirectoryToOpen) {
|
||||
const info = normalizeMarkdownPath(defaultDirectoryToOpen)
|
||||
if (info) {
|
||||
_openFilesCache.unshift(info)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.setMenu(dockMenu)
|
||||
|
||||
@ -135,11 +179,12 @@ class App {
|
||||
)
|
||||
}
|
||||
|
||||
if (this._openFilesCache.length) {
|
||||
this.openFileCache()
|
||||
if (_openFilesCache.length) {
|
||||
this._openFilesToOpen()
|
||||
} else {
|
||||
this.createEditorWindow()
|
||||
this._createEditorWindow()
|
||||
}
|
||||
|
||||
// this.shortcutCapture = new ShortcutCapture()
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// this.shortcutCapture.dirname = path.resolve(path.join(__dirname, '../../../node_modules/shortcut-capture'))
|
||||
@ -165,7 +210,7 @@ class App {
|
||||
|
||||
openFile = (event, pathname) => {
|
||||
event.preventDefault()
|
||||
const info = this.normalizePath(pathname)
|
||||
const info = normalizeMarkdownPath(pathname)
|
||||
if (info) {
|
||||
this._openFilesCache.push(info)
|
||||
|
||||
@ -176,53 +221,37 @@ class App {
|
||||
}
|
||||
this._openFilesTimer = setTimeout(() => {
|
||||
this._openFilesTimer = null
|
||||
this.openFileCache()
|
||||
this._openFilesToOpen()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openFileCache = () => {
|
||||
// TODO: Allow to open multiple files in the same window.
|
||||
this._openFilesCache.forEach(fileInfo => this.createEditorWindow(fileInfo.path))
|
||||
this._openFilesCache.length = 0 // empty the open file path cache
|
||||
}
|
||||
|
||||
normalizePath = pathname => {
|
||||
const isDir = isDirectory(pathname)
|
||||
if (isDir || isMarkdownFileOrLink(pathname)) {
|
||||
// Normalize and resolve the path or link target.
|
||||
const resolved = normalizeAndResolvePath(pathname)
|
||||
if (resolved) {
|
||||
return { isDir, path: resolved }
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot resolve "${pathname}".`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// --- private --------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new editor window.
|
||||
*
|
||||
* @param {string} [pathname] Path to a file, directory or link.
|
||||
* @param {string} [markdown] Markdown content.
|
||||
* @param {*} [options] BrowserWindow options.
|
||||
* @param {string} [rootDirectory] The root directory to open.
|
||||
* @param {string[]} [fileList] A list of markdown files to open.
|
||||
* @param {string[]} [markdownList] Array of markdown data to open.
|
||||
* @param {*} [options] The BrowserWindow options.
|
||||
* @returns {EditorWindow} The created editor window.
|
||||
*/
|
||||
createEditorWindow (pathname = null, markdown = '', options = {}) {
|
||||
_createEditorWindow (rootDirectory = null, fileList = [], markdownList = [], options = {}) {
|
||||
const editor = new EditorWindow(this._accessor)
|
||||
editor.createWindow(pathname, markdown, options)
|
||||
editor.createWindow(rootDirectory, fileList, markdownList, options)
|
||||
this._windowManager.add(editor)
|
||||
if (this._windowManager.windowCount === 1) {
|
||||
this._accessor.menu.setActiveWindow(editor.id)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new setting window.
|
||||
*/
|
||||
createSettingWindow () {
|
||||
_createSettingWindow () {
|
||||
const setting = new SettingWindow(this._accessor)
|
||||
setting.createWindow()
|
||||
this._windowManager.add(setting)
|
||||
@ -231,25 +260,139 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(sessions): ...
|
||||
// // Make Mark Text a single instance application.
|
||||
// _makeSingleInstance() {
|
||||
// if (process.mas) return
|
||||
//
|
||||
// app.requestSingleInstanceLock()
|
||||
//
|
||||
// app.on('second-instance', (event, argv, workingDirectory) => {
|
||||
// // // TODO: Get active/last active window and open process arvg etc
|
||||
// // if (currentWindow) {
|
||||
// // if (currentWindow.isMinimized()) currentWindow.restore()
|
||||
// // currentWindow.focus()
|
||||
// // }
|
||||
// })
|
||||
// }
|
||||
_openFilesToOpen () {
|
||||
this._openPathList(this._openFilesCache, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the path list in the best window(s).
|
||||
*
|
||||
* @param {string[]} pathsToOpen The path list to open.
|
||||
* @param {boolean} openFilesInSameWindow Open all files in the same window with
|
||||
* the first directory and discard other directories.
|
||||
*/
|
||||
_openPathList (pathsToOpen, openFilesInSameWindow=false) {
|
||||
const { _windowManager } = this
|
||||
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||
|
||||
const fileSet = new Set()
|
||||
const directorySet = new Set()
|
||||
for (const { isDir, path } of pathsToOpen) {
|
||||
if (isDir) {
|
||||
directorySet.add(path)
|
||||
} else {
|
||||
fileSet.add(path)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out directories that are already opened.
|
||||
for (const window of _windowManager.windows.values()) {
|
||||
if (window.type === WindowType.EDITOR) {
|
||||
const { openedRootDirectory } = window
|
||||
if (directorySet.has(openedRootDirectory)) {
|
||||
window.bringToFront()
|
||||
directorySet.delete(openedRootDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const directoriesToOpen = Array.from(directorySet).map(dir => ({ rootDirectory: dir, fileList: [] }))
|
||||
const filesToOpen = Array.from(fileSet)
|
||||
|
||||
// Discard all directories except first one and add files.
|
||||
if (openFilesInSameWindow) {
|
||||
if (directoriesToOpen.length) {
|
||||
directoriesToOpen[0].fileList.push(...filesToOpen)
|
||||
directoriesToOpen.length = 1
|
||||
} else {
|
||||
directoriesToOpen.push({ rootDirectory: null, fileList: filesToOpen })
|
||||
}
|
||||
filesToOpen.length = 0
|
||||
}
|
||||
|
||||
// Find the best window(s) to open the files in.
|
||||
if (!openFilesInSameWindow && !openFilesInNewWindow) {
|
||||
const isFirstWindow = _windowManager.getActiveEditorId() === null
|
||||
|
||||
// Prefer new directories
|
||||
for (let i = 0; i < directoriesToOpen.length; ++i) {
|
||||
const { fileList, rootDirectory } = directoriesToOpen[i]
|
||||
|
||||
let breakOuterLoop = false
|
||||
for (let j = 0; j < filesToOpen.length; ++j) {
|
||||
const pathname = filesToOpen[j]
|
||||
if (isChildOfDirectory(rootDirectory, pathname)) {
|
||||
if (isFirstWindow) {
|
||||
fileList.push(...filesToOpen)
|
||||
filesToOpen.length = 0
|
||||
breakOuterLoop = true
|
||||
break
|
||||
}
|
||||
fileList.push(pathname)
|
||||
filesToOpen.splice(j, 1)
|
||||
--j
|
||||
}
|
||||
}
|
||||
|
||||
if (breakOuterLoop) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find for the remaining files the best window to open the files in.
|
||||
if (isFirstWindow && directoriesToOpen.length && filesToOpen.length) {
|
||||
const { fileList } = directoriesToOpen[0]
|
||||
fileList.push(...filesToOpen)
|
||||
filesToOpen.length = 0
|
||||
} else {
|
||||
const windowList = _windowManager.findBestWindowToOpenIn(filesToOpen)
|
||||
for (const item of windowList) {
|
||||
const { windowId, fileList } = item
|
||||
|
||||
// File list is empty when all files are already opened.
|
||||
if (fileList.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (windowId !== null) {
|
||||
const window = _windowManager.get(windowId)
|
||||
if (window) {
|
||||
window.openTabs(fileList, 0)
|
||||
window.bringToFront()
|
||||
continue
|
||||
}
|
||||
// else: fallthrough
|
||||
}
|
||||
this._createEditorWindow(null, fileList)
|
||||
}
|
||||
}
|
||||
|
||||
// Directores are always opened in a new window if not already opened.
|
||||
for (const item of directoriesToOpen) {
|
||||
const { rootDirectory, fileList } = item
|
||||
this._createEditorWindow(rootDirectory, fileList)
|
||||
}
|
||||
}
|
||||
|
||||
// Open each file and directory in a new window.
|
||||
else {
|
||||
for (const pathname of filesToOpen) {
|
||||
this._createEditorWindow(null, [ pathname ])
|
||||
}
|
||||
|
||||
for (const item of directoriesToOpen) {
|
||||
const { rootDirectory, fileList } = item
|
||||
this._createEditorWindow(rootDirectory, fileList)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty the file list
|
||||
pathsToOpen.length = 0
|
||||
}
|
||||
|
||||
_listenForIpcMain () {
|
||||
ipcMain.on('app-create-editor-window', () => {
|
||||
this.createEditorWindow()
|
||||
this._createEditorWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('screen-capture', win => {
|
||||
@ -281,7 +424,7 @@ class App {
|
||||
})
|
||||
|
||||
ipcMain.on('app-create-settings-window', () => {
|
||||
const settingWins = this._windowManager.windowsOfType(WindowType.SETTING)
|
||||
const settingWins = this._windowManager.getWindowsByType(WindowType.SETTING)
|
||||
if (settingWins.length >= 1) {
|
||||
// A setting window is already created
|
||||
const browserSettingWindow = settingWins[0].win.browserWindow
|
||||
@ -292,42 +435,77 @@ class App {
|
||||
}
|
||||
return
|
||||
}
|
||||
this.createSettingWindow()
|
||||
this._createSettingWindow()
|
||||
})
|
||||
|
||||
// ipcMain.on('app-open-file', filePath => {
|
||||
// const windowId = this._windowManager.getActiveWindow()
|
||||
// ipcMain.emit('app-open-file-by-id', windowId, filePath)
|
||||
// })
|
||||
|
||||
ipcMain.on('app-open-file-by-id', (windowId, filePath) => {
|
||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
||||
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||
if (openFilesInNewWindow) {
|
||||
this.createEditorWindow(filePath)
|
||||
this._createEditorWindow(null, [ filePath ])
|
||||
} else {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor && !editor.quitting) {
|
||||
if (editor) {
|
||||
editor.openTab(filePath, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
ipcMain.on('app-open-files-by-id', (windowId, fileList) => {
|
||||
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||
if (openFilesInNewWindow) {
|
||||
this._createEditorWindow(null, fileList)
|
||||
} else {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor) {
|
||||
editor.openTabs(
|
||||
fileList.map(p => normalizeMarkdownPath(p))
|
||||
.filter(i => i && !i.isDir)
|
||||
.map(i => i.path),
|
||||
0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('app-open-markdown-by-id', (windowId, data) => {
|
||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
||||
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||
if (openFilesInNewWindow) {
|
||||
this.createEditorWindow(undefined, data)
|
||||
this._createEditorWindow(null, [], [ data ])
|
||||
} else {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor && !editor.quitting) {
|
||||
if (editor) {
|
||||
editor.openUntitledTab(true, data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('app-open-directory-by-id', (windowId, pathname) => {
|
||||
// TODO: Open the directory in an existing window if prefered.
|
||||
this.createEditorWindow(pathname)
|
||||
ipcMain.on('app-open-directory-by-id', (windowId, pathname, openInSameWindow) => {
|
||||
const { openFolderInNewWindow } = this._accessor.preferences.getAll()
|
||||
if (openInSameWindow || !openFolderInNewWindow) {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor) {
|
||||
editor.openFolder(pathname)
|
||||
return
|
||||
}
|
||||
}
|
||||
this._createEditorWindow(pathname)
|
||||
})
|
||||
|
||||
// --- renderer -------------------
|
||||
|
||||
ipcMain.on('mt::select-default-directory-to-open', e => {
|
||||
const { preferences } = this._accessor
|
||||
const { defaultDirectoryToOpen } = preferences.getAll()
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
dialog.showOpenDialog(win, {
|
||||
defaultPath: defaultDirectoryToOpen,
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
}, paths => {
|
||||
if (paths) {
|
||||
preferences.setItems({ defaultDirectoryToOpen: paths[0] })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,22 +1,8 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import EventEmitter from 'events'
|
||||
import log from 'electron-log'
|
||||
import Watcher from '../filesystem/watcher'
|
||||
|
||||
/**
|
||||
* A Mark Text window.
|
||||
* @typedef {EditorWindow} IApplicationWindow
|
||||
* @property {number | null} id Identifier (= browserWindow.id) or null during initialization.
|
||||
* @property {Electron.BrowserWindow} browserWindow The browse window.
|
||||
* @property {WindowType} type The window type.
|
||||
*/
|
||||
|
||||
// Window type marktext support.
|
||||
export const WindowType = {
|
||||
BASE: 'base', // You shold never create a `BASE` window.
|
||||
EDITOR: 'editor',
|
||||
SETTING: 'setting'
|
||||
}
|
||||
import Watcher, { WATCHER_STABILITY_THRESHOLD, WATCHER_STABILITY_POLL_INTERVAL } from '../filesystem/watcher'
|
||||
import { WindowType } from '../windows/base'
|
||||
|
||||
class WindowActivityList {
|
||||
constructor() {
|
||||
@ -33,6 +19,14 @@ class WindowActivityList {
|
||||
return null
|
||||
}
|
||||
|
||||
getSecondNewest () {
|
||||
const { _buf } = this
|
||||
if (_buf.length >= 2) {
|
||||
return _buf[_buf.length - 2]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
setNewest (id) {
|
||||
// I think we do not need a linked list for only a few windows.
|
||||
const { _buf } = this
|
||||
@ -72,7 +66,7 @@ class WindowManager extends EventEmitter {
|
||||
this._windows = new Map()
|
||||
this._windowActivity = new WindowActivityList()
|
||||
|
||||
// TODO(need::refactor): We should move watcher and search into another process/thread(?)
|
||||
// TODO(need::refactor): Please see #1035.
|
||||
this._watcher = new Watcher(preferences)
|
||||
|
||||
this._listenForIpcMain()
|
||||
@ -84,19 +78,23 @@ class WindowManager extends EventEmitter {
|
||||
* @param {IApplicationWindow} window The application window. We take ownership!
|
||||
*/
|
||||
add (window) {
|
||||
this._windows.set(window.id, window)
|
||||
const { id: windowId } = window
|
||||
this._windows.set(windowId, window)
|
||||
|
||||
if (!this._appMenu.has(window.id)) {
|
||||
this._appMenu.addDefaultMenu(window.id)
|
||||
if (!this._appMenu.has(windowId)) {
|
||||
this._appMenu.addDefaultMenu(windowId)
|
||||
}
|
||||
|
||||
if (this.windowCount === 1) {
|
||||
this.setActiveWindow(window.id)
|
||||
this.setActiveWindow(windowId)
|
||||
}
|
||||
|
||||
const { browserWindow } = window
|
||||
window.on('window-focus', () => {
|
||||
this.setActiveWindow(browserWindow.id)
|
||||
this.setActiveWindow(windowId)
|
||||
})
|
||||
window.on('window-closed', () => {
|
||||
this.remove(windowId)
|
||||
this._watcher.unwatchByWindowId(windowId)
|
||||
})
|
||||
}
|
||||
|
||||
@ -104,7 +102,7 @@ class WindowManager extends EventEmitter {
|
||||
* Return the application window by id.
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @returns {IApplicationWindow} The application window or undefined.
|
||||
* @returns {BaseWindow} The application window or undefined.
|
||||
*/
|
||||
get (windowId) {
|
||||
return this._windows.get(windowId)
|
||||
@ -127,7 +125,7 @@ class WindowManager extends EventEmitter {
|
||||
/**
|
||||
* Remove the given window by id.
|
||||
*
|
||||
* NOTE: All window event listeners are removed!
|
||||
* NOTE: All window "window-focus" events listeners are removed!
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @returns {IApplicationWindow} Returns the application window. We no longer take ownership.
|
||||
@ -136,7 +134,7 @@ class WindowManager extends EventEmitter {
|
||||
const { _windows } = this
|
||||
const window = this.get(windowId)
|
||||
if (window) {
|
||||
window.removeAllListeners()
|
||||
window.removeAllListeners('window-focus')
|
||||
|
||||
this._windowActivity.delete(windowId)
|
||||
let nextWindowId = this._windowActivity.getNewest()
|
||||
@ -160,29 +158,54 @@ class WindowManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active window id or null if no window is registred.
|
||||
* @returns {number|null}
|
||||
* Returns the active window or null if no window is registered.
|
||||
* @returns {BaseWindow|undefined}
|
||||
*/
|
||||
getActiveWindow () {
|
||||
return this._windows.get(this._activeWindowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active window id or null if no window is registered.
|
||||
* @returns {number|null}
|
||||
*/
|
||||
getActiveWindowId () {
|
||||
return this._activeWindowId
|
||||
}
|
||||
|
||||
get windows () {
|
||||
return this._windows
|
||||
/**
|
||||
* Returns the (last) active editor window or null if no editor is registered.
|
||||
* @returns {EditorWindow|undefined}
|
||||
*/
|
||||
getActiveEditor () {
|
||||
let win = this.getActiveWindow()
|
||||
if (win && win.type !== WindowType.EDITOR) {
|
||||
win = this._windows.get(this._windowActivity.getSecondNewest())
|
||||
if (win && win.type === WindowType.EDITOR) {
|
||||
return win
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
return win
|
||||
}
|
||||
|
||||
get windowCount () {
|
||||
return this._windows.size
|
||||
/**
|
||||
* Returns the (last) active editor window id or null if no editor is registered.
|
||||
* @returns {number|null}
|
||||
*/
|
||||
getActiveEditorId () {
|
||||
const win = this.getActiveEditor()
|
||||
return win ? win.id : null
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {type} type the WindowType one of ['base', 'editor', 'setting']
|
||||
* Return the windows of the given {type}
|
||||
* @param {WindowType} type the WindowType one of ['base', 'editor', 'setting']
|
||||
* @returns {{id: number, win: BaseWindow}[]} Return the windows of the given {type}
|
||||
*/
|
||||
windowsOfType (type) {
|
||||
getWindowsByType (type) {
|
||||
if (!WindowType[type.toUpperCase()]) {
|
||||
console.error(`${type} is not a valid window type.`)
|
||||
console.error(`"${type}" is not a valid window type.`)
|
||||
}
|
||||
const { windows } = this
|
||||
const result = []
|
||||
@ -197,10 +220,75 @@ class WindowManager extends EventEmitter {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best window to open the files in.
|
||||
*
|
||||
* @param {string[]} fileList File full paths.
|
||||
* @returns {{windowId: string, fileList: string[]}[]} An array of files mapped to a window id or null to open in a new window.
|
||||
*/
|
||||
findBestWindowToOpenIn (fileList) {
|
||||
if (!fileList || !Array.isArray(fileList) || !fileList.length) return []
|
||||
const { windows } = this
|
||||
const lastActiveEditorId = this.getActiveEditorId() // editor id or null
|
||||
|
||||
if (this.windowCount <= 1) {
|
||||
return [ { windowId: lastActiveEditorId, fileList } ]
|
||||
}
|
||||
|
||||
// Array of scores, same order like fileList.
|
||||
let filePathScores = null
|
||||
for (const window of windows.values()) {
|
||||
if (window.type === WindowType.EDITOR) {
|
||||
const scores = window.getCandidateScores(fileList)
|
||||
if (!filePathScores) {
|
||||
filePathScores = scores
|
||||
} else {
|
||||
const len = filePathScores.length
|
||||
for (let i = 0; i < len; ++i) {
|
||||
// Update score only if the file is not already opened.
|
||||
if (filePathScores[i].score !== -1 && filePathScores[i].score < scores[i].score) {
|
||||
filePathScores[i] = scores[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buf = []
|
||||
const len = filePathScores.length
|
||||
for (let i = 0; i < len; ++i) {
|
||||
let { id: windowId, score } = filePathScores[i]
|
||||
|
||||
if (score === -1) {
|
||||
// Skip files that already opened.
|
||||
continue
|
||||
} else if (score === 0) {
|
||||
// There is no best window to open the file(s) in.
|
||||
windowId = lastActiveEditorId
|
||||
}
|
||||
|
||||
let item = buf.find(w => w.windowId === windowId)
|
||||
if (!item) {
|
||||
item = { windowId, fileList: [] }
|
||||
buf.push(item)
|
||||
}
|
||||
item.fileList.push(fileList[i])
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
get windows () {
|
||||
return this._windows
|
||||
}
|
||||
|
||||
get windowCount () {
|
||||
return this._windows.size
|
||||
}
|
||||
|
||||
// --- helper ---------------------------------
|
||||
|
||||
closeWatcher () {
|
||||
this._watcher.clear()
|
||||
this._watcher.close()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,15 +301,15 @@ class WindowManager extends EventEmitter {
|
||||
return false
|
||||
}
|
||||
|
||||
const { id } = browserWindow
|
||||
const { id: windowId } = browserWindow
|
||||
const { _appMenu, _windows } = this
|
||||
|
||||
// Free watchers used by this window
|
||||
this._watcher.unWatchWin(browserWindow)
|
||||
this._watcher.unwatchByWindowId(windowId)
|
||||
|
||||
// Application clearup and remove listeners
|
||||
_appMenu.removeWindowMenu(id)
|
||||
const window = this.remove(id)
|
||||
_appMenu.removeWindowMenu(windowId)
|
||||
const window = this.remove(windowId)
|
||||
|
||||
// Destroy window wrapper and browser window
|
||||
if (window) {
|
||||
@ -251,32 +339,39 @@ class WindowManager extends EventEmitter {
|
||||
return false
|
||||
}
|
||||
|
||||
// --- events ---------------------------------
|
||||
// --- private --------------------------------
|
||||
|
||||
_listenForIpcMain () {
|
||||
// listen for file watch from renderer process eg
|
||||
// 1. click file in folder.
|
||||
// 2. new tab and save it.
|
||||
// 3. close tab(s) need unwatch.
|
||||
ipcMain.on('AGANI::file-watch', (e, { pathname, watch }) => {
|
||||
// HACK: Don't use this event! Please see #1034 and #1035
|
||||
ipcMain.on('AGANI::window-add-file-path', (e, filePath) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (watch) {
|
||||
// listen for file `change` and `unlink`
|
||||
this._watcher.watch(win, pathname, 'file')
|
||||
} else {
|
||||
// unlisten for file `change` and `unlink`
|
||||
this._watcher.unWatch(win, pathname, 'file')
|
||||
const editor = this.get(win.id)
|
||||
if (!editor) {
|
||||
log.error(`Cannot find window id "${win.id}" to add opened file.`)
|
||||
return
|
||||
}
|
||||
editor.addToOpenedFiles(filePath)
|
||||
})
|
||||
|
||||
// Force close a BrowserWindow
|
||||
ipcMain.on('AGANI::close-window', e => {
|
||||
ipcMain.on('mt::close-window', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
this.forceClose(win)
|
||||
})
|
||||
|
||||
ipcMain.on('mt::window-tab-closed', (e, pathname) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const editor = this.get(win.id)
|
||||
if (editor) {
|
||||
editor.removeFromOpenedFiles(pathname)
|
||||
}
|
||||
})
|
||||
|
||||
// --- local events ---------------
|
||||
|
||||
ipcMain.on('watcher-unwatch-all-by-id', windowId => {
|
||||
this._watcher.unwatchByWindowId(windowId)
|
||||
})
|
||||
ipcMain.on('watcher-watch-file', (win, filePath) => {
|
||||
this._watcher.watch(win, filePath, 'file')
|
||||
})
|
||||
@ -284,17 +379,44 @@ class WindowManager extends EventEmitter {
|
||||
this._watcher.watch(win, pathname, 'dir')
|
||||
})
|
||||
ipcMain.on('watcher-unwatch-file', (win, filePath) => {
|
||||
this._watcher.unWatch(win, filePath, 'file')
|
||||
this._watcher.unwatch(win, filePath, 'file')
|
||||
})
|
||||
ipcMain.on('watcher-unwatch-directory', (win, pathname) => {
|
||||
this._watcher.unWatch(win, pathname, 'dir')
|
||||
this._watcher.unwatch(win, pathname, 'dir')
|
||||
})
|
||||
|
||||
ipcMain.on('window-add-file-path', (windowId, filePath) => {
|
||||
const editor = this.get(windowId)
|
||||
if (!editor) {
|
||||
log.error(`Cannot find window id "${windowId}" to add opened file.`)
|
||||
return
|
||||
}
|
||||
editor.addToOpenedFiles(filePath)
|
||||
})
|
||||
ipcMain.on('window-change-file-path', (windowId, pathname, oldPathname) => {
|
||||
const editor = this.get(windowId)
|
||||
if (!editor) {
|
||||
log.error(`Cannot find window id "${windowId}" to change file path.`)
|
||||
return
|
||||
}
|
||||
editor.changeOpenedFilePath(pathname, oldPathname)
|
||||
})
|
||||
|
||||
ipcMain.on('window-file-saved', (windowId, pathname) => {
|
||||
// A changed event is emitted earliest after the stability threshold.
|
||||
const duration = WATCHER_STABILITY_THRESHOLD + (WATCHER_STABILITY_POLL_INTERVAL * 2)
|
||||
this._watcher.ignoreChangedEvent(windowId, pathname, duration)
|
||||
})
|
||||
|
||||
// Force close a window by id.
|
||||
ipcMain.on('window-close-by-id', id => {
|
||||
this.forceCloseById(id)
|
||||
})
|
||||
|
||||
ipcMain.on('window-reload-by-id', id => {
|
||||
const window = this.get(id)
|
||||
if (window) {
|
||||
window.reload()
|
||||
}
|
||||
})
|
||||
ipcMain.on('window-toggle-always-on-top', win => {
|
||||
const flag = !win.isAlwaysOnTop()
|
||||
win.setAlwaysOnTop(flag)
|
||||
|
@ -24,7 +24,9 @@ const cli = () => {
|
||||
--debug Enable debug mode
|
||||
--safe Disable plugins and other user configuration
|
||||
--dump-keyboard-layout Dump keyboard information
|
||||
-n, --new-window Open a new window on second-instance
|
||||
--user-data-dir Change the user data directory
|
||||
--disable-gpu Disable GPU hardware acceleration
|
||||
-v, --verbose Be verbose
|
||||
--version Print version information
|
||||
-h, --help Print this help message
|
||||
|
@ -8,7 +8,7 @@ import arg from 'arg'
|
||||
* @returns {arg.Result} Parsed arguments
|
||||
*/
|
||||
const parseArgs = (argv = null, permissive = true) => {
|
||||
if (argv == null) {
|
||||
if (argv === null) {
|
||||
argv = process.argv.slice(1)
|
||||
}
|
||||
const spec = {
|
||||
@ -16,6 +16,10 @@ const parseArgs = (argv=null, permissive=true) => {
|
||||
'--safe': Boolean,
|
||||
'--dump-keyboard-layout': Boolean,
|
||||
|
||||
'--new-window': Boolean,
|
||||
'-n': '--new-window',
|
||||
|
||||
'--disable-gpu': Boolean,
|
||||
'--user-data-dir': String,
|
||||
|
||||
// Misc
|
||||
|
@ -2,7 +2,7 @@ export const isOsx = process.platform === 'darwin'
|
||||
export const isWindows = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
export const defaultWinOptions = {
|
||||
export const editorWinOptions = {
|
||||
minWidth: 450,
|
||||
minHeight: 220,
|
||||
webPreferences: {
|
||||
@ -10,7 +10,7 @@ export const defaultWinOptions = {
|
||||
webSecurity: false
|
||||
},
|
||||
useContentSize: true,
|
||||
show: false,
|
||||
show: true,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset'
|
||||
}
|
||||
@ -31,8 +31,7 @@ export const defaultPreferenceWinOptions = {
|
||||
show: false,
|
||||
frame: false,
|
||||
thickFrame: !isOsx,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
center: true
|
||||
titleBarStyle: 'hiddenInset'
|
||||
}
|
||||
|
||||
export const EXTENSIONS = [
|
||||
|
@ -22,16 +22,14 @@ const getOSInformation = () => {
|
||||
return `${os.type()} ${os.arch()} ${os.release()} (${os.platform()})`
|
||||
}
|
||||
|
||||
const bundleException = (error, type) => {
|
||||
const exceptionToString = (error, type) => {
|
||||
const { message, stack } = error
|
||||
return {
|
||||
version: global.MARKTEXT_VERSION_STRING || app.getVersion(),
|
||||
os: getOSInformation(),
|
||||
type,
|
||||
date: new Date().toGMTString(),
|
||||
message,
|
||||
stack
|
||||
}
|
||||
return `Version: ${global.MARKTEXT_VERSION_STRING || app.getVersion()}\n` +
|
||||
`OS: ${getOSInformation()}\n` +
|
||||
`Type: ${type}\n` +
|
||||
`Date: ${new Date().toGMTString()}\n` +
|
||||
`Message: ${message}\n` +
|
||||
`Stack: ${stack}\n`
|
||||
}
|
||||
|
||||
const handleError = (title, error, type) => {
|
||||
@ -39,8 +37,7 @@ const handleError = (title, error, type) => {
|
||||
|
||||
// Write error into file
|
||||
if (type === 'main') {
|
||||
const info = bundleException(error, type)
|
||||
logger(JSON.stringify(info, null, 2))
|
||||
logger(exceptionToString(error, type))
|
||||
}
|
||||
|
||||
if (EXIT_ON_ERROR) {
|
||||
@ -48,12 +45,13 @@ const handleError = (title, error, type) => {
|
||||
process.exit(1)
|
||||
// eslint, don't lie to me, the return statement is important!
|
||||
return // eslint-disable-line no-unreachable
|
||||
} else if (!SHOW_ERROR_DIALOG || (global.MARKTEXT_IS_OFFICIAL_RELEASE && type === 'renderer')) {
|
||||
} else if (!SHOW_ERROR_DIALOG || (global.MARKTEXT_IS_STABLE && type === 'renderer')) {
|
||||
return
|
||||
}
|
||||
|
||||
// show error dialog
|
||||
if (app.isReady()) {
|
||||
// Blocking message box
|
||||
const result = dialog.showMessageBox({
|
||||
type: 'error',
|
||||
buttons: [
|
||||
|
@ -3,6 +3,22 @@ import path from 'path'
|
||||
import { hasMarkdownExtension } from '../utils'
|
||||
import { IMAGE_EXTENSIONS } from '../config'
|
||||
|
||||
/**
|
||||
* Test whether or not the given path exists.
|
||||
*
|
||||
* @param {string} p The path to the file or directory.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const exists = async p => {
|
||||
// fs.exists is deprecated.
|
||||
try {
|
||||
await fs.access(p)
|
||||
return true
|
||||
} catch(_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a directory exist.
|
||||
*
|
||||
@ -97,6 +113,45 @@ export const isMarkdownFileOrLink = filepath => {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the both paths point to the same file.
|
||||
*
|
||||
* @param {string} pathA The first path.
|
||||
* @param {string} pathB The second path.
|
||||
* @param {boolean} [isNormalized] Are both paths already normalized.
|
||||
*/
|
||||
export const isSamePathSync = (pathA, pathB, isNormalized = false) => {
|
||||
if (!pathA || !pathB) return false
|
||||
const a = isNormalized ? pathA : path.normalize(pathA)
|
||||
const b = isNormalized ? pathB : path.normalize(pathB)
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
} else if (a === b) {
|
||||
return true
|
||||
} else if (a.toLowerCase() === b.toLowerCase()) {
|
||||
try {
|
||||
const fiA = fs.statSync(a)
|
||||
const fiB = fs.statSync(b)
|
||||
return fiA.ino === fiB.ino
|
||||
} catch (_) {
|
||||
// Ignore error
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a file or directory is a child of the given directory.
|
||||
*
|
||||
* @param {string} dir The parent directory.
|
||||
* @param {string} child The file or directory path to check.
|
||||
*/
|
||||
export const isChildOfDirectory = (dir, child) => {
|
||||
if (!dir || !child) return false
|
||||
const relative = path.relative(dir, child)
|
||||
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the path into an absolute path and resolves the link target if needed.
|
||||
*
|
||||
|
@ -2,7 +2,7 @@ import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import log from 'electron-log'
|
||||
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config'
|
||||
import { writeFile } from '../filesystem'
|
||||
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath , writeFile } from '../filesystem'
|
||||
|
||||
const getLineEnding = lineEnding => {
|
||||
if (lineEnding === 'lf') {
|
||||
@ -20,6 +20,27 @@ const convertLineEndings = (text, lineEnding) => {
|
||||
return text.replace(LINE_ENDING_REG, getLineEnding(lineEnding))
|
||||
}
|
||||
|
||||
/**
|
||||
* Special function to normalize directory and markdown file paths.
|
||||
*
|
||||
* @param {string} pathname The path to the file or directory.
|
||||
* @returns {{isDir: boolean, path: string}?} Returns the normalize path and a
|
||||
* directory hint or null if it's not a directory or markdown file.
|
||||
*/
|
||||
export const normalizeMarkdownPath = pathname => {
|
||||
const isDir = isDirectory(pathname)
|
||||
if (isDir || isMarkdownFileOrLink(pathname)) {
|
||||
// Normalize and resolve the path or link target.
|
||||
const resolved = normalizeAndResolvePath(pathname)
|
||||
if (resolved) {
|
||||
return { isDir, path: resolved }
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot resolve "${pathname}".`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the content into a file.
|
||||
*
|
||||
|
@ -1,23 +1,24 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import fs from 'fs-extra'
|
||||
import log from 'electron-log'
|
||||
import { promisify } from 'util'
|
||||
import chokidar from 'chokidar'
|
||||
import { getUniqueId, hasMarkdownExtension } from '../utils'
|
||||
import { exists } from '../filesystem'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
import { isLinux } from '../config'
|
||||
|
||||
// TODO(need::refactor):
|
||||
// - Refactor this file
|
||||
// - Outsource watcher/search features into worker (per window) and use something like "file-matcher" for searching on disk.
|
||||
// TODO(refactor): Please see GH#1035.
|
||||
|
||||
export const WATCHER_STABILITY_THRESHOLD = 1000
|
||||
export const WATCHER_STABILITY_POLL_INTERVAL = 150
|
||||
|
||||
const EVENT_NAME = {
|
||||
dir: 'AGANI::update-object-tree',
|
||||
file: 'AGANI::update-file'
|
||||
}
|
||||
|
||||
const add = async (win, pathname, endOfLine) => {
|
||||
const stats = await promisify(fs.stat)(pathname)
|
||||
const add = async (win, pathname, type, endOfLine) => {
|
||||
const stats = await fs.stat(pathname)
|
||||
const birthTime = stats.birthtime
|
||||
const isMarkdown = hasMarkdownExtension(pathname)
|
||||
const file = {
|
||||
@ -29,11 +30,24 @@ const add = async (win, pathname, endOfLine) => {
|
||||
isMarkdown
|
||||
}
|
||||
if (isMarkdown) {
|
||||
// HACK: But this should be removed completely in #1034/#1035.
|
||||
try {
|
||||
const data = await loadMarkdownFile(pathname, endOfLine)
|
||||
file.data = data
|
||||
} catch(err) {
|
||||
// Only notify user about opened files.
|
||||
if (type === 'file') {
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'Watcher I/O error',
|
||||
type: 'error',
|
||||
message: err.message
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
win.webContents.send('AGANI::update-object-tree', {
|
||||
win.webContents.send(EVENT_NAME[type], {
|
||||
type: 'add',
|
||||
change: file
|
||||
})
|
||||
@ -49,8 +63,10 @@ const unlink = (win, pathname, type) => {
|
||||
|
||||
const change = async (win, pathname, type, endOfLine) => {
|
||||
const isMarkdown = hasMarkdownExtension(pathname)
|
||||
|
||||
if (isMarkdown) {
|
||||
// HACK: Markdown data should be removed completely in #1034/#1035 and
|
||||
// should be only loaded after user interaction.
|
||||
try {
|
||||
const data = await loadMarkdownFile(pathname, endOfLine)
|
||||
const file = {
|
||||
pathname,
|
||||
@ -60,10 +76,22 @@ const change = async (win, pathname, type, endOfLine) => {
|
||||
type: 'change',
|
||||
change: file
|
||||
})
|
||||
} catch (err) {
|
||||
// Only notify user about opened files.
|
||||
if (type === 'file') {
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'Watcher I/O error',
|
||||
type: 'error',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addDir = (win, pathname) => {
|
||||
const addDir = (win, pathname, type) => {
|
||||
if (type === 'file') return
|
||||
|
||||
const directory = {
|
||||
pathname,
|
||||
name: path.basename(pathname),
|
||||
@ -81,7 +109,9 @@ const addDir = (win, pathname) => {
|
||||
})
|
||||
}
|
||||
|
||||
const unlinkDir = (win, pathname) => {
|
||||
const unlinkDir = (win, pathname, type) => {
|
||||
if (type === 'file') return
|
||||
|
||||
const directory = { pathname }
|
||||
win.webContents.send('AGANI::update-object-tree', {
|
||||
type: 'unlinkDir',
|
||||
@ -96,61 +126,117 @@ class Watcher {
|
||||
*/
|
||||
constructor (preferences) {
|
||||
this._preferences = preferences
|
||||
this._ignoreChangeEvents = []
|
||||
this.watchers = {}
|
||||
}
|
||||
|
||||
// return a unwatch function
|
||||
// Watch a file or directory and return a unwatch function.
|
||||
watch (win, watchPath, type = 'dir'/* file or dir */) {
|
||||
const id = getUniqueId()
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
ignored: /(^|[/\\])(\..|node_modules)/,
|
||||
ignoreInitial: type === 'file',
|
||||
persistent: true
|
||||
persistent: true,
|
||||
ignorePermissionErrors: true,
|
||||
|
||||
// Just to be sure when a file is replaced with a directory don't watch recursively.
|
||||
depth: type === 'file' ? 0 : undefined,
|
||||
|
||||
// Please see GH#1043
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: WATCHER_STABILITY_THRESHOLD,
|
||||
pollInterval: WATCHER_STABILITY_POLL_INTERVAL
|
||||
}
|
||||
})
|
||||
|
||||
let disposed = false
|
||||
let enospcReached = false
|
||||
let renameTimer = null
|
||||
|
||||
watcher
|
||||
.on('add', pathname => add(win, pathname, this._preferences.getPreferedEOL()))
|
||||
.on('change', pathname => change(win, pathname, type, this._preferences.getPreferedEOL()))
|
||||
.on('add', pathname => {
|
||||
if (!this._shouldIgnoreEvent(win.id, pathname, type)) {
|
||||
add(win, pathname, type, this._preferences.getPreferedEOL())
|
||||
}
|
||||
})
|
||||
.on('change', pathname => {
|
||||
if (!this._shouldIgnoreEvent(win.id, pathname, type)) {
|
||||
change(win, pathname, type, this._preferences.getPreferedEOL())
|
||||
}
|
||||
})
|
||||
.on('unlink', pathname => unlink(win, pathname, type))
|
||||
.on('addDir', pathname => addDir(win, pathname))
|
||||
.on('unlinkDir', pathname => unlinkDir(win, pathname))
|
||||
.on('raw', (event, path, details) => {
|
||||
.on('addDir', pathname => addDir(win, pathname, type))
|
||||
.on('unlinkDir', pathname => unlinkDir(win, pathname, type))
|
||||
.on('raw', (event, subpath, details) => {
|
||||
if (global.MARKTEXT_DEBUG_VERBOSE >= 3) {
|
||||
console.log(event, path, details)
|
||||
console.log('watcher: ', event, subpath, details)
|
||||
}
|
||||
|
||||
// rename syscall on Linux (chokidar#591)
|
||||
// Fix atomic rename on Linux (chokidar#591).
|
||||
// TODO: This should also apply to macOS.
|
||||
// TODO: Do we need to rewatch when the watched directory was renamed?
|
||||
if (isLinux && type === 'file' && event === 'rename') {
|
||||
const { watchedPath } = details
|
||||
// Use the same watcher and re-watch the file.
|
||||
watcher.unwatch(watchedPath)
|
||||
watcher.add(watchedPath)
|
||||
if (renameTimer) {
|
||||
clearTimeout(renameTimer)
|
||||
}
|
||||
renameTimer = setTimeout(async () => {
|
||||
renameTimer = null
|
||||
if (disposed) return
|
||||
|
||||
const fileExists = await exists(watchPath)
|
||||
if (fileExists) {
|
||||
// File still exists but we need to rewatch the file because the inode has changed.
|
||||
watcher.unwatch(watchPath)
|
||||
watcher.add(watchPath)
|
||||
}
|
||||
}, 150)
|
||||
}
|
||||
})
|
||||
.on('error', error => {
|
||||
const msg = `Watcher error: ${error}`
|
||||
console.log(msg)
|
||||
log.error(msg)
|
||||
// Check if too many file descriptors are opened and notify the user about this issue.
|
||||
if (error.code === 'ENOSPC') {
|
||||
if (!enospcReached) {
|
||||
enospcReached = true
|
||||
log.warn('inotify limit reached: Too many file descriptors are opened.')
|
||||
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'inotify limit reached',
|
||||
type: 'warning',
|
||||
message: 'Cannot watch all files and file changes because too many file descriptors are opened.'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
const closeFn = () => {
|
||||
disposed = true
|
||||
if (this.watchers[id]) {
|
||||
delete this.watchers[id]
|
||||
}
|
||||
if (renameTimer) {
|
||||
clearTimeout(renameTimer)
|
||||
renameTimer = null
|
||||
}
|
||||
watcher.close()
|
||||
}
|
||||
|
||||
this.watchers[id] = {
|
||||
win,
|
||||
watcher,
|
||||
pathname: watchPath,
|
||||
type
|
||||
type,
|
||||
|
||||
close: closeFn
|
||||
}
|
||||
|
||||
// unwatcher function
|
||||
return () => {
|
||||
if (this.watchers[id]) {
|
||||
delete this.watchers[id]
|
||||
}
|
||||
watcher.close()
|
||||
}
|
||||
return closeFn
|
||||
}
|
||||
|
||||
// unWatch some single watch
|
||||
unWatch (win, watchPath, type = 'dir') {
|
||||
// Remove a single watcher.
|
||||
unwatch (win, watchPath, type = 'dir') {
|
||||
for (const id of Object.keys(this.watchers)) {
|
||||
const w = this.watchers[id]
|
||||
if (
|
||||
@ -165,13 +251,13 @@ class Watcher {
|
||||
}
|
||||
}
|
||||
|
||||
// unwatch for one window, (remove all the watchers in one window)
|
||||
unWatchWin (win) {
|
||||
// Remove all watchers from the given window id.
|
||||
unwatchByWindowId (windowId) {
|
||||
const watchers = []
|
||||
const watchIds = []
|
||||
for (const id of Object.keys(this.watchers)) {
|
||||
const w = this.watchers[id]
|
||||
if (w.win === win) {
|
||||
if (w.win.id === windowId) {
|
||||
watchers.push(w.watcher)
|
||||
watchIds.push(id)
|
||||
}
|
||||
@ -182,9 +268,49 @@ class Watcher {
|
||||
}
|
||||
}
|
||||
|
||||
clear () {
|
||||
Object.keys(this.watchers).forEach(id => this.watchers[id].watcher.close())
|
||||
close () {
|
||||
Object.keys(this.watchers).forEach(id => this.watchers[id].close())
|
||||
this.watchers = {}
|
||||
this._ignoreChangeEvents = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore the next changed event within a certain time for the current file and window.
|
||||
*
|
||||
* NOTE: Only valid for files and "add"/"change" event!
|
||||
*
|
||||
* @param {number} windowId The window id.
|
||||
* @param {string} pathname The path to ignore.
|
||||
* @param {number} [duration] The duration in ms to ignore the changed event.
|
||||
*/
|
||||
ignoreChangedEvent (windowId, pathname, duration=WATCHER_STABILITY_THRESHOLD + WATCHER_STABILITY_POLL_INTERVAL + 1000) {
|
||||
this._ignoreChangeEvents.push({ windowId, pathname, duration, start: new Date() })
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we should ignore the current event because the file may be changed from Mark Text itself.
|
||||
*
|
||||
* @param {number} winId
|
||||
* @param {string} pathname
|
||||
* @param {string} type
|
||||
*/
|
||||
_shouldIgnoreEvent (winId, pathname, type) {
|
||||
if (type === 'file') {
|
||||
const { _ignoreChangeEvents } = this
|
||||
const currentTime = new Date()
|
||||
const len = _ignoreChangeEvents.length
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const { windowId, pathname: pathToIgnore, start, duration } = _ignoreChangeEvents[i]
|
||||
if (windowId === winId && pathToIgnore === pathname) {
|
||||
_ignoreChangeEvents.splice(i)
|
||||
--i
|
||||
if (currentTime - start < duration) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,11 @@
|
||||
/**
|
||||
* This file is used specifically and only for development. It installs
|
||||
* `electron-debug` & `vue-devtools`. There shouldn't be any need to
|
||||
* modify this file, but it can be used to extend your development
|
||||
* environment.
|
||||
* `vue-devtools`. There shouldn't be any need to modify this file,
|
||||
* but it can be used to extend your development environment.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
require('dotenv').config()
|
||||
// Install `electron-debug` with `devtron`
|
||||
require('electron-debug')({ showDevTools: false })
|
||||
|
||||
// Install `vue-devtools`
|
||||
require('electron').app.on('ready', () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import './globalSetting'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import cli from './cli'
|
||||
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
|
||||
import log from 'electron-log'
|
||||
@ -32,6 +33,19 @@ const args = cli()
|
||||
const appEnvironment = setupEnvironment(args)
|
||||
initializeLogger(appEnvironment)
|
||||
|
||||
if (args['--disable-gpu']) {
|
||||
app.disableHardwareAcceleration()
|
||||
}
|
||||
|
||||
// Make Mark Text a single instance application.
|
||||
if (!process.mas && process.env.NODE_ENV !== 'development') {
|
||||
const gotSingleInstanceLock = app.requestSingleInstanceLock()
|
||||
if (!gotSingleInstanceLock) {
|
||||
process.stdout.write('Other Mark Text instance detected: exiting...\n')
|
||||
app.exit()
|
||||
}
|
||||
}
|
||||
|
||||
// Mark Text environment is configured successfully. You can now access paths, use the logger etc.
|
||||
// Create other instances that need access to the modules from above.
|
||||
const accessor = new Accessor(appEnvironment)
|
||||
@ -40,5 +54,5 @@ const accessor = new Accessor(appEnvironment)
|
||||
// Be careful when changing code before this line!
|
||||
// NOTE: Do not create classes or other code before this line!
|
||||
|
||||
const app = new App(accessor, args)
|
||||
app.init()
|
||||
const marktext = new App(accessor, args)
|
||||
marktext.init()
|
||||
|
@ -31,12 +31,13 @@ class Keybindings {
|
||||
['fileNewTab', 'CmdOrCtrl+Shift+T'],
|
||||
['fileOpenFile', 'CmdOrCtrl+O'],
|
||||
['fileOpenFolder', 'CmdOrCtrl+Shift+O'],
|
||||
['fileCloseTab', 'CmdOrCtrl+W'],
|
||||
['fileSave', 'CmdOrCtrl+S'],
|
||||
['fileSaveAs', 'CmdOrCtrl+Shift+S'],
|
||||
['filePrint', 'CmdOrCtrl+P'],
|
||||
['filePreferences', 'CmdOrCtrl+,'], // marktext menu in macOS
|
||||
['fileQuit', isOsx ? 'Command+Q' : 'Alt+F4'],
|
||||
['fileCloseTab', 'CmdOrCtrl+W'],
|
||||
['fileCloseWindow', 'CmdOrCtrl+Shift+W'],
|
||||
['fileQuit', 'CmdOrCtrl+Q'],
|
||||
|
||||
// edit menu
|
||||
['editUndo', 'CmdOrCtrl+Z'],
|
||||
@ -91,18 +92,16 @@ class Keybindings {
|
||||
['formatClearFormat', 'Shift+CmdOrCtrl+R'],
|
||||
|
||||
// window menu
|
||||
// ['windowMinimize', 'CmdOrCtrl+M'], deprecated for math.
|
||||
['windowCloseWindow', 'CmdOrCtrl+Shift+W'],
|
||||
['windowMinimize', ''], // 'CmdOrCtrl+M' deprecated for math
|
||||
|
||||
// view menu
|
||||
['viewToggleFullScreen', isOsx ? 'Ctrl+Command+F' : 'F11'],
|
||||
['viewChangeFont', 'CmdOrCtrl+.'],
|
||||
['viewSourceCodeMode', 'CmdOrCtrl+Alt+S'],
|
||||
['viewTypewriterMode', 'CmdOrCtrl+Alt+T'],
|
||||
['viewFocusMode', 'CmdOrCtrl+Shift+F'],
|
||||
['viewToggleSideBar', 'CmdOrCtrl+J'],
|
||||
['viewToggleTabBar', 'CmdOrCtrl+Alt+B'],
|
||||
['viewDevToggleDeveloperTools', isOsx ? 'Alt+Command+I' : 'Ctrl+Shift+I'],
|
||||
['viewDevToggleDeveloperTools', 'CmdOrCtrl+Alt+I'],
|
||||
['viewDevReload', 'CmdOrCtrl+R']
|
||||
])
|
||||
|
||||
|
@ -9,9 +9,9 @@ import { writeMarkdownFile } from '../../filesystem/markdown'
|
||||
import { getPath, getRecommendTitleFromMarkdownString } from '../../utils'
|
||||
import pandoc from '../../utils/pandoc'
|
||||
|
||||
// TODO:
|
||||
// - use async dialog version to not block the main process.
|
||||
// - catch "fs." exceptions. Otherwise the main process crashes...
|
||||
// TODO(refactor): "save" and "save as" should be moved to the editor window (editor.js) and
|
||||
// the renderer should communicate only with the editor window for file relevant stuff.
|
||||
// E.g. "mt::save-tabs" --> "mt::window-save-tabs$wid:<windowId>"
|
||||
|
||||
// Handle the export response from renderer process.
|
||||
const handleResponseForExport = async (e, { type, content, pathname, markdown }) => {
|
||||
@ -24,11 +24,9 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
||||
}
|
||||
const defaultPath = path.join(dirname, `${nakedFilename}${extension}`)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filePath = dialog.showSaveDialog(win, {
|
||||
dialog.showSaveDialog(win, {
|
||||
defaultPath
|
||||
})
|
||||
|
||||
}, async filePath => {
|
||||
if (filePath) {
|
||||
let data = content
|
||||
try {
|
||||
@ -44,7 +42,7 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
||||
log.error(err)
|
||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'Export File Error',
|
||||
title: 'Export failure',
|
||||
type: 'error',
|
||||
message: ERROR_MSG
|
||||
})
|
||||
@ -55,57 +53,72 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
||||
removePrintServiceFromWindow(win)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleResponseForPrint = e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
// See GH#749, Electron#16085 and Electron#17523.
|
||||
dialog.showMessageBox({
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
defaultId: 0,
|
||||
noLink: true,
|
||||
message: 'Printing doesn\'t work',
|
||||
detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
|
||||
})
|
||||
// const win = BrowserWindow.fromWebContents(e.sender)
|
||||
}, () => {})
|
||||
// win.webContents.print({ printBackground: true }, () => {
|
||||
// removePrintServiceFromWindow(win)
|
||||
// })
|
||||
}
|
||||
|
||||
const handleResponseForSave = (e, { id, markdown, pathname, options }) => {
|
||||
const handleResponseForSave = async (e, { id, markdown, pathname, options }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
let recommendFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||
if (!recommendFilename) {
|
||||
recommendFilename = 'Untitled'
|
||||
}
|
||||
|
||||
// If the file doesn't exist on disk add it to the recently used documents later.
|
||||
// If the file doesn't exist on disk add it to the recently used documents later
|
||||
// and execute file from filesystem watcher for a short time. The file may exists
|
||||
// on disk nevertheless but is already tracked by Mark Text.
|
||||
const alreadyExistOnDisk = !!pathname
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
pathname = pathname || dialog.showSaveDialog(win, {
|
||||
let filePath = pathname
|
||||
if (!filePath) {
|
||||
filePath = await new Promise((resolve, reject) => {
|
||||
// TODO: Use asynchronous version that returns a "Promise" with Electron 6.
|
||||
dialog.showSaveDialog(win, {
|
||||
defaultPath: path.join(getPath('documents'), `${recommendFilename}.md`)
|
||||
}, resolve)
|
||||
})
|
||||
|
||||
if (pathname && typeof pathname === 'string') {
|
||||
if (!alreadyExistOnDisk) {
|
||||
ipcMain.emit('menu-clear-recently-used')
|
||||
}
|
||||
|
||||
return writeMarkdownFile(pathname, markdown, options, win)
|
||||
.then(() => {
|
||||
if (!alreadyExistOnDisk) {
|
||||
// it's a new created file, need watch
|
||||
ipcMain.emit('watcher-watch-file', win, pathname)
|
||||
}
|
||||
const filename = path.basename(pathname)
|
||||
win.webContents.send('AGANI::set-pathname', { id, pathname, filename })
|
||||
return id
|
||||
})
|
||||
} else {
|
||||
// Save dialog canceled by user - no error.
|
||||
if (!filePath) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
filePath = path.resolve(filePath)
|
||||
return writeMarkdownFile(filePath, markdown, options, win)
|
||||
.then(() => {
|
||||
if (!alreadyExistOnDisk) {
|
||||
ipcMain.emit('window-add-file-path', win.id, filePath)
|
||||
ipcMain.emit('menu-add-recently-used', filePath)
|
||||
|
||||
const filename = path.basename(filePath)
|
||||
win.webContents.send('mt::set-pathname', { id, pathname: filePath, filename })
|
||||
} else {
|
||||
ipcMain.emit('window-file-saved', win.id, filePath)
|
||||
win.webContents.send('mt::tab-saved', id)
|
||||
}
|
||||
return id
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
win.webContents.send('mt::tab-save-failure', id, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showUnsavedFilesMessage = (win, files) => {
|
||||
@ -159,28 +172,26 @@ const removePrintServiceFromWindow = win => {
|
||||
|
||||
// --- events -----------------------------------
|
||||
|
||||
ipcMain.on('AGANI::save-all', (e, unsavedFiles) => {
|
||||
ipcMain.on('mt::save-tabs', (e, unsavedFiles) => {
|
||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.catch(log.error)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::save-close', async (e, unsavedFiles, isSingle) => {
|
||||
ipcMain.on('mt::save-and-close-tabs', async (e, unsavedFiles) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||
const EVENT = isSingle ? 'AGANI::save-single-response' : 'AGANI::save-all-response'
|
||||
if (needSave) {
|
||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.then(arr => {
|
||||
const data = arr.filter(id => id)
|
||||
win.send(EVENT, { err: null, data })
|
||||
const tabIds = arr.filter(id => id != null)
|
||||
win.send('mt::force-close-tabs-by-id', tabIds)
|
||||
})
|
||||
.catch(err => {
|
||||
win.send(EVENT, { err, data: null })
|
||||
log.error(err.error)
|
||||
})
|
||||
} else {
|
||||
const data = unsavedFiles.map(f => f.id)
|
||||
win.send(EVENT, { err: null, data })
|
||||
const tabIds = unsavedFiles.map(f => f.id)
|
||||
win.send('mt::force-close-tabs-by-id', tabIds)
|
||||
}
|
||||
})
|
||||
|
||||
@ -191,28 +202,44 @@ ipcMain.on('AGANI::response-file-save-as', (e, { id, markdown, pathname, options
|
||||
recommendFilename = 'Untitled'
|
||||
}
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filePath = dialog.showSaveDialog(win, {
|
||||
defaultPath: pathname || getPath('documents') + `/${recommendFilename}.md`
|
||||
})
|
||||
// If the file doesn't exist on disk add it to the recently used documents later
|
||||
// and execute file from filesystem watcher for a short time. The file may exists
|
||||
// on disk nevertheless but is already tracked by Mark Text.
|
||||
const alreadyExistOnDisk = !!pathname
|
||||
|
||||
dialog.showSaveDialog(win, {
|
||||
defaultPath: pathname || getPath('documents') + `/${recommendFilename}.md`
|
||||
}, filePath => {
|
||||
if (filePath) {
|
||||
filePath = path.resolve(filePath)
|
||||
writeMarkdownFile(filePath, markdown, options, win)
|
||||
.then(() => {
|
||||
// need watch file after `save as`
|
||||
if (pathname !== filePath) {
|
||||
// unwatch the old file
|
||||
ipcMain.emit('watcher-unwatch-file', win, pathname)
|
||||
ipcMain.emit('watcher-watch-file', win, filePath)
|
||||
}
|
||||
if (!alreadyExistOnDisk) {
|
||||
ipcMain.emit('window-add-file-path', win.id, filePath)
|
||||
ipcMain.emit('menu-add-recently-used', filePath)
|
||||
|
||||
const filename = path.basename(filePath)
|
||||
win.webContents.send('AGANI::set-pathname', { id, pathname: filePath, filename })
|
||||
})
|
||||
.catch(log.error)
|
||||
win.webContents.send('mt::set-pathname', { id, pathname: filePath, filename })
|
||||
} else if (pathname !== filePath) {
|
||||
// Update window file list and watcher.
|
||||
ipcMain.emit('window-change-file-path', win.id, filePath, pathname)
|
||||
|
||||
const filename = path.basename(filePath)
|
||||
win.webContents.send('mt::set-pathname', { id, pathname: filePath, filename })
|
||||
} else {
|
||||
ipcMain.emit('window-file-saved', win.id, filePath)
|
||||
win.webContents.send('mt::tab-saved', id)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
win.webContents.send('mt::tab-save-failure', id, err.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::response-close-confirm', async (e, unsavedFiles) => {
|
||||
ipcMain.on('mt::close-window-confirm', async (e, unsavedFiles) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||
if (needSave) {
|
||||
@ -223,6 +250,18 @@ ipcMain.on('AGANI::response-close-confirm', async (e, unsavedFiles) => {
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
log.error(err)
|
||||
|
||||
// Notify user about the problem.
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'error',
|
||||
buttons: ['Close', 'Keep It Open'],
|
||||
message: 'Failure while saving files',
|
||||
detail: err.message
|
||||
}, code => {
|
||||
if (win.id && code === 0) {
|
||||
ipcMain.emit('window-close-by-id', win.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
ipcMain.emit('window-close-by-id', win.id)
|
||||
@ -240,9 +279,10 @@ ipcMain.on('AGANI::window::drop', async (e, fileList) => {
|
||||
for (const file of fileList) {
|
||||
if (isMarkdownFileOrLink(file)) {
|
||||
openFileOrFolder(win, file)
|
||||
break
|
||||
continue
|
||||
}
|
||||
// handle import file
|
||||
|
||||
// Try to import the file
|
||||
if (PANDOC_EXTENSIONS.some(ext => file.endsWith(ext))) {
|
||||
const existsPandoc = pandoc.exists()
|
||||
if (!existsPandoc) {
|
||||
@ -255,15 +295,28 @@ ipcMain.on('AGANI::window::drop', async (e, fileList) => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
||||
ipcMain.on('mt::rename', (e, { id, pathname, newPathname }) => {
|
||||
if (pathname === newPathname) return
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (!isFile(newPathname)) {
|
||||
fs.renameSync(pathname, newPathname)
|
||||
e.sender.send('AGANI::set-pathname', {
|
||||
|
||||
const doRename = () => {
|
||||
fs.rename(pathname, newPathname, err => {
|
||||
if (err) {
|
||||
log.error(`mt::rename: Cannot rename "${pathname}" to "${newPathname}".\n${err.stack}`)
|
||||
return
|
||||
}
|
||||
|
||||
ipcMain.emit('window-change-file-path', win.id, newPathname, pathname)
|
||||
e.sender.send('mt::set-pathname', {
|
||||
id,
|
||||
pathname: newPathname,
|
||||
filename: path.basename(newPathname)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!isFile(newPathname)) {
|
||||
doRename()
|
||||
} else {
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'warning',
|
||||
@ -274,12 +327,7 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
||||
noLink: true
|
||||
}, index => {
|
||||
if (index === 0) {
|
||||
fs.renameSync(pathname, newPathname)
|
||||
e.sender.send('AGANI::set-pathname', {
|
||||
id,
|
||||
pathname: newPathname,
|
||||
filename: path.basename(newPathname)
|
||||
})
|
||||
doRename()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -287,28 +335,34 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
||||
|
||||
ipcMain.on('AGANI::response-file-move-to', (e, { id, pathname }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
let newPath = dialog.showSaveDialog(win, {
|
||||
dialog.showSaveDialog(win, {
|
||||
buttonLabel: 'Move to',
|
||||
nameFieldLabel: 'Filename:',
|
||||
defaultPath: pathname
|
||||
})
|
||||
if (newPath === undefined) return
|
||||
fs.renameSync(pathname, newPath)
|
||||
e.sender.send('AGANI::set-pathname', { id, pathname: newPath, filename: path.basename(newPath) })
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::ask-for-open-project-in-sidebar', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const pathname = dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
if (pathname && pathname[0]) {
|
||||
ipcMain.emit('app-open-directory-by-id', win.id, pathname[0])
|
||||
}, newPath => {
|
||||
if (newPath) {
|
||||
fs.rename(pathname, newPath, err => {
|
||||
if (err) {
|
||||
log.error(`mt::rename: Cannot rename "${pathname}" to "${newPath}".\n${err.stack}`)
|
||||
return
|
||||
}
|
||||
|
||||
ipcMain.emit('window-change-file-path', win.id, newPath, pathname)
|
||||
e.sender.send('mt::set-pathname', { id, pathname: newPath, filename: path.basename(newPath) })
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('mt::ask-for-open-project-in-sidebar', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
}, directories => {
|
||||
if (directories && directories[0]) {
|
||||
ipcMain.emit('app-open-directory-by-id', win.id, directories[0], true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => {
|
||||
@ -341,18 +395,17 @@ export const importFile = async win => {
|
||||
return noticePandocNotFound(win)
|
||||
}
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filename = dialog.showOpenDialog(win, {
|
||||
dialog.showOpenDialog(win, {
|
||||
properties: [ 'openFile' ],
|
||||
filters: [{
|
||||
name: 'All Files',
|
||||
extensions: PANDOC_EXTENSIONS
|
||||
}]
|
||||
})
|
||||
|
||||
if (filename && filename[0]) {
|
||||
openPandocFile(win.id, filename[0])
|
||||
}, filePath => {
|
||||
if (filePath) {
|
||||
openPandocFile(win.id, filePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const print = win => {
|
||||
@ -360,27 +413,27 @@ export const print = win => {
|
||||
}
|
||||
|
||||
export const openFile = win => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const fileList = dialog.showOpenDialog(win, {
|
||||
properties: ['openFile'],
|
||||
dialog.showOpenDialog(win, {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [{
|
||||
name: 'text',
|
||||
extensions: EXTENSIONS
|
||||
}]
|
||||
})
|
||||
if (fileList && fileList[0]) {
|
||||
openFileOrFolder(win, fileList[0])
|
||||
}, paths => {
|
||||
if (paths && Array.isArray(paths)) {
|
||||
ipcMain.emit('app-open-files-by-id', win.id, paths)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const openFolder = win => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const dirList = dialog.showOpenDialog(win, {
|
||||
dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
if (dirList && dirList[0]) {
|
||||
openFileOrFolder(win, dirList[0])
|
||||
}, directories => {
|
||||
if (directories && directories[0]) {
|
||||
openFileOrFolder(win, directories[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const openFileOrFolder = (win, pathname) => {
|
||||
|
@ -7,6 +7,12 @@ import { ensureDirSync, isDirectory, isFile } from '../filesystem'
|
||||
import { parseMenu } from '../keyboard/shortcutHandler'
|
||||
import configureMenu, { configSettingMenu } from '../menu/templates'
|
||||
|
||||
export const MenuType = {
|
||||
DEFAULT: 0,
|
||||
EDITOR: 1,
|
||||
SETTINGS: 2
|
||||
}
|
||||
|
||||
class AppMenu {
|
||||
|
||||
/**
|
||||
@ -105,13 +111,13 @@ class AppMenu {
|
||||
windowMenus.set(window.id, menu)
|
||||
}
|
||||
|
||||
addEditorMenu (window) {
|
||||
addEditorMenu (window, options = {}) {
|
||||
const { windowMenus } = this
|
||||
windowMenus.set(window.id, this.buildDefaultMenu(true))
|
||||
windowMenus.set(window.id, this.buildEditorMenu(true))
|
||||
|
||||
const { menu, shortcutMap } = windowMenus.get(window.id)
|
||||
const currentMenu = Menu.getApplicationMenu() // the menu may be null
|
||||
updateMenuItemSafe(currentMenu, menu, 'sourceCodeModeMenuItem', false)
|
||||
updateMenuItemSafe(currentMenu, menu, 'sourceCodeModeMenuItem', !!options.sourceCodeModeEnabled)
|
||||
updateMenuItemSafe(currentMenu, menu, 'typewriterModeMenuItem', false)
|
||||
|
||||
// FIXME: Focus mode is being ignored when you open a new window - inconsistency.
|
||||
@ -159,7 +165,7 @@ class AppMenu {
|
||||
}
|
||||
}
|
||||
|
||||
buildDefaultMenu (createShortcutMap, recentUsedDocuments) {
|
||||
buildEditorMenu (createShortcutMap, recentUsedDocuments) {
|
||||
if (!recentUsedDocuments) {
|
||||
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
||||
}
|
||||
@ -174,7 +180,8 @@ class AppMenu {
|
||||
|
||||
return {
|
||||
shortcutMap,
|
||||
menu
|
||||
menu,
|
||||
type: MenuType.EDITOR
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,9 +189,9 @@ class AppMenu {
|
||||
if (this.isOsx) {
|
||||
const menuTemplate = configSettingMenu(this._keybindings)
|
||||
const menu = Menu.buildFromTemplate(menuTemplate)
|
||||
return { menu }
|
||||
return { menu, type: MenuType.SETTINGS }
|
||||
}
|
||||
return { menu: null }
|
||||
return { menu: null, type: MenuType.SETTINGS }
|
||||
}
|
||||
|
||||
updateAppMenu (recentUsedDocuments) {
|
||||
@ -193,13 +200,15 @@ class AppMenu {
|
||||
}
|
||||
|
||||
// "we don't support changing menu object after calling setMenu, the behavior
|
||||
// is undefined if user does that." That means we have to recreate the
|
||||
// is undefined if user does that." That mean we have to recreate the editor
|
||||
// application menu each time.
|
||||
|
||||
// rebuild all window menus
|
||||
this.windowMenus.forEach((value, key) => {
|
||||
const { menu: oldMenu } = value
|
||||
const { menu: newMenu } = this.buildDefaultMenu(false, recentUsedDocuments)
|
||||
const { menu: oldMenu, type } = value
|
||||
if (type !== MenuType.EDITOR) return
|
||||
|
||||
const { menu: newMenu } = this.buildEditorMenu(false, recentUsedDocuments)
|
||||
|
||||
// all other menu items are set automatically
|
||||
updateMenuItem(oldMenu, newMenu, 'sourceCodeModeMenuItem')
|
||||
@ -231,7 +240,9 @@ class AppMenu {
|
||||
|
||||
updateThemeMenu = theme => {
|
||||
this.windowMenus.forEach((value, key) => {
|
||||
const { menu } = value
|
||||
const { menu, type } = value
|
||||
if (type !== MenuType.EDITOR) return
|
||||
|
||||
const themeMenus = menu.getMenuItemById('themeMenu')
|
||||
if (!themeMenus) {
|
||||
return
|
||||
@ -248,7 +259,9 @@ class AppMenu {
|
||||
|
||||
updateAutoSaveMenu = autoSave => {
|
||||
this.windowMenus.forEach((value, key) => {
|
||||
const { menu } = value
|
||||
const { menu, type } = value
|
||||
if (type !== MenuType.EDITOR) return
|
||||
|
||||
const autoSaveMenu = menu.getMenuItemById('autoSaveMenuItem')
|
||||
if (!autoSaveMenu) {
|
||||
return
|
||||
@ -259,7 +272,9 @@ class AppMenu {
|
||||
|
||||
updateAidouMenu = bool => {
|
||||
this.windowMenus.forEach((value, key) => {
|
||||
const { menu } = value
|
||||
const { menu, type } = value
|
||||
if (type !== MenuType.EDITOR) return
|
||||
|
||||
const aidouMenu = menu.getMenuItemById('aidou')
|
||||
if (!aidouMenu) {
|
||||
return
|
||||
@ -283,6 +298,9 @@ class AppMenu {
|
||||
this.addRecentlyUsedDocument(pathname)
|
||||
})
|
||||
|
||||
ipcMain.on('menu-add-recently-used', pathname => {
|
||||
this.addRecentlyUsedDocument(pathname)
|
||||
})
|
||||
ipcMain.on('menu-clear-recently-used', () => {
|
||||
this.clearRecentlyUsedDocuments()
|
||||
})
|
||||
|
@ -2,10 +2,10 @@ import { app } from 'electron'
|
||||
import * as actions from '../actions/file'
|
||||
import { userSetting } from '../actions/marktext'
|
||||
import { showTabBar } from '../actions/view'
|
||||
import { isOsx } from '../../config'
|
||||
|
||||
export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
const { autoSave } = userPreference.getAll()
|
||||
const notOsx = process.platform !== 'darwin'
|
||||
let fileMenu = {
|
||||
label: 'File',
|
||||
submenu: [{
|
||||
@ -38,7 +38,7 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
}]
|
||||
}
|
||||
|
||||
if (notOsx) {
|
||||
if (!isOsx) {
|
||||
let recentlyUsedMenu = {
|
||||
label: 'Open Recent',
|
||||
submenu: []
|
||||
@ -77,12 +77,6 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
|
||||
fileMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Close Tab',
|
||||
accelerator: keybindings.getAccelerator('fileCloseTab'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.closeTab(browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
@ -139,8 +133,6 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Print',
|
||||
accelerator: keybindings.getAccelerator('filePrint'),
|
||||
@ -149,23 +141,34 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: notOsx
|
||||
visible: !isOsx
|
||||
}, {
|
||||
label: 'Preferences',
|
||||
accelerator: keybindings.getAccelerator('filePreferences'),
|
||||
visible: notOsx,
|
||||
visible: !isOsx,
|
||||
click (menuItem, browserWindow) {
|
||||
userSetting(menuItem, browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: notOsx
|
||||
}, {
|
||||
label: 'Close Tab',
|
||||
accelerator: keybindings.getAccelerator('fileCloseTab'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.closeTab(browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'Close Window',
|
||||
accelerator: keybindings.getAccelerator('fileCloseWindow'),
|
||||
role: 'close'
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: !isOsx
|
||||
}, {
|
||||
label: 'Quit',
|
||||
accelerator: keybindings.getAccelerator('fileQuit'),
|
||||
visible: notOsx,
|
||||
visible: !isOsx,
|
||||
click: app.quit
|
||||
})
|
||||
|
||||
return fileMenu
|
||||
}
|
||||
|
@ -1,20 +1,10 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import * as actions from '../actions/view'
|
||||
import { isOsx } from '../../config'
|
||||
|
||||
export default function (keybindings) {
|
||||
let viewMenu = {
|
||||
label: 'View',
|
||||
submenu: [{
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'sourceCodeModeMenuItem',
|
||||
label: 'Source Code Mode',
|
||||
accelerator: keybindings.getAccelerator('viewSourceCodeMode'),
|
||||
@ -89,7 +79,6 @@ export default function (keybindings) {
|
||||
}
|
||||
|
||||
if (global.MARKTEXT_DEBUG) {
|
||||
// add devtool when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: keybindings.getAccelerator('viewDevToggleDeveloperTools'),
|
||||
@ -99,25 +88,15 @@ export default function (keybindings) {
|
||||
}
|
||||
}
|
||||
})
|
||||
// add reload when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Reload',
|
||||
accelerator: keybindings.getAccelerator('viewDevReload'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload()
|
||||
ipcMain.emit('window-reload-by-id', focusedWindow.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isOsx) {
|
||||
viewMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Bring All to Front',
|
||||
role: 'front'
|
||||
})
|
||||
}
|
||||
return viewMenu
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { toggleAlwaysOnTop } from '../actions/window'
|
||||
import { isOsx } from '../../config'
|
||||
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
const menu = {
|
||||
label: 'Window',
|
||||
role: 'window',
|
||||
submenu: [{
|
||||
@ -18,9 +19,21 @@ export default function (keybindings) {
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Close Window',
|
||||
accelerator: keybindings.getAccelerator('windowCloseWindow'),
|
||||
role: 'close'
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
if (isOsx) {
|
||||
menu.submenu.push({
|
||||
label: 'Bring All to Front',
|
||||
role: 'front'
|
||||
})
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
@ -16,7 +16,11 @@
|
||||
]
|
||||
},
|
||||
"openFilesInNewWindow": {
|
||||
"description": "General--Open file in new window",
|
||||
"description": "General--Open files in a new window.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"openFolderInNewWindow": {
|
||||
"description": "General--Open folder via menu in a new window.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"aidou": {
|
||||
@ -31,7 +35,7 @@
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"startUp": {
|
||||
"startUpAction": {
|
||||
"description": "General--The action after Mark Text startup, open the last edited content, open the specified folder or blank page",
|
||||
"enum": [
|
||||
"folder",
|
||||
@ -39,6 +43,10 @@
|
||||
"blank"
|
||||
]
|
||||
},
|
||||
"defaultDirectoryToOpen": {
|
||||
"description": "General--The default directory that should be opened on startup when startUp=folder.",
|
||||
"type": "string"
|
||||
},
|
||||
"language": {
|
||||
"description": "General--The language Mark Text use.",
|
||||
"type": "string"
|
||||
@ -160,7 +168,6 @@
|
||||
"description": "Theme--Select the theme used in Mark Text",
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"imageInsertAction": {
|
||||
"description": "Image--The default behavior after insert image from local folder",
|
||||
"enum": [
|
||||
@ -168,5 +175,17 @@
|
||||
"folder",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"sideBarVisibility": {
|
||||
"description": "View--Whether the side bar is visible.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"tabBarVisibility": {
|
||||
"description": "View--Whether the tabs are shown.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceCodeModeEnabled": {
|
||||
"description": "View--Whether the source-code mode is enabled by default.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,28 @@
|
||||
import EventEmitter from 'events'
|
||||
import { WindowType } from '../app/windowManager'
|
||||
import { isLinux } from '../config'
|
||||
|
||||
/**
|
||||
* A Mark Text window.
|
||||
* @typedef {BaseWindow} IApplicationWindow
|
||||
* @property {number | null} id Identifier (= browserWindow.id) or null during initialization.
|
||||
* @property {Electron.BrowserWindow} browserWindow The browse window.
|
||||
* @property {WindowLifecycle} lifecycle The window lifecycle state.
|
||||
* @property {WindowType} type The window type.
|
||||
*/
|
||||
|
||||
// Window type marktext support.
|
||||
export const WindowType = {
|
||||
BASE: 'base', // You shold never create a `BASE` window.
|
||||
EDITOR: 'editor',
|
||||
SETTING: 'setting'
|
||||
}
|
||||
|
||||
export const WindowLifecycle = {
|
||||
NONE: 0,
|
||||
LOADING: 1,
|
||||
READY: 2,
|
||||
QUITTED: 3
|
||||
}
|
||||
|
||||
class BaseWindow extends EventEmitter {
|
||||
|
||||
@ -10,26 +33,43 @@ class BaseWindow extends EventEmitter {
|
||||
super()
|
||||
|
||||
this._accessor = accessor
|
||||
this.type = WindowType.BASE
|
||||
this.id = null
|
||||
this.browserWindow = null
|
||||
this.quitting = false
|
||||
this.lifecycle = WindowLifecycle.NONE
|
||||
this.type = WindowType.BASE
|
||||
}
|
||||
|
||||
bringToFront () {
|
||||
const { browserWindow: win } = this
|
||||
if (win.isMinimized()) win.restore()
|
||||
if (!win.isVisible()) win.show()
|
||||
if (isLinux) {
|
||||
win.focus()
|
||||
} else {
|
||||
win.moveTop()
|
||||
}
|
||||
}
|
||||
|
||||
reload () {
|
||||
this.browserWindow.reload()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.quitting = true
|
||||
this.emit('bye')
|
||||
this.lifecycle = WindowLifecycle.QUITTED
|
||||
this.emit('window-closed')
|
||||
|
||||
this.removeAllListeners()
|
||||
if (this.browserWindow) {
|
||||
this.browserWindow.destroy()
|
||||
this.browserWindow = null
|
||||
}
|
||||
this.id = null
|
||||
}
|
||||
|
||||
// --- private ---------------------------------
|
||||
|
||||
_buildUrlWithSettings (windowId, env, userPreference) {
|
||||
// NOTE: Only send absolutely necessary values. Theme and titlebar settings
|
||||
// are sended because we delay load the preferences.
|
||||
// NOTE: Only send absolutely necessary values. Full settings are delay loaded.
|
||||
const { type } = this
|
||||
const { debug, paths } = env
|
||||
const { codeFontFamily, codeFontSize, theme, titleBarStyle } = userPreference.getAll()
|
||||
@ -50,7 +90,11 @@ class BaseWindow extends EventEmitter {
|
||||
url.searchParams.set('theme', theme)
|
||||
url.searchParams.set('tbs', titleBarStyle)
|
||||
|
||||
return url.toString()
|
||||
return url
|
||||
}
|
||||
|
||||
_buildUrlString (windowId, env, userPreference) {
|
||||
return this._buildUrlWithSettings(windowId, env, userPreference).toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
import path from 'path'
|
||||
import BaseWindow from './base'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { WindowType } from '../app/windowManager'
|
||||
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux, isOsx } from '../config'
|
||||
import { isDirectory, isMarkdownFile, normalizeAndResolvePath } from '../filesystem'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
||||
import { ensureWindowPosition } from './utils'
|
||||
import { TITLE_BAR_HEIGHT, editorWinOptions, isLinux, isOsx } from '../config'
|
||||
import { isChildOfDirectory, isSamePathSync } from '../filesystem'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
|
||||
class EditorWindow extends BaseWindow {
|
||||
|
||||
@ -17,22 +16,29 @@ class EditorWindow extends BaseWindow {
|
||||
constructor (accessor) {
|
||||
super(accessor)
|
||||
this.type = WindowType.EDITOR
|
||||
|
||||
// Root directory and file list to open when the window is ready.
|
||||
this._directoryToOpen = null
|
||||
this._filesToOpen = [] // {doc: IMarkdownDocumentRaw, selected: boolean}
|
||||
this._markdownToOpen = [] // List of markdown strings or an empty string will open a new untitled tab
|
||||
|
||||
// Root directory and file list that are currently opened. These lists are
|
||||
// used to find the best window to open new files in.
|
||||
this._openedRootDirectory = ''
|
||||
this._openedFiles = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new editor window.
|
||||
*
|
||||
* @param {string} [pathname] Path to a file, directory or link.
|
||||
* @param {string} [markdown] Markdown content.
|
||||
* @param {*} [options] BrowserWindow options.
|
||||
* @param {string} [rootDirectory] The root directory to open.
|
||||
* @param {string[]} [fileList] A list of markdown files to open.
|
||||
* @param {string[]} [markdownList] Array of markdown data to open.
|
||||
* @param {*} [options] The BrowserWindow options.
|
||||
*/
|
||||
createWindow (pathname = null, markdown = '', options = {}) {
|
||||
createWindow (rootDirectory = null, fileList = [], markdownList = [], options = {}) {
|
||||
const { menu: appMenu, env, preferences } = this._accessor
|
||||
|
||||
// Ensure path is normalized
|
||||
if (pathname) {
|
||||
pathname = normalizeAndResolvePath(pathname)
|
||||
}
|
||||
const addBlankTab = !rootDirectory && fileList.length === 0 && markdownList.length === 0
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1200,
|
||||
@ -40,13 +46,19 @@ class EditorWindow extends BaseWindow {
|
||||
})
|
||||
|
||||
const { x, y, width, height } = ensureWindowPosition(mainWindowState)
|
||||
const winOptions = Object.assign({ x, y, width, height }, defaultWinOptions, options)
|
||||
const winOptions = Object.assign({ x, y, width, height }, editorWinOptions, options)
|
||||
if (isLinux) {
|
||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
||||
}
|
||||
|
||||
// Enable native or custom/frameless window and titlebar
|
||||
const { titleBarStyle } = preferences.getAll()
|
||||
const {
|
||||
titleBarStyle,
|
||||
theme,
|
||||
sideBarVisibility,
|
||||
tabBarVisibility,
|
||||
sourceCodeModeEnabled
|
||||
} = preferences.getAll()
|
||||
if (!isOsx) {
|
||||
winOptions.titleBarStyle = 'default'
|
||||
if (titleBarStyle === 'native') {
|
||||
@ -54,35 +66,58 @@ class EditorWindow extends BaseWindow {
|
||||
}
|
||||
}
|
||||
|
||||
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
|
||||
|
||||
let win = this.browserWindow = new BrowserWindow(winOptions)
|
||||
this.id = win.id
|
||||
|
||||
// Create a menu for the current window
|
||||
appMenu.addEditorMenu(win)
|
||||
appMenu.addEditorMenu(win, { sourceCodeModeEnabled })
|
||||
|
||||
win.once('ready-to-show', async () => {
|
||||
mainWindowState.manage(win)
|
||||
win.show()
|
||||
win.webContents.once('did-finish-load', () => {
|
||||
this.lifecycle = WindowLifecycle.READY
|
||||
this.emit('window-ready')
|
||||
|
||||
this.emit('window-ready-to-show')
|
||||
// Restore and focus window
|
||||
this.bringToFront()
|
||||
|
||||
if (pathname && isMarkdownFile(pathname)) {
|
||||
// Open single markdown file
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
this._openFile(pathname)
|
||||
} else if (pathname && isDirectory(pathname)) {
|
||||
// Open directory / folder
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
this.openFolder(pathname)
|
||||
} else {
|
||||
// Open a blank window
|
||||
const lineEnding = preferences.getPreferedEOL()
|
||||
win.webContents.send('mt::bootstrap-blank-window', {
|
||||
lineEnding,
|
||||
markdown
|
||||
})
|
||||
appMenu.updateLineEndingMenu(lineEnding)
|
||||
|
||||
win.webContents.send('mt::bootstrap-editor', {
|
||||
addBlankTab,
|
||||
markdownList: this._markdownToOpen,
|
||||
lineEnding,
|
||||
sideBarVisibility,
|
||||
tabBarVisibility,
|
||||
sourceCodeModeEnabled
|
||||
})
|
||||
|
||||
this._doOpenFilesToOpen()
|
||||
this._markdownToOpen.length = 0
|
||||
})
|
||||
|
||||
win.webContents.once('did-fail-load', (event, errorCode, errorDescription) => {
|
||||
log.error(`The window failed to load or was cancelled: ${errorCode}; ${errorDescription}`)
|
||||
})
|
||||
|
||||
win.webContents.once('crashed', (event, killed) => {
|
||||
const msg = `The renderer process has crashed unexpected or is killed (${killed}).`
|
||||
log.error(msg)
|
||||
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'warning',
|
||||
buttons: ['Close', 'Reload', 'Keep It Open'],
|
||||
message: 'Mark Text has crashed',
|
||||
detail: msg
|
||||
}, code => {
|
||||
if (win.id) {
|
||||
switch(code) {
|
||||
case 0: return this.destroy()
|
||||
case 1: return this.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
win.on('focus', () => {
|
||||
@ -114,94 +149,295 @@ class EditorWindow extends BaseWindow {
|
||||
|
||||
// The window is now destroyed.
|
||||
win.on('closed', () => {
|
||||
this.lifecycle = WindowLifecycle.QUITTED
|
||||
this.emit('window-closed')
|
||||
|
||||
// Free window reference
|
||||
win = null
|
||||
})
|
||||
|
||||
win.loadURL(this._buildUrlWithSettings(this.id, env, preferences))
|
||||
this.lifecycle = WindowLifecycle.LOADING
|
||||
win.loadURL(this._buildUrlString(this.id, env, preferences))
|
||||
win.setSheetOffset(TITLE_BAR_HEIGHT)
|
||||
|
||||
mainWindowState.manage(win)
|
||||
|
||||
// Delay load files and directories after the current control flow.
|
||||
setTimeout(() => {
|
||||
if (rootDirectory) {
|
||||
this.openFolder(rootDirectory)
|
||||
}
|
||||
if (fileList.length) {
|
||||
this.openTabs(fileList, 0)
|
||||
}
|
||||
}, 0)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
openTab (filePath, selectTab=true) {
|
||||
if (this.quitting) return
|
||||
/**
|
||||
* Open a new tab from a markdown file.
|
||||
*
|
||||
* @param {string} filePath The markdown file path.
|
||||
* @param {[boolean]} selected Whether the tab should become the selected tab (true if not set).
|
||||
*/
|
||||
openTab (filePath, selected=true) {
|
||||
if (this.lifecycle === WindowLifecycle.QUITTED) return
|
||||
this.openTabs([ filePath ], selected ? 0 : -1 )
|
||||
}
|
||||
|
||||
/**
|
||||
* Open new tabs from markdown files.
|
||||
*
|
||||
* @param {string[]} filePath The markdown file path list to open.
|
||||
* @param {[number]} selectedIndex Whether one of the given tabs should become the selected tab (-1 if not set).
|
||||
*/
|
||||
openTabs (fileList, selectedIndex = -1) {
|
||||
if (this.lifecycle === WindowLifecycle.QUITTED) return
|
||||
|
||||
const { browserWindow } = this
|
||||
const { menu: appMenu, preferences } = this._accessor
|
||||
const { preferences } = this._accessor
|
||||
const eol = preferences.getPreferedEOL()
|
||||
|
||||
// Listen for file changed.
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
||||
|
||||
loadMarkdownFile(filePath, preferences.getPreferedEOL()).then(rawDocument => {
|
||||
appMenu.addRecentlyUsedDocument(filePath)
|
||||
browserWindow.webContents.send('AGANI::new-tab', rawDocument, selectTab)
|
||||
for (let i = 0; i < fileList.length; ++i) {
|
||||
const filePath = fileList[i]
|
||||
const selected = i === selectedIndex
|
||||
loadMarkdownFile(filePath, eol).then(rawDocument => {
|
||||
if (this.lifecycle === WindowLifecycle.READY) {
|
||||
this._doOpenTab(rawDocument, selected)
|
||||
} else {
|
||||
this._filesToOpen.push({ doc: rawDocument, selected })
|
||||
}
|
||||
}).catch(err => {
|
||||
// TODO: Handle error --> create a end-user error handler.
|
||||
console.error('[ERROR] Cannot open file or directory.')
|
||||
log.error(err)
|
||||
browserWindow.webContents.send('AGANI::show-notification', {
|
||||
title: 'Cannot open tab',
|
||||
type: 'error',
|
||||
message: err.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
openUntitledTab (selectTab=true, markdownString='') {
|
||||
if (this.quitting) return
|
||||
|
||||
const { browserWindow } = this
|
||||
browserWindow.webContents.send('mt::new-untitled-tab', selectTab, markdownString)
|
||||
}
|
||||
|
||||
openFolder (pathname) {
|
||||
if (this.quitting) return
|
||||
/**
|
||||
* Open a new untitled tab optional with a markdown string.
|
||||
*
|
||||
* @param {[boolean]} selected Whether the tab should become the selected tab (true if not set).
|
||||
* @param {[string]} markdown The markdown string.
|
||||
*/
|
||||
openUntitledTab (selected=true, markdown='') {
|
||||
if (this.lifecycle === WindowLifecycle.QUITTED) return
|
||||
|
||||
if (this.lifecycle === WindowLifecycle.READY) {
|
||||
const { browserWindow } = this
|
||||
browserWindow.webContents.send('mt::new-untitled-tab', selected, markdown)
|
||||
} else {
|
||||
this._markdownToOpen.push(markdown)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a (new) directory and replaces the old one.
|
||||
*
|
||||
* @param {string} pathname The directory path.
|
||||
*/
|
||||
openFolder (pathname) {
|
||||
if (this.lifecycle === WindowLifecycle.QUITTED ||
|
||||
isSamePathSync(pathname, this._openedRootDirectory)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.lifecycle === WindowLifecycle.READY) {
|
||||
const { browserWindow } = this
|
||||
if (this._openedRootDirectory) {
|
||||
ipcMain.emit('watcher-unwatch-directory', browserWindow, this._openedRootDirectory)
|
||||
}
|
||||
|
||||
this._openedRootDirectory = pathname
|
||||
ipcMain.emit('watcher-watch-directory', browserWindow, pathname)
|
||||
browserWindow.webContents.send('AGANI::open-project', pathname)
|
||||
browserWindow.webContents.send('mt::open-directory', pathname)
|
||||
} else {
|
||||
this._directoryToOpen = pathname
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new path to the file list and watch the given path.
|
||||
*
|
||||
* @param {string} filePath The file path.
|
||||
*/
|
||||
addToOpenedFiles (filePath) {
|
||||
const { _openedFiles, browserWindow } = this
|
||||
_openedFiles.push(filePath)
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a path in the opened file list and update the watcher.
|
||||
*
|
||||
* @param {string} pathname
|
||||
* @param {string} oldPathname
|
||||
*/
|
||||
changeOpenedFilePath (pathname, oldPathname) {
|
||||
const { _openedFiles, browserWindow } = this
|
||||
const index = _openedFiles.findIndex(p => p === oldPathname)
|
||||
if (index === -1) {
|
||||
// The old path was not found but add the new one.
|
||||
_openedFiles.push(pathname)
|
||||
} else {
|
||||
_openedFiles[index] = pathname
|
||||
}
|
||||
ipcMain.emit('watcher-unwatch-file', browserWindow, oldPathname)
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, pathname)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a path from the opened file list and stop watching the path.
|
||||
*
|
||||
* @param {string} pathname The full path.
|
||||
*/
|
||||
removeFromOpenedFiles (pathname) {
|
||||
const { _openedFiles, browserWindow } = this
|
||||
const index = _openedFiles.findIndex(p => p === pathname)
|
||||
if (index !== -1) {
|
||||
_openedFiles.splice(index, 1)
|
||||
}
|
||||
ipcMain.emit('watcher-unwatch-file', browserWindow, pathname)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a score list for a given file list.
|
||||
*
|
||||
* @param {string[]} fileList The file list.
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getCandidateScores (fileList) {
|
||||
const { _openedFiles, _openedRootDirectory, id } = this
|
||||
const buf = []
|
||||
for (const pathname of fileList) {
|
||||
let score = 0
|
||||
if (_openedFiles.some(p => p === pathname)) {
|
||||
score = -1
|
||||
} else {
|
||||
if (isChildOfDirectory(_openedRootDirectory, pathname)) {
|
||||
score += 5
|
||||
}
|
||||
for (const item of _openedFiles) {
|
||||
if (isChildOfDirectory(path.dirname(item), pathname)) {
|
||||
score += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.push({ id, score })
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
reload () {
|
||||
const { id, browserWindow } = this
|
||||
|
||||
// Close watchers
|
||||
ipcMain.emit('watcher-unwatch-all-by-id', id)
|
||||
|
||||
// Reset saved state
|
||||
this._directoryToOpen = ''
|
||||
this._filesToOpen = []
|
||||
this._markdownToOpen = []
|
||||
this._openedRootDirectory = ''
|
||||
this._openedFiles = []
|
||||
|
||||
browserWindow.webContents.once('did-finish-load', () => {
|
||||
this.lifecycle = WindowLifecycle.READY
|
||||
const { preferences } = this._accessor
|
||||
const { sideBarVisibility, tabBarVisibility, sourceCodeModeEnabled } = preferences.getAll()
|
||||
const lineEnding = preferences.getPreferedEOL()
|
||||
browserWindow.webContents.send('mt::bootstrap-editor', {
|
||||
addBlankTab: true,
|
||||
markdownList: [],
|
||||
lineEnding,
|
||||
sideBarVisibility,
|
||||
tabBarVisibility,
|
||||
sourceCodeModeEnabled
|
||||
})
|
||||
})
|
||||
|
||||
this.lifecycle = WindowLifecycle.LOADING
|
||||
super.reload()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
super.destroy()
|
||||
|
||||
// Watchers are freed from WindowManager.
|
||||
|
||||
this._directoryToOpen = null
|
||||
this._filesToOpen = null
|
||||
this._markdownToOpen = null
|
||||
this._openedRootDirectory = null
|
||||
this._openedFiles = null
|
||||
}
|
||||
|
||||
get openedRootDirectory () {
|
||||
return this._openedRootDirectory
|
||||
}
|
||||
|
||||
// --- private ---------------------------------
|
||||
|
||||
// Only called once during window bootstrapping.
|
||||
_openFile = async filePath => {
|
||||
const { browserWindow } = this
|
||||
const { menu: appMenu, preferences } = this._accessor
|
||||
|
||||
const data = await loadMarkdownFile(filePath, preferences.getPreferedEOL())
|
||||
const {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
isMixedLineEndings
|
||||
} = data
|
||||
|
||||
appMenu.updateLineEndingMenu(lineEnding)
|
||||
browserWindow.webContents.send('mt::bootstrap-window', {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
options: {
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave
|
||||
_getPreferredBackgroundColor (theme) {
|
||||
// Hardcode the theme background color and show the window direct for the fastet window ready time.
|
||||
// Later with custom themes we need the background color (e.g. from meta information) and wait
|
||||
// that the window is loaded and then pass theme data to the renderer.
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
return '#282828'
|
||||
case 'material-dark':
|
||||
return '#34393f'
|
||||
case 'ulysses':
|
||||
return '#f3f3f3'
|
||||
case 'graphite':
|
||||
return '#f7f7f7'
|
||||
case 'one-dark':
|
||||
return '#282c34'
|
||||
case 'light':
|
||||
default:
|
||||
return '#ffffff'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new new tab from the markdown document.
|
||||
*
|
||||
* @param {IMarkdownDocumentRaw} rawDocument The markdown document.
|
||||
* @param {boolean} selected Whether the tab should become the selected tab (true if not set).
|
||||
*/
|
||||
_doOpenTab (rawDocument, selected) {
|
||||
const { _accessor, _openedFiles, browserWindow } = this
|
||||
const { menu: appMenu } = _accessor
|
||||
const { pathname } = rawDocument
|
||||
|
||||
// Listen for file changed.
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, pathname)
|
||||
|
||||
// Notify user about mixed endings
|
||||
if (isMixedLineEndings) {
|
||||
browserWindow.webContents.send('AGANI::show-notification', {
|
||||
title: 'Mixed Line Endings',
|
||||
type: 'error',
|
||||
message: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||
time: 20000
|
||||
})
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
_openedFiles.push(pathname)
|
||||
browserWindow.webContents.send('AGANI::new-tab', rawDocument, selected)
|
||||
}
|
||||
|
||||
_doOpenFilesToOpen () {
|
||||
if (this.lifecycle !== WindowLifecycle.READY) {
|
||||
throw new Error('Invalid state.')
|
||||
}
|
||||
|
||||
if (this._directoryToOpen) {
|
||||
this.openFolder(this._directoryToOpen)
|
||||
}
|
||||
this._directoryToOpen = null
|
||||
|
||||
for(const { doc, selected } of this._filesToOpen) {
|
||||
this._doOpenTab(doc, selected)
|
||||
}
|
||||
this._filesToOpen.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import path from 'path'
|
||||
import BaseWindow from './base'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { WindowType } from '../app/windowManager'
|
||||
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
||||
import { centerWindowOptions } from './utils'
|
||||
import { TITLE_BAR_HEIGHT, defaultPreferenceWinOptions, isLinux, isOsx } from '../config'
|
||||
|
||||
|
||||
class SettingWindow extends BaseWindow {
|
||||
|
||||
/**
|
||||
@ -23,6 +22,7 @@ class SettingWindow extends BaseWindow {
|
||||
createWindow (options = {}) {
|
||||
const { menu: appMenu, env, preferences } = this._accessor
|
||||
const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
|
||||
centerWindowOptions(winOptions)
|
||||
if (isLinux) {
|
||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
||||
}
|
||||
@ -42,10 +42,10 @@ class SettingWindow extends BaseWindow {
|
||||
// Create a menu for the current window
|
||||
appMenu.addSettingMenu(win)
|
||||
|
||||
win.once('ready-to-show', async () => {
|
||||
win.once('ready-to-show', () => {
|
||||
win.show()
|
||||
|
||||
this.emit('window-ready-to-show')
|
||||
this.lifecycle = WindowLifecycle.READY
|
||||
this.emit('window-ready')
|
||||
})
|
||||
|
||||
win.on('focus', () => {
|
||||
@ -74,7 +74,8 @@ class SettingWindow extends BaseWindow {
|
||||
win = null
|
||||
})
|
||||
|
||||
win.loadURL(this._buildUrlWithSettings(this.id, env, preferences))
|
||||
this.lifecycle = WindowLifecycle.LOADING
|
||||
win.loadURL(this._buildUrlString(this.id, env, preferences))
|
||||
win.setSheetOffset(TITLE_BAR_HEIGHT)
|
||||
|
||||
return win
|
||||
|
@ -1,6 +1,15 @@
|
||||
import { screen } from 'electron'
|
||||
import { isLinux } from '../config'
|
||||
|
||||
export const centerWindowOptions = options => {
|
||||
// "workArea" doesn't work on Linux
|
||||
const { bounds, workArea } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
|
||||
const screenArea = isLinux ? bounds : workArea
|
||||
const { width, height } = options
|
||||
options.x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
||||
options.y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
||||
}
|
||||
|
||||
export const ensureWindowPosition = windowState => {
|
||||
// "workArea" doesn't work on Linux
|
||||
const { bounds, workArea } = screen.getPrimaryDisplay()
|
||||
@ -21,7 +30,6 @@ export const ensureWindowPosition = windowState => {
|
||||
.some(display => display)
|
||||
}
|
||||
if (center) {
|
||||
// win.center() doesn't work on Linux
|
||||
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
||||
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
||||
}
|
||||
|
@ -52,7 +52,8 @@
|
||||
--maskColor: rgba(255, 255, 255, .7);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--floatHoverColor);
|
||||
}
|
||||
::-webkit-scrollbar:vertical {
|
||||
|
@ -12,7 +12,7 @@ body article.print-container {
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
background: transparent !important;
|
||||
background: #ffffff !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,8 @@
|
||||
--editorAreaWidth: 750px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
::-webkit-scrollbar,
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--editorBgColor);
|
||||
}
|
||||
::-webkit-scrollbar:vertical {
|
||||
|
6
src/renderer/bootstrap.js
vendored
6
src/renderer/bootstrap.js
vendored
@ -64,6 +64,12 @@ const bootstrapRenderer = () => {
|
||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||
})
|
||||
|
||||
// Remove the initial drag area.
|
||||
const initDragRegion = document.getElementById('init-drag-region')
|
||||
if (initDragRegion) {
|
||||
initDragRegion.remove()
|
||||
}
|
||||
|
||||
const { debug, initialState, userDataPath, windowId, type } = parseUrlArgs()
|
||||
const marktext = {
|
||||
initialState,
|
||||
|
@ -142,6 +142,7 @@
|
||||
'theme': state => state.preferences.theme,
|
||||
|
||||
'currentFile': state => state.editor.currentFile,
|
||||
|
||||
// edit modes
|
||||
'typewriter': state => state.preferences.typewriter,
|
||||
'focus': state => state.preferences.focus,
|
||||
|
@ -64,7 +64,7 @@
|
||||
},
|
||||
methods: {
|
||||
newFile () {
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB', {})
|
||||
},
|
||||
handleTabScroll (event) {
|
||||
// Use mouse wheel value first but prioritize X value more (e.g. touchpad input).
|
||||
|
@ -23,7 +23,7 @@
|
||||
},
|
||||
methods: {
|
||||
newFile () {
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB', {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(need::refactor): See TODO in src/main/filesystem/watcher.js.
|
||||
// TODO(need::refactor): Please see #1034.
|
||||
|
||||
this.searchResult = this.fileList.filter(f => f.data.markdown.indexOf(keyword) >= 0)
|
||||
}
|
||||
|
@ -41,9 +41,6 @@ import './assets/styles/printService.css'
|
||||
// -----------------------------------------------
|
||||
|
||||
// Decode source map in production - must be registered first
|
||||
|
||||
addElementStyle()
|
||||
|
||||
sourceMapSupport.install({
|
||||
environment: 'node',
|
||||
handleUncaughtExceptions: false,
|
||||
@ -53,6 +50,8 @@ sourceMapSupport.install({
|
||||
global.marktext = {}
|
||||
bootstrapRenderer()
|
||||
|
||||
addElementStyle()
|
||||
|
||||
// -----------------------------------------------
|
||||
// Be careful when changing code before this line!
|
||||
|
||||
|
@ -11,9 +11,9 @@ export const tabsMixins = {
|
||||
removeFileInTab (file) {
|
||||
const { isSaved } = file
|
||||
if (isSaved) {
|
||||
this.$store.dispatch('REMOVE_FILE_IN_TABS', file)
|
||||
this.$store.dispatch('FORCE_CLOSE_TAB', file)
|
||||
} else {
|
||||
this.$store.dispatch('CLOSE_SINGLE_FILE', file)
|
||||
this.$store.dispatch('CLOSE_UNSAVED_TAB', file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,19 +22,17 @@ export const tabsMixins = {
|
||||
export const fileMixins = {
|
||||
methods: {
|
||||
handleFileClick () {
|
||||
// HACK: Please see #1034 and #1035
|
||||
const { data, isMarkdown, pathname } = this.file
|
||||
if (!isMarkdown || this.currentFile.pathname === pathname) return
|
||||
const { isMixedLineEndings, filename, lineEnding } = data
|
||||
const isOpened = this.tabs.filter(file => file.pathname === pathname)[0]
|
||||
const isOpened = this.tabs.find(file => file.pathname === pathname)
|
||||
|
||||
const fileState = isOpened || getFileStateFromData(data)
|
||||
this.$store.dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
// ask main process to watch this file changes
|
||||
this.$store.dispatch('ASK_FILE_WATCH', {
|
||||
pathname,
|
||||
watch: true
|
||||
})
|
||||
|
||||
// HACK: notify main process. Main process should notify the browser window.
|
||||
ipcRenderer.send('AGANI::window-add-file-path', pathname)
|
||||
ipcRenderer.send('mt::add-recently-used-document', pathname)
|
||||
|
||||
if (isMixedLineEndings && !isOpened) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="editor-container"
|
||||
>
|
||||
<side-bar></side-bar>
|
||||
<side-bar v-if="init"></side-bar>
|
||||
<div class="editor-middle">
|
||||
<title-bar
|
||||
:project="projectTree"
|
||||
@ -13,6 +13,7 @@
|
||||
:platform="platform"
|
||||
:is-saved="isSaved"
|
||||
></title-bar>
|
||||
<div class="editor-placeholder" v-if="!init"></div>
|
||||
<recent
|
||||
v-if="!hasCurrentFile && init"
|
||||
></recent>
|
||||
@ -180,6 +181,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-placeholder,
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -197,6 +199,9 @@
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
.editor-placeholder {
|
||||
background: var(--editorBgColor);
|
||||
}
|
||||
.editor-middle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -42,12 +42,12 @@
|
||||
:onChange="value => onSelectChange('fileSortBy', value)"
|
||||
:disable="true"
|
||||
></cur-select>
|
||||
<section class="startup-ctrl ag-underdevelop">
|
||||
<section class="startup-action-ctrl">
|
||||
<div>The action after Mark Text startup, open the last edited content, open the specified folder or blank page</div>
|
||||
<el-radio v-model="startUp" label="lastState">Open the last closed folder and files</el-radio>
|
||||
<el-radio v-model="startUp" label="folder">Open the subfolder</el-radio>
|
||||
<el-button size="small">Select Folder</el-button>
|
||||
<el-radio v-model="startUp" label="blank">Open blank page</el-radio>
|
||||
<el-radio class="ag-underdevelop" v-model="startUpAction" label="lastState">Open the last window state</el-radio>
|
||||
<el-radio v-model="startUpAction" label="folder">Open a default directory</el-radio>
|
||||
<el-button size="small" @click="selectDefaultDirectoryToOpen">Select Folder</el-button>
|
||||
<el-radio v-model="startUpAction" label="blank">Open blank page</el-radio>
|
||||
</section>
|
||||
<cur-select
|
||||
description="The language Mark Text use"
|
||||
@ -95,13 +95,16 @@ export default {
|
||||
openFilesInNewWindow: state => state.preferences.openFilesInNewWindow,
|
||||
aidou: state => state.preferences.aidou,
|
||||
fileSortBy: state => state.preferences.fileSortBy,
|
||||
startUp: state => state.preferences.startUp,
|
||||
startUpAction: state => state.preferences.startUpAction,
|
||||
language: state => state.preferences.language
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onSelectChange (type, value) {
|
||||
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
|
||||
},
|
||||
selectDefaultDirectoryToOpen () {
|
||||
this.$store.dispatch('SELECT_DEFAULT_DIRECTORY_TO_OPEN')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,7 +117,7 @@ export default {
|
||||
margin: 0;
|
||||
font-weight: 100;
|
||||
}
|
||||
& .startup-ctrl {
|
||||
& .startup-action-ctrl {
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
margin: 20px 0;
|
||||
|
@ -2,7 +2,7 @@ import { clipboard, ipcRenderer, shell } from 'electron'
|
||||
import path from 'path'
|
||||
import bus from '../bus'
|
||||
import { hasKeys, getUniqueId } from '../util'
|
||||
import { isSameFileSync } from '../util/fileSystem'
|
||||
import { isSamePathSync } from '../util/fileSystem'
|
||||
import listToTree from '../util/listToTree'
|
||||
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
||||
import notice from '../services/notification'
|
||||
@ -39,7 +39,6 @@ const mutations = {
|
||||
const { tabs, currentFile } = state
|
||||
const index = tabs.indexOf(file)
|
||||
tabs.splice(index, 1)
|
||||
state.tabs = tabs
|
||||
if (file.id === currentFile.id) {
|
||||
const fileState = state.tabs[index] || state.tabs[index - 1] || state.tabs[0] || {}
|
||||
state.currentFile = fileState
|
||||
@ -91,37 +90,40 @@ const mutations = {
|
||||
})
|
||||
}
|
||||
|
||||
let fileState = null
|
||||
for (const tab of tabs) {
|
||||
if (tab.pathname === pathname) {
|
||||
const oldId = tab.id
|
||||
Object.assign(tab, newFileState)
|
||||
tab.id = oldId
|
||||
fileState = tab
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileState) {
|
||||
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||
if (!tab) {
|
||||
throw new Error('LOAD_CHANGE: Cannot find tab in tab list.')
|
||||
}
|
||||
|
||||
// Upate file content but not tab id.
|
||||
const oldId = tab.id
|
||||
Object.assign(tab, newFileState)
|
||||
tab.id = oldId
|
||||
|
||||
if (pathname === currentFile.pathname) {
|
||||
state.currentFile = fileState
|
||||
const { id, cursor, history } = fileState
|
||||
state.currentFile = tab
|
||||
const { id, cursor, history } = tab
|
||||
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
|
||||
}
|
||||
},
|
||||
SET_PATHNAME (state, file) {
|
||||
const { filename, pathname, id } = file
|
||||
if (id === state.currentFile.id && pathname) {
|
||||
// NOTE: Please call this function only from main process via "AGANI::set-pathname" and free resources before!
|
||||
SET_PATHNAME (state, { tab, fileInfo }) {
|
||||
const { currentFile } = state
|
||||
const { filename, pathname, id } = fileInfo
|
||||
|
||||
// Change reference path for images.
|
||||
if (id === currentFile.id && pathname) {
|
||||
window.DIRNAME = path.dirname(pathname)
|
||||
}
|
||||
|
||||
const targetFile = state.tabs.filter(f => f.id === id)[0]
|
||||
if (targetFile) {
|
||||
const isSaved = true
|
||||
Object.assign(targetFile, { filename, pathname, isSaved })
|
||||
if (tab) {
|
||||
Object.assign(tab, { filename, pathname, isSaved: true })
|
||||
}
|
||||
},
|
||||
SET_SAVE_STATUS_BY_TAB (state, { tab, status }) {
|
||||
if (hasKeys(tab)) {
|
||||
tab.isSaved = status
|
||||
}
|
||||
},
|
||||
SET_SAVE_STATUS (state, status) {
|
||||
@ -171,25 +173,29 @@ const mutations = {
|
||||
state.currentFile.history = history
|
||||
}
|
||||
},
|
||||
CLOSE_TABS (state, arr) {
|
||||
CLOSE_TABS (state, tabIdList) {
|
||||
if (!tabIdList || tabIdList.length === 0) return
|
||||
|
||||
let tabIndex = 0
|
||||
arr.forEach(id => {
|
||||
tabIdList.forEach(id => {
|
||||
const index = state.tabs.findIndex(f => f.id === id)
|
||||
const { pathname } = state.tabs.find(f => f.id === id)
|
||||
const { pathname } = state.tabs[index]
|
||||
|
||||
// Notify main process to remove the file from the window and free resources.
|
||||
if (pathname) {
|
||||
// close tab and unwatch this file
|
||||
ipcRenderer.send('AGANI::file-watch', { pathname, watch: false })
|
||||
ipcRenderer.send('mt::window-tab-closed', pathname)
|
||||
}
|
||||
|
||||
state.tabs.splice(index, 1)
|
||||
if (state.currentFile.id === id) {
|
||||
state.currentFile = {}
|
||||
window.DIRNAME = ''
|
||||
if (arr.length === 1) {
|
||||
if (tabIdList.length === 1) {
|
||||
tabIndex = index
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!state.currentFile.id && state.tabs.length) {
|
||||
state.currentFile = state.tabs[tabIndex] || state.tabs[tabIndex - 1] || state.tabs[0] || {}
|
||||
if (typeof state.currentFile.markdown === 'string') {
|
||||
@ -261,11 +267,14 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
REMOVE_FILE_IN_TABS ({ commit, dispatch }, file) {
|
||||
FORCE_CLOSE_TAB ({ commit, dispatch }, file) {
|
||||
commit('REMOVE_FILE_WITHIN_TABS', file)
|
||||
// unwatch this file
|
||||
const { pathname } = file
|
||||
dispatch('ASK_FILE_WATCH', { pathname, watch: false })
|
||||
|
||||
// Notify main process to remove the file from the window and free resources.
|
||||
if (pathname) {
|
||||
ipcRenderer.send('mt::window-tab-closed', pathname)
|
||||
}
|
||||
},
|
||||
|
||||
EXCHANGE_TABS_BY_ID ({ commit }, tabIDs) {
|
||||
@ -287,10 +296,12 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_SINGLE_FILE ({ commit, state }, file) {
|
||||
CLOSE_UNSAVED_TAB ({ commit, state }, file) {
|
||||
const { id, pathname, filename, markdown } = file
|
||||
const options = getOptionsFromState(file)
|
||||
ipcRenderer.send('AGANI::save-close', [{ id, pathname, filename, markdown, options }], true)
|
||||
|
||||
// Save the file content via main process and send a close tab response.
|
||||
ipcRenderer.send('mt::save-and-close-tabs', [{ id, pathname, filename, markdown, options }])
|
||||
},
|
||||
|
||||
// need pass some data to main process when `save` menu item clicked
|
||||
@ -315,56 +326,89 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_SET_PATHNAME ({ commit }) {
|
||||
ipcRenderer.on('AGANI::set-pathname', (e, file) => {
|
||||
commit('SET_PATHNAME', file)
|
||||
LISTEN_FOR_SET_PATHNAME ({ commit, dispatch, state }) {
|
||||
ipcRenderer.on('mt::set-pathname', (e, fileInfo) => {
|
||||
const { tabs } = state
|
||||
const { pathname, id } = fileInfo
|
||||
const tab = tabs.find(f => f.id === id)
|
||||
if (!tab) {
|
||||
console.err('[ERROR] Cannot change file path from unknown tab.')
|
||||
return
|
||||
}
|
||||
|
||||
// If a tab with the same file path already exists we need to close the tab.
|
||||
// The existing tab is overwritten by this tab.
|
||||
const existingTab = tabs.find(t => t.id !== id && isSamePathSync(t.pathname, pathname))
|
||||
if (existingTab) {
|
||||
dispatch('CLOSE_TAB', existingTab)
|
||||
}
|
||||
commit('SET_PATHNAME', { tab, fileInfo })
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::tab-saved', (e, tabId) => {
|
||||
const { tabs } = state
|
||||
const tab = tabs.find(f => f.id === tabId)
|
||||
if (tab) {
|
||||
Object.assign(tab, { isSaved: true })
|
||||
}
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::tab-save-failure', (e, tabId, msg) => {
|
||||
notice.notify({
|
||||
title: 'Save failure',
|
||||
message: msg,
|
||||
type: 'error',
|
||||
time: 20000,
|
||||
showConfirm: false
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_CLOSE ({ commit, state }) {
|
||||
LISTEN_FOR_CLOSE ({ state }) {
|
||||
ipcRenderer.on('AGANI::ask-for-close', e => {
|
||||
const unSavedFiles = state.tabs.filter(file => !file.isSaved)
|
||||
const unsavedFiles = state.tabs
|
||||
.filter(file => !file.isSaved)
|
||||
.map(file => {
|
||||
const { id, filename, pathname, markdown } = file
|
||||
const options = getOptionsFromState(file)
|
||||
return { id, filename, pathname, markdown, options }
|
||||
})
|
||||
|
||||
if (unSavedFiles.length) {
|
||||
ipcRenderer.send('AGANI::response-close-confirm', unSavedFiles)
|
||||
if (unsavedFiles.length) {
|
||||
ipcRenderer.send('mt::close-window-confirm', unsavedFiles)
|
||||
} else {
|
||||
ipcRenderer.send('AGANI::close-window')
|
||||
ipcRenderer.send('mt::close-window')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_SAVE_CLOSE ({ commit, state }) {
|
||||
ipcRenderer.on('AGANI::save-all-response', (e, { err, data }) => {
|
||||
if (!err && Array.isArray(data)) {
|
||||
const toBeClosedTabs = [...state.tabs.filter(f => f.isSaved), ...data]
|
||||
commit('CLOSE_TABS', toBeClosedTabs)
|
||||
}
|
||||
})
|
||||
ipcRenderer.on('AGANI::save-single-response', (e, { err, data }) => {
|
||||
if (!err && Array.isArray(data) && data.length) {
|
||||
commit('CLOSE_TABS', data)
|
||||
LISTEN_FOR_SAVE_CLOSE ({ commit }) {
|
||||
ipcRenderer.on('mt::force-close-tabs-by-id', (e, tabIdList) => {
|
||||
if (Array.isArray(tabIdList) && tabIdList.length) {
|
||||
commit('CLOSE_TABS', tabIdList)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
ASK_FOR_SAVE_ALL ({ commit, state }, isClose) {
|
||||
const unSavedFiles = state.tabs.filter(file => !(file.isSaved && /[^\n]/.test(file.markdown)))
|
||||
ASK_FOR_SAVE_ALL ({ commit, state }, closeTabs) {
|
||||
const { tabs } = state
|
||||
const unsavedFiles = tabs
|
||||
.filter(file => !(file.isSaved && /[^\n]/.test(file.markdown)))
|
||||
.map(file => {
|
||||
const { id, filename, pathname, markdown } = file
|
||||
const options = getOptionsFromState(file)
|
||||
return { id, filename, pathname, markdown, options }
|
||||
})
|
||||
if (unSavedFiles.length) {
|
||||
const EVENT_NAME = isClose ? 'AGANI::save-close' : 'AGANI::save-all'
|
||||
const isSingle = false
|
||||
ipcRenderer.send(EVENT_NAME, unSavedFiles, isSingle)
|
||||
} else if (isClose) {
|
||||
commit('CLOSE_TABS', state.tabs.map(f => f.id))
|
||||
|
||||
if (closeTabs) {
|
||||
if (unsavedFiles.length) {
|
||||
commit('CLOSE_TABS', tabs.filter(f => f.isSaved).map(f => f.id))
|
||||
ipcRenderer.send('mt::save-and-close-tabs', unsavedFiles)
|
||||
} else {
|
||||
commit('CLOSE_TABS', tabs.map(f => f.id))
|
||||
}
|
||||
} else {
|
||||
ipcRenderer.send('mt::save-tabs', unsavedFiles)
|
||||
}
|
||||
},
|
||||
|
||||
@ -406,7 +450,7 @@ const actions = {
|
||||
const { id, pathname, filename } = state.currentFile
|
||||
if (typeof filename === 'string' && filename !== newFilename) {
|
||||
const newPathname = path.join(path.dirname(pathname), newFilename)
|
||||
ipcRenderer.send('AGANI::rename', { id, pathname, newPathname })
|
||||
ipcRenderer.send('mt::rename', { id, pathname, newPathname })
|
||||
}
|
||||
},
|
||||
|
||||
@ -420,56 +464,57 @@ const actions = {
|
||||
|
||||
// This events are only used during window creation.
|
||||
LISTEN_FOR_BOOTSTRAP_WINDOW ({ commit, state, dispatch }) {
|
||||
ipcRenderer.on('mt::bootstrap-window', (e, { markdown, filename, pathname, options }) => {
|
||||
const fileState = getSingleFileState({ markdown, filename, pathname, options })
|
||||
const { id } = fileState
|
||||
const { lineEnding } = options
|
||||
ipcRenderer.on('mt::bootstrap-editor', (e, config) => {
|
||||
const {
|
||||
addBlankTab,
|
||||
markdownList,
|
||||
lineEnding,
|
||||
sideBarVisibility,
|
||||
tabBarVisibility,
|
||||
sourceCodeModeEnabled
|
||||
} = config
|
||||
|
||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
||||
dispatch('INIT_STATUS', true)
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
dispatch('SEND_INITIALIZED')
|
||||
commit('SET_LAYOUT', {
|
||||
rightColumn: 'files',
|
||||
showSideBar: false,
|
||||
showTabBar: false
|
||||
showSideBar: !!sideBarVisibility,
|
||||
showTabBar: !!tabBarVisibility
|
||||
})
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
|
||||
commit('SET_MODE', {
|
||||
type: 'sourceCode',
|
||||
checked: !!sourceCodeModeEnabled
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::bootstrap-blank-window', (e, { lineEnding, markdown: source }) => {
|
||||
const { tabs } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding, source)
|
||||
const { id, markdown } = fileState
|
||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
||||
dispatch('INIT_STATUS', true)
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
commit('SET_LAYOUT', {
|
||||
rightColumn: 'files',
|
||||
showSideBar: false,
|
||||
showTabBar: false
|
||||
})
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
if (addBlankTab) {
|
||||
dispatch('NEW_UNTITLED_TAB', {})
|
||||
} else if (markdownList.length) {
|
||||
let isFirst = true
|
||||
for (const markdown of markdownList) {
|
||||
isFirst = false
|
||||
dispatch('NEW_UNTITLED_TAB', { markdown, selected: isFirst })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Open a new tab, optionally with content.
|
||||
LISTEN_FOR_NEW_TAB ({ dispatch }) {
|
||||
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument, selectTab=true) => {
|
||||
// TODO: allow to add a tab without selecting it
|
||||
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument, selected=true) => {
|
||||
if (markdownDocument) {
|
||||
// Create tab with content.
|
||||
dispatch('NEW_TAB_WITH_CONTENT', markdownDocument)
|
||||
dispatch('NEW_TAB_WITH_CONTENT', { markdownDocument, selected })
|
||||
} else {
|
||||
// Fallback: create a blank tab
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
// Fallback: create a blank tab and always select it
|
||||
dispatch('NEW_UNTITLED_TAB', {})
|
||||
}
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::new-untitled-tab', (e, selectTab=true, markdownString='', ) => {
|
||||
// TODO: allow to add a tab without selecting it
|
||||
ipcRenderer.on('mt::new-untitled-tab', (e, selected=true, markdown='', ) => {
|
||||
// Create a blank tab
|
||||
dispatch('NEW_UNTITLED_TAB', markdownString)
|
||||
dispatch('NEW_UNTITLED_TAB', { markdown, selected })
|
||||
})
|
||||
},
|
||||
|
||||
@ -477,56 +522,100 @@ const actions = {
|
||||
ipcRenderer.on('AGANI::close-tab', e => {
|
||||
const file = state.currentFile
|
||||
if (!hasKeys(file)) return
|
||||
const { isSaved } = file
|
||||
if (isSaved) {
|
||||
dispatch('REMOVE_FILE_IN_TABS', file)
|
||||
} else {
|
||||
dispatch('CLOSE_SINGLE_FILE', file)
|
||||
}
|
||||
dispatch('CLOSE_TAB', file)
|
||||
})
|
||||
},
|
||||
|
||||
// Create a new untitled tab optional with markdown string.
|
||||
NEW_UNTITLED_TAB ({ commit, state, dispatch }, markdownString) {
|
||||
dispatch('SHOW_TAB_VIEW', false)
|
||||
const { tabs, lineEnding } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding, markdownString)
|
||||
const { id, markdown } = fileState
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
CLOSE_TAB ({ dispatch }, file) {
|
||||
const { isSaved } = file
|
||||
if (isSaved) {
|
||||
dispatch('FORCE_CLOSE_TAB', file)
|
||||
} else {
|
||||
dispatch('CLOSE_UNSAVED_TAB', file)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new tab from the given markdown document
|
||||
* Create a new untitled tab optional from a markdown string.
|
||||
*
|
||||
* @param {*} context Store context
|
||||
* @param {IMarkdownDocumentRaw} markdownDocument Class that represent a markdown document
|
||||
* @param {*} context The store context.
|
||||
* @param {{markdown?: string, selected?: boolean}} obj Optional markdown string
|
||||
* and whether the tab should become the selected tab (true if not set).
|
||||
*/
|
||||
NEW_TAB_WITH_CONTENT ({ commit, state, dispatch }, markdownDocument) {
|
||||
NEW_UNTITLED_TAB ({ commit, state, dispatch }, { markdown: markdownString, selected }) {
|
||||
// If not set select the tab.
|
||||
if (selected == null) {
|
||||
selected = true
|
||||
}
|
||||
|
||||
dispatch('SHOW_TAB_VIEW', false)
|
||||
const { tabs, lineEnding } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding, markdownString)
|
||||
|
||||
if (selected) {
|
||||
const { id, markdown } = fileState
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
} else {
|
||||
commit('ADD_FILE_TO_TABS', fileState)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new tab from the given markdown document.
|
||||
*
|
||||
* @param {*} context The store context.
|
||||
* @param {{markdownDocument: IMarkdownDocumentRaw, selected?: boolean}} obj The markdown document
|
||||
* and optional whether the tab should become the selected tab (true if not set).
|
||||
*/
|
||||
NEW_TAB_WITH_CONTENT ({ commit, state, dispatch }, { markdownDocument, selected }) {
|
||||
if (!markdownDocument) {
|
||||
console.warn('Cannot create a file tab without a markdown document!')
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
dispatch('NEW_UNTITLED_TAB', {})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if tab already exist and select existing tab if so.
|
||||
const { tabs } = state
|
||||
// Select the tab if not value is specified.
|
||||
if (selected == null) {
|
||||
selected = true
|
||||
}
|
||||
|
||||
// Check if tab already exist and always select existing tab if so.
|
||||
const { currentFile, tabs } = state
|
||||
const { pathname } = markdownDocument
|
||||
const existingTab = tabs.find(t => isSameFileSync(t.pathname, pathname))
|
||||
const existingTab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||
if (existingTab) {
|
||||
dispatch('UPDATE_CURRENT_FILE', existingTab)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace/close selected untitled empty tab
|
||||
let keepTabBarState = false
|
||||
if (currentFile) {
|
||||
const { isSaved, pathname } = currentFile
|
||||
if (isSaved && !pathname) {
|
||||
keepTabBarState = true
|
||||
dispatch('FORCE_CLOSE_TAB', currentFile)
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepTabBarState) {
|
||||
dispatch('SHOW_TAB_VIEW', false)
|
||||
}
|
||||
|
||||
const { markdown, isMixedLineEndings } = markdownDocument
|
||||
const docState = createDocumentState(markdownDocument)
|
||||
const { id } = docState
|
||||
|
||||
if (selected) {
|
||||
dispatch('UPDATE_CURRENT_FILE', docState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
} else {
|
||||
commit('ADD_FILE_TO_TABS', docState)
|
||||
}
|
||||
|
||||
if (isMixedLineEndings) {
|
||||
// TODO: Show (this) notification(s) per tab.
|
||||
const { filename, lineEnding } = markdownDocument
|
||||
notice.notify({
|
||||
title: 'Line Ending',
|
||||
@ -540,7 +629,7 @@ const actions = {
|
||||
|
||||
SHOW_TAB_VIEW ({ commit, state, dispatch }, always) {
|
||||
const { tabs } = state
|
||||
if (always || tabs.length <= 1) {
|
||||
if (always || tabs.length === 1) {
|
||||
commit('SET_LAYOUT', { showTabBar: true })
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
}
|
||||
@ -676,21 +765,32 @@ const actions = {
|
||||
|
||||
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
||||
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
||||
// TODO: Set `isSaved` to false.
|
||||
// TODO: A new "changed" notification from different files overwrite the old notification - the old notification disappears.
|
||||
if (type === 'unlink') {
|
||||
return notice.notify({
|
||||
|
||||
// TODO: A new "changed" notification from different files overwrite the old notification
|
||||
// and the old notification disappears. I think we should bind the notification to the tab.
|
||||
|
||||
const { tabs } = state
|
||||
const { pathname } = change
|
||||
const tab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||
if (tab) {
|
||||
commit('SET_SAVE_STATUS_BY_TAB', { tab, status: false })
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'unlink': {
|
||||
notice.notify({
|
||||
title: 'File Removed on Disk',
|
||||
message: `${change.pathname} has been removed or moved to other place`,
|
||||
message: `${pathname} has been removed or moved.`,
|
||||
type: 'warning',
|
||||
time: 0,
|
||||
showConfirm: false
|
||||
})
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case 'add':
|
||||
case 'change': {
|
||||
const { autoSave } = rootState.preferences
|
||||
const { windowActive } = rootState
|
||||
const { filename } = change.data
|
||||
if (windowActive) return
|
||||
if (autoSave) {
|
||||
commit('LOAD_CHANGE', change)
|
||||
} else {
|
||||
@ -705,14 +805,14 @@ const actions = {
|
||||
commit('LOAD_CHANGE', change)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error(`LISTEN_FOR_FILE_CHANGE: Invalid type "${type}"`)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
ASK_FILE_WATCH ({ commit }, { pathname, watch }) {
|
||||
ipcRenderer.send('AGANI::file-watch', { pathname, watch })
|
||||
},
|
||||
|
||||
ASK_FOR_IMAGE_PATH ({ commit }) {
|
||||
return ipcRenderer.sendSync('mt::ask-for-image-path')
|
||||
}
|
||||
|
@ -18,9 +18,8 @@ Vue.use(Vuex)
|
||||
const state = {
|
||||
platform: process.platform, // platform of system `darwin` | `win32` | `linux`
|
||||
appVersion: process.versions.MARKTEXT_VERSION_STRING, // Mark Text version string
|
||||
windowActive: true, // weather current window is active or focused
|
||||
// TODO: "init" is nowhere used
|
||||
init: process.env.NODE_ENV === 'development' // whether Mark Text is inited
|
||||
windowActive: true, // whether current window is active or focused
|
||||
init: false, // whether Mark Text is initialized
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
@ -29,8 +28,8 @@ const mutations = {
|
||||
SET_WIN_STATUS (state, status) {
|
||||
state.windowActive = status
|
||||
},
|
||||
SET_INIT_STATUS (state, status) {
|
||||
state.init = status
|
||||
SET_INITIALIZED (state) {
|
||||
state.init = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,8 +39,9 @@ const actions = {
|
||||
commit('SET_WIN_STATUS', status)
|
||||
})
|
||||
},
|
||||
INIT_STATUS ({ commit }, status) {
|
||||
commit('SET_INIT_STATUS', status)
|
||||
|
||||
SEND_INITIALIZED ({ commit }) {
|
||||
commit('SET_INITIALIZED')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,7 @@ const state = {}
|
||||
|
||||
const getters = {}
|
||||
|
||||
const mutations = {
|
||||
}
|
||||
const mutations = {}
|
||||
|
||||
const actions = {
|
||||
LISTEN_FOR_NOTIFICATION ({ commit }) {
|
||||
|
@ -3,13 +3,15 @@ import { getOptionsFromState } from './help'
|
||||
|
||||
// user preference
|
||||
const state = {
|
||||
autoSave: true,
|
||||
autoSaveDelay: 3000,
|
||||
titleBarStyle: 'csd',
|
||||
autoSave: false,
|
||||
autoSaveDelay: 5000,
|
||||
titleBarStyle: 'custom',
|
||||
openFilesInNewWindow: false,
|
||||
openFolderInNewWindow: false,
|
||||
aidou: true,
|
||||
fileSortBy: 'created',
|
||||
startUp: 'folder',
|
||||
startUpAction: 'lastState',
|
||||
defaultDirectoryToOpen: '',
|
||||
language: 'en',
|
||||
|
||||
editorFontFamily: 'Open Sans',
|
||||
@ -33,7 +35,13 @@ const state = {
|
||||
listIndentation: 1,
|
||||
|
||||
theme: 'light',
|
||||
// edit modes (they are not in preference.md, but still put them here)
|
||||
|
||||
// Default values that are overwritten with the entries below.
|
||||
sideBarVisibility: false,
|
||||
tabBarVisibility: false,
|
||||
sourceCodeModeEnabled: false,
|
||||
|
||||
// Edit modes of the current window (not part of persistent settings)
|
||||
typewriter: false, // typewriter mode
|
||||
focus: false, // focus mode
|
||||
sourceCode: false, // source code mode
|
||||
@ -101,7 +109,6 @@ const actions = {
|
||||
},
|
||||
|
||||
SET_SINGLE_PREFERENCE ({ commit }, { type, value }) {
|
||||
// commit('SET_USER_PREFERENCE', { [type]: value })
|
||||
// save to electron-store
|
||||
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
||||
},
|
||||
@ -112,6 +119,10 @@ const actions = {
|
||||
|
||||
SET_IMAGE_FOLDER_PATH ({ commit }) {
|
||||
ipcRenderer.send('mt::ask-for-modify-image-folder-path')
|
||||
},
|
||||
|
||||
SELECT_DEFAULT_DIRECTORY_TO_OPEN ({ commit }) {
|
||||
ipcRenderer.send('mt::select-default-directory-to-open')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ const getters = {
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_PROJECT_TREE (state, pathname) {
|
||||
SET_ROOT_DIRECTORY (state, pathname) {
|
||||
let name = path.basename(pathname)
|
||||
if (!name) {
|
||||
// Root directory such "/" or "C:\"
|
||||
@ -108,12 +108,8 @@ const mutations = {
|
||||
|
||||
const actions = {
|
||||
LISTEN_FOR_LOAD_PROJECT ({ commit, dispatch }) {
|
||||
ipcRenderer.on('AGANI::open-project', (e, pathname) => {
|
||||
// Initialize editor and show empty/new tab
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
|
||||
dispatch('INIT_STATUS', true)
|
||||
commit('SET_PROJECT_TREE', pathname)
|
||||
ipcRenderer.on('mt::open-directory', (e, pathname) => {
|
||||
commit('SET_ROOT_DIRECTORY', pathname)
|
||||
commit('SET_LAYOUT', {
|
||||
rightColumn: 'files',
|
||||
showSideBar: true,
|
||||
@ -163,7 +159,7 @@ const actions = {
|
||||
commit('SET_CLIPBOARD', data)
|
||||
},
|
||||
ASK_FOR_OPEN_PROJECT ({ commit }) {
|
||||
ipcRenderer.send('AGANI::ask-for-open-project-in-sidebar')
|
||||
ipcRenderer.send('mt::ask-for-open-project-in-sidebar')
|
||||
},
|
||||
LISTEN_FOR_SIDEBAR_CONTEXT_MENU ({ commit, state }) {
|
||||
bus.$on('SIDEBAR::show-in-folder', () => {
|
||||
|
@ -25,11 +25,11 @@ export const rename = (src, dest) => {
|
||||
/**
|
||||
* Check if the both paths point to the same file.
|
||||
*
|
||||
* @param {*} pathA The first path.
|
||||
* @param {*} pathB The second path.
|
||||
* @param {*} isNormalized Are both paths already normalized.
|
||||
* @param {string} pathA The first path.
|
||||
* @param {string} pathB The second path.
|
||||
* @param {boolean} [isNormalized] Are both paths already normalized.
|
||||
*/
|
||||
export const isSameFileSync = (pathA, pathB, isNormalized=false) => {
|
||||
export const isSamePathSync = (pathA, pathB, isNormalized = false) => {
|
||||
if (!pathA || !pathB) return false
|
||||
const a = isNormalized ? pathA : path.normalize(pathA)
|
||||
const b = isNormalized ? pathB : path.normalize(pathB)
|
||||
|
@ -3,9 +3,11 @@
|
||||
"autoSaveDelay": 5000,
|
||||
"titleBarStyle": "custom",
|
||||
"openFilesInNewWindow": false,
|
||||
"openFolderInNewWindow": false,
|
||||
"aidou": true,
|
||||
"fileSortBy": "created",
|
||||
"startUp": "folder",
|
||||
"startUpAction": "lastState",
|
||||
"defaultDirectoryToOpen": "",
|
||||
"language": "en",
|
||||
|
||||
"editorFontFamily": "Open Sans",
|
||||
@ -28,5 +30,9 @@
|
||||
"tabSize": 4,
|
||||
"listIndentation": 1,
|
||||
|
||||
"theme": "light"
|
||||
"theme": "light",
|
||||
|
||||
"sideBarVisibility": false,
|
||||
"tabBarVisibility": false,
|
||||
"sourceCodeModeEnabled": false
|
||||
}
|
||||
|
40
yarn.lock
40
yarn.lock
@ -3993,14 +3993,6 @@ electron-chromedriver@~3.0.0:
|
||||
electron-download "^4.1.0"
|
||||
extract-zip "^1.6.5"
|
||||
|
||||
electron-debug@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-3.0.0.tgz#55b7895df7f371558d0595d14dc2f8a738a41c81"
|
||||
integrity sha512-rLrnn7L2soeIqwB6FIzn4+pj6RwT66XhUxadcrS3okjB3ezAv8LsolqrFrO2UrTrvchZgTCcEapV4J0UxlYWIw==
|
||||
dependencies:
|
||||
electron-is-dev "^1.1.0"
|
||||
electron-localshortcut "^3.1.0"
|
||||
|
||||
electron-devtools-installer@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-2.2.4.tgz#261a50337e37121d338b966f07922eb4939a8763"
|
||||
@ -4031,21 +4023,6 @@ electron-is-accelerator@^0.1.0, electron-is-accelerator@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
|
||||
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
|
||||
|
||||
electron-is-dev@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.1.0.tgz#b15a2a600bdc48a51a857d460e05f15b19a2522c"
|
||||
integrity sha512-Z1qA/1oHNowGtSBIcWk0pcLEqYT/j+13xUw/MYOrBUOL4X7VN0i0KCTf5SqyvMPmW5pSPKbo28wkxMxzZ20YnQ==
|
||||
|
||||
electron-localshortcut@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz#10c1ffd537b8d39170aaf6e1551341f7780dd2ce"
|
||||
integrity sha512-MgL/j5jdjW7iA0R6cI7S045B0GlKXWM1FjjujVPjlrmyXRa6yH0bGSaIAfxXAF9tpJm3pLEiQzerYHkRh9JG/A==
|
||||
dependencies:
|
||||
debug "^2.6.8"
|
||||
electron-is-accelerator "^0.1.0"
|
||||
keyboardevent-from-electron-accelerator "^1.1.0"
|
||||
keyboardevents-areequal "^0.2.1"
|
||||
|
||||
electron-log@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-3.0.5.tgz#9bdd307f1f1aec85c0873babd6bdfffb1a661436"
|
||||
@ -4622,11 +4599,6 @@ eve-raphael@0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
|
||||
integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=
|
||||
|
||||
eve@~0.5.1:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/eve/-/eve-0.5.4.tgz#67d080b9725291d7e389e34c26860dd97f1debaa"
|
||||
integrity sha1-Z9CAuXJSkdfjieNMJoYN2X8d66o=
|
||||
|
||||
event-kit@^2.0.0:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/event-kit/-/event-kit-2.5.3.tgz#d47e4bc116ec0aacd00263791fa1a55eb5e79ba1"
|
||||
@ -6686,11 +6658,6 @@ keyboard-layout@^2.0.15:
|
||||
event-kit "^2.0.0"
|
||||
nan "^2.10.0"
|
||||
|
||||
keyboardevent-from-electron-accelerator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.0.tgz#324614f6e33490c37ffc5be5876b3e85fe223c84"
|
||||
integrity sha512-VDC4vKWGrR3VgIKCE4CsXnvObGgP8C2idnTKEMUkuEuvDGE1GEBX9FtNdJzrD00iQlhI3xFxRaeItsUmlERVng==
|
||||
|
||||
keyboardevents-areequal@^0.2.1:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
||||
@ -10030,13 +9997,6 @@ snapdragon@^0.8.1:
|
||||
source-map-resolve "^0.5.0"
|
||||
use "^3.1.0"
|
||||
|
||||
snapsvg@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/snapsvg/-/snapsvg-0.5.1.tgz#0caf52c79189a290746fc446cc5e863f6bdddfe3"
|
||||
integrity sha1-DK9Sx5GJopB0b8RGzF6GP2vd3+M=
|
||||
dependencies:
|
||||
eve "~0.5.1"
|
||||
|
||||
socket.io-adapter@~1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
|
||||
|
Loading…
Reference in New Issue
Block a user