mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 06:30:15 +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(_) {
|
} catch(_) {
|
||||||
// Ignore error if we build without git.
|
// 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 {
|
return {
|
||||||
'global.MARKTEXT_GIT_SHORT_HASH': JSON.stringify(shortHash),
|
'global.MARKTEXT_GIT_SHORT_HASH': JSON.stringify(shortHash),
|
||||||
'global.MARKTEXT_GIT_HASH': JSON.stringify(fullHash),
|
'global.MARKTEXT_GIT_HASH': JSON.stringify(fullHash),
|
||||||
|
|
||||||
'global.MARKTEXT_VERSION': JSON.stringify(version),
|
'global.MARKTEXT_VERSION': JSON.stringify(version),
|
||||||
'global.MARKTEXT_VERSION_STRING': JSON.stringify(`v${version}${versionSuffix}`),
|
'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.
|
- `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.
|
- 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**
|
**:cactus:Feature**
|
||||||
|
|
||||||
|
- GUI settings (#1028)
|
||||||
- The cursor jump to the end of format or to the next brackets when press `tab`(#976)
|
- The cursor jump to the end of format or to the next brackets when press `tab`(#976)
|
||||||
- Tab drag & drop inside the window
|
- Tab drag & drop inside the window
|
||||||
- Scrollable tabs
|
- 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**
|
**:butterfly:Optimization**
|
||||||
|
|
||||||
- Rewrite `select all` when press `CtrlOrCmd + A` (#937)
|
- 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)
|
- Set the cursor at the end of `#` in header when press arrow down to jump to the next paragraph.(#978)
|
||||||
- Improved startup time
|
- Improved startup time
|
||||||
|
- Replace empty untitled tabs (#830)
|
||||||
|
- Editor window is shown immediately while loading
|
||||||
|
|
||||||
**:beetle:Bug fix**
|
**:beetle:Bug fix**
|
||||||
|
|
||||||
@ -23,6 +33,14 @@
|
|||||||
- Fixed some bugs after press `backspace` (#934, #938)
|
- Fixed some bugs after press `backspace` (#934, #938)
|
||||||
- Change `inline math` vertical align to `top` (#977)
|
- Change `inline math` vertical align to `top` (#977)
|
||||||
- Prevent to open the same file twice, instead select the existing tab (#878)
|
- 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
|
### 0.14.0
|
||||||
|
|
||||||
|
@ -7,11 +7,11 @@ matrix:
|
|||||||
include:
|
include:
|
||||||
- os: osx
|
- os: osx
|
||||||
osx_image: xcode9.2
|
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
|
compiler: clang
|
||||||
- os: linux
|
- os: linux
|
||||||
dist: trusty
|
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
|
compiler: clang
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
|
@ -14,7 +14,7 @@ branches:
|
|||||||
skip_tags: true
|
skip_tags: true
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
MARKTEXT_IS_OFFICIAL_RELEASE: 1
|
MARKTEXT_IS_STABLE: 1
|
||||||
MARKTEXT_EXIT_ON_ERROR: 1
|
MARKTEXT_EXIT_ON_ERROR: 1
|
||||||
GH_TOKEN:
|
GH_TOKEN:
|
||||||
secure: Ki5AJWygDYhzMJxl0b0rDx3bhAYmar2aPdwVHiai9IigqsvZpWHLeI3qpTiiaOWL
|
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
|
# Command Line Interface
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage: marktext [commands] [path]
|
Usage: marktext [commands] [path ...]
|
||||||
|
|
||||||
Available commands:
|
Available commands:
|
||||||
|
|
||||||
--debug Enable debug mode
|
--debug Enable debug mode
|
||||||
--safe Disable plugins and other user configuration
|
--safe Disable plugins and other user configuration
|
||||||
--dump-keyboard-layout Dump keyboard information
|
--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
|
--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
|
```sh
|
||||||
alias marktext="/Applications/Mark\ Text.app/Contents/MacOS/Mark\ Text"
|
alias marktext="/Applications/Mark\ Text.app/Contents/MacOS/Mark\ Text"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -33,9 +33,26 @@ Here is an example:
|
|||||||
**Mark Text menu (macOS only):**
|
**Mark Text menu (macOS only):**
|
||||||
|
|
||||||
| Id | Description |
|
| Id | Description |
|
||||||
| -------------- | --------------------------------------- |
|
| ----------------- | --------------------------------------- |
|
||||||
| `mtHide` | Hide Mark Text |
|
| `mtHide` | Hide Mark Text |
|
||||||
| `mtHideOthers` | Hide all other windows except 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:**
|
**Edit menu:**
|
||||||
|
|
||||||
@ -100,16 +117,14 @@ Here is an example:
|
|||||||
**Window menu:**
|
**Window menu:**
|
||||||
|
|
||||||
| Id | Description |
|
| Id | Description |
|
||||||
| ------------------- | ------------------- |
|
| ------------------------ | ---------------------- |
|
||||||
| `windowMinimize` | Minimize the window |
|
| `windowMinimize` | Minimize the window |
|
||||||
| `windowCloseWindow` | Close the window |
|
| `windowToggleFullScreen` | Toggle fullscreen mode |
|
||||||
|
|
||||||
**View menu:**
|
**View menu:**
|
||||||
|
|
||||||
| Id | Description |
|
| Id | Description |
|
||||||
| ----------------------------- | ---------------------------------------- |
|
| ----------------------------- | ---------------------------------------- |
|
||||||
| `viewToggleFullScreen` | Toggle fullscreen mode |
|
|
||||||
| `viewChangeFont` | Open font dialog |
|
|
||||||
| `viewSourceCodeMode` | Switch to source code mode |
|
| `viewSourceCodeMode` | Switch to source code mode |
|
||||||
| `viewTypewriterMode` | Enable typewriter mode |
|
| `viewTypewriterMode` | Enable typewriter mode |
|
||||||
| `viewFocusMode` | Enable focus mode |
|
| `viewFocusMode` | Enable focus mode |
|
||||||
@ -117,4 +132,3 @@ Here is an example:
|
|||||||
| `viewToggleTabBar` | Toggle tabbar |
|
| `viewToggleTabBar` | Toggle tabbar |
|
||||||
| `viewDevToggleDeveloperTools` | Toggle developer tools (debug mode only) |
|
| `viewDevToggleDeveloperTools` | Toggle developer tools (debug mode only) |
|
||||||
| `viewDevReload` | Reload window (debug mode only) |
|
| `viewDevReload` | Reload window (debug mode only) |
|
||||||
|
|
||||||
|
@ -3,14 +3,16 @@
|
|||||||
#### General
|
#### General
|
||||||
|
|
||||||
| Key | Type | Default Value | Description |
|
| Key | Type | Default Value | Description |
|
||||||
| -------------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| autoSave | Boolean | ture | Automatically save the content being edited. option value: true, false |
|
| autoSave | Boolean | false | 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 |
|
| autoSaveDelay | Number | 5000 | 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` |
|
| titleBarStyle | String | custom | The title bar style on Linux and Window: `custom` or `native` |
|
||||||
| openFilesInNewWindow | Boolean | false | true, false |
|
| openFilesInNewWindow | Boolean | false | true, false |
|
||||||
|
| openFolderInNewWindow | Boolean | false | true, false |
|
||||||
| aidou | Boolean | true | Enable aidou. Optional value: 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. |
|
| fileSortBy | String | created | 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` |
|
| 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. |
|
| language | String | en | The language Mark Text use. |
|
||||||
|
|
||||||
#### Editor
|
#### Editor
|
||||||
@ -33,7 +35,7 @@
|
|||||||
#### Markdown
|
#### Markdown
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| preferLooseListItem | Boolean | true | The preferred list type. |
|
| preferLooseListItem | Boolean | true | The preferred list type. |
|
||||||
| bulletListMarker | String | `-` | The preferred marker used in bullet list, optional value: `-`, `*` `+` |
|
| bulletListMarker | String | `-` | The preferred marker used in bullet list, optional value: `-`, `*` `+` |
|
||||||
| orderListDelimiter | String | `.` | The preferred delimiter used in order 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 |
|
| 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 |
|
| 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
|
#### Theme
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| 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 a release candidate
|
||||||
- Create branch `release-v%version%`
|
- 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
|
- Ensure [changelog](https://github.com/marktext/marktext/blob/master/.github/CHANGELOG.md) is up-to-date
|
||||||
- Bump version in `package.json` and changelog
|
- Bump version in `package.json` and changelog
|
||||||
- Update all `README.md` files
|
- Update all `README.md` files
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"release:mac": "node .electron-vue/build.js && electron-builder build --mac --publish always",
|
"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",
|
"release:win": "node .electron-vue/build.js && electron-builder build --win --publish always",
|
||||||
"build": "node .electron-vue/build.js && electron-builder",
|
"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:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
|
||||||
"build:dev": "node .electron-vue/build.js",
|
"build:dev": "node .electron-vue/build.js",
|
||||||
"dev": "node .electron-vue/dev-runner.js",
|
"dev": "node .electron-vue/dev-runner.js",
|
||||||
@ -189,7 +189,6 @@
|
|||||||
"prismjs": "^1.16.0",
|
"prismjs": "^1.16.0",
|
||||||
"snabbdom": "^0.7.3",
|
"snabbdom": "^0.7.3",
|
||||||
"snabbdom-to-html": "^5.1.1",
|
"snabbdom-to-html": "^5.1.1",
|
||||||
"snapsvg": "^0.5.1",
|
|
||||||
"source-map-support": "^0.5.12",
|
"source-map-support": "^0.5.12",
|
||||||
"turndown": "^5.0.3",
|
"turndown": "^5.0.3",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
@ -226,7 +225,6 @@
|
|||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"electron": "^5.0.1",
|
"electron": "^5.0.1",
|
||||||
"electron-builder": "^20.40.2",
|
"electron-builder": "^20.40.2",
|
||||||
"electron-debug": "^3.0.0",
|
|
||||||
"electron-devtools-installer": "^2.2.4",
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"electron-rebuild": "^1.8.4",
|
"electron-rebuild": "^1.8.4",
|
||||||
"electron-updater": "^4.0.6",
|
"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;
|
MimeType=text/markdown;
|
||||||
Keywords=marktext;
|
Keywords=marktext;
|
||||||
StartupWMClass=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>
|
<title>Mark Text</title>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
background: #ffffff;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -18,7 +17,12 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<div id="app"></div>
|
||||||
|
|
||||||
<!-- Set `__static` path to static files in production -->
|
<!-- Set `__static` path to static files in production -->
|
||||||
<script>
|
<script>
|
||||||
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fse from 'fs-extra'
|
import fse from 'fs-extra'
|
||||||
import log from 'electron-log'
|
|
||||||
import { exec } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import { app, ipcMain, systemPreferences, clipboard } from 'electron'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import log from 'electron-log'
|
||||||
|
import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron'
|
||||||
import { isLinux, isOsx } from '../config'
|
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 { getMenuItemById } from '../menu'
|
||||||
import { selectTheme } from '../menu/actions/theme'
|
import { selectTheme } from '../menu/actions/theme'
|
||||||
import { dockMenu } from '../menu/templates'
|
import { dockMenu } from '../menu/templates'
|
||||||
import { watchers } from '../utils/imagePathAutoComplement'
|
import { watchers } from '../utils/imagePathAutoComplement'
|
||||||
|
import { WindowType } from '../windows/base'
|
||||||
import EditorWindow from '../windows/editor'
|
import EditorWindow from '../windows/editor'
|
||||||
import SettingWindow from '../windows/setting'
|
import SettingWindow from '../windows/setting'
|
||||||
import { WindowType } from './windowManager'
|
|
||||||
// import ShortcutCapture from 'shortcut-capture'
|
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
|
||||||
@ -42,6 +43,39 @@ class App {
|
|||||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features', 'true')
|
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('open-file', this.openFile) // macOS only
|
||||||
|
|
||||||
app.on('ready', this.ready)
|
app.on('ready', this.ready)
|
||||||
@ -89,7 +123,9 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ready = () => {
|
ready = () => {
|
||||||
const { _args: args } = this
|
const { _args: args, _openFilesCache } = this
|
||||||
|
const { preferences } = this._accessor
|
||||||
|
|
||||||
if (!isOsx && args._.length) {
|
if (!isOsx && args._.length) {
|
||||||
for (const pathname of args._) {
|
for (const pathname of args._) {
|
||||||
// Ignore all unknown flags
|
// Ignore all unknown flags
|
||||||
@ -97,13 +133,21 @@ class App {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = this.normalizePath(pathname)
|
const info = normalizeMarkdownPath(pathname)
|
||||||
if (info) {
|
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') {
|
if (process.platform === 'darwin') {
|
||||||
app.dock.setMenu(dockMenu)
|
app.dock.setMenu(dockMenu)
|
||||||
|
|
||||||
@ -135,11 +179,12 @@ class App {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._openFilesCache.length) {
|
if (_openFilesCache.length) {
|
||||||
this.openFileCache()
|
this._openFilesToOpen()
|
||||||
} else {
|
} else {
|
||||||
this.createEditorWindow()
|
this._createEditorWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.shortcutCapture = new ShortcutCapture()
|
// this.shortcutCapture = new ShortcutCapture()
|
||||||
// if (process.env.NODE_ENV === 'development') {
|
// if (process.env.NODE_ENV === 'development') {
|
||||||
// this.shortcutCapture.dirname = path.resolve(path.join(__dirname, '../../../node_modules/shortcut-capture'))
|
// this.shortcutCapture.dirname = path.resolve(path.join(__dirname, '../../../node_modules/shortcut-capture'))
|
||||||
@ -165,7 +210,7 @@ class App {
|
|||||||
|
|
||||||
openFile = (event, pathname) => {
|
openFile = (event, pathname) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const info = this.normalizePath(pathname)
|
const info = normalizeMarkdownPath(pathname)
|
||||||
if (info) {
|
if (info) {
|
||||||
this._openFilesCache.push(info)
|
this._openFilesCache.push(info)
|
||||||
|
|
||||||
@ -176,53 +221,37 @@ class App {
|
|||||||
}
|
}
|
||||||
this._openFilesTimer = setTimeout(() => {
|
this._openFilesTimer = setTimeout(() => {
|
||||||
this._openFilesTimer = null
|
this._openFilesTimer = null
|
||||||
this.openFileCache()
|
this._openFilesToOpen()
|
||||||
}, 100)
|
}, 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 --------------------------------
|
// --- private --------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new editor window.
|
* Creates a new editor window.
|
||||||
*
|
*
|
||||||
* @param {string} [pathname] Path to a file, directory or link.
|
* @param {string} [rootDirectory] The root directory to open.
|
||||||
* @param {string} [markdown] Markdown content.
|
* @param {string[]} [fileList] A list of markdown files to open.
|
||||||
* @param {*} [options] BrowserWindow options.
|
* @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)
|
const editor = new EditorWindow(this._accessor)
|
||||||
editor.createWindow(pathname, markdown, options)
|
editor.createWindow(rootDirectory, fileList, markdownList, options)
|
||||||
this._windowManager.add(editor)
|
this._windowManager.add(editor)
|
||||||
if (this._windowManager.windowCount === 1) {
|
if (this._windowManager.windowCount === 1) {
|
||||||
this._accessor.menu.setActiveWindow(editor.id)
|
this._accessor.menu.setActiveWindow(editor.id)
|
||||||
}
|
}
|
||||||
|
return editor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new setting window.
|
* Create a new setting window.
|
||||||
*/
|
*/
|
||||||
createSettingWindow () {
|
_createSettingWindow () {
|
||||||
const setting = new SettingWindow(this._accessor)
|
const setting = new SettingWindow(this._accessor)
|
||||||
setting.createWindow()
|
setting.createWindow()
|
||||||
this._windowManager.add(setting)
|
this._windowManager.add(setting)
|
||||||
@ -231,25 +260,139 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(sessions): ...
|
_openFilesToOpen () {
|
||||||
// // Make Mark Text a single instance application.
|
this._openPathList(this._openFilesCache, false)
|
||||||
// _makeSingleInstance() {
|
}
|
||||||
// if (process.mas) return
|
|
||||||
//
|
/**
|
||||||
// app.requestSingleInstanceLock()
|
* Open the path list in the best window(s).
|
||||||
//
|
*
|
||||||
// app.on('second-instance', (event, argv, workingDirectory) => {
|
* @param {string[]} pathsToOpen The path list to open.
|
||||||
// // // TODO: Get active/last active window and open process arvg etc
|
* @param {boolean} openFilesInSameWindow Open all files in the same window with
|
||||||
// // if (currentWindow) {
|
* the first directory and discard other directories.
|
||||||
// // if (currentWindow.isMinimized()) currentWindow.restore()
|
*/
|
||||||
// // currentWindow.focus()
|
_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 () {
|
_listenForIpcMain () {
|
||||||
ipcMain.on('app-create-editor-window', () => {
|
ipcMain.on('app-create-editor-window', () => {
|
||||||
this.createEditorWindow()
|
this._createEditorWindow()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('screen-capture', win => {
|
ipcMain.on('screen-capture', win => {
|
||||||
@ -281,7 +424,7 @@ class App {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('app-create-settings-window', () => {
|
ipcMain.on('app-create-settings-window', () => {
|
||||||
const settingWins = this._windowManager.windowsOfType(WindowType.SETTING)
|
const settingWins = this._windowManager.getWindowsByType(WindowType.SETTING)
|
||||||
if (settingWins.length >= 1) {
|
if (settingWins.length >= 1) {
|
||||||
// A setting window is already created
|
// A setting window is already created
|
||||||
const browserSettingWindow = settingWins[0].win.browserWindow
|
const browserSettingWindow = settingWins[0].win.browserWindow
|
||||||
@ -292,42 +435,77 @@ class App {
|
|||||||
}
|
}
|
||||||
return
|
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) => {
|
ipcMain.on('app-open-file-by-id', (windowId, filePath) => {
|
||||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||||
if (openFilesInNewWindow) {
|
if (openFilesInNewWindow) {
|
||||||
this.createEditorWindow(filePath)
|
this._createEditorWindow(null, [ filePath ])
|
||||||
} else {
|
} else {
|
||||||
const editor = this._windowManager.get(windowId)
|
const editor = this._windowManager.get(windowId)
|
||||||
if (editor && !editor.quitting) {
|
if (editor) {
|
||||||
editor.openTab(filePath, true)
|
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) => {
|
ipcMain.on('app-open-markdown-by-id', (windowId, data) => {
|
||||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow')
|
||||||
if (openFilesInNewWindow) {
|
if (openFilesInNewWindow) {
|
||||||
this.createEditorWindow(undefined, data)
|
this._createEditorWindow(null, [], [ data ])
|
||||||
} else {
|
} else {
|
||||||
const editor = this._windowManager.get(windowId)
|
const editor = this._windowManager.get(windowId)
|
||||||
if (editor && !editor.quitting) {
|
if (editor) {
|
||||||
editor.openUntitledTab(true, data)
|
editor.openUntitledTab(true, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('app-open-directory-by-id', (windowId, pathname) => {
|
ipcMain.on('app-open-directory-by-id', (windowId, pathname, openInSameWindow) => {
|
||||||
// TODO: Open the directory in an existing window if prefered.
|
const { openFolderInNewWindow } = this._accessor.preferences.getAll()
|
||||||
this.createEditorWindow(pathname)
|
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 { app, BrowserWindow, ipcMain } from 'electron'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import Watcher from '../filesystem/watcher'
|
import Watcher, { WATCHER_STABILITY_THRESHOLD, WATCHER_STABILITY_POLL_INTERVAL } from '../filesystem/watcher'
|
||||||
|
import { WindowType } from '../windows/base'
|
||||||
/**
|
|
||||||
* 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'
|
|
||||||
}
|
|
||||||
|
|
||||||
class WindowActivityList {
|
class WindowActivityList {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -33,6 +19,14 @@ class WindowActivityList {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSecondNewest () {
|
||||||
|
const { _buf } = this
|
||||||
|
if (_buf.length >= 2) {
|
||||||
|
return _buf[_buf.length - 2]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
setNewest (id) {
|
setNewest (id) {
|
||||||
// I think we do not need a linked list for only a few windows.
|
// I think we do not need a linked list for only a few windows.
|
||||||
const { _buf } = this
|
const { _buf } = this
|
||||||
@ -72,7 +66,7 @@ class WindowManager extends EventEmitter {
|
|||||||
this._windows = new Map()
|
this._windows = new Map()
|
||||||
this._windowActivity = new WindowActivityList()
|
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._watcher = new Watcher(preferences)
|
||||||
|
|
||||||
this._listenForIpcMain()
|
this._listenForIpcMain()
|
||||||
@ -84,19 +78,23 @@ class WindowManager extends EventEmitter {
|
|||||||
* @param {IApplicationWindow} window The application window. We take ownership!
|
* @param {IApplicationWindow} window The application window. We take ownership!
|
||||||
*/
|
*/
|
||||||
add (window) {
|
add (window) {
|
||||||
this._windows.set(window.id, window)
|
const { id: windowId } = window
|
||||||
|
this._windows.set(windowId, window)
|
||||||
|
|
||||||
if (!this._appMenu.has(window.id)) {
|
if (!this._appMenu.has(windowId)) {
|
||||||
this._appMenu.addDefaultMenu(window.id)
|
this._appMenu.addDefaultMenu(windowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.windowCount === 1) {
|
if (this.windowCount === 1) {
|
||||||
this.setActiveWindow(window.id)
|
this.setActiveWindow(windowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { browserWindow } = window
|
|
||||||
window.on('window-focus', () => {
|
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.
|
* Return the application window by id.
|
||||||
*
|
*
|
||||||
* @param {string} windowId The window id.
|
* @param {string} windowId The window id.
|
||||||
* @returns {IApplicationWindow} The application window or undefined.
|
* @returns {BaseWindow} The application window or undefined.
|
||||||
*/
|
*/
|
||||||
get (windowId) {
|
get (windowId) {
|
||||||
return this._windows.get(windowId)
|
return this._windows.get(windowId)
|
||||||
@ -127,7 +125,7 @@ class WindowManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Remove the given window by id.
|
* 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.
|
* @param {string} windowId The window id.
|
||||||
* @returns {IApplicationWindow} Returns the application window. We no longer take ownership.
|
* @returns {IApplicationWindow} Returns the application window. We no longer take ownership.
|
||||||
@ -136,7 +134,7 @@ class WindowManager extends EventEmitter {
|
|||||||
const { _windows } = this
|
const { _windows } = this
|
||||||
const window = this.get(windowId)
|
const window = this.get(windowId)
|
||||||
if (window) {
|
if (window) {
|
||||||
window.removeAllListeners()
|
window.removeAllListeners('window-focus')
|
||||||
|
|
||||||
this._windowActivity.delete(windowId)
|
this._windowActivity.delete(windowId)
|
||||||
let nextWindowId = this._windowActivity.getNewest()
|
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 the active window or null if no window is registered.
|
||||||
* @returns {number|null}
|
* @returns {BaseWindow|undefined}
|
||||||
*/
|
*/
|
||||||
getActiveWindow () {
|
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
|
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']
|
* @param {WindowType} type the WindowType one of ['base', 'editor', 'setting']
|
||||||
* Return the windows of the given {type}
|
* @returns {{id: number, win: BaseWindow}[]} Return the windows of the given {type}
|
||||||
*/
|
*/
|
||||||
windowsOfType (type) {
|
getWindowsByType (type) {
|
||||||
if (!WindowType[type.toUpperCase()]) {
|
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 { windows } = this
|
||||||
const result = []
|
const result = []
|
||||||
@ -197,10 +220,75 @@ class WindowManager extends EventEmitter {
|
|||||||
return result
|
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 ---------------------------------
|
// --- helper ---------------------------------
|
||||||
|
|
||||||
closeWatcher () {
|
closeWatcher () {
|
||||||
this._watcher.clear()
|
this._watcher.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -213,15 +301,15 @@ class WindowManager extends EventEmitter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = browserWindow
|
const { id: windowId } = browserWindow
|
||||||
const { _appMenu, _windows } = this
|
const { _appMenu, _windows } = this
|
||||||
|
|
||||||
// Free watchers used by this window
|
// Free watchers used by this window
|
||||||
this._watcher.unWatchWin(browserWindow)
|
this._watcher.unwatchByWindowId(windowId)
|
||||||
|
|
||||||
// Application clearup and remove listeners
|
// Application clearup and remove listeners
|
||||||
_appMenu.removeWindowMenu(id)
|
_appMenu.removeWindowMenu(windowId)
|
||||||
const window = this.remove(id)
|
const window = this.remove(windowId)
|
||||||
|
|
||||||
// Destroy window wrapper and browser window
|
// Destroy window wrapper and browser window
|
||||||
if (window) {
|
if (window) {
|
||||||
@ -251,32 +339,39 @@ class WindowManager extends EventEmitter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- events ---------------------------------
|
// --- private --------------------------------
|
||||||
|
|
||||||
_listenForIpcMain () {
|
_listenForIpcMain () {
|
||||||
// listen for file watch from renderer process eg
|
// HACK: Don't use this event! Please see #1034 and #1035
|
||||||
// 1. click file in folder.
|
ipcMain.on('AGANI::window-add-file-path', (e, filePath) => {
|
||||||
// 2. new tab and save it.
|
|
||||||
// 3. close tab(s) need unwatch.
|
|
||||||
ipcMain.on('AGANI::file-watch', (e, { pathname, watch }) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
if (watch) {
|
const editor = this.get(win.id)
|
||||||
// listen for file `change` and `unlink`
|
if (!editor) {
|
||||||
this._watcher.watch(win, pathname, 'file')
|
log.error(`Cannot find window id "${win.id}" to add opened file.`)
|
||||||
} else {
|
return
|
||||||
// unlisten for file `change` and `unlink`
|
|
||||||
this._watcher.unWatch(win, pathname, 'file')
|
|
||||||
}
|
}
|
||||||
|
editor.addToOpenedFiles(filePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Force close a BrowserWindow
|
// Force close a BrowserWindow
|
||||||
ipcMain.on('AGANI::close-window', e => {
|
ipcMain.on('mt::close-window', e => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
this.forceClose(win)
|
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 ---------------
|
// --- local events ---------------
|
||||||
|
|
||||||
|
ipcMain.on('watcher-unwatch-all-by-id', windowId => {
|
||||||
|
this._watcher.unwatchByWindowId(windowId)
|
||||||
|
})
|
||||||
ipcMain.on('watcher-watch-file', (win, filePath) => {
|
ipcMain.on('watcher-watch-file', (win, filePath) => {
|
||||||
this._watcher.watch(win, filePath, 'file')
|
this._watcher.watch(win, filePath, 'file')
|
||||||
})
|
})
|
||||||
@ -284,17 +379,44 @@ class WindowManager extends EventEmitter {
|
|||||||
this._watcher.watch(win, pathname, 'dir')
|
this._watcher.watch(win, pathname, 'dir')
|
||||||
})
|
})
|
||||||
ipcMain.on('watcher-unwatch-file', (win, filePath) => {
|
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) => {
|
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 => {
|
ipcMain.on('window-close-by-id', id => {
|
||||||
this.forceCloseById(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 => {
|
ipcMain.on('window-toggle-always-on-top', win => {
|
||||||
const flag = !win.isAlwaysOnTop()
|
const flag = !win.isAlwaysOnTop()
|
||||||
win.setAlwaysOnTop(flag)
|
win.setAlwaysOnTop(flag)
|
||||||
|
@ -24,7 +24,9 @@ const cli = () => {
|
|||||||
--debug Enable debug mode
|
--debug Enable debug mode
|
||||||
--safe Disable plugins and other user configuration
|
--safe Disable plugins and other user configuration
|
||||||
--dump-keyboard-layout Dump keyboard information
|
--dump-keyboard-layout Dump keyboard information
|
||||||
|
-n, --new-window Open a new window on second-instance
|
||||||
--user-data-dir Change the user data directory
|
--user-data-dir Change the user data directory
|
||||||
|
--disable-gpu Disable GPU hardware acceleration
|
||||||
-v, --verbose Be verbose
|
-v, --verbose Be verbose
|
||||||
--version Print version information
|
--version Print version information
|
||||||
-h, --help Print this help message
|
-h, --help Print this help message
|
||||||
|
@ -8,7 +8,7 @@ import arg from 'arg'
|
|||||||
* @returns {arg.Result} Parsed arguments
|
* @returns {arg.Result} Parsed arguments
|
||||||
*/
|
*/
|
||||||
const parseArgs = (argv = null, permissive = true) => {
|
const parseArgs = (argv = null, permissive = true) => {
|
||||||
if (argv == null) {
|
if (argv === null) {
|
||||||
argv = process.argv.slice(1)
|
argv = process.argv.slice(1)
|
||||||
}
|
}
|
||||||
const spec = {
|
const spec = {
|
||||||
@ -16,6 +16,10 @@ const parseArgs = (argv=null, permissive=true) => {
|
|||||||
'--safe': Boolean,
|
'--safe': Boolean,
|
||||||
'--dump-keyboard-layout': Boolean,
|
'--dump-keyboard-layout': Boolean,
|
||||||
|
|
||||||
|
'--new-window': Boolean,
|
||||||
|
'-n': '--new-window',
|
||||||
|
|
||||||
|
'--disable-gpu': Boolean,
|
||||||
'--user-data-dir': String,
|
'--user-data-dir': String,
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
|
@ -2,7 +2,7 @@ export const isOsx = process.platform === 'darwin'
|
|||||||
export const isWindows = process.platform === 'win32'
|
export const isWindows = process.platform === 'win32'
|
||||||
export const isLinux = process.platform === 'linux'
|
export const isLinux = process.platform === 'linux'
|
||||||
|
|
||||||
export const defaultWinOptions = {
|
export const editorWinOptions = {
|
||||||
minWidth: 450,
|
minWidth: 450,
|
||||||
minHeight: 220,
|
minHeight: 220,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@ -10,7 +10,7 @@ export const defaultWinOptions = {
|
|||||||
webSecurity: false
|
webSecurity: false
|
||||||
},
|
},
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
show: false,
|
show: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
titleBarStyle: 'hiddenInset'
|
titleBarStyle: 'hiddenInset'
|
||||||
}
|
}
|
||||||
@ -31,8 +31,7 @@ export const defaultPreferenceWinOptions = {
|
|||||||
show: false,
|
show: false,
|
||||||
frame: false,
|
frame: false,
|
||||||
thickFrame: !isOsx,
|
thickFrame: !isOsx,
|
||||||
titleBarStyle: 'hiddenInset',
|
titleBarStyle: 'hiddenInset'
|
||||||
center: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EXTENSIONS = [
|
export const EXTENSIONS = [
|
||||||
|
@ -22,16 +22,14 @@ const getOSInformation = () => {
|
|||||||
return `${os.type()} ${os.arch()} ${os.release()} (${os.platform()})`
|
return `${os.type()} ${os.arch()} ${os.release()} (${os.platform()})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundleException = (error, type) => {
|
const exceptionToString = (error, type) => {
|
||||||
const { message, stack } = error
|
const { message, stack } = error
|
||||||
return {
|
return `Version: ${global.MARKTEXT_VERSION_STRING || app.getVersion()}\n` +
|
||||||
version: global.MARKTEXT_VERSION_STRING || app.getVersion(),
|
`OS: ${getOSInformation()}\n` +
|
||||||
os: getOSInformation(),
|
`Type: ${type}\n` +
|
||||||
type,
|
`Date: ${new Date().toGMTString()}\n` +
|
||||||
date: new Date().toGMTString(),
|
`Message: ${message}\n` +
|
||||||
message,
|
`Stack: ${stack}\n`
|
||||||
stack
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleError = (title, error, type) => {
|
const handleError = (title, error, type) => {
|
||||||
@ -39,8 +37,7 @@ const handleError = (title, error, type) => {
|
|||||||
|
|
||||||
// Write error into file
|
// Write error into file
|
||||||
if (type === 'main') {
|
if (type === 'main') {
|
||||||
const info = bundleException(error, type)
|
logger(exceptionToString(error, type))
|
||||||
logger(JSON.stringify(info, null, 2))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EXIT_ON_ERROR) {
|
if (EXIT_ON_ERROR) {
|
||||||
@ -48,12 +45,13 @@ const handleError = (title, error, type) => {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
// eslint, don't lie to me, the return statement is important!
|
// eslint, don't lie to me, the return statement is important!
|
||||||
return // eslint-disable-line no-unreachable
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// show error dialog
|
// show error dialog
|
||||||
if (app.isReady()) {
|
if (app.isReady()) {
|
||||||
|
// Blocking message box
|
||||||
const result = dialog.showMessageBox({
|
const result = dialog.showMessageBox({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
buttons: [
|
buttons: [
|
||||||
|
@ -3,6 +3,22 @@ import path from 'path'
|
|||||||
import { hasMarkdownExtension } from '../utils'
|
import { hasMarkdownExtension } from '../utils'
|
||||||
import { IMAGE_EXTENSIONS } from '../config'
|
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.
|
* Ensure that a directory exist.
|
||||||
*
|
*
|
||||||
@ -97,6 +113,45 @@ export const isMarkdownFileOrLink = filepath => {
|
|||||||
return false
|
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.
|
* 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 path from 'path'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config'
|
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 => {
|
const getLineEnding = lineEnding => {
|
||||||
if (lineEnding === 'lf') {
|
if (lineEnding === 'lf') {
|
||||||
@ -20,6 +20,27 @@ const convertLineEndings = (text, lineEnding) => {
|
|||||||
return text.replace(LINE_ENDING_REG, getLineEnding(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.
|
* Write the content into a file.
|
||||||
*
|
*
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs-extra'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { promisify } from 'util'
|
|
||||||
import chokidar from 'chokidar'
|
import chokidar from 'chokidar'
|
||||||
import { getUniqueId, hasMarkdownExtension } from '../utils'
|
import { getUniqueId, hasMarkdownExtension } from '../utils'
|
||||||
|
import { exists } from '../filesystem'
|
||||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||||
import { isLinux } from '../config'
|
import { isLinux } from '../config'
|
||||||
|
|
||||||
// TODO(need::refactor):
|
// TODO(refactor): Please see GH#1035.
|
||||||
// - Refactor this file
|
|
||||||
// - Outsource watcher/search features into worker (per window) and use something like "file-matcher" for searching on disk.
|
export const WATCHER_STABILITY_THRESHOLD = 1000
|
||||||
|
export const WATCHER_STABILITY_POLL_INTERVAL = 150
|
||||||
|
|
||||||
const EVENT_NAME = {
|
const EVENT_NAME = {
|
||||||
dir: 'AGANI::update-object-tree',
|
dir: 'AGANI::update-object-tree',
|
||||||
file: 'AGANI::update-file'
|
file: 'AGANI::update-file'
|
||||||
}
|
}
|
||||||
|
|
||||||
const add = async (win, pathname, endOfLine) => {
|
const add = async (win, pathname, type, endOfLine) => {
|
||||||
const stats = await promisify(fs.stat)(pathname)
|
const stats = await fs.stat(pathname)
|
||||||
const birthTime = stats.birthtime
|
const birthTime = stats.birthtime
|
||||||
const isMarkdown = hasMarkdownExtension(pathname)
|
const isMarkdown = hasMarkdownExtension(pathname)
|
||||||
const file = {
|
const file = {
|
||||||
@ -29,11 +30,24 @@ const add = async (win, pathname, endOfLine) => {
|
|||||||
isMarkdown
|
isMarkdown
|
||||||
}
|
}
|
||||||
if (isMarkdown) {
|
if (isMarkdown) {
|
||||||
|
// HACK: But this should be removed completely in #1034/#1035.
|
||||||
|
try {
|
||||||
const data = await loadMarkdownFile(pathname, endOfLine)
|
const data = await loadMarkdownFile(pathname, endOfLine)
|
||||||
file.data = data
|
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',
|
type: 'add',
|
||||||
change: file
|
change: file
|
||||||
})
|
})
|
||||||
@ -49,8 +63,10 @@ const unlink = (win, pathname, type) => {
|
|||||||
|
|
||||||
const change = async (win, pathname, type, endOfLine) => {
|
const change = async (win, pathname, type, endOfLine) => {
|
||||||
const isMarkdown = hasMarkdownExtension(pathname)
|
const isMarkdown = hasMarkdownExtension(pathname)
|
||||||
|
|
||||||
if (isMarkdown) {
|
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 data = await loadMarkdownFile(pathname, endOfLine)
|
||||||
const file = {
|
const file = {
|
||||||
pathname,
|
pathname,
|
||||||
@ -60,10 +76,22 @@ const change = async (win, pathname, type, endOfLine) => {
|
|||||||
type: 'change',
|
type: 'change',
|
||||||
change: file
|
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 = {
|
const directory = {
|
||||||
pathname,
|
pathname,
|
||||||
name: path.basename(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 }
|
const directory = { pathname }
|
||||||
win.webContents.send('AGANI::update-object-tree', {
|
win.webContents.send('AGANI::update-object-tree', {
|
||||||
type: 'unlinkDir',
|
type: 'unlinkDir',
|
||||||
@ -96,61 +126,117 @@ class Watcher {
|
|||||||
*/
|
*/
|
||||||
constructor (preferences) {
|
constructor (preferences) {
|
||||||
this._preferences = preferences
|
this._preferences = preferences
|
||||||
|
this._ignoreChangeEvents = []
|
||||||
this.watchers = {}
|
this.watchers = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return a unwatch function
|
// Watch a file or directory and return a unwatch function.
|
||||||
watch (win, watchPath, type = 'dir'/* file or dir */) {
|
watch (win, watchPath, type = 'dir'/* file or dir */) {
|
||||||
const id = getUniqueId()
|
const id = getUniqueId()
|
||||||
const watcher = chokidar.watch(watchPath, {
|
const watcher = chokidar.watch(watchPath, {
|
||||||
ignored: /(^|[/\\])(\..|node_modules)/,
|
ignored: /(^|[/\\])(\..|node_modules)/,
|
||||||
ignoreInitial: type === 'file',
|
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
|
watcher
|
||||||
.on('add', pathname => add(win, pathname, this._preferences.getPreferedEOL()))
|
.on('add', pathname => {
|
||||||
.on('change', pathname => change(win, pathname, type, this._preferences.getPreferedEOL()))
|
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('unlink', pathname => unlink(win, pathname, type))
|
||||||
.on('addDir', pathname => addDir(win, pathname))
|
.on('addDir', pathname => addDir(win, pathname, type))
|
||||||
.on('unlinkDir', pathname => unlinkDir(win, pathname))
|
.on('unlinkDir', pathname => unlinkDir(win, pathname, type))
|
||||||
.on('raw', (event, path, details) => {
|
.on('raw', (event, subpath, details) => {
|
||||||
if (global.MARKTEXT_DEBUG_VERBOSE >= 3) {
|
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') {
|
if (isLinux && type === 'file' && event === 'rename') {
|
||||||
const { watchedPath } = details
|
if (renameTimer) {
|
||||||
// Use the same watcher and re-watch the file.
|
clearTimeout(renameTimer)
|
||||||
watcher.unwatch(watchedPath)
|
}
|
||||||
watcher.add(watchedPath)
|
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 => {
|
.on('error', error => {
|
||||||
const msg = `Watcher error: ${error}`
|
// Check if too many file descriptors are opened and notify the user about this issue.
|
||||||
console.log(msg)
|
if (error.code === 'ENOSPC') {
|
||||||
log.error(msg)
|
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] = {
|
this.watchers[id] = {
|
||||||
win,
|
win,
|
||||||
watcher,
|
watcher,
|
||||||
pathname: watchPath,
|
pathname: watchPath,
|
||||||
type
|
type,
|
||||||
|
|
||||||
|
close: closeFn
|
||||||
}
|
}
|
||||||
|
|
||||||
// unwatcher function
|
// unwatcher function
|
||||||
return () => {
|
return closeFn
|
||||||
if (this.watchers[id]) {
|
|
||||||
delete this.watchers[id]
|
|
||||||
}
|
|
||||||
watcher.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unWatch some single watch
|
// Remove a single watcher.
|
||||||
unWatch (win, watchPath, type = 'dir') {
|
unwatch (win, watchPath, type = 'dir') {
|
||||||
for (const id of Object.keys(this.watchers)) {
|
for (const id of Object.keys(this.watchers)) {
|
||||||
const w = this.watchers[id]
|
const w = this.watchers[id]
|
||||||
if (
|
if (
|
||||||
@ -165,13 +251,13 @@ class Watcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// unwatch for one window, (remove all the watchers in one window)
|
// Remove all watchers from the given window id.
|
||||||
unWatchWin (win) {
|
unwatchByWindowId (windowId) {
|
||||||
const watchers = []
|
const watchers = []
|
||||||
const watchIds = []
|
const watchIds = []
|
||||||
for (const id of Object.keys(this.watchers)) {
|
for (const id of Object.keys(this.watchers)) {
|
||||||
const w = this.watchers[id]
|
const w = this.watchers[id]
|
||||||
if (w.win === win) {
|
if (w.win.id === windowId) {
|
||||||
watchers.push(w.watcher)
|
watchers.push(w.watcher)
|
||||||
watchIds.push(id)
|
watchIds.push(id)
|
||||||
}
|
}
|
||||||
@ -182,9 +268,49 @@ class Watcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clear () {
|
close () {
|
||||||
Object.keys(this.watchers).forEach(id => this.watchers[id].watcher.close())
|
Object.keys(this.watchers).forEach(id => this.watchers[id].close())
|
||||||
this.watchers = {}
|
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
|
* This file is used specifically and only for development. It installs
|
||||||
* `electron-debug` & `vue-devtools`. There shouldn't be any need to
|
* `vue-devtools`. There shouldn't be any need to modify this file,
|
||||||
* modify this file, but it can be used to extend your development
|
* but it can be used to extend your development environment.
|
||||||
* environment.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
// Install `electron-debug` with `devtron`
|
|
||||||
require('electron-debug')({ showDevTools: false })
|
|
||||||
|
|
||||||
// Install `vue-devtools`
|
// Install `vue-devtools`
|
||||||
require('electron').app.on('ready', () => {
|
require('electron').app.on('ready', () => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import './globalSetting'
|
import './globalSetting'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
import cli from './cli'
|
import cli from './cli'
|
||||||
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
|
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
@ -32,6 +33,19 @@ const args = cli()
|
|||||||
const appEnvironment = setupEnvironment(args)
|
const appEnvironment = setupEnvironment(args)
|
||||||
initializeLogger(appEnvironment)
|
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.
|
// 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.
|
// Create other instances that need access to the modules from above.
|
||||||
const accessor = new Accessor(appEnvironment)
|
const accessor = new Accessor(appEnvironment)
|
||||||
@ -40,5 +54,5 @@ const accessor = new Accessor(appEnvironment)
|
|||||||
// Be careful when changing code before this line!
|
// Be careful when changing code before this line!
|
||||||
// NOTE: Do not create classes or other code before this line!
|
// NOTE: Do not create classes or other code before this line!
|
||||||
|
|
||||||
const app = new App(accessor, args)
|
const marktext = new App(accessor, args)
|
||||||
app.init()
|
marktext.init()
|
||||||
|
@ -31,12 +31,13 @@ class Keybindings {
|
|||||||
['fileNewTab', 'CmdOrCtrl+Shift+T'],
|
['fileNewTab', 'CmdOrCtrl+Shift+T'],
|
||||||
['fileOpenFile', 'CmdOrCtrl+O'],
|
['fileOpenFile', 'CmdOrCtrl+O'],
|
||||||
['fileOpenFolder', 'CmdOrCtrl+Shift+O'],
|
['fileOpenFolder', 'CmdOrCtrl+Shift+O'],
|
||||||
['fileCloseTab', 'CmdOrCtrl+W'],
|
|
||||||
['fileSave', 'CmdOrCtrl+S'],
|
['fileSave', 'CmdOrCtrl+S'],
|
||||||
['fileSaveAs', 'CmdOrCtrl+Shift+S'],
|
['fileSaveAs', 'CmdOrCtrl+Shift+S'],
|
||||||
['filePrint', 'CmdOrCtrl+P'],
|
['filePrint', 'CmdOrCtrl+P'],
|
||||||
['filePreferences', 'CmdOrCtrl+,'], // marktext menu in macOS
|
['filePreferences', 'CmdOrCtrl+,'], // marktext menu in macOS
|
||||||
['fileQuit', isOsx ? 'Command+Q' : 'Alt+F4'],
|
['fileCloseTab', 'CmdOrCtrl+W'],
|
||||||
|
['fileCloseWindow', 'CmdOrCtrl+Shift+W'],
|
||||||
|
['fileQuit', 'CmdOrCtrl+Q'],
|
||||||
|
|
||||||
// edit menu
|
// edit menu
|
||||||
['editUndo', 'CmdOrCtrl+Z'],
|
['editUndo', 'CmdOrCtrl+Z'],
|
||||||
@ -91,18 +92,16 @@ class Keybindings {
|
|||||||
['formatClearFormat', 'Shift+CmdOrCtrl+R'],
|
['formatClearFormat', 'Shift+CmdOrCtrl+R'],
|
||||||
|
|
||||||
// window menu
|
// window menu
|
||||||
// ['windowMinimize', 'CmdOrCtrl+M'], deprecated for math.
|
['windowMinimize', ''], // 'CmdOrCtrl+M' deprecated for math
|
||||||
['windowCloseWindow', 'CmdOrCtrl+Shift+W'],
|
|
||||||
|
|
||||||
// view menu
|
// view menu
|
||||||
['viewToggleFullScreen', isOsx ? 'Ctrl+Command+F' : 'F11'],
|
['viewToggleFullScreen', isOsx ? 'Ctrl+Command+F' : 'F11'],
|
||||||
['viewChangeFont', 'CmdOrCtrl+.'],
|
|
||||||
['viewSourceCodeMode', 'CmdOrCtrl+Alt+S'],
|
['viewSourceCodeMode', 'CmdOrCtrl+Alt+S'],
|
||||||
['viewTypewriterMode', 'CmdOrCtrl+Alt+T'],
|
['viewTypewriterMode', 'CmdOrCtrl+Alt+T'],
|
||||||
['viewFocusMode', 'CmdOrCtrl+Shift+F'],
|
['viewFocusMode', 'CmdOrCtrl+Shift+F'],
|
||||||
['viewToggleSideBar', 'CmdOrCtrl+J'],
|
['viewToggleSideBar', 'CmdOrCtrl+J'],
|
||||||
['viewToggleTabBar', 'CmdOrCtrl+Alt+B'],
|
['viewToggleTabBar', 'CmdOrCtrl+Alt+B'],
|
||||||
['viewDevToggleDeveloperTools', isOsx ? 'Alt+Command+I' : 'Ctrl+Shift+I'],
|
['viewDevToggleDeveloperTools', 'CmdOrCtrl+Alt+I'],
|
||||||
['viewDevReload', 'CmdOrCtrl+R']
|
['viewDevReload', 'CmdOrCtrl+R']
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import { writeMarkdownFile } from '../../filesystem/markdown'
|
|||||||
import { getPath, getRecommendTitleFromMarkdownString } from '../../utils'
|
import { getPath, getRecommendTitleFromMarkdownString } from '../../utils'
|
||||||
import pandoc from '../../utils/pandoc'
|
import pandoc from '../../utils/pandoc'
|
||||||
|
|
||||||
// TODO:
|
// TODO(refactor): "save" and "save as" should be moved to the editor window (editor.js) and
|
||||||
// - use async dialog version to not block the main process.
|
// the renderer should communicate only with the editor window for file relevant stuff.
|
||||||
// - catch "fs." exceptions. Otherwise the main process crashes...
|
// E.g. "mt::save-tabs" --> "mt::window-save-tabs$wid:<windowId>"
|
||||||
|
|
||||||
// Handle the export response from renderer process.
|
// Handle the export response from renderer process.
|
||||||
const handleResponseForExport = async (e, { type, content, pathname, markdown }) => {
|
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}`)
|
const defaultPath = path.join(dirname, `${nakedFilename}${extension}`)
|
||||||
|
|
||||||
// TODO(need::refactor): use async dialog version
|
dialog.showSaveDialog(win, {
|
||||||
const filePath = dialog.showSaveDialog(win, {
|
|
||||||
defaultPath
|
defaultPath
|
||||||
})
|
}, async filePath => {
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
let data = content
|
let data = content
|
||||||
try {
|
try {
|
||||||
@ -44,7 +42,7 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
|||||||
log.error(err)
|
log.error(err)
|
||||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||||
win.webContents.send('AGANI::show-notification', {
|
win.webContents.send('AGANI::show-notification', {
|
||||||
title: 'Export File Error',
|
title: 'Export failure',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: ERROR_MSG
|
message: ERROR_MSG
|
||||||
})
|
})
|
||||||
@ -55,57 +53,72 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
|||||||
removePrintServiceFromWindow(win)
|
removePrintServiceFromWindow(win)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResponseForPrint = e => {
|
const handleResponseForPrint = e => {
|
||||||
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
|
|
||||||
// See GH#749, Electron#16085 and Electron#17523.
|
// See GH#749, Electron#16085 and Electron#17523.
|
||||||
dialog.showMessageBox({
|
dialog.showMessageBox(win, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
buttons: ['OK'],
|
buttons: ['OK'],
|
||||||
defaultId: 0,
|
defaultId: 0,
|
||||||
noLink: true,
|
noLink: true,
|
||||||
message: 'Printing doesn\'t work',
|
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!'
|
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 }, () => {
|
// win.webContents.print({ printBackground: true }, () => {
|
||||||
// removePrintServiceFromWindow(win)
|
// removePrintServiceFromWindow(win)
|
||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResponseForSave = (e, { id, markdown, pathname, options }) => {
|
const handleResponseForSave = async (e, { id, markdown, pathname, options }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
let recommendFilename = getRecommendTitleFromMarkdownString(markdown)
|
let recommendFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||||
if (!recommendFilename) {
|
if (!recommendFilename) {
|
||||||
recommendFilename = 'Untitled'
|
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
|
const alreadyExistOnDisk = !!pathname
|
||||||
|
|
||||||
// TODO(need::refactor): use async dialog version
|
let filePath = pathname
|
||||||
pathname = pathname || dialog.showSaveDialog(win, {
|
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`)
|
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)
|
// Save dialog canceled by user - no error.
|
||||||
.then(() => {
|
if (!filePath) {
|
||||||
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 {
|
|
||||||
return Promise.resolve()
|
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) => {
|
const showUnsavedFilesMessage = (win, files) => {
|
||||||
@ -159,28 +172,26 @@ const removePrintServiceFromWindow = win => {
|
|||||||
|
|
||||||
// --- events -----------------------------------
|
// --- events -----------------------------------
|
||||||
|
|
||||||
ipcMain.on('AGANI::save-all', (e, unsavedFiles) => {
|
ipcMain.on('mt::save-tabs', (e, unsavedFiles) => {
|
||||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||||
.catch(log.error)
|
.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 win = BrowserWindow.fromWebContents(e.sender)
|
||||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||||
const EVENT = isSingle ? 'AGANI::save-single-response' : 'AGANI::save-all-response'
|
|
||||||
if (needSave) {
|
if (needSave) {
|
||||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||||
.then(arr => {
|
.then(arr => {
|
||||||
const data = arr.filter(id => id)
|
const tabIds = arr.filter(id => id != null)
|
||||||
win.send(EVENT, { err: null, data })
|
win.send('mt::force-close-tabs-by-id', tabIds)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
win.send(EVENT, { err, data: null })
|
|
||||||
log.error(err.error)
|
log.error(err.error)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const data = unsavedFiles.map(f => f.id)
|
const tabIds = unsavedFiles.map(f => f.id)
|
||||||
win.send(EVENT, { err: null, data })
|
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'
|
recommendFilename = 'Untitled'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(need::refactor): use async dialog version
|
// If the file doesn't exist on disk add it to the recently used documents later
|
||||||
const filePath = dialog.showSaveDialog(win, {
|
// and execute file from filesystem watcher for a short time. The file may exists
|
||||||
defaultPath: pathname || getPath('documents') + `/${recommendFilename}.md`
|
// 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) {
|
if (filePath) {
|
||||||
|
filePath = path.resolve(filePath)
|
||||||
writeMarkdownFile(filePath, markdown, options, win)
|
writeMarkdownFile(filePath, markdown, options, win)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// need watch file after `save as`
|
if (!alreadyExistOnDisk) {
|
||||||
if (pathname !== filePath) {
|
ipcMain.emit('window-add-file-path', win.id, filePath)
|
||||||
// unwatch the old file
|
ipcMain.emit('menu-add-recently-used', filePath)
|
||||||
ipcMain.emit('watcher-unwatch-file', win, pathname)
|
|
||||||
ipcMain.emit('watcher-watch-file', win, filePath)
|
|
||||||
}
|
|
||||||
const filename = path.basename(filePath)
|
const filename = path.basename(filePath)
|
||||||
win.webContents.send('AGANI::set-pathname', { id, pathname: filePath, filename })
|
win.webContents.send('mt::set-pathname', { id, pathname: filePath, filename })
|
||||||
})
|
} else if (pathname !== filePath) {
|
||||||
.catch(log.error)
|
// 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 win = BrowserWindow.fromWebContents(e.sender)
|
||||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||||
if (needSave) {
|
if (needSave) {
|
||||||
@ -223,6 +250,18 @@ ipcMain.on('AGANI::response-close-confirm', async (e, unsavedFiles) => {
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
log.error(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 {
|
} else {
|
||||||
ipcMain.emit('window-close-by-id', win.id)
|
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) {
|
for (const file of fileList) {
|
||||||
if (isMarkdownFileOrLink(file)) {
|
if (isMarkdownFileOrLink(file)) {
|
||||||
openFileOrFolder(win, file)
|
openFileOrFolder(win, file)
|
||||||
break
|
continue
|
||||||
}
|
}
|
||||||
// handle import file
|
|
||||||
|
// Try to import the file
|
||||||
if (PANDOC_EXTENSIONS.some(ext => file.endsWith(ext))) {
|
if (PANDOC_EXTENSIONS.some(ext => file.endsWith(ext))) {
|
||||||
const existsPandoc = pandoc.exists()
|
const existsPandoc = pandoc.exists()
|
||||||
if (!existsPandoc) {
|
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)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
if (!isFile(newPathname)) {
|
|
||||||
fs.renameSync(pathname, newPathname)
|
const doRename = () => {
|
||||||
e.sender.send('AGANI::set-pathname', {
|
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,
|
id,
|
||||||
pathname: newPathname,
|
pathname: newPathname,
|
||||||
filename: path.basename(newPathname)
|
filename: path.basename(newPathname)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFile(newPathname)) {
|
||||||
|
doRename()
|
||||||
} else {
|
} else {
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -274,12 +327,7 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
|||||||
noLink: true
|
noLink: true
|
||||||
}, index => {
|
}, index => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
fs.renameSync(pathname, newPathname)
|
doRename()
|
||||||
e.sender.send('AGANI::set-pathname', {
|
|
||||||
id,
|
|
||||||
pathname: newPathname,
|
|
||||||
filename: path.basename(newPathname)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -287,28 +335,34 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
|||||||
|
|
||||||
ipcMain.on('AGANI::response-file-move-to', (e, { id, pathname }) => {
|
ipcMain.on('AGANI::response-file-move-to', (e, { id, pathname }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
|
dialog.showSaveDialog(win, {
|
||||||
// TODO(need::refactor): use async dialog version
|
|
||||||
let newPath = dialog.showSaveDialog(win, {
|
|
||||||
buttonLabel: 'Move to',
|
buttonLabel: 'Move to',
|
||||||
nameFieldLabel: 'Filename:',
|
nameFieldLabel: 'Filename:',
|
||||||
defaultPath: pathname
|
defaultPath: pathname
|
||||||
})
|
}, newPath => {
|
||||||
if (newPath === undefined) return
|
if (newPath) {
|
||||||
fs.renameSync(pathname, newPath)
|
fs.rename(pathname, newPath, err => {
|
||||||
e.sender.send('AGANI::set-pathname', { id, pathname: newPath, filename: path.basename(newPath) })
|
if (err) {
|
||||||
})
|
log.error(`mt::rename: Cannot rename "${pathname}" to "${newPath}".\n${err.stack}`)
|
||||||
|
return
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }) => {
|
ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => {
|
||||||
@ -341,18 +395,17 @@ export const importFile = async win => {
|
|||||||
return noticePandocNotFound(win)
|
return noticePandocNotFound(win)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(need::refactor): use async dialog version
|
dialog.showOpenDialog(win, {
|
||||||
const filename = dialog.showOpenDialog(win, {
|
|
||||||
properties: [ 'openFile' ],
|
properties: [ 'openFile' ],
|
||||||
filters: [{
|
filters: [{
|
||||||
name: 'All Files',
|
name: 'All Files',
|
||||||
extensions: PANDOC_EXTENSIONS
|
extensions: PANDOC_EXTENSIONS
|
||||||
}]
|
}]
|
||||||
})
|
}, filePath => {
|
||||||
|
if (filePath) {
|
||||||
if (filename && filename[0]) {
|
openPandocFile(win.id, filePath)
|
||||||
openPandocFile(win.id, filename[0])
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const print = win => {
|
export const print = win => {
|
||||||
@ -360,27 +413,27 @@ export const print = win => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const openFile = win => {
|
export const openFile = win => {
|
||||||
// TODO(need::refactor): use async dialog version
|
dialog.showOpenDialog(win, {
|
||||||
const fileList = dialog.showOpenDialog(win, {
|
properties: ['openFile', 'multiSelections'],
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [{
|
filters: [{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
extensions: EXTENSIONS
|
extensions: EXTENSIONS
|
||||||
}]
|
}]
|
||||||
})
|
}, paths => {
|
||||||
if (fileList && fileList[0]) {
|
if (paths && Array.isArray(paths)) {
|
||||||
openFileOrFolder(win, fileList[0])
|
ipcMain.emit('app-open-files-by-id', win.id, paths)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openFolder = win => {
|
export const openFolder = win => {
|
||||||
// TODO(need::refactor): use async dialog version
|
dialog.showOpenDialog(win, {
|
||||||
const dirList = dialog.showOpenDialog(win, {
|
|
||||||
properties: ['openDirectory', 'createDirectory']
|
properties: ['openDirectory', 'createDirectory']
|
||||||
})
|
}, directories => {
|
||||||
if (dirList && dirList[0]) {
|
if (directories && directories[0]) {
|
||||||
openFileOrFolder(win, dirList[0])
|
openFileOrFolder(win, directories[0])
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openFileOrFolder = (win, pathname) => {
|
export const openFileOrFolder = (win, pathname) => {
|
||||||
|
@ -7,6 +7,12 @@ import { ensureDirSync, isDirectory, isFile } from '../filesystem'
|
|||||||
import { parseMenu } from '../keyboard/shortcutHandler'
|
import { parseMenu } from '../keyboard/shortcutHandler'
|
||||||
import configureMenu, { configSettingMenu } from '../menu/templates'
|
import configureMenu, { configSettingMenu } from '../menu/templates'
|
||||||
|
|
||||||
|
export const MenuType = {
|
||||||
|
DEFAULT: 0,
|
||||||
|
EDITOR: 1,
|
||||||
|
SETTINGS: 2
|
||||||
|
}
|
||||||
|
|
||||||
class AppMenu {
|
class AppMenu {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,13 +111,13 @@ class AppMenu {
|
|||||||
windowMenus.set(window.id, menu)
|
windowMenus.set(window.id, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
addEditorMenu (window) {
|
addEditorMenu (window, options = {}) {
|
||||||
const { windowMenus } = this
|
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 { menu, shortcutMap } = windowMenus.get(window.id)
|
||||||
const currentMenu = Menu.getApplicationMenu() // the menu may be null
|
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)
|
updateMenuItemSafe(currentMenu, menu, 'typewriterModeMenuItem', false)
|
||||||
|
|
||||||
// FIXME: Focus mode is being ignored when you open a new window - inconsistency.
|
// 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) {
|
if (!recentUsedDocuments) {
|
||||||
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
||||||
}
|
}
|
||||||
@ -174,7 +180,8 @@ class AppMenu {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
shortcutMap,
|
shortcutMap,
|
||||||
menu
|
menu,
|
||||||
|
type: MenuType.EDITOR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,9 +189,9 @@ class AppMenu {
|
|||||||
if (this.isOsx) {
|
if (this.isOsx) {
|
||||||
const menuTemplate = configSettingMenu(this._keybindings)
|
const menuTemplate = configSettingMenu(this._keybindings)
|
||||||
const menu = Menu.buildFromTemplate(menuTemplate)
|
const menu = Menu.buildFromTemplate(menuTemplate)
|
||||||
return { menu }
|
return { menu, type: MenuType.SETTINGS }
|
||||||
}
|
}
|
||||||
return { menu: null }
|
return { menu: null, type: MenuType.SETTINGS }
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAppMenu (recentUsedDocuments) {
|
updateAppMenu (recentUsedDocuments) {
|
||||||
@ -193,13 +200,15 @@ class AppMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "we don't support changing menu object after calling setMenu, the behavior
|
// "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.
|
// application menu each time.
|
||||||
|
|
||||||
// rebuild all window menus
|
// rebuild all window menus
|
||||||
this.windowMenus.forEach((value, key) => {
|
this.windowMenus.forEach((value, key) => {
|
||||||
const { menu: oldMenu } = value
|
const { menu: oldMenu, type } = value
|
||||||
const { menu: newMenu } = this.buildDefaultMenu(false, recentUsedDocuments)
|
if (type !== MenuType.EDITOR) return
|
||||||
|
|
||||||
|
const { menu: newMenu } = this.buildEditorMenu(false, recentUsedDocuments)
|
||||||
|
|
||||||
// all other menu items are set automatically
|
// all other menu items are set automatically
|
||||||
updateMenuItem(oldMenu, newMenu, 'sourceCodeModeMenuItem')
|
updateMenuItem(oldMenu, newMenu, 'sourceCodeModeMenuItem')
|
||||||
@ -231,7 +240,9 @@ class AppMenu {
|
|||||||
|
|
||||||
updateThemeMenu = theme => {
|
updateThemeMenu = theme => {
|
||||||
this.windowMenus.forEach((value, key) => {
|
this.windowMenus.forEach((value, key) => {
|
||||||
const { menu } = value
|
const { menu, type } = value
|
||||||
|
if (type !== MenuType.EDITOR) return
|
||||||
|
|
||||||
const themeMenus = menu.getMenuItemById('themeMenu')
|
const themeMenus = menu.getMenuItemById('themeMenu')
|
||||||
if (!themeMenus) {
|
if (!themeMenus) {
|
||||||
return
|
return
|
||||||
@ -248,7 +259,9 @@ class AppMenu {
|
|||||||
|
|
||||||
updateAutoSaveMenu = autoSave => {
|
updateAutoSaveMenu = autoSave => {
|
||||||
this.windowMenus.forEach((value, key) => {
|
this.windowMenus.forEach((value, key) => {
|
||||||
const { menu } = value
|
const { menu, type } = value
|
||||||
|
if (type !== MenuType.EDITOR) return
|
||||||
|
|
||||||
const autoSaveMenu = menu.getMenuItemById('autoSaveMenuItem')
|
const autoSaveMenu = menu.getMenuItemById('autoSaveMenuItem')
|
||||||
if (!autoSaveMenu) {
|
if (!autoSaveMenu) {
|
||||||
return
|
return
|
||||||
@ -259,7 +272,9 @@ class AppMenu {
|
|||||||
|
|
||||||
updateAidouMenu = bool => {
|
updateAidouMenu = bool => {
|
||||||
this.windowMenus.forEach((value, key) => {
|
this.windowMenus.forEach((value, key) => {
|
||||||
const { menu } = value
|
const { menu, type } = value
|
||||||
|
if (type !== MenuType.EDITOR) return
|
||||||
|
|
||||||
const aidouMenu = menu.getMenuItemById('aidou')
|
const aidouMenu = menu.getMenuItemById('aidou')
|
||||||
if (!aidouMenu) {
|
if (!aidouMenu) {
|
||||||
return
|
return
|
||||||
@ -283,6 +298,9 @@ class AppMenu {
|
|||||||
this.addRecentlyUsedDocument(pathname)
|
this.addRecentlyUsedDocument(pathname)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('menu-add-recently-used', pathname => {
|
||||||
|
this.addRecentlyUsedDocument(pathname)
|
||||||
|
})
|
||||||
ipcMain.on('menu-clear-recently-used', () => {
|
ipcMain.on('menu-clear-recently-used', () => {
|
||||||
this.clearRecentlyUsedDocuments()
|
this.clearRecentlyUsedDocuments()
|
||||||
})
|
})
|
||||||
|
@ -2,10 +2,10 @@ import { app } from 'electron'
|
|||||||
import * as actions from '../actions/file'
|
import * as actions from '../actions/file'
|
||||||
import { userSetting } from '../actions/marktext'
|
import { userSetting } from '../actions/marktext'
|
||||||
import { showTabBar } from '../actions/view'
|
import { showTabBar } from '../actions/view'
|
||||||
|
import { isOsx } from '../../config'
|
||||||
|
|
||||||
export default function (keybindings, userPreference, recentlyUsedFiles) {
|
export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||||
const { autoSave } = userPreference.getAll()
|
const { autoSave } = userPreference.getAll()
|
||||||
const notOsx = process.platform !== 'darwin'
|
|
||||||
let fileMenu = {
|
let fileMenu = {
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [{
|
submenu: [{
|
||||||
@ -38,7 +38,7 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notOsx) {
|
if (!isOsx) {
|
||||||
let recentlyUsedMenu = {
|
let recentlyUsedMenu = {
|
||||||
label: 'Open Recent',
|
label: 'Open Recent',
|
||||||
submenu: []
|
submenu: []
|
||||||
@ -77,12 +77,6 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
|||||||
|
|
||||||
fileMenu.submenu.push({
|
fileMenu.submenu.push({
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}, {
|
|
||||||
label: 'Close Tab',
|
|
||||||
accelerator: keybindings.getAccelerator('fileCloseTab'),
|
|
||||||
click (menuItem, browserWindow) {
|
|
||||||
actions.closeTab(browserWindow)
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}, {
|
}, {
|
||||||
@ -139,8 +133,6 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, {
|
|
||||||
type: 'separator'
|
|
||||||
}, {
|
}, {
|
||||||
label: 'Print',
|
label: 'Print',
|
||||||
accelerator: keybindings.getAccelerator('filePrint'),
|
accelerator: keybindings.getAccelerator('filePrint'),
|
||||||
@ -149,23 +141,34 @@ export default function (keybindings, userPreference, recentlyUsedFiles) {
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
visible: notOsx
|
visible: !isOsx
|
||||||
}, {
|
}, {
|
||||||
label: 'Preferences',
|
label: 'Preferences',
|
||||||
accelerator: keybindings.getAccelerator('filePreferences'),
|
accelerator: keybindings.getAccelerator('filePreferences'),
|
||||||
visible: notOsx,
|
visible: !isOsx,
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
userSetting(menuItem, browserWindow)
|
userSetting(menuItem, browserWindow)
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
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',
|
label: 'Quit',
|
||||||
accelerator: keybindings.getAccelerator('fileQuit'),
|
accelerator: keybindings.getAccelerator('fileQuit'),
|
||||||
visible: notOsx,
|
visible: !isOsx,
|
||||||
click: app.quit
|
click: app.quit
|
||||||
})
|
})
|
||||||
|
|
||||||
return fileMenu
|
return fileMenu
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
|
import { ipcMain } from 'electron'
|
||||||
import * as actions from '../actions/view'
|
import * as actions from '../actions/view'
|
||||||
import { isOsx } from '../../config'
|
|
||||||
|
|
||||||
export default function (keybindings) {
|
export default function (keybindings) {
|
||||||
let viewMenu = {
|
let viewMenu = {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [{
|
submenu: [{
|
||||||
label: 'Toggle Full Screen',
|
|
||||||
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
|
||||||
click (item, focusedWindow) {
|
|
||||||
if (focusedWindow) {
|
|
||||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
type: 'separator'
|
|
||||||
}, {
|
|
||||||
id: 'sourceCodeModeMenuItem',
|
id: 'sourceCodeModeMenuItem',
|
||||||
label: 'Source Code Mode',
|
label: 'Source Code Mode',
|
||||||
accelerator: keybindings.getAccelerator('viewSourceCodeMode'),
|
accelerator: keybindings.getAccelerator('viewSourceCodeMode'),
|
||||||
@ -89,7 +79,6 @@ export default function (keybindings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (global.MARKTEXT_DEBUG) {
|
if (global.MARKTEXT_DEBUG) {
|
||||||
// add devtool when development
|
|
||||||
viewMenu.submenu.push({
|
viewMenu.submenu.push({
|
||||||
label: 'Toggle Developer Tools',
|
label: 'Toggle Developer Tools',
|
||||||
accelerator: keybindings.getAccelerator('viewDevToggleDeveloperTools'),
|
accelerator: keybindings.getAccelerator('viewDevToggleDeveloperTools'),
|
||||||
@ -99,25 +88,15 @@ export default function (keybindings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// add reload when development
|
|
||||||
viewMenu.submenu.push({
|
viewMenu.submenu.push({
|
||||||
label: 'Reload',
|
label: 'Reload',
|
||||||
accelerator: keybindings.getAccelerator('viewDevReload'),
|
accelerator: keybindings.getAccelerator('viewDevReload'),
|
||||||
click (item, focusedWindow) {
|
click (item, focusedWindow) {
|
||||||
if (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
|
return viewMenu
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { toggleAlwaysOnTop } from '../actions/window'
|
import { toggleAlwaysOnTop } from '../actions/window'
|
||||||
|
import { isOsx } from '../../config'
|
||||||
|
|
||||||
export default function (keybindings) {
|
export default function (keybindings) {
|
||||||
return {
|
const menu = {
|
||||||
label: 'Window',
|
label: 'Window',
|
||||||
role: 'window',
|
role: 'window',
|
||||||
submenu: [{
|
submenu: [{
|
||||||
@ -18,9 +19,21 @@ export default function (keybindings) {
|
|||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}, {
|
}, {
|
||||||
label: 'Close Window',
|
label: 'Toggle Full Screen',
|
||||||
accelerator: keybindings.getAccelerator('windowCloseWindow'),
|
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
||||||
role: 'close'
|
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": {
|
"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"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"aidou": {
|
"aidou": {
|
||||||
@ -31,7 +35,7 @@
|
|||||||
"title"
|
"title"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"startUp": {
|
"startUpAction": {
|
||||||
"description": "General--The action after Mark Text startup, open the last edited content, open the specified folder or blank page",
|
"description": "General--The action after Mark Text startup, open the last edited content, open the specified folder or blank page",
|
||||||
"enum": [
|
"enum": [
|
||||||
"folder",
|
"folder",
|
||||||
@ -39,6 +43,10 @@
|
|||||||
"blank"
|
"blank"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"defaultDirectoryToOpen": {
|
||||||
|
"description": "General--The default directory that should be opened on startup when startUp=folder.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"description": "General--The language Mark Text use.",
|
"description": "General--The language Mark Text use.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -160,7 +168,6 @@
|
|||||||
"description": "Theme--Select the theme used in Mark Text",
|
"description": "Theme--Select the theme used in Mark Text",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
||||||
"imageInsertAction": {
|
"imageInsertAction": {
|
||||||
"description": "Image--The default behavior after insert image from local folder",
|
"description": "Image--The default behavior after insert image from local folder",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -168,5 +175,17 @@
|
|||||||
"folder",
|
"folder",
|
||||||
"path"
|
"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 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 {
|
class BaseWindow extends EventEmitter {
|
||||||
|
|
||||||
@ -10,26 +33,43 @@ class BaseWindow extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
|
|
||||||
this._accessor = accessor
|
this._accessor = accessor
|
||||||
this.type = WindowType.BASE
|
|
||||||
this.id = null
|
this.id = null
|
||||||
this.browserWindow = 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 () {
|
destroy () {
|
||||||
this.quitting = true
|
this.lifecycle = WindowLifecycle.QUITTED
|
||||||
this.emit('bye')
|
this.emit('window-closed')
|
||||||
|
|
||||||
this.removeAllListeners()
|
this.removeAllListeners()
|
||||||
|
if (this.browserWindow) {
|
||||||
this.browserWindow.destroy()
|
this.browserWindow.destroy()
|
||||||
this.browserWindow = null
|
this.browserWindow = null
|
||||||
|
}
|
||||||
this.id = null
|
this.id = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- private ---------------------------------
|
// --- private ---------------------------------
|
||||||
|
|
||||||
_buildUrlWithSettings (windowId, env, userPreference) {
|
_buildUrlWithSettings (windowId, env, userPreference) {
|
||||||
// NOTE: Only send absolutely necessary values. Theme and titlebar settings
|
// NOTE: Only send absolutely necessary values. Full settings are delay loaded.
|
||||||
// are sended because we delay load the preferences.
|
|
||||||
const { type } = this
|
const { type } = this
|
||||||
const { debug, paths } = env
|
const { debug, paths } = env
|
||||||
const { codeFontFamily, codeFontSize, theme, titleBarStyle } = userPreference.getAll()
|
const { codeFontFamily, codeFontSize, theme, titleBarStyle } = userPreference.getAll()
|
||||||
@ -50,7 +90,11 @@ class BaseWindow extends EventEmitter {
|
|||||||
url.searchParams.set('theme', theme)
|
url.searchParams.set('theme', theme)
|
||||||
url.searchParams.set('tbs', titleBarStyle)
|
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 path from 'path'
|
||||||
import BaseWindow from './base'
|
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||||
import { BrowserWindow, ipcMain } from 'electron'
|
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { WindowType } from '../app/windowManager'
|
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
||||||
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux, isOsx } from '../config'
|
|
||||||
import { isDirectory, isMarkdownFile, normalizeAndResolvePath } from '../filesystem'
|
|
||||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
|
||||||
import { ensureWindowPosition } from './utils'
|
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 {
|
class EditorWindow extends BaseWindow {
|
||||||
|
|
||||||
@ -17,22 +16,29 @@ class EditorWindow extends BaseWindow {
|
|||||||
constructor (accessor) {
|
constructor (accessor) {
|
||||||
super(accessor)
|
super(accessor)
|
||||||
this.type = WindowType.EDITOR
|
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.
|
* Creates a new editor window.
|
||||||
*
|
*
|
||||||
* @param {string} [pathname] Path to a file, directory or link.
|
* @param {string} [rootDirectory] The root directory to open.
|
||||||
* @param {string} [markdown] Markdown content.
|
* @param {string[]} [fileList] A list of markdown files to open.
|
||||||
* @param {*} [options] BrowserWindow options.
|
* @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
|
const { menu: appMenu, env, preferences } = this._accessor
|
||||||
|
const addBlankTab = !rootDirectory && fileList.length === 0 && markdownList.length === 0
|
||||||
// Ensure path is normalized
|
|
||||||
if (pathname) {
|
|
||||||
pathname = normalizeAndResolvePath(pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainWindowState = windowStateKeeper({
|
const mainWindowState = windowStateKeeper({
|
||||||
defaultWidth: 1200,
|
defaultWidth: 1200,
|
||||||
@ -40,13 +46,19 @@ class EditorWindow extends BaseWindow {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { x, y, width, height } = ensureWindowPosition(mainWindowState)
|
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) {
|
if (isLinux) {
|
||||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
winOptions.icon = path.join(__static, 'logo-96px.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable native or custom/frameless window and titlebar
|
// Enable native or custom/frameless window and titlebar
|
||||||
const { titleBarStyle } = preferences.getAll()
|
const {
|
||||||
|
titleBarStyle,
|
||||||
|
theme,
|
||||||
|
sideBarVisibility,
|
||||||
|
tabBarVisibility,
|
||||||
|
sourceCodeModeEnabled
|
||||||
|
} = preferences.getAll()
|
||||||
if (!isOsx) {
|
if (!isOsx) {
|
||||||
winOptions.titleBarStyle = 'default'
|
winOptions.titleBarStyle = 'default'
|
||||||
if (titleBarStyle === 'native') {
|
if (titleBarStyle === 'native') {
|
||||||
@ -54,35 +66,58 @@ class EditorWindow extends BaseWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
|
||||||
|
|
||||||
let win = this.browserWindow = new BrowserWindow(winOptions)
|
let win = this.browserWindow = new BrowserWindow(winOptions)
|
||||||
this.id = win.id
|
this.id = win.id
|
||||||
|
|
||||||
// Create a menu for the current window
|
// Create a menu for the current window
|
||||||
appMenu.addEditorMenu(win)
|
appMenu.addEditorMenu(win, { sourceCodeModeEnabled })
|
||||||
|
|
||||||
win.once('ready-to-show', async () => {
|
win.webContents.once('did-finish-load', () => {
|
||||||
mainWindowState.manage(win)
|
this.lifecycle = WindowLifecycle.READY
|
||||||
win.show()
|
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()
|
const lineEnding = preferences.getPreferedEOL()
|
||||||
win.webContents.send('mt::bootstrap-blank-window', {
|
|
||||||
lineEnding,
|
|
||||||
markdown
|
|
||||||
})
|
|
||||||
appMenu.updateLineEndingMenu(lineEnding)
|
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', () => {
|
win.on('focus', () => {
|
||||||
@ -114,94 +149,295 @@ class EditorWindow extends BaseWindow {
|
|||||||
|
|
||||||
// The window is now destroyed.
|
// The window is now destroyed.
|
||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
|
this.lifecycle = WindowLifecycle.QUITTED
|
||||||
this.emit('window-closed')
|
this.emit('window-closed')
|
||||||
|
|
||||||
// Free window reference
|
// Free window reference
|
||||||
win = null
|
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)
|
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
|
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 { browserWindow } = this
|
||||||
const { menu: appMenu, preferences } = this._accessor
|
const { preferences } = this._accessor
|
||||||
|
const eol = preferences.getPreferedEOL()
|
||||||
|
|
||||||
// Listen for file changed.
|
for (let i = 0; i < fileList.length; ++i) {
|
||||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
const filePath = fileList[i]
|
||||||
|
const selected = i === selectedIndex
|
||||||
loadMarkdownFile(filePath, preferences.getPreferedEOL()).then(rawDocument => {
|
loadMarkdownFile(filePath, eol).then(rawDocument => {
|
||||||
appMenu.addRecentlyUsedDocument(filePath)
|
if (this.lifecycle === WindowLifecycle.READY) {
|
||||||
browserWindow.webContents.send('AGANI::new-tab', rawDocument, selectTab)
|
this._doOpenTab(rawDocument, selected)
|
||||||
|
} else {
|
||||||
|
this._filesToOpen.push({ doc: rawDocument, selected })
|
||||||
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// TODO: Handle error --> create a end-user error handler.
|
|
||||||
console.error('[ERROR] Cannot open file or directory.')
|
console.error('[ERROR] Cannot open file or directory.')
|
||||||
log.error(err)
|
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
|
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)
|
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 ---------------------------------
|
// --- private ---------------------------------
|
||||||
|
|
||||||
// Only called once during window bootstrapping.
|
_getPreferredBackgroundColor (theme) {
|
||||||
_openFile = async filePath => {
|
// Hardcode the theme background color and show the window direct for the fastet window ready time.
|
||||||
const { browserWindow } = this
|
// Later with custom themes we need the background color (e.g. from meta information) and wait
|
||||||
const { menu: appMenu, preferences } = this._accessor
|
// that the window is loaded and then pass theme data to the renderer.
|
||||||
|
switch (theme) {
|
||||||
const data = await loadMarkdownFile(filePath, preferences.getPreferedEOL())
|
case 'dark':
|
||||||
const {
|
return '#282828'
|
||||||
markdown,
|
case 'material-dark':
|
||||||
filename,
|
return '#34393f'
|
||||||
pathname,
|
case 'ulysses':
|
||||||
encoding,
|
return '#f3f3f3'
|
||||||
lineEnding,
|
case 'graphite':
|
||||||
adjustLineEndingOnSave,
|
return '#f7f7f7'
|
||||||
isMixedLineEndings
|
case 'one-dark':
|
||||||
} = data
|
return '#282c34'
|
||||||
|
case 'light':
|
||||||
appMenu.updateLineEndingMenu(lineEnding)
|
default:
|
||||||
browserWindow.webContents.send('mt::bootstrap-window', {
|
return '#ffffff'
|
||||||
markdown,
|
|
||||||
filename,
|
|
||||||
pathname,
|
|
||||||
options: {
|
|
||||||
encoding,
|
|
||||||
lineEnding,
|
|
||||||
adjustLineEndingOnSave
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
// Listen for file changed.
|
||||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
ipcMain.emit('watcher-watch-file', browserWindow, pathname)
|
||||||
|
|
||||||
// Notify user about mixed endings
|
appMenu.addRecentlyUsedDocument(pathname)
|
||||||
if (isMixedLineEndings) {
|
_openedFiles.push(pathname)
|
||||||
browserWindow.webContents.send('AGANI::show-notification', {
|
browserWindow.webContents.send('AGANI::new-tab', rawDocument, selected)
|
||||||
title: 'Mixed Line Endings',
|
|
||||||
type: 'error',
|
|
||||||
message: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
|
||||||
time: 20000
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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 path from 'path'
|
||||||
import BaseWindow from './base'
|
|
||||||
import { BrowserWindow, ipcMain } from 'electron'
|
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'
|
import { TITLE_BAR_HEIGHT, defaultPreferenceWinOptions, isLinux, isOsx } from '../config'
|
||||||
|
|
||||||
|
|
||||||
class SettingWindow extends BaseWindow {
|
class SettingWindow extends BaseWindow {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,6 +22,7 @@ class SettingWindow extends BaseWindow {
|
|||||||
createWindow (options = {}) {
|
createWindow (options = {}) {
|
||||||
const { menu: appMenu, env, preferences } = this._accessor
|
const { menu: appMenu, env, preferences } = this._accessor
|
||||||
const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
|
const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
|
||||||
|
centerWindowOptions(winOptions)
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
winOptions.icon = path.join(__static, 'logo-96px.png')
|
||||||
}
|
}
|
||||||
@ -42,10 +42,10 @@ class SettingWindow extends BaseWindow {
|
|||||||
// Create a menu for the current window
|
// Create a menu for the current window
|
||||||
appMenu.addSettingMenu(win)
|
appMenu.addSettingMenu(win)
|
||||||
|
|
||||||
win.once('ready-to-show', async () => {
|
win.once('ready-to-show', () => {
|
||||||
win.show()
|
win.show()
|
||||||
|
this.lifecycle = WindowLifecycle.READY
|
||||||
this.emit('window-ready-to-show')
|
this.emit('window-ready')
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('focus', () => {
|
win.on('focus', () => {
|
||||||
@ -74,7 +74,8 @@ class SettingWindow extends BaseWindow {
|
|||||||
win = null
|
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)
|
win.setSheetOffset(TITLE_BAR_HEIGHT)
|
||||||
|
|
||||||
return win
|
return win
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
import { screen } from 'electron'
|
import { screen } from 'electron'
|
||||||
import { isLinux } from '../config'
|
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 => {
|
export const ensureWindowPosition = windowState => {
|
||||||
// "workArea" doesn't work on Linux
|
// "workArea" doesn't work on Linux
|
||||||
const { bounds, workArea } = screen.getPrimaryDisplay()
|
const { bounds, workArea } = screen.getPrimaryDisplay()
|
||||||
@ -21,7 +30,6 @@ export const ensureWindowPosition = windowState => {
|
|||||||
.some(display => display)
|
.some(display => display)
|
||||||
}
|
}
|
||||||
if (center) {
|
if (center) {
|
||||||
// win.center() doesn't work on Linux
|
|
||||||
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
||||||
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
--maskColor: rgba(255, 255, 255, .7);
|
--maskColor: rgba(255, 255, 255, .7);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar,
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
background: var(--floatHoverColor);
|
background: var(--floatHoverColor);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar:vertical {
|
::-webkit-scrollbar:vertical {
|
||||||
|
@ -12,7 +12,7 @@ body article.print-container {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
background: transparent !important;
|
background: #ffffff !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,8 @@
|
|||||||
--editorAreaWidth: 750px;
|
--editorAreaWidth: 750px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar,
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
background: var(--editorBgColor);
|
background: var(--editorBgColor);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar:vertical {
|
::-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)
|
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 { debug, initialState, userDataPath, windowId, type } = parseUrlArgs()
|
||||||
const marktext = {
|
const marktext = {
|
||||||
initialState,
|
initialState,
|
||||||
|
@ -142,6 +142,7 @@
|
|||||||
'theme': state => state.preferences.theme,
|
'theme': state => state.preferences.theme,
|
||||||
|
|
||||||
'currentFile': state => state.editor.currentFile,
|
'currentFile': state => state.editor.currentFile,
|
||||||
|
|
||||||
// edit modes
|
// edit modes
|
||||||
'typewriter': state => state.preferences.typewriter,
|
'typewriter': state => state.preferences.typewriter,
|
||||||
'focus': state => state.preferences.focus,
|
'focus': state => state.preferences.focus,
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newFile () {
|
newFile () {
|
||||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
this.$store.dispatch('NEW_UNTITLED_TAB', {})
|
||||||
},
|
},
|
||||||
handleTabScroll (event) {
|
handleTabScroll (event) {
|
||||||
// Use mouse wheel value first but prioritize X value more (e.g. touchpad input).
|
// Use mouse wheel value first but prioritize X value more (e.g. touchpad input).
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
newFile () {
|
newFile () {
|
||||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
this.$store.dispatch('NEW_UNTITLED_TAB', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
return
|
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)
|
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
|
// Decode source map in production - must be registered first
|
||||||
|
|
||||||
addElementStyle()
|
|
||||||
|
|
||||||
sourceMapSupport.install({
|
sourceMapSupport.install({
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
handleUncaughtExceptions: false,
|
handleUncaughtExceptions: false,
|
||||||
@ -53,6 +50,8 @@ sourceMapSupport.install({
|
|||||||
global.marktext = {}
|
global.marktext = {}
|
||||||
bootstrapRenderer()
|
bootstrapRenderer()
|
||||||
|
|
||||||
|
addElementStyle()
|
||||||
|
|
||||||
// -----------------------------------------------
|
// -----------------------------------------------
|
||||||
// Be careful when changing code before this line!
|
// Be careful when changing code before this line!
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@ export const tabsMixins = {
|
|||||||
removeFileInTab (file) {
|
removeFileInTab (file) {
|
||||||
const { isSaved } = file
|
const { isSaved } = file
|
||||||
if (isSaved) {
|
if (isSaved) {
|
||||||
this.$store.dispatch('REMOVE_FILE_IN_TABS', file)
|
this.$store.dispatch('FORCE_CLOSE_TAB', file)
|
||||||
} else {
|
} 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 = {
|
export const fileMixins = {
|
||||||
methods: {
|
methods: {
|
||||||
handleFileClick () {
|
handleFileClick () {
|
||||||
|
// HACK: Please see #1034 and #1035
|
||||||
const { data, isMarkdown, pathname } = this.file
|
const { data, isMarkdown, pathname } = this.file
|
||||||
if (!isMarkdown || this.currentFile.pathname === pathname) return
|
if (!isMarkdown || this.currentFile.pathname === pathname) return
|
||||||
const { isMixedLineEndings, filename, lineEnding } = data
|
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)
|
const fileState = isOpened || getFileStateFromData(data)
|
||||||
this.$store.dispatch('UPDATE_CURRENT_FILE', fileState)
|
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)
|
ipcRenderer.send('mt::add-recently-used-document', pathname)
|
||||||
|
|
||||||
if (isMixedLineEndings && !isOpened) {
|
if (isMixedLineEndings && !isOpened) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="editor-container"
|
class="editor-container"
|
||||||
>
|
>
|
||||||
<side-bar></side-bar>
|
<side-bar v-if="init"></side-bar>
|
||||||
<div class="editor-middle">
|
<div class="editor-middle">
|
||||||
<title-bar
|
<title-bar
|
||||||
:project="projectTree"
|
:project="projectTree"
|
||||||
@ -13,6 +13,7 @@
|
|||||||
:platform="platform"
|
:platform="platform"
|
||||||
:is-saved="isSaved"
|
:is-saved="isSaved"
|
||||||
></title-bar>
|
></title-bar>
|
||||||
|
<div class="editor-placeholder" v-if="!init"></div>
|
||||||
<recent
|
<recent
|
||||||
v-if="!hasCurrentFile && init"
|
v-if="!hasCurrentFile && init"
|
||||||
></recent>
|
></recent>
|
||||||
@ -180,6 +181,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.editor-placeholder,
|
||||||
.editor-container {
|
.editor-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -197,6 +199,9 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: -10000px;
|
left: -10000px;
|
||||||
}
|
}
|
||||||
|
.editor-placeholder {
|
||||||
|
background: var(--editorBgColor);
|
||||||
|
}
|
||||||
.editor-middle {
|
.editor-middle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -42,12 +42,12 @@
|
|||||||
:onChange="value => onSelectChange('fileSortBy', value)"
|
:onChange="value => onSelectChange('fileSortBy', value)"
|
||||||
:disable="true"
|
:disable="true"
|
||||||
></cur-select>
|
></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>
|
<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 class="ag-underdevelop" v-model="startUpAction" label="lastState">Open the last window state</el-radio>
|
||||||
<el-radio v-model="startUp" label="folder">Open the subfolder</el-radio>
|
<el-radio v-model="startUpAction" label="folder">Open a default directory</el-radio>
|
||||||
<el-button size="small">Select Folder</el-button>
|
<el-button size="small" @click="selectDefaultDirectoryToOpen">Select Folder</el-button>
|
||||||
<el-radio v-model="startUp" label="blank">Open blank page</el-radio>
|
<el-radio v-model="startUpAction" label="blank">Open blank page</el-radio>
|
||||||
</section>
|
</section>
|
||||||
<cur-select
|
<cur-select
|
||||||
description="The language Mark Text use"
|
description="The language Mark Text use"
|
||||||
@ -95,13 +95,16 @@ export default {
|
|||||||
openFilesInNewWindow: state => state.preferences.openFilesInNewWindow,
|
openFilesInNewWindow: state => state.preferences.openFilesInNewWindow,
|
||||||
aidou: state => state.preferences.aidou,
|
aidou: state => state.preferences.aidou,
|
||||||
fileSortBy: state => state.preferences.fileSortBy,
|
fileSortBy: state => state.preferences.fileSortBy,
|
||||||
startUp: state => state.preferences.startUp,
|
startUpAction: state => state.preferences.startUpAction,
|
||||||
language: state => state.preferences.language
|
language: state => state.preferences.language
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSelectChange (type, value) {
|
onSelectChange (type, value) {
|
||||||
this.$store.dispatch('SET_SINGLE_PREFERENCE', { 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;
|
margin: 0;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
& .startup-ctrl {
|
& .startup-action-ctrl {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
@ -2,7 +2,7 @@ import { clipboard, ipcRenderer, shell } from 'electron'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import bus from '../bus'
|
import bus from '../bus'
|
||||||
import { hasKeys, getUniqueId } from '../util'
|
import { hasKeys, getUniqueId } from '../util'
|
||||||
import { isSameFileSync } from '../util/fileSystem'
|
import { isSamePathSync } from '../util/fileSystem'
|
||||||
import listToTree from '../util/listToTree'
|
import listToTree from '../util/listToTree'
|
||||||
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
||||||
import notice from '../services/notification'
|
import notice from '../services/notification'
|
||||||
@ -39,7 +39,6 @@ const mutations = {
|
|||||||
const { tabs, currentFile } = state
|
const { tabs, currentFile } = state
|
||||||
const index = tabs.indexOf(file)
|
const index = tabs.indexOf(file)
|
||||||
tabs.splice(index, 1)
|
tabs.splice(index, 1)
|
||||||
state.tabs = tabs
|
|
||||||
if (file.id === currentFile.id) {
|
if (file.id === currentFile.id) {
|
||||||
const fileState = state.tabs[index] || state.tabs[index - 1] || state.tabs[0] || {}
|
const fileState = state.tabs[index] || state.tabs[index - 1] || state.tabs[0] || {}
|
||||||
state.currentFile = fileState
|
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.')
|
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) {
|
if (pathname === currentFile.pathname) {
|
||||||
state.currentFile = fileState
|
state.currentFile = tab
|
||||||
const { id, cursor, history } = fileState
|
const { id, cursor, history } = tab
|
||||||
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
|
bus.$emit('file-changed', { id, markdown, cursor, renderCursor: true, history })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SET_PATHNAME (state, file) {
|
// NOTE: Please call this function only from main process via "AGANI::set-pathname" and free resources before!
|
||||||
const { filename, pathname, id } = file
|
SET_PATHNAME (state, { tab, fileInfo }) {
|
||||||
if (id === state.currentFile.id && pathname) {
|
const { currentFile } = state
|
||||||
|
const { filename, pathname, id } = fileInfo
|
||||||
|
|
||||||
|
// Change reference path for images.
|
||||||
|
if (id === currentFile.id && pathname) {
|
||||||
window.DIRNAME = path.dirname(pathname)
|
window.DIRNAME = path.dirname(pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetFile = state.tabs.filter(f => f.id === id)[0]
|
if (tab) {
|
||||||
if (targetFile) {
|
Object.assign(tab, { filename, pathname, isSaved: true })
|
||||||
const isSaved = true
|
}
|
||||||
Object.assign(targetFile, { filename, pathname, isSaved })
|
},
|
||||||
|
SET_SAVE_STATUS_BY_TAB (state, { tab, status }) {
|
||||||
|
if (hasKeys(tab)) {
|
||||||
|
tab.isSaved = status
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SET_SAVE_STATUS (state, status) {
|
SET_SAVE_STATUS (state, status) {
|
||||||
@ -171,25 +173,29 @@ const mutations = {
|
|||||||
state.currentFile.history = history
|
state.currentFile.history = history
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CLOSE_TABS (state, arr) {
|
CLOSE_TABS (state, tabIdList) {
|
||||||
|
if (!tabIdList || tabIdList.length === 0) return
|
||||||
|
|
||||||
let tabIndex = 0
|
let tabIndex = 0
|
||||||
arr.forEach(id => {
|
tabIdList.forEach(id => {
|
||||||
const index = state.tabs.findIndex(f => f.id === 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) {
|
if (pathname) {
|
||||||
// close tab and unwatch this file
|
ipcRenderer.send('mt::window-tab-closed', pathname)
|
||||||
ipcRenderer.send('AGANI::file-watch', { pathname, watch: false })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.tabs.splice(index, 1)
|
state.tabs.splice(index, 1)
|
||||||
if (state.currentFile.id === id) {
|
if (state.currentFile.id === id) {
|
||||||
state.currentFile = {}
|
state.currentFile = {}
|
||||||
window.DIRNAME = ''
|
window.DIRNAME = ''
|
||||||
if (arr.length === 1) {
|
if (tabIdList.length === 1) {
|
||||||
tabIndex = index
|
tabIndex = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!state.currentFile.id && state.tabs.length) {
|
if (!state.currentFile.id && state.tabs.length) {
|
||||||
state.currentFile = state.tabs[tabIndex] || state.tabs[tabIndex - 1] || state.tabs[0] || {}
|
state.currentFile = state.tabs[tabIndex] || state.tabs[tabIndex - 1] || state.tabs[0] || {}
|
||||||
if (typeof state.currentFile.markdown === 'string') {
|
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)
|
commit('REMOVE_FILE_WITHIN_TABS', file)
|
||||||
// unwatch this file
|
|
||||||
const { pathname } = 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) {
|
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 { id, pathname, filename, markdown } = file
|
||||||
const options = getOptionsFromState(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
|
// need pass some data to main process when `save` menu item clicked
|
||||||
@ -315,56 +326,89 @@ const actions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_SET_PATHNAME ({ commit }) {
|
LISTEN_FOR_SET_PATHNAME ({ commit, dispatch, state }) {
|
||||||
ipcRenderer.on('AGANI::set-pathname', (e, file) => {
|
ipcRenderer.on('mt::set-pathname', (e, fileInfo) => {
|
||||||
commit('SET_PATHNAME', file)
|
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 => {
|
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 => {
|
.map(file => {
|
||||||
const { id, filename, pathname, markdown } = file
|
const { id, filename, pathname, markdown } = file
|
||||||
const options = getOptionsFromState(file)
|
const options = getOptionsFromState(file)
|
||||||
return { id, filename, pathname, markdown, options }
|
return { id, filename, pathname, markdown, options }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (unSavedFiles.length) {
|
if (unsavedFiles.length) {
|
||||||
ipcRenderer.send('AGANI::response-close-confirm', unSavedFiles)
|
ipcRenderer.send('mt::close-window-confirm', unsavedFiles)
|
||||||
} else {
|
} else {
|
||||||
ipcRenderer.send('AGANI::close-window')
|
ipcRenderer.send('mt::close-window')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_SAVE_CLOSE ({ commit, state }) {
|
LISTEN_FOR_SAVE_CLOSE ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::save-all-response', (e, { err, data }) => {
|
ipcRenderer.on('mt::force-close-tabs-by-id', (e, tabIdList) => {
|
||||||
if (!err && Array.isArray(data)) {
|
if (Array.isArray(tabIdList) && tabIdList.length) {
|
||||||
const toBeClosedTabs = [...state.tabs.filter(f => f.isSaved), ...data]
|
commit('CLOSE_TABS', tabIdList)
|
||||||
commit('CLOSE_TABS', toBeClosedTabs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ipcRenderer.on('AGANI::save-single-response', (e, { err, data }) => {
|
|
||||||
if (!err && Array.isArray(data) && data.length) {
|
|
||||||
commit('CLOSE_TABS', data)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
ASK_FOR_SAVE_ALL ({ commit, state }, isClose) {
|
ASK_FOR_SAVE_ALL ({ commit, state }, closeTabs) {
|
||||||
const unSavedFiles = state.tabs.filter(file => !(file.isSaved && /[^\n]/.test(file.markdown)))
|
const { tabs } = state
|
||||||
|
const unsavedFiles = tabs
|
||||||
|
.filter(file => !(file.isSaved && /[^\n]/.test(file.markdown)))
|
||||||
.map(file => {
|
.map(file => {
|
||||||
const { id, filename, pathname, markdown } = file
|
const { id, filename, pathname, markdown } = file
|
||||||
const options = getOptionsFromState(file)
|
const options = getOptionsFromState(file)
|
||||||
return { id, filename, pathname, markdown, options }
|
return { id, filename, pathname, markdown, options }
|
||||||
})
|
})
|
||||||
if (unSavedFiles.length) {
|
|
||||||
const EVENT_NAME = isClose ? 'AGANI::save-close' : 'AGANI::save-all'
|
if (closeTabs) {
|
||||||
const isSingle = false
|
if (unsavedFiles.length) {
|
||||||
ipcRenderer.send(EVENT_NAME, unSavedFiles, isSingle)
|
commit('CLOSE_TABS', tabs.filter(f => f.isSaved).map(f => f.id))
|
||||||
} else if (isClose) {
|
ipcRenderer.send('mt::save-and-close-tabs', unsavedFiles)
|
||||||
commit('CLOSE_TABS', state.tabs.map(f => f.id))
|
} 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
|
const { id, pathname, filename } = state.currentFile
|
||||||
if (typeof filename === 'string' && filename !== newFilename) {
|
if (typeof filename === 'string' && filename !== newFilename) {
|
||||||
const newPathname = path.join(path.dirname(pathname), 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.
|
// This events are only used during window creation.
|
||||||
LISTEN_FOR_BOOTSTRAP_WINDOW ({ commit, state, dispatch }) {
|
LISTEN_FOR_BOOTSTRAP_WINDOW ({ commit, state, dispatch }) {
|
||||||
ipcRenderer.on('mt::bootstrap-window', (e, { markdown, filename, pathname, options }) => {
|
ipcRenderer.on('mt::bootstrap-editor', (e, config) => {
|
||||||
const fileState = getSingleFileState({ markdown, filename, pathname, options })
|
const {
|
||||||
const { id } = fileState
|
addBlankTab,
|
||||||
const { lineEnding } = options
|
markdownList,
|
||||||
|
lineEnding,
|
||||||
|
sideBarVisibility,
|
||||||
|
tabBarVisibility,
|
||||||
|
sourceCodeModeEnabled
|
||||||
|
} = config
|
||||||
|
|
||||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
||||||
dispatch('INIT_STATUS', true)
|
dispatch('SEND_INITIALIZED')
|
||||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
|
||||||
bus.$emit('file-loaded', { id, markdown })
|
|
||||||
commit('SET_LAYOUT', {
|
commit('SET_LAYOUT', {
|
||||||
rightColumn: 'files',
|
rightColumn: 'files',
|
||||||
showSideBar: false,
|
showSideBar: !!sideBarVisibility,
|
||||||
showTabBar: false
|
showTabBar: !!tabBarVisibility
|
||||||
})
|
})
|
||||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||||
|
|
||||||
|
commit('SET_MODE', {
|
||||||
|
type: 'sourceCode',
|
||||||
|
checked: !!sourceCodeModeEnabled
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcRenderer.on('mt::bootstrap-blank-window', (e, { lineEnding, markdown: source }) => {
|
if (addBlankTab) {
|
||||||
const { tabs } = state
|
dispatch('NEW_UNTITLED_TAB', {})
|
||||||
const fileState = getBlankFileState(tabs, lineEnding, source)
|
} else if (markdownList.length) {
|
||||||
const { id, markdown } = fileState
|
let isFirst = true
|
||||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
for (const markdown of markdownList) {
|
||||||
dispatch('INIT_STATUS', true)
|
isFirst = false
|
||||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
dispatch('NEW_UNTITLED_TAB', { markdown, selected: isFirst })
|
||||||
bus.$emit('file-loaded', { id, markdown })
|
}
|
||||||
commit('SET_LAYOUT', {
|
}
|
||||||
rightColumn: 'files',
|
|
||||||
showSideBar: false,
|
|
||||||
showTabBar: false
|
|
||||||
})
|
|
||||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Open a new tab, optionally with content.
|
// Open a new tab, optionally with content.
|
||||||
LISTEN_FOR_NEW_TAB ({ dispatch }) {
|
LISTEN_FOR_NEW_TAB ({ dispatch }) {
|
||||||
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument, selectTab=true) => {
|
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument, selected=true) => {
|
||||||
// TODO: allow to add a tab without selecting it
|
|
||||||
if (markdownDocument) {
|
if (markdownDocument) {
|
||||||
// Create tab with content.
|
// Create tab with content.
|
||||||
dispatch('NEW_TAB_WITH_CONTENT', markdownDocument)
|
dispatch('NEW_TAB_WITH_CONTENT', { markdownDocument, selected })
|
||||||
} else {
|
} else {
|
||||||
// Fallback: create a blank tab
|
// Fallback: create a blank tab and always select it
|
||||||
dispatch('NEW_UNTITLED_TAB')
|
dispatch('NEW_UNTITLED_TAB', {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcRenderer.on('mt::new-untitled-tab', (e, selectTab=true, markdownString='', ) => {
|
ipcRenderer.on('mt::new-untitled-tab', (e, selected=true, markdown='', ) => {
|
||||||
// TODO: allow to add a tab without selecting it
|
|
||||||
// Create a blank tab
|
// 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 => {
|
ipcRenderer.on('AGANI::close-tab', e => {
|
||||||
const file = state.currentFile
|
const file = state.currentFile
|
||||||
if (!hasKeys(file)) return
|
if (!hasKeys(file)) return
|
||||||
const { isSaved } = file
|
dispatch('CLOSE_TAB', file)
|
||||||
if (isSaved) {
|
|
||||||
dispatch('REMOVE_FILE_IN_TABS', file)
|
|
||||||
} else {
|
|
||||||
dispatch('CLOSE_SINGLE_FILE', file)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create a new untitled tab optional with markdown string.
|
CLOSE_TAB ({ dispatch }, file) {
|
||||||
NEW_UNTITLED_TAB ({ commit, state, dispatch }, markdownString) {
|
const { isSaved } = file
|
||||||
dispatch('SHOW_TAB_VIEW', false)
|
if (isSaved) {
|
||||||
const { tabs, lineEnding } = state
|
dispatch('FORCE_CLOSE_TAB', file)
|
||||||
const fileState = getBlankFileState(tabs, lineEnding, markdownString)
|
} else {
|
||||||
const { id, markdown } = fileState
|
dispatch('CLOSE_UNSAVED_TAB', file)
|
||||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
}
|
||||||
bus.$emit('file-loaded', { id, markdown })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new tab from the given markdown document
|
* Create a new untitled tab optional from a markdown string.
|
||||||
*
|
*
|
||||||
* @param {*} context Store context
|
* @param {*} context The store context.
|
||||||
* @param {IMarkdownDocumentRaw} markdownDocument Class that represent a markdown document
|
* @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) {
|
if (!markdownDocument) {
|
||||||
console.warn('Cannot create a file tab without a markdown document!')
|
console.warn('Cannot create a file tab without a markdown document!')
|
||||||
dispatch('NEW_UNTITLED_TAB')
|
dispatch('NEW_UNTITLED_TAB', {})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if tab already exist and select existing tab if so.
|
// Select the tab if not value is specified.
|
||||||
const { tabs } = state
|
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 { pathname } = markdownDocument
|
||||||
const existingTab = tabs.find(t => isSameFileSync(t.pathname, pathname))
|
const existingTab = tabs.find(t => isSamePathSync(t.pathname, pathname))
|
||||||
if (existingTab) {
|
if (existingTab) {
|
||||||
dispatch('UPDATE_CURRENT_FILE', existingTab)
|
dispatch('UPDATE_CURRENT_FILE', existingTab)
|
||||||
return
|
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)
|
dispatch('SHOW_TAB_VIEW', false)
|
||||||
|
}
|
||||||
|
|
||||||
const { markdown, isMixedLineEndings } = markdownDocument
|
const { markdown, isMixedLineEndings } = markdownDocument
|
||||||
const docState = createDocumentState(markdownDocument)
|
const docState = createDocumentState(markdownDocument)
|
||||||
const { id } = docState
|
const { id } = docState
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
dispatch('UPDATE_CURRENT_FILE', docState)
|
dispatch('UPDATE_CURRENT_FILE', docState)
|
||||||
bus.$emit('file-loaded', { id, markdown })
|
bus.$emit('file-loaded', { id, markdown })
|
||||||
|
} else {
|
||||||
|
commit('ADD_FILE_TO_TABS', docState)
|
||||||
|
}
|
||||||
|
|
||||||
if (isMixedLineEndings) {
|
if (isMixedLineEndings) {
|
||||||
|
// TODO: Show (this) notification(s) per tab.
|
||||||
const { filename, lineEnding } = markdownDocument
|
const { filename, lineEnding } = markdownDocument
|
||||||
notice.notify({
|
notice.notify({
|
||||||
title: 'Line Ending',
|
title: 'Line Ending',
|
||||||
@ -540,7 +629,7 @@ const actions = {
|
|||||||
|
|
||||||
SHOW_TAB_VIEW ({ commit, state, dispatch }, always) {
|
SHOW_TAB_VIEW ({ commit, state, dispatch }, always) {
|
||||||
const { tabs } = state
|
const { tabs } = state
|
||||||
if (always || tabs.length <= 1) {
|
if (always || tabs.length === 1) {
|
||||||
commit('SET_LAYOUT', { showTabBar: true })
|
commit('SET_LAYOUT', { showTabBar: true })
|
||||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||||
}
|
}
|
||||||
@ -676,21 +765,32 @@ const actions = {
|
|||||||
|
|
||||||
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
||||||
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
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.
|
// TODO: A new "changed" notification from different files overwrite the old notification
|
||||||
if (type === 'unlink') {
|
// and the old notification disappears. I think we should bind the notification to the tab.
|
||||||
return notice.notify({
|
|
||||||
|
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',
|
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',
|
type: 'warning',
|
||||||
time: 0,
|
time: 0,
|
||||||
showConfirm: false
|
showConfirm: false
|
||||||
})
|
})
|
||||||
} else {
|
break
|
||||||
|
}
|
||||||
|
case 'add':
|
||||||
|
case 'change': {
|
||||||
const { autoSave } = rootState.preferences
|
const { autoSave } = rootState.preferences
|
||||||
const { windowActive } = rootState
|
|
||||||
const { filename } = change.data
|
const { filename } = change.data
|
||||||
if (windowActive) return
|
|
||||||
if (autoSave) {
|
if (autoSave) {
|
||||||
commit('LOAD_CHANGE', change)
|
commit('LOAD_CHANGE', change)
|
||||||
} else {
|
} else {
|
||||||
@ -705,14 +805,14 @@ const actions = {
|
|||||||
commit('LOAD_CHANGE', change)
|
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 }) {
|
ASK_FOR_IMAGE_PATH ({ commit }) {
|
||||||
return ipcRenderer.sendSync('mt::ask-for-image-path')
|
return ipcRenderer.sendSync('mt::ask-for-image-path')
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,8 @@ Vue.use(Vuex)
|
|||||||
const state = {
|
const state = {
|
||||||
platform: process.platform, // platform of system `darwin` | `win32` | `linux`
|
platform: process.platform, // platform of system `darwin` | `win32` | `linux`
|
||||||
appVersion: process.versions.MARKTEXT_VERSION_STRING, // Mark Text version string
|
appVersion: process.versions.MARKTEXT_VERSION_STRING, // Mark Text version string
|
||||||
windowActive: true, // weather current window is active or focused
|
windowActive: true, // whether current window is active or focused
|
||||||
// TODO: "init" is nowhere used
|
init: false, // whether Mark Text is initialized
|
||||||
init: process.env.NODE_ENV === 'development' // whether Mark Text is inited
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {}
|
const getters = {}
|
||||||
@ -29,8 +28,8 @@ const mutations = {
|
|||||||
SET_WIN_STATUS (state, status) {
|
SET_WIN_STATUS (state, status) {
|
||||||
state.windowActive = status
|
state.windowActive = status
|
||||||
},
|
},
|
||||||
SET_INIT_STATUS (state, status) {
|
SET_INITIALIZED (state) {
|
||||||
state.init = status
|
state.init = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +39,9 @@ const actions = {
|
|||||||
commit('SET_WIN_STATUS', status)
|
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 getters = {}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {}
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
LISTEN_FOR_NOTIFICATION ({ commit }) {
|
LISTEN_FOR_NOTIFICATION ({ commit }) {
|
||||||
|
@ -3,13 +3,15 @@ import { getOptionsFromState } from './help'
|
|||||||
|
|
||||||
// user preference
|
// user preference
|
||||||
const state = {
|
const state = {
|
||||||
autoSave: true,
|
autoSave: false,
|
||||||
autoSaveDelay: 3000,
|
autoSaveDelay: 5000,
|
||||||
titleBarStyle: 'csd',
|
titleBarStyle: 'custom',
|
||||||
openFilesInNewWindow: false,
|
openFilesInNewWindow: false,
|
||||||
|
openFolderInNewWindow: false,
|
||||||
aidou: true,
|
aidou: true,
|
||||||
fileSortBy: 'created',
|
fileSortBy: 'created',
|
||||||
startUp: 'folder',
|
startUpAction: 'lastState',
|
||||||
|
defaultDirectoryToOpen: '',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
|
|
||||||
editorFontFamily: 'Open Sans',
|
editorFontFamily: 'Open Sans',
|
||||||
@ -33,7 +35,13 @@ const state = {
|
|||||||
listIndentation: 1,
|
listIndentation: 1,
|
||||||
|
|
||||||
theme: 'light',
|
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
|
typewriter: false, // typewriter mode
|
||||||
focus: false, // focus mode
|
focus: false, // focus mode
|
||||||
sourceCode: false, // source code mode
|
sourceCode: false, // source code mode
|
||||||
@ -101,7 +109,6 @@ const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
SET_SINGLE_PREFERENCE ({ commit }, { type, value }) {
|
SET_SINGLE_PREFERENCE ({ commit }, { type, value }) {
|
||||||
// commit('SET_USER_PREFERENCE', { [type]: value })
|
|
||||||
// save to electron-store
|
// save to electron-store
|
||||||
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
||||||
},
|
},
|
||||||
@ -112,6 +119,10 @@ const actions = {
|
|||||||
|
|
||||||
SET_IMAGE_FOLDER_PATH ({ commit }) {
|
SET_IMAGE_FOLDER_PATH ({ commit }) {
|
||||||
ipcRenderer.send('mt::ask-for-modify-image-folder-path')
|
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 = {
|
const mutations = {
|
||||||
SET_PROJECT_TREE (state, pathname) {
|
SET_ROOT_DIRECTORY (state, pathname) {
|
||||||
let name = path.basename(pathname)
|
let name = path.basename(pathname)
|
||||||
if (!name) {
|
if (!name) {
|
||||||
// Root directory such "/" or "C:\"
|
// Root directory such "/" or "C:\"
|
||||||
@ -108,12 +108,8 @@ const mutations = {
|
|||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
LISTEN_FOR_LOAD_PROJECT ({ commit, dispatch }) {
|
LISTEN_FOR_LOAD_PROJECT ({ commit, dispatch }) {
|
||||||
ipcRenderer.on('AGANI::open-project', (e, pathname) => {
|
ipcRenderer.on('mt::open-directory', (e, pathname) => {
|
||||||
// Initialize editor and show empty/new tab
|
commit('SET_ROOT_DIRECTORY', pathname)
|
||||||
dispatch('NEW_UNTITLED_TAB')
|
|
||||||
|
|
||||||
dispatch('INIT_STATUS', true)
|
|
||||||
commit('SET_PROJECT_TREE', pathname)
|
|
||||||
commit('SET_LAYOUT', {
|
commit('SET_LAYOUT', {
|
||||||
rightColumn: 'files',
|
rightColumn: 'files',
|
||||||
showSideBar: true,
|
showSideBar: true,
|
||||||
@ -163,7 +159,7 @@ const actions = {
|
|||||||
commit('SET_CLIPBOARD', data)
|
commit('SET_CLIPBOARD', data)
|
||||||
},
|
},
|
||||||
ASK_FOR_OPEN_PROJECT ({ commit }) {
|
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 }) {
|
LISTEN_FOR_SIDEBAR_CONTEXT_MENU ({ commit, state }) {
|
||||||
bus.$on('SIDEBAR::show-in-folder', () => {
|
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.
|
* Check if the both paths point to the same file.
|
||||||
*
|
*
|
||||||
* @param {*} pathA The first path.
|
* @param {string} pathA The first path.
|
||||||
* @param {*} pathB The second path.
|
* @param {string} pathB The second path.
|
||||||
* @param {*} isNormalized Are both paths already normalized.
|
* @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
|
if (!pathA || !pathB) return false
|
||||||
const a = isNormalized ? pathA : path.normalize(pathA)
|
const a = isNormalized ? pathA : path.normalize(pathA)
|
||||||
const b = isNormalized ? pathB : path.normalize(pathB)
|
const b = isNormalized ? pathB : path.normalize(pathB)
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
"autoSaveDelay": 5000,
|
"autoSaveDelay": 5000,
|
||||||
"titleBarStyle": "custom",
|
"titleBarStyle": "custom",
|
||||||
"openFilesInNewWindow": false,
|
"openFilesInNewWindow": false,
|
||||||
|
"openFolderInNewWindow": false,
|
||||||
"aidou": true,
|
"aidou": true,
|
||||||
"fileSortBy": "created",
|
"fileSortBy": "created",
|
||||||
"startUp": "folder",
|
"startUpAction": "lastState",
|
||||||
|
"defaultDirectoryToOpen": "",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
|
|
||||||
"editorFontFamily": "Open Sans",
|
"editorFontFamily": "Open Sans",
|
||||||
@ -28,5 +30,9 @@
|
|||||||
"tabSize": 4,
|
"tabSize": 4,
|
||||||
"listIndentation": 1,
|
"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"
|
electron-download "^4.1.0"
|
||||||
extract-zip "^1.6.5"
|
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:
|
electron-devtools-installer@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-2.2.4.tgz#261a50337e37121d338b966f07922eb4939a8763"
|
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"
|
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
|
||||||
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
|
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:
|
electron-log@^3.0.5:
|
||||||
version "3.0.5"
|
version "3.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-3.0.5.tgz#9bdd307f1f1aec85c0873babd6bdfffb1a661436"
|
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"
|
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
|
||||||
integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=
|
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:
|
event-kit@^2.0.0:
|
||||||
version "2.5.3"
|
version "2.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/event-kit/-/event-kit-2.5.3.tgz#d47e4bc116ec0aacd00263791fa1a55eb5e79ba1"
|
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"
|
event-kit "^2.0.0"
|
||||||
nan "^2.10.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:
|
keyboardevents-areequal@^0.2.1:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
|
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"
|
source-map-resolve "^0.5.0"
|
||||||
use "^3.1.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:
|
socket.io-adapter@~1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
|
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
|
||||||
|
Loading…
Reference in New Issue
Block a user