mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Added search list for configuration font input fields (#10248)
This commit is contained in:
parent
dd28c9f4d7
commit
7ec02fc8d8
@ -167,6 +167,7 @@ packages/app-desktop/gui/Button/Button.js
|
||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -147,6 +147,7 @@ packages/app-desktop/gui/Button/Button.js
|
||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
|
@ -4,7 +4,7 @@ import ButtonBar from './ButtonBar';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../services/bridge';
|
||||
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType, SettingItemSubType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
@ -20,12 +20,23 @@ import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButto
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
import FontSearch from './FontSearch';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const settingKeyToControl: any = {
|
||||
'plugins.states': control_PluginsStates,
|
||||
};
|
||||
|
||||
interface Font {
|
||||
family: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
queryLocalFonts(): Promise<Font[]>;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
@ -44,6 +55,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
screenName: '',
|
||||
changedSettingKeys: [],
|
||||
needRestart: false,
|
||||
fonts: [],
|
||||
};
|
||||
|
||||
this.rowStyle_ = {
|
||||
@ -78,12 +90,16 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
public async componentDidMount() {
|
||||
if (this.props.defaultSection) {
|
||||
this.setState({ selectedSectionName: this.props.defaultSection }, () => {
|
||||
void this.switchSection(this.props.defaultSection);
|
||||
});
|
||||
}
|
||||
|
||||
const fonts = (await window.queryLocalFonts()).map((font: Font) => font.family);
|
||||
const uniqueFonts = [...new Set(fonts)];
|
||||
this.setState({ fonts: uniqueFonts });
|
||||
}
|
||||
|
||||
private async handleSettingButton(key: string) {
|
||||
@ -591,22 +607,32 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
const onTextChange = (event: any) => {
|
||||
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]}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onTextChange(event);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{
|
||||
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
|
||||
<FontSearch
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
availableFonts={this.state.fonts}
|
||||
onChange={fontFamily => updateSettingValue(key, fontFamily)}
|
||||
subtype={md.subType}
|
||||
/> :
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onChange={(event: any) => {
|
||||
onTextChange(event);
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
}
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
|
232
packages/app-desktop/gui/ConfigScreen/FontSearch.tsx
Normal file
232
packages/app-desktop/gui/ConfigScreen/FontSearch.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import React = require('react');
|
||||
import { useMemo, useState, useCallback, CSSProperties, useEffect, useRef } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { SettingItemSubType } from '@joplin/lib/models/Setting';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface Props {
|
||||
type: string;
|
||||
style: CSSProperties;
|
||||
value: string;
|
||||
availableFonts: string[];
|
||||
onChange: (font: string)=> void;
|
||||
subtype: string;
|
||||
}
|
||||
|
||||
const FontSearch = (props: Props) => {
|
||||
const { type, style, value, availableFonts, onChange, subtype } = props;
|
||||
const [filteredAvailableFonts, setFilteredAvailableFonts] = useState(availableFonts);
|
||||
const [inputText, setInputText] = useState(value);
|
||||
const [showList, setShowList] = useState(false);
|
||||
const [isListHovered, setIsListHovered] = useState(false);
|
||||
const [isFontSelected, setIsFontSelected] = useState(value !== '');
|
||||
const [visibleFonts, setVisibleFonts] = useState<string[]>([]);
|
||||
const [isMonoBoxChecked, setIsMonoBoxChecked] = useState(false);
|
||||
const isLoadingFonts = filteredAvailableFonts.length === 0;
|
||||
const fontInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (subtype === SettingItemSubType.MonospaceFontFamily) {
|
||||
setIsMonoBoxChecked(true);
|
||||
}
|
||||
}, [subtype]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMonoBoxChecked) return setFilteredAvailableFonts(availableFonts);
|
||||
const localMonospacedFonts = availableFonts.filter((font: string) =>
|
||||
monospaceKeywords.some((word: string) => font.toLowerCase().includes(word)) ||
|
||||
knownMonospacedFonts.includes(font.toLowerCase()),
|
||||
);
|
||||
setFilteredAvailableFonts(localMonospacedFonts);
|
||||
}, [isMonoBoxChecked, availableFonts]);
|
||||
|
||||
const displayedFonts = useMemo(() => {
|
||||
if (isFontSelected) return filteredAvailableFonts;
|
||||
return filteredAvailableFonts.filter((font: string) =>
|
||||
font.toLowerCase().startsWith(inputText.toLowerCase()),
|
||||
);
|
||||
}, [filteredAvailableFonts, inputText, isFontSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleFonts(displayedFonts.slice(0, 20));
|
||||
}, [displayedFonts]);
|
||||
|
||||
// Lazy loading
|
||||
const handleListScroll: React.UIEventHandler<HTMLDivElement> = useCallback((event) => {
|
||||
const scrollTop = (event.target as HTMLDivElement).scrollTop;
|
||||
const scrollHeight = (event.target as HTMLDivElement).scrollHeight;
|
||||
const clientHeight = (event.target as HTMLDivElement).clientHeight;
|
||||
|
||||
// Check if the user has scrolled to the bottom of the container
|
||||
// A small buffer of 20 pixels is subtracted from the total scrollHeight to ensure new content starts loading slightly before the user reaches the absolute bottom, providing a smoother experience.
|
||||
if (scrollTop + clientHeight >= scrollHeight - 20) {
|
||||
// Load the next 20 fonts
|
||||
const remainingFonts = displayedFonts.slice(visibleFonts.length, visibleFonts.length + 20);
|
||||
|
||||
setVisibleFonts([...visibleFonts, ...remainingFonts]);
|
||||
}
|
||||
}, [displayedFonts, visibleFonts]);
|
||||
|
||||
const handleTextChange: React.ChangeEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
setIsFontSelected(false);
|
||||
setInputText(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}, [onChange]);
|
||||
|
||||
const handleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback(() => setShowList(true), []);
|
||||
|
||||
const handleBlur: React.FocusEventHandler<HTMLInputElement> = useCallback(() => {
|
||||
if (!isListHovered) {
|
||||
setShowList(false);
|
||||
}
|
||||
}, [isListHovered]);
|
||||
|
||||
const handleFontClick: React.MouseEventHandler<HTMLDivElement> = useCallback((event) => {
|
||||
const font = (event.target as HTMLDivElement).innerText;
|
||||
setInputText(font);
|
||||
setShowList(false);
|
||||
onChange(font);
|
||||
setIsFontSelected(true);
|
||||
}, [onChange]);
|
||||
|
||||
const handleListHover: React.MouseEventHandler<HTMLDivElement> = useCallback(() => setIsListHovered(true), []);
|
||||
|
||||
const handleListLeave: React.MouseEventHandler<HTMLDivElement> = useCallback(() => setIsListHovered(false), []);
|
||||
|
||||
const handleMonoBoxCheck: React.ChangeEventHandler<HTMLInputElement> = useCallback(() => {
|
||||
setIsMonoBoxChecked(!isMonoBoxChecked);
|
||||
focus('FontSearch::fontInputRef', fontInputRef.current);
|
||||
}, [isMonoBoxChecked]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type={type}
|
||||
style={style}
|
||||
value={inputText}
|
||||
onChange={handleTextChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
spellCheck={false}
|
||||
ref={fontInputRef}
|
||||
/>
|
||||
<div
|
||||
className={'font-search-list'}
|
||||
style={{ display: showList ? 'block' : 'none' }}
|
||||
onMouseEnter={handleListHover}
|
||||
onMouseLeave={handleListLeave}
|
||||
onScroll={handleListScroll}
|
||||
>
|
||||
{
|
||||
isLoadingFonts ? <div>{_('Loading...')}</div> :
|
||||
<>
|
||||
<div className='monospace-checkbox'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={isMonoBoxChecked}
|
||||
onChange={handleMonoBoxCheck}
|
||||
id={`show-monospace-fonts_${subtype}`}
|
||||
/>
|
||||
<label htmlFor={`show-monospace-fonts_${subtype}`}>{_('Show monospace fonts only.')}</label>
|
||||
</div>
|
||||
{
|
||||
visibleFonts.map((font: string) =>
|
||||
<div
|
||||
key={font}
|
||||
style={{ fontFamily: `"${font}"` }}
|
||||
onClick={handleFontClick}
|
||||
className='font-search-item'
|
||||
>
|
||||
{font}
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontSearch;
|
||||
|
||||
// Known monospaced fonts from wikipedia
|
||||
// https://en.wikipedia.org/wiki/List_of_monospaced_typefaces
|
||||
// https://en.wikipedia.org/wiki/Category:Monospaced_typefaces
|
||||
// Make sure to add the fonts in lower case
|
||||
// cSpell:disable
|
||||
const knownMonospacedFonts = [
|
||||
'andalé mono',
|
||||
'anonymous pro',
|
||||
'bitstream vera sans mono',
|
||||
'cascadia code',
|
||||
'century schoolbook monospace',
|
||||
'comic mono',
|
||||
'computer modern mono/typewriter',
|
||||
'consolas',
|
||||
'courier',
|
||||
'courier final draft',
|
||||
'courier new',
|
||||
'courier prime',
|
||||
'courier screenplay',
|
||||
'cousine',
|
||||
'dejavu sans mono',
|
||||
'droid sans mono',
|
||||
'envy code r',
|
||||
'everson mono',
|
||||
'fantasque sans mono',
|
||||
'fira code',
|
||||
'fira mono',
|
||||
'fixed',
|
||||
'fixedsys',
|
||||
'freemono',
|
||||
'go mono',
|
||||
'hack',
|
||||
'hyperfont',
|
||||
'ibm courier',
|
||||
'ibm plex mono',
|
||||
'inconsolata',
|
||||
'input',
|
||||
'iosevka',
|
||||
'jetbrains mono',
|
||||
'juliamono',
|
||||
'letter gothic',
|
||||
'liberation mono',
|
||||
'lucida console',
|
||||
'menlo',
|
||||
'monaco',
|
||||
'monofur',
|
||||
'monospace (unicode)',
|
||||
'nimbus mono l',
|
||||
'nk57 monospace',
|
||||
'noto mono',
|
||||
'ocr-a',
|
||||
'ocr-b',
|
||||
'operator mono',
|
||||
'overpass mono',
|
||||
'oxygen mono',
|
||||
'pragmatapro',
|
||||
'profont',
|
||||
'pt mono',
|
||||
'recursive mono',
|
||||
'roboto mono',
|
||||
'sf mono',
|
||||
'source code pro',
|
||||
'spleen',
|
||||
'terminal',
|
||||
'terminus',
|
||||
'tex gyre cursor',
|
||||
'ubuntu mono',
|
||||
'victor mono',
|
||||
'wumpus mono',
|
||||
];
|
||||
|
||||
const monospaceKeywords = [
|
||||
'mono',
|
||||
'code',
|
||||
'courier',
|
||||
'console',
|
||||
'source code',
|
||||
'terminal',
|
||||
'fixed',
|
||||
];
|
@ -12,3 +12,35 @@
|
||||
.config-screen-content > .section:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.font-search-list {
|
||||
background-color: var(--joplin-background-color);
|
||||
max-height: 200px;
|
||||
width: 50%;
|
||||
min-width: 20em;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--joplin-border-color4);
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.font-search-list > div {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.font-search-item {
|
||||
border-bottom: 1px solid var(--joplin-border-color4);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.font-search-item:hover {
|
||||
color: var(--joplin-background-color);
|
||||
background-color: var(--joplin-color);
|
||||
}
|
||||
|
||||
.monospace-checkbox {
|
||||
background-color: var(--joplin-background-color3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
@ -36,6 +36,8 @@ export enum SettingItemSubType {
|
||||
FilePathAndArgs = 'file_path_and_args',
|
||||
FilePath = 'file_path', // Not supported on mobile!
|
||||
DirectoryPath = 'directory_path', // Not supported on mobile!
|
||||
FontFamily = 'font_family',
|
||||
MonospaceFontFamily = 'monospace_font_family',
|
||||
}
|
||||
|
||||
interface KeysOptions {
|
||||
@ -1360,6 +1362,7 @@ class Setting extends BaseModel {
|
||||
_('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
subType: SettingItemSubType.FontFamily,
|
||||
},
|
||||
'style.editor.monospaceFontFamily': {
|
||||
value: '',
|
||||
@ -1372,6 +1375,7 @@ class Setting extends BaseModel {
|
||||
_('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
subType: SettingItemSubType.MonospaceFontFamily,
|
||||
},
|
||||
|
||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||
|
Loading…
Reference in New Issue
Block a user