mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Desktop: Allow selecting editor path with dialog window
This commit is contained in:
parent
bda3ea9a35
commit
cc8f8fcd2c
@ -1,6 +1,6 @@
|
|||||||
require('app-module-path').addPath(__dirname);
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
const { friendlySafeFilename } = require('lib/path-utils.js');
|
const { extractExecutablePath, quotePath, unquotePath, friendlySafeFilename } = require('lib/path-utils.js');
|
||||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
@ -36,4 +36,41 @@ describe('pathUtils', function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should quote and unquote paths', async (done) => {
|
||||||
|
const testCases = [
|
||||||
|
['', ''],
|
||||||
|
['/my/path', '/my/path'],
|
||||||
|
['/my/path with spaces', '"/my/path with spaces"'],
|
||||||
|
['/my/weird"path', '"/my/weird\\"path"'],
|
||||||
|
['c:\\Windows\\test.dll', 'c:\\Windows\\test.dll'],
|
||||||
|
['c:\\Windows\\test test.dll', '"c:\\Windows\\test test.dll"'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
|
const t = testCases[i];
|
||||||
|
expect(quotePath(t[0])).toBe(t[1]);
|
||||||
|
expect(unquotePath(quotePath(t[0]))).toBe(t[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract executable path from command', async (done) => {
|
||||||
|
const testCases = [
|
||||||
|
['', ''],
|
||||||
|
['/my/cmd -some -args', '/my/cmd'],
|
||||||
|
['"/my/cmd" -some -args', '"/my/cmd"'],
|
||||||
|
['"/my/cmd"', '"/my/cmd"'],
|
||||||
|
['"/my/cmd and space" -some -flags', '"/my/cmd and space"'],
|
||||||
|
['"" -some -flags', '""'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
|
const t = testCases[i];
|
||||||
|
expect(extractExecutablePath(t[0])).toBe(t[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -7,6 +7,7 @@ const { Header } = require('./Header.min.js');
|
|||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
const pathUtils = require('lib/path-utils.js');
|
const pathUtils = require('lib/path-utils.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { commandArgumentsToString } = require('lib/string-utils');
|
||||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||||
const shared = require('lib/components/shared/config-shared.js');
|
const shared = require('lib/components/shared/config-shared.js');
|
||||||
|
|
||||||
@ -95,6 +96,15 @@ class ConfigScreenComponent extends React.Component {
|
|||||||
color: theme.color,
|
color: theme.color,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const subLabel = Object.assign({}, labelStyle, {
|
||||||
|
opacity: 0.7,
|
||||||
|
marginBottom: Math.round(rowStyle.marginBottom * 0.7),
|
||||||
|
});
|
||||||
|
|
||||||
|
const invisibleLabel = Object.assign({}, labelStyle, {
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const controlStyle = {
|
const controlStyle = {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
color: theme.color,
|
color: theme.color,
|
||||||
@ -109,6 +119,7 @@ class ConfigScreenComponent extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateSettingValue = (key, value) => {
|
const updateSettingValue = (key, value) => {
|
||||||
|
// console.info(key + ' = ' + value);
|
||||||
return shared.updateSettingValue(this, key, value);
|
return shared.updateSettingValue(this, key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,23 +169,93 @@ class ConfigScreenComponent extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (md.type === Setting.TYPE_STRING) {
|
} else if (md.type === Setting.TYPE_STRING) {
|
||||||
const onTextChange = (event) => {
|
|
||||||
updateSettingValue(key, event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputStyle = Object.assign({}, controlStyle, {
|
const inputStyle = Object.assign({}, controlStyle, {
|
||||||
width: '50%',
|
width: '50%',
|
||||||
minWidth: '20em',
|
minWidth: '20em',
|
||||||
border: '1px solid' });
|
border: '1px solid' });
|
||||||
const inputType = md.secure === true ? 'password' : 'text';
|
const inputType = md.secure === true ? 'password' : 'text';
|
||||||
|
|
||||||
return (
|
if (md.subType === 'file_path_and_args') {
|
||||||
<div key={key} style={rowStyle}>
|
inputStyle.marginBottom = subLabel.marginBottom;
|
||||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
|
||||||
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
const splitCmd = cmdString => {
|
||||||
{ descriptionComp }
|
const path = pathUtils.extractExecutablePath(cmdString);
|
||||||
</div>
|
const args = cmdString.substr(path.length + 1);
|
||||||
);
|
return [pathUtils.unquotePath(path), args];
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinCmd = cmdArray => {
|
||||||
|
if (!cmdArray[0] && !cmdArray[1]) return '';
|
||||||
|
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
||||||
|
if (!cmdString) cmdString = '""';
|
||||||
|
if (cmdArray[1]) cmdString += ' ' + cmdArray[1];
|
||||||
|
return cmdString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPathChange = event => {
|
||||||
|
const cmd = splitCmd(this.state.settings[key]);
|
||||||
|
cmd[0] = event.target.value;
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onArgsChange = event => {
|
||||||
|
const cmd = splitCmd(this.state.settings[key]);
|
||||||
|
cmd[1] = event.target.value;
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
const browseButtonClick = () => {
|
||||||
|
const paths = bridge().showOpenDialog();
|
||||||
|
if (!paths || !paths.length) return;
|
||||||
|
const cmd = splitCmd(this.state.settings[key]);
|
||||||
|
cmd[0] = paths[0]
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = splitCmd(this.state.settings[key]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} style={rowStyle}>
|
||||||
|
<div style={{display:'flex'}}>
|
||||||
|
<div style={{flex:0, whiteSpace: 'nowrap'}}>
|
||||||
|
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||||
|
</div>
|
||||||
|
<div style={{flex:0}}>
|
||||||
|
<div style={subLabel}>Path:</div>
|
||||||
|
<div style={subLabel}>Arguments:</div>
|
||||||
|
</div>
|
||||||
|
<div style={{flex:1}}>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom}}>
|
||||||
|
<input type={inputType} style={Object.assign({}, inputStyle, {marginBottom:0})} onChange={(event) => {onPathChange(event)}} value={cmd[0]} />
|
||||||
|
<button onClick={browseButtonClick} style={Object.assign({}, theme.buttonStyle, { marginLeft: 5, minHeight: 20, height: 20 })}>{_('Browse...')}</button>
|
||||||
|
</div>
|
||||||
|
<input type={inputType} style={inputStyle} onChange={(event) => {onArgsChange(event)}} value={cmd[1]}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{display:'flex'}}>
|
||||||
|
<div style={{flex:0, whiteSpace: 'nowrap'}}>
|
||||||
|
<div style={invisibleLabel}><label>{md.label()}</label></div>
|
||||||
|
</div>
|
||||||
|
<div style={{flex:1}}>
|
||||||
|
{ descriptionComp }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const onTextChange = (event) => {
|
||||||
|
updateSettingValue(key, event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} style={rowStyle}>
|
||||||
|
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||||
|
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||||
|
{ descriptionComp }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (md.type === Setting.TYPE_INT) {
|
} else if (md.type === Setting.TYPE_INT) {
|
||||||
const onNumChange = (event) => {
|
const onNumChange = (event) => {
|
||||||
updateSettingValue(key, event.target.value);
|
updateSettingValue(key, event.target.value);
|
||||||
|
@ -122,7 +122,7 @@ class Setting extends BaseModel {
|
|||||||
'sidebarVisibility': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
'sidebarVisibility': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
||||||
'tagHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
'tagHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
||||||
'folderHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
'folderHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
||||||
'editor': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
'editor': { value: '', type: Setting.TYPE_STRING, subType: 'file_path_and_args', public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||||
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
||||||
'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, section:'sync', label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, section:'sync', label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
|
||||||
return SyncTargetRegistry.idAndLabelPlainObject();
|
return SyncTargetRegistry.idAndLabelPlainObject();
|
||||||
|
@ -114,4 +114,43 @@ function ltrimSlashes(path) {
|
|||||||
return path.replace(/^\/+/, '');
|
return path.replace(/^\/+/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFilename, friendlySafeFilename, safeFileExtension, toSystemSlashes, rtrimSlashes, ltrimSlashes };
|
function quotePath(path) {
|
||||||
|
if (!path) return '';
|
||||||
|
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
|
||||||
|
path = path.replace(/"/, '\\"');
|
||||||
|
return '"' + path + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function unquotePath(path) {
|
||||||
|
if (!path.length) return '';
|
||||||
|
if (path.length && path[0] === '"') {
|
||||||
|
path = path.substr(1, path.length - 2);
|
||||||
|
}
|
||||||
|
path = path.replace(/\\"/, '"');
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractExecutablePath(cmd) {
|
||||||
|
if (!cmd.length) return '';
|
||||||
|
|
||||||
|
const quoteType = ['"', "'"].indexOf(cmd[0]) >= 0 ? cmd[0] : '';
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
for (let i = 0; i < cmd.length; i++) {
|
||||||
|
const c = cmd[i];
|
||||||
|
if (quoteType) {
|
||||||
|
if (i > 0 && c === quoteType) {
|
||||||
|
output += c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (c === ' ') break;
|
||||||
|
}
|
||||||
|
|
||||||
|
output += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { extractExecutablePath, basename, dirname, filename, isHidden, fileExtension, safeFilename, friendlySafeFilename, safeFileExtension, toSystemSlashes, rtrimSlashes, ltrimSlashes, quotePath, unquotePath };
|
@ -123,6 +123,19 @@ function wrap(text, indent, width) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commandArgumentsToString(args) {
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
let arg = args[i];
|
||||||
|
const quote = arg.indexOf('"') >= 0 ? "'" : '"';
|
||||||
|
if (arg.indexOf(' ') >= 0) {
|
||||||
|
arg = quote + arg + quote;
|
||||||
|
}
|
||||||
|
output.push(arg);
|
||||||
|
}
|
||||||
|
return output.join(' ');;
|
||||||
|
}
|
||||||
|
|
||||||
function splitCommandString(command, options = null) {
|
function splitCommandString(command, options = null) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
if (!('handleEscape' in options)) {
|
if (!('handleEscape' in options)) {
|
||||||
@ -252,6 +265,6 @@ function scriptType(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Object.assign(
|
module.exports = Object.assign(
|
||||||
{ removeDiacritics, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType },
|
{ removeDiacritics, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString },
|
||||||
stringUtilsCommon,
|
stringUtilsCommon,
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user