mirror of https://github.com/aseprite/aseprite.git
509 lines
15 KiB
C++
509 lines
15 KiB
C++
// Aseprite
|
|
// Copyright (c) 2024-2025 Igara Studio S.A.
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/ui/font_entry.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/console.h"
|
|
#include "app/i18n/strings.h"
|
|
#include "app/recent_files.h"
|
|
#include "app/ui/font_popup.h"
|
|
#include "app/ui/skin/skin_theme.h"
|
|
#include "base/contains.h"
|
|
#include "base/convert_to.h"
|
|
#include "base/scoped_value.h"
|
|
#include "fmt/format.h"
|
|
#include "ui/display.h"
|
|
#include "ui/fit_bounds.h"
|
|
#include "ui/manager.h"
|
|
#include "ui/message.h"
|
|
#include "ui/scale.h"
|
|
|
|
#include "font_style.xml.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <vector>
|
|
|
|
namespace app {
|
|
|
|
using namespace ui;
|
|
|
|
FontEntry::FontFace::FontFace()
|
|
{
|
|
}
|
|
|
|
void FontEntry::FontFace::onInitTheme(InitThemeEvent& ev)
|
|
{
|
|
SearchEntry::onInitTheme(ev);
|
|
if (m_popup)
|
|
m_popup->initTheme();
|
|
}
|
|
|
|
bool FontEntry::FontFace::onProcessMessage(Message* msg)
|
|
{
|
|
switch (msg->type()) {
|
|
// If we press the mouse button in the FontFace widget, and drag
|
|
// the mouse (without releasing the mouse button) to the popup, we
|
|
// send the mouse message to the popup.
|
|
case kMouseMoveMessage:
|
|
if (hasCapture() && m_popup) {
|
|
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
|
const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
|
|
mouseMsg->position());
|
|
Manager* mgr = manager();
|
|
Widget* pick = mgr->pickFromScreenPos(screenPos);
|
|
Widget* target = m_popup->getListBox();
|
|
|
|
if (pick && (pick == target || pick->hasAncestor(target))) {
|
|
mgr->transferAsMouseDownMessage(this, pick, mouseMsg);
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case kMouseDownMessage:
|
|
case kFocusEnterMessage:
|
|
if (!isEnabled())
|
|
break;
|
|
|
|
if (!m_popup) {
|
|
try {
|
|
const FontInfo info = fontEntry()->info();
|
|
|
|
m_popup.reset(new FontPopup(info));
|
|
m_popup->FontChange.connect([this](const FontInfo& fontInfo) {
|
|
FontChange(fontInfo,
|
|
m_fromEntryChange ? FontEntry::From::Face : FontEntry::From::Popup);
|
|
});
|
|
|
|
// If we press ESC in the popup we focus this FontFace field.
|
|
m_popup->EscKey.connect([this]() { requestFocus(); });
|
|
}
|
|
catch (const std::exception& ex) {
|
|
Console::showException(ex);
|
|
}
|
|
}
|
|
if (!m_popup->isVisible()) {
|
|
// Reset the search filter before opening the popup window.
|
|
m_popup->setSearchText(std::string());
|
|
|
|
m_popup->showPopup(display(), bounds());
|
|
requestFocus();
|
|
}
|
|
break;
|
|
|
|
case kFocusLeaveMessage: {
|
|
// If we lost focus by a widget that is not part of the popup,
|
|
// we close the popup.
|
|
auto* newFocus = static_cast<FocusMessage*>(msg)->newFocus();
|
|
if (m_popup && newFocus && newFocus->window() != m_popup.get()) {
|
|
m_popup->closeWindow(nullptr);
|
|
}
|
|
|
|
// Restore the face name (e.g. when we press Escape key)
|
|
const FontInfo info = fontEntry()->info();
|
|
setText(info.title());
|
|
break;
|
|
}
|
|
|
|
case kKeyDownMessage:
|
|
case kKeyUpMessage:
|
|
// If the popup is visible and we press the Up/Down arrow key,
|
|
// we start navigating the popup list.
|
|
if (hasFocus() && m_popup) {
|
|
const auto* keymsg = static_cast<const KeyMessage*>(msg);
|
|
switch (keymsg->scancode()) {
|
|
case kKeyEsc:
|
|
case kKeyEnter:
|
|
case kKeyEnterPad:
|
|
if (m_popup && m_popup->isVisible()) {
|
|
m_popup->closeWindow(nullptr);
|
|
|
|
// This final signal will release the focus from this
|
|
// entry and give the chance to the client to focus
|
|
// their own text box.
|
|
FontChange(m_popup->selectedFont(), From::Popup);
|
|
return true;
|
|
}
|
|
break;
|
|
case kKeyUp:
|
|
case kKeyDown:
|
|
if (m_popup->isVisible()) {
|
|
base::ScopedValue lock(fontEntry()->m_lockFace, true);
|
|
|
|
// Redirect key message to the list box
|
|
if (msg->recipient() == this) {
|
|
// Redirect the Up/Down arrow key to the popup list
|
|
// box, so we move through the list items. This will
|
|
// not generate a FontChange (as it'd modify the
|
|
// focused widget and other unexpected behaviors).
|
|
m_popup->getListBox()->sendMessage(msg);
|
|
|
|
// We are explicitly firing the FontChange signal so
|
|
// the client knows the new selected font from the
|
|
// popup.
|
|
FontChange(m_popup->selectedFont(), From::Face);
|
|
}
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
return SearchEntry::onProcessMessage(msg);
|
|
}
|
|
|
|
void FontEntry::FontFace::onChange()
|
|
{
|
|
base::ScopedValue lock(m_fromEntryChange, true);
|
|
SearchEntry::onChange();
|
|
|
|
// This shouldn't happen, but we received crash reports where the
|
|
// m_popup is nullptr here.
|
|
ASSERT(m_popup);
|
|
if (!m_popup)
|
|
return;
|
|
|
|
m_popup->setSearchText(text());
|
|
|
|
// Changing the search text doesn't generate a FontChange
|
|
// signal. Here we are forcing a FontChange signal with the first
|
|
// selected font from the search. Indicating "From::Face" we avoid
|
|
// changing the FontEntry text with the face font name (as the user
|
|
// is writing the text to search, we don't want to touch this Entry
|
|
// field).
|
|
FontChange(m_popup->selectedFont(), From::Face);
|
|
}
|
|
|
|
os::Surface* FontEntry::FontFace::onGetCloseIcon() const
|
|
{
|
|
auto& pinnedFonts = App::instance()->recentFiles()->pinnedFonts();
|
|
const FontInfo info = fontEntry()->info();
|
|
const std::string fontInfoStr = base::convert_to<std::string>(info);
|
|
auto it = std::find(pinnedFonts.begin(), pinnedFonts.end(), fontInfoStr);
|
|
if (it != pinnedFonts.end()) {
|
|
return skin::SkinTheme::get(this)->parts.pinned()->bitmap(0);
|
|
}
|
|
return skin::SkinTheme::get(this)->parts.unpinned()->bitmap(0);
|
|
}
|
|
|
|
void FontEntry::FontFace::onCloseIconPressed()
|
|
{
|
|
const FontInfo info = fontEntry()->info();
|
|
if (info.size() == 0) // Don't save fonts with size=0pt
|
|
return;
|
|
|
|
auto& pinnedFonts = App::instance()->recentFiles()->pinnedFonts();
|
|
const std::string fontInfoStr = base::convert_to<std::string>(info);
|
|
|
|
auto it = std::find(pinnedFonts.begin(), pinnedFonts.end(), fontInfoStr);
|
|
if (it != pinnedFonts.end()) {
|
|
pinnedFonts.erase(it);
|
|
}
|
|
else {
|
|
pinnedFonts.push_back(fontInfoStr);
|
|
std::sort(pinnedFonts.begin(), pinnedFonts.end());
|
|
}
|
|
|
|
// This shouldn't happen, but we received crash reports where the
|
|
// m_popup is nullptr here.
|
|
ASSERT(m_popup);
|
|
if (m_popup) {
|
|
// Refill the list with the new pinned/unpinned item
|
|
m_popup->recreatePinnedItems();
|
|
}
|
|
|
|
invalidate();
|
|
}
|
|
|
|
FontEntry::FontSize::FontSize()
|
|
{
|
|
setEditable(true);
|
|
}
|
|
|
|
void FontEntry::FontSize::updateForFont(const FontInfo& info)
|
|
{
|
|
std::vector<int> values = { 8, 9, 10, 11, 12, 14, 16, 18, 22, 24, 26, 28, 36, 48, 72 };
|
|
int h = 0;
|
|
|
|
// For SpriteSheet fonts we can offer the specific size that matches
|
|
// the bitmap font (+ x2 + x3)
|
|
text::FontRef font = Fonts::instance()->fontFromInfo(info);
|
|
if (font && font->type() == text::FontType::SpriteSheet) {
|
|
h = int(font->defaultSize());
|
|
if (h > 0) {
|
|
for (int i = h; i < h * 4; i += h) {
|
|
if (!base::contains(values, i))
|
|
values.insert(std::upper_bound(values.begin(), values.end(), i), i);
|
|
}
|
|
}
|
|
}
|
|
|
|
deleteAllItems();
|
|
for (int i : values) {
|
|
if (h && (i % h) == 0)
|
|
addItem(fmt::format("{}*", i));
|
|
else
|
|
addItem(fmt::format("{}", i));
|
|
}
|
|
}
|
|
|
|
void FontEntry::FontSize::onEntryChange()
|
|
{
|
|
ComboBox::onEntryChange();
|
|
Change();
|
|
}
|
|
|
|
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
|
|
{
|
|
addItem("B");
|
|
addItem("I");
|
|
addItem("...");
|
|
setMultiMode(MultiMode::Set);
|
|
|
|
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
|
|
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
|
|
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
|
|
}
|
|
|
|
FontEntry::FontStroke::FontStroke(ui::TooltipManager* tooltips) : m_fill(2)
|
|
{
|
|
auto* theme = skin::SkinTheme::get(this);
|
|
|
|
m_fill.addItem(theme->parts.toolFilledRectangle(), theme->styles.contextBarButton());
|
|
m_fill.addItem(theme->parts.toolRectangle(), theme->styles.contextBarButton());
|
|
m_fill.setSelectedItem(0);
|
|
m_fill.ItemChange.connect([this] { Change(); });
|
|
|
|
m_stroke.setText("0");
|
|
m_stroke.setSuffix("pt");
|
|
m_stroke.ValueChange.connect([this] { Change(); });
|
|
|
|
addChild(&m_fill);
|
|
addChild(&m_stroke);
|
|
|
|
tooltips->addTooltipFor(m_fill.getItem(0), Strings::shape_fill(), BOTTOM);
|
|
tooltips->addTooltipFor(m_fill.getItem(1), Strings::shape_stroke(), BOTTOM);
|
|
tooltips->addTooltipFor(&m_stroke, Strings::shape_stroke_width(), BOTTOM);
|
|
}
|
|
|
|
bool FontEntry::FontStroke::fill() const
|
|
{
|
|
return const_cast<FontStroke*>(this)->m_fill.getItem(0)->isSelected();
|
|
}
|
|
|
|
float FontEntry::FontStroke::stroke() const
|
|
{
|
|
return m_stroke.textDouble();
|
|
}
|
|
|
|
FontEntry::FontStroke::WidthEntry::WidthEntry() : ui::IntEntry(0, 100, this)
|
|
{
|
|
}
|
|
|
|
void FontEntry::FontStroke::WidthEntry::onValueChange()
|
|
{
|
|
ui::IntEntry::onValueChange();
|
|
ValueChange();
|
|
}
|
|
|
|
bool FontEntry::FontStroke::WidthEntry::onAcceptUnicodeChar(int unicodeChar)
|
|
{
|
|
return (IntEntry::onAcceptUnicodeChar(unicodeChar) || unicodeChar == '.');
|
|
}
|
|
|
|
std::string FontEntry::FontStroke::WidthEntry::onGetTextFromValue(int value)
|
|
{
|
|
return fmt::format("{:.1f}", value / 10.0);
|
|
}
|
|
|
|
int FontEntry::FontStroke::WidthEntry::onGetValueFromText(const std::string& text)
|
|
{
|
|
return int(10.0 * base::convert_to<double>(text));
|
|
}
|
|
|
|
FontEntry::FontEntry(const bool withStrokeAndFill)
|
|
: m_style(&m_tooltips)
|
|
, m_stroke(withStrokeAndFill ? std::make_unique<FontStroke>(&m_tooltips) : nullptr)
|
|
{
|
|
m_face.setExpansive(true);
|
|
m_size.setExpansive(false);
|
|
m_style.setExpansive(false);
|
|
|
|
addChild(&m_tooltips);
|
|
addChild(&m_face);
|
|
addChild(&m_size);
|
|
addChild(&m_style);
|
|
if (m_stroke)
|
|
addChild(m_stroke.get());
|
|
|
|
m_tooltips.addTooltipFor(&m_face, Strings::text_tool_font_family(), BOTTOM);
|
|
m_tooltips.addTooltipFor(m_size.getEntryWidget(), Strings::text_tool_font_size(), BOTTOM);
|
|
|
|
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
|
|
|
|
m_face.FontChange.connect([this](const FontInfo& newTypeName, const From from) {
|
|
if (newTypeName.size() > 0)
|
|
setInfo(newTypeName, from);
|
|
else {
|
|
setInfo(
|
|
FontInfo(newTypeName, m_info.size(), m_info.style(), m_info.flags(), m_info.hinting()),
|
|
from);
|
|
}
|
|
invalidate();
|
|
});
|
|
|
|
m_size.Change.connect([this]() {
|
|
const float newSize = std::strtof(m_size.getValue().c_str(), nullptr);
|
|
setInfo(FontInfo(m_info, newSize, m_info.style(), m_info.flags(), m_info.hinting()),
|
|
From::Size);
|
|
});
|
|
|
|
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
|
|
if (m_stroke)
|
|
m_stroke->Change.connect(&FontEntry::onStrokeChange, this);
|
|
}
|
|
|
|
// Defined here as FontPopup type is not fully defined in the header
|
|
// file (and we have a std::unique_ptr<FontPopup> in FontEntry::FontFace).
|
|
FontEntry::~FontEntry()
|
|
{
|
|
}
|
|
|
|
void FontEntry::setInfo(const FontInfo& info, const From fromField)
|
|
{
|
|
m_info = info;
|
|
|
|
if (fromField != From::Face)
|
|
m_face.setText(info.title());
|
|
|
|
if (fromField != From::Size) {
|
|
m_size.updateForFont(info);
|
|
m_size.setValue(fmt::format("{}", info.size()));
|
|
}
|
|
|
|
if (fromField != From::Style) {
|
|
m_style.getItem(0)->setSelected(info.style().weight() >= text::FontStyle::Weight::SemiBold);
|
|
m_style.getItem(1)->setSelected(info.style().slant() != text::FontStyle::Slant::Upright);
|
|
}
|
|
|
|
FontChange(m_info, fromField);
|
|
}
|
|
|
|
ui::Paint FontEntry::paint()
|
|
{
|
|
ui::Paint paint;
|
|
ui::Paint::Style style = ui::Paint::Fill;
|
|
|
|
if (m_stroke) {
|
|
const float stroke = m_stroke->stroke();
|
|
if (m_stroke->fill()) {
|
|
if (stroke > 0.0f) {
|
|
style = ui::Paint::StrokeAndFill;
|
|
paint.strokeWidth(stroke);
|
|
}
|
|
}
|
|
else {
|
|
style = ui::Paint::Stroke;
|
|
paint.strokeWidth(stroke);
|
|
}
|
|
}
|
|
|
|
paint.style(style);
|
|
return paint;
|
|
}
|
|
|
|
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
|
|
{
|
|
text::FontStyle style = m_info.style();
|
|
|
|
switch (m_style.getItemIndex(item)) {
|
|
// Bold button changed
|
|
case 0: {
|
|
const bool bold = m_style.getItem(0)->isSelected();
|
|
style = text::FontStyle(
|
|
bold ? text::FontStyle::Weight::Bold : text::FontStyle::Weight::Normal,
|
|
style.width(),
|
|
style.slant());
|
|
|
|
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
|
From::Style);
|
|
break;
|
|
}
|
|
// Italic button changed
|
|
case 1: {
|
|
const bool italic = m_style.getItem(1)->isSelected();
|
|
style = text::FontStyle(
|
|
style.weight(),
|
|
style.width(),
|
|
italic ? text::FontStyle::Slant::Italic : text::FontStyle::Slant::Upright);
|
|
|
|
setInfo(FontInfo(m_info, m_info.size(), style, m_info.flags(), m_info.hinting()),
|
|
From::Style);
|
|
break;
|
|
}
|
|
case 2: {
|
|
item->setSelected(false); // Unselect the "..." button
|
|
|
|
ui::PopupWindow popup;
|
|
app::gen::FontStyle content;
|
|
|
|
content.antialias()->setSelected(m_info.antialias());
|
|
content.ligatures()->setSelected(m_info.ligatures());
|
|
content.hinting()->setSelected(m_info.hinting() == text::FontHinting::Normal);
|
|
|
|
auto flagsChange = [this, &content]() {
|
|
FontInfo::Flags flags = FontInfo::Flags::None;
|
|
if (content.antialias()->isSelected())
|
|
flags |= FontInfo::Flags::Antialias;
|
|
if (content.ligatures()->isSelected())
|
|
flags |= FontInfo::Flags::Ligatures;
|
|
setInfo(FontInfo(m_info, m_info.size(), m_info.style(), flags, m_info.hinting()),
|
|
From::Flags);
|
|
};
|
|
|
|
auto hintingChange = [this, &content]() {
|
|
auto hinting = (content.hinting()->isSelected() ? text::FontHinting::Normal :
|
|
text::FontHinting::None);
|
|
|
|
setInfo(FontInfo(m_info, m_info.size(), m_info.style(), m_info.flags(), hinting),
|
|
From::Hinting);
|
|
};
|
|
|
|
content.antialias()->Click.connect(flagsChange);
|
|
content.ligatures()->Click.connect(flagsChange);
|
|
content.hinting()->Click.connect(hintingChange);
|
|
|
|
popup.addChild(&content);
|
|
popup.remapWindow();
|
|
|
|
gfx::Rect rc = item->bounds();
|
|
rc.y += rc.h - popup.border().bottom();
|
|
|
|
ui::fit_bounds(display(), &popup, gfx::Rect(rc.origin(), popup.sizeHint()));
|
|
|
|
popup.Open.connect([&popup] { popup.setHotRegion(gfx::Region(popup.boundsOnScreen())); });
|
|
|
|
popup.openWindowInForeground();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FontEntry::onStrokeChange()
|
|
{
|
|
FontChange(m_info, From::Paint);
|
|
}
|
|
|
|
} // namespace app
|