mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +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);
|
||||
|
||||
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');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
@ -36,4 +36,41 @@ describe('pathUtils', function() {
|
||||
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 pathUtils = require('lib/path-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { commandArgumentsToString } = require('lib/string-utils');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
|
||||
@ -95,6 +96,15 @@ class ConfigScreenComponent extends React.Component {
|
||||
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 = {
|
||||
display: 'inline-block',
|
||||
color: theme.color,
|
||||
@ -109,6 +119,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
});
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
// console.info(key + ' = ' + value);
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
}
|
||||
|
||||
@ -158,23 +169,93 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
const onTextChange = (event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}
|
||||
|
||||
const inputStyle = Object.assign({}, controlStyle, {
|
||||
width: '50%',
|
||||
minWidth: '20em',
|
||||
border: '1px solid' });
|
||||
const inputType = md.secure === true ? 'password' : 'text';
|
||||
|
||||
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>
|
||||
);
|
||||
if (md.subType === 'file_path_and_args') {
|
||||
inputStyle.marginBottom = subLabel.marginBottom;
|
||||
|
||||
const splitCmd = cmdString => {
|
||||
const path = pathUtils.extractExecutablePath(cmdString);
|
||||
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) {
|
||||
const onNumChange = (event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
|
@ -122,7 +122,7 @@ class Setting extends BaseModel {
|
||||
'sidebarVisibility': { 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'] },
|
||||
'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') },
|
||||
'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();
|
||||
|
@ -114,4 +114,43 @@ function ltrimSlashes(path) {
|
||||
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) {
|
||||
options = options || {};
|
||||
if (!('handleEscape' in options)) {
|
||||
@ -252,6 +265,6 @@ function scriptType(s) {
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user