1
0
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:
Abdelrrahman Elhaddad 2024-04-20 15:23:07 +02:00 committed by GitHub
parent dd28c9f4d7
commit 7ec02fc8d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 309 additions and 13 deletions

View File

@ -167,6 +167,7 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.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/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js

1
.gitignore vendored
View File

@ -147,6 +147,7 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.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/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js

View File

@ -4,7 +4,7 @@ import ButtonBar from './ButtonBar';
import Button, { ButtonLevel, ButtonSize } from '../Button/Button'; import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge'; 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 control_PluginsStates from './controls/plugins/PluginsStates';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen'; import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
import { reg } from '@joplin/lib/registry'; 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 shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink'; import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import FontSearch from './FontSearch';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const settingKeyToControl: any = { const settingKeyToControl: any = {
'plugins.states': control_PluginsStates, '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 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
class ConfigScreenComponent extends React.Component<any, any> { class ConfigScreenComponent extends React.Component<any, any> {
@ -44,6 +55,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
screenName: '', screenName: '',
changedSettingKeys: [], changedSettingKeys: [],
needRestart: false, needRestart: false,
fonts: [],
}; };
this.rowStyle_ = { this.rowStyle_ = {
@ -78,12 +90,16 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.setState({ settings: this.props.settings }); this.setState({ settings: this.props.settings });
} }
public componentDidMount() { public async componentDidMount() {
if (this.props.defaultSection) { if (this.props.defaultSection) {
this.setState({ selectedSectionName: this.props.defaultSection }, () => { this.setState({ selectedSectionName: this.props.defaultSection }, () => {
void this.switchSection(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) { private async handleSettingButton(key: string) {
@ -591,22 +607,32 @@ class ConfigScreenComponent extends React.Component<any, any> {
const onTextChange = (event: any) => { const onTextChange = (event: any) => {
updateSettingValue(key, event.target.value); updateSettingValue(key, event.target.value);
}; };
return ( return (
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
<div style={labelStyle}> <div style={labelStyle}>
<label>{md.label()}</label> <label>{md.label()}</label>
</div> </div>
<input {
type={inputType} md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
style={inputStyle} <FontSearch
value={this.state.settings[key]} type={inputType}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied style={inputStyle}
onChange={(event: any) => { value={this.state.settings[key]}
onTextChange(event); availableFonts={this.state.fonts}
}} onChange={fontFamily => updateSettingValue(key, fontFamily)}
spellCheck={false} 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 }}> <div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp} {descriptionComp}
</div> </div>

View 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',
];

View File

@ -12,3 +12,35 @@
.config-screen-content > .section:last-child { .config-screen-content > .section:last-child {
border-bottom: 0; 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;
}

View File

@ -36,6 +36,8 @@ export enum SettingItemSubType {
FilePathAndArgs = 'file_path_and_args', FilePathAndArgs = 'file_path_and_args',
FilePath = 'file_path', // Not supported on mobile! FilePath = 'file_path', // Not supported on mobile!
DirectoryPath = 'directory_path', // Not supported on mobile! DirectoryPath = 'directory_path', // Not supported on mobile!
FontFamily = 'font_family',
MonospaceFontFamily = 'monospace_font_family',
} }
interface KeysOptions { 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.'), _('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
storage: SettingStorage.File, storage: SettingStorage.File,
isGlobal: true, isGlobal: true,
subType: SettingItemSubType.FontFamily,
}, },
'style.editor.monospaceFontFamily': { 'style.editor.monospaceFontFamily': {
value: '', 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.'), _('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, storage: SettingStorage.File,
isGlobal: true, 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.') }, '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.') },