-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Desktop: feat: Added search list for font input fields #10248
Conversation
<FontSearch | ||
_key={key} | ||
updateSettingValue={updateSettingValue} | ||
inputType={inputType} | ||
inputStyle={inputStyle} | ||
settings={this.state.settings} | ||
fonts={this.state.fonts} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't it mean that all text input fields are replaced with a font search field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, you are right. my bad
I have fixed it now.
bef11bb
cursor: 'pointer', | ||
}; | ||
|
||
const FontSearch = (props: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No "any"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed with 9832a9e
const child = fork(join(__dirname, 'loadFonts')); | ||
child.on('message', (fonts: string[]) => { | ||
this.setState({ fonts }, () => child.kill()); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That will fail if the component is unmounted before the loadFonts script returns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Killing the child in componentWillUnmount
will prevent any failures, right?
const showList = (display: boolean) => { | ||
document.getElementById(`searchList-${key}`).style.display = display ? 'block' : 'none'; | ||
}; | ||
const onTextChange = (event: any) => { | ||
setInputText(event.target.value); | ||
updateSettingValue(key, event.target.value); | ||
showList(true); | ||
}; | ||
const onFocusHandle = (event: any) => { | ||
setInputText(event.target.value); // To handle the case when the value is already set | ||
showList(true); | ||
}; | ||
const onFontClick = (font: string) => { | ||
(document.getElementById(key) as HTMLDivElement).innerText = font; | ||
updateSettingValue(key, font); | ||
showList(false); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All callback handler should use useCallback
onTextChange(event); | ||
}} | ||
onFocus={onFocusHandle} | ||
onBlur={() => setTimeout(() => showList(false), 150)} // Delay the hiding of the list to allow the click event to fire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a useCallback too
onChange={(event: any) => { | ||
onTextChange(event); | ||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useCallback
}; | ||
|
||
const FontSearch = (props: any) => { | ||
const { _key: key, updateSettingValue, inputType, inputStyle, settings, fonts } = props; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only pass what you need, and you don't need all settings here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed with 9832a9e
onClick={() => onFontClick(font)} | ||
onMouseEnter={() => setHoveredFont(font)} | ||
onMouseLeave={() => setHoveredFont('')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the indirection? Just set onClick={onFontClick}
, etc.
<div id={`searchList-${key}`} style={searchListStyle} > | ||
{ | ||
areFontsLoading ? <div style={{ padding: '5px' }}>Loading...</div> : | ||
filteredFonts.map((font: string) => | ||
<div | ||
key={font} | ||
style={{ ...optionStyle, fontFamily: `"${font}"`, | ||
color: hoveredFont === font ? 'var(--joplin-background-color)' : 'var(--joplin-color)', | ||
backgroundColor: hoveredFont === font ? 'var(--joplin-color)' : 'var(--joplin-background-color)', | ||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No dynamic styles, search for "rscss" in the doc
import fontList = require('font-list'); | ||
|
||
fontList.getFonts({ disableQuoting: true }) | ||
.then((fonts: string[]) => process.send(fonts)) | ||
.catch((error: any) => console.error(error)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use async/await. How does it actually handle errors? It's printed to the console?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/app-desktop/package.json
Outdated
@@ -161,6 +161,7 @@ | |||
"countable": "3.0.1", | |||
"debounce": "1.2.1", | |||
"electron-window-state": "5.0.3", | |||
"font-list": "1.5.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than adding a dependency, consider using window.queryLocalFonts
(which should be available in Electron >= 25).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ab-elhaddad, indeed we would rather not add this font-list package, so please confirm if you can update the PR using the above API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can get the same result with the above API. Although typescript doesn't seem to recognize it, it exists and works efficiently.
updated with 1362f78
spellCheck={false} | ||
/> | ||
{ | ||
md.label() === 'Editor font family' || md.label() === 'Editor monospace font family' ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than comparing md.label()
with a hardcoded string, consider adding a new setting subType
to the font settings. This approach would be similar to how the file/directory chooser is currently handled.
Comparing the label with a string could be problematic if the label for one of the font settings is updated at some point in the future or if the user changes Joplin to a non-English language.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed with 67800cd
{ | ||
md.subType === SettingItemSubType.FontFamily ? | ||
<FontSearch | ||
_key={key} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is _key
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main purpose of passing the key is to be able to update the settings value (updateSettingValue
), as it takes key
as the first argument. But if you are asking about the naming and why didn't I use just key
because key is a reserved word in react components and when using it, I get this warning ``key`` is not a prop. Trying to access it will result in ``undefined`` being returned. If you need to access the same value within the child component, you should pass it as a different prop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normal pattern for a React controlled component is to have a "value" and "onChange" (or similar) event. If you do that you won't have to pass around unnecessary properties and can create more reusable components.
So in this case, add an onChange
event to your component which would pass to the parent the selected font. Then here just do onChange={fontFamily => updateSettingValue(key, fontFamily)}
I know there's a lot of code in this project and you can't know everything, but it often helps to see how things are done in the code near where you want to add something. For example you can see that all components here follow this value/onChange pattern. Maybe also read about controlled components and why they're generally preferred.
@@ -0,0 +1,82 @@ | |||
import React = require('react'); | |||
import { useMemo, useState, useCallback } from 'react'; | |||
import { CSSProperties } from 'styled-components'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import this from React - we don't use styled-components for new code
updateSettingValue={updateSettingValue} | ||
inputType={inputType} | ||
inputStyle={inputStyle} | ||
fieldValue={this.state.settings[key]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fieldValue => value
<FontSearch | ||
_key={key} | ||
updateSettingValue={updateSettingValue} | ||
inputType={inputType} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inputType => type
_key={key} | ||
updateSettingValue={updateSettingValue} | ||
inputType={inputType} | ||
inputStyle={inputStyle} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inputStyle => style
}, [showList]); | ||
|
||
const onBlurHandle = useCallback(() => | ||
setTimeout(() => showList(false), 150) // Delay the hiding of the list to allow the click event to fire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we want the click event to fire on blur? And it looks like you're already handling the click event below. Please remove the requirement for this timeout in any case because it will randomly fail
|
||
const onFontClick = useCallback((event: any) => { | ||
const font = event.target.innerText; | ||
(document.getElementById(key) as HTMLDivElement).innerText = font; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use document.getElementById. Set the font as data-font
on the element, then use event.currentTarget.getAttribute('data-font')
to get it back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was using getElementById
to set the value in the input field when a font is clicked from the list, But I replaced it using the inputText
state. But I am not sure if this is better or worse from the performance perspective. e86079f
|
||
const settingKeyToControl: any = { | ||
'plugins.states': control_PluginsStates, | ||
}; | ||
|
||
interface FontData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why "FontData"? Everything is data. It's just a Font
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it would be better to define the type as it mentioned in the docs, but no problem I will change it
family: string; | ||
fullName: string; | ||
postscriptName: string; | ||
style: string; | ||
blob: ()=> Promise<Blob>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my comment I mentioned defining the properties that you need and you don't need most of these. I think you only use .family?
When a type is not built-in we just define what we need, otherwise it's too much noise especially for the DOM which can have hundreds of properties per object.
{ | ||
md.subType === SettingItemSubType.FontFamily ? | ||
<FontSearch | ||
_key={key} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The normal pattern for a React controlled component is to have a "value" and "onChange" (or similar) event. If you do that you won't have to pass around unnecessary properties and can create more reusable components.
So in this case, add an onChange
event to your component which would pass to the parent the selected font. Then here just do onChange={fontFamily => updateSettingValue(key, fontFamily)}
I know there's a lot of code in this project and you can't know everything, but it often helps to see how things are done in the code near where you want to add something. For example you can see that all components here follow this value/onChange pattern. Maybe also read about controlled components and why they're generally preferred.
/> | ||
<div className={'font-search-list'} style={{ display: showList ? 'block' : 'none' }}> | ||
{ | ||
areFontsLoading ? <div>Loading...</div> : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_('Loading...')
- please always make sure that user-facing text can be localised.
setTimeout(() => setShowList(false), 150) // Delay the hiding of the list to allow the click event to fire | ||
, []); | ||
|
||
const onFontClick = useCallback((event: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No "any", please always use the correct types. It should be "onFontClick: React.MouseEventHandler", then you TypeScript should infer the type for "event"
); | ||
}, [fonts, inputText]); | ||
|
||
const onTextChange = useCallback((event: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no "any"
About filtering the monospaced fonts, there is no straightforward way to do this because the To solve this issue, we have two options:
const monospacedFonts = [
'Courier',
'Courier New',
'Lucida Console',
'Monaco',
'Consolas',
'Andale Mono',
'Monospace',
'Roboto Mono',
'Source Code Pro',
// More monospaced fonts
];
const isMonospacedFont = (fontName: string) => {
const testElement = document.createElement('span');
testElement.style.fontFamily = fontName;
testElement.style.fontSize = '16px';
testElement.innerText = 'i';
document.body.appendChild(testElement);
const iWidth = testElement.getBoundingClientRect().width;
testElement.innerText = 'm';
const mWidth = testElement.getBoundingClientRect().width;
return Math.abs(iWidth - mWidth) < 0.01; // Tolerance for minor variations
} |
Let's do this but please search for an existing list of monospaced fonts, as exhaustive as possible. Ideally something that's regularly updated and add the link as a comment on top of your list (so that we can update it later on). You may also want to add some heuristics such as any font that certain keywords such as "mono", "courier", "source code", "console", etc. should be considered monospace. |
I don't think it will be possible to find one really complete list. The reason is that if you search for "monospace font list" in the Web, you will find mainly fonts that are for English (and possibly other Western languages). However, those lists completely miss all monospace fonts that exist e.g. for East Asian languages. Also, the fonts may even be named in non-Latin scripts, so the generic keywords will likely not catch them. In other words, what I want to say is that it would be so much more universal if the script could just list all installed monospace fonts as they are reported by the OS without looking for specific names 🙁. |
That's true, but unfortunately we can't do that. But I'm thinking there should be an option to show either only the monospace ones or all fonts as a fallback. So @ab-elhaddad, would you mind adding a "Show monospace fonts only" checkbox to your list? It would be checked by default when picking the monospace font, and unchecked when picking the editor font. |
Maybe it would still be better to use one of those 3rd party packages that can filter monospace fonts with no guesswork involved? It seems that |
And if the list of all fonts is slow to load, can it be loaded once on startup and kept in memory? I don't suppose the memory overhead will be huge. |
I think the main problem with using a third-party package is its maintenance. The only package I have found (until now) exposing whether the fonts are monospaced or not is the |
We can't use this unfortunately. It's a binary package so will cause too many problems for little benefits. |
Thanks for the update @ab-elhaddad but the "Show monospace fonts only" checkbox needs to be within the font list component and only shows up when the list is open, like this: ![]() Otherwise it's not clear at all what this "monospace" setting is for. |
No problem, it's done |
Looks good now, thanks @ab-elhaddad! |
@laurent22 |
Added a list showing the installed fonts, that acts as a search suggestion based on the user input, so the user can choose from them.
Notes:
font-list.mp4
Styling with various themes
@PackElend label me please