/* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "MainWidget.h" #include "GlyphEditorWidget.h" #include "NewFontDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace FontEditor { Resources g_resources; static constexpr Array pangrams = { "quick fox jumps nightly above wizard"sv, "five quacking zephyrs jolt my wax bed"sv, "pack my box with five dozen liquor jugs"sv, "quick brown fox jumps over the lazy dog"sv, "waxy and quivering jocks fumble the pizza"sv, "~#:[@_1%]*{$2.3}/4^(5'6\")-&|7+8!=<9,0\\>?;"sv, "byxfjärmat föl gick på duvshowen"sv, "         "sv, "float Fox.quick(h){ is_brown && it_jumps_over(doges.lazy) }"sv, "lazy dog"sv }; ErrorOr> MainWidget::try_create() { auto main_widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) MainWidget())); TRY(main_widget->create_widgets()); TRY(main_widget->create_actions()); TRY(main_widget->create_models()); TRY(main_widget->create_toolbars()); TRY(main_widget->create_undo_stack()); return main_widget; } ErrorOr> MainWidget::create_preview_window() { auto window = TRY(GUI::Window::try_create(this)); window->set_window_mode(GUI::WindowMode::RenderAbove); window->set_title("Preview"); window->resize(400, 150); window->center_within(*this->window()); auto main_widget = TRY(window->set_main_widget()); TRY(main_widget->load_from_gml(font_preview_window_gml)); m_preview_label = find_descendant_of_type_named("preview_label"); m_preview_label->set_font(m_font); m_preview_textbox = find_descendant_of_type_named("preview_textbox"); m_preview_textbox->on_change = [this] { auto maybe_preview = [](StringView text) -> ErrorOr { return TRY(String::formatted("{}\n{}", text, TRY(Unicode::to_unicode_uppercase_full(text)))); }(m_preview_textbox->text()); if (maybe_preview.is_error()) return show_error(maybe_preview.release_error(), "Formatting preview text failed"sv); m_preview_label->set_text(maybe_preview.release_value()); }; m_preview_textbox->set_text(pangrams[0]); auto& reload_button = *find_descendant_of_type_named("reload_button"); reload_button.on_click = [this](auto) { static size_t i = 1; if (i >= pangrams.size()) i = 0; m_preview_textbox->set_text(pangrams[i]); i++; }; return window; } ErrorOr MainWidget::create_actions() { m_new_action = GUI::Action::create("&New Font...", { Mod_Ctrl, Key_N }, g_resources.new_font, [this](auto&) { if (!request_close()) return; auto maybe_wizard = NewFontDialog::create(window()); if (maybe_wizard.is_error()) return show_error(maybe_wizard.release_error(), "Creating font wizard failed"sv); auto wizard = maybe_wizard.release_value(); if (wizard->exec() != GUI::Dialog::ExecResult::OK) return; auto maybe_font = wizard->create_font(); if (maybe_font.is_error()) return show_error(maybe_font.release_error(), "Creating new font failed"sv); if (auto result = initialize({}, move(maybe_font.value())); result.is_error()) show_error(result.release_error(), "Initializing new font failed"sv); }); m_new_action->set_status_tip("Create a new font"_string); m_open_action = GUI::CommonActions::make_open_action([this](auto&) { if (!request_close()) return; FileSystemAccessClient::OpenFileOptions options { .window_title = "Open"sv, .path = "/res/fonts"sv, .allowed_file_types = { { GUI::FileTypeFilter { "Bitmap Font Files", { { "font" } } }, GUI::FileTypeFilter::all_files() } }, }; auto response = FileSystemAccessClient::Client::the().open_file(window(), options); if (response.is_error()) return; auto file = response.release_value(); if (auto result = open_file(file.filename(), file.release_stream()); result.is_error()) show_error(result.release_error(), "Opening"sv, file.filename()); }); m_save_action = GUI::CommonActions::make_save_action([this](auto&) { if (m_path.is_empty()) return m_save_as_action->activate(); auto response = FileSystemAccessClient::Client::the().request_file(window(), m_path.to_deprecated_string(), Core::File::OpenMode::Truncate | Core::File::OpenMode::Write); if (response.is_error()) return; auto file = response.release_value(); if (auto result = save_file(m_path, file.release_stream()); result.is_error()) show_error(result.release_error(), "Saving"sv, m_path); }); m_save_as_action = GUI::CommonActions::make_save_as_action([this](auto&) { auto default_path = LexicalPath(m_path.is_empty() ? "Untitled.font"sv : m_path); auto response = FileSystemAccessClient::Client::the().save_file(window(), default_path.title(), default_path.extension()); if (response.is_error()) return; auto file = response.release_value(); if (auto result = save_file(file.filename(), file.release_stream()); result.is_error()) show_error(result.release_error(), "Saving"sv, file.filename()); else GUI::Application::the()->set_most_recently_open_file(file.filename()); }); m_cut_action = GUI::CommonActions::make_cut_action([this](auto&) { if (auto result = cut_selected_glyphs(); result.is_error()) show_error(result.release_error(), "Cutting selection failed"sv); }); m_copy_action = GUI::CommonActions::make_copy_action([this](auto&) { if (auto result = copy_selected_glyphs(); result.is_error()) show_error(result.release_error(), "Copying selection failed"sv); }); m_paste_action = GUI::CommonActions::make_paste_action([this](auto&) { paste_glyphs(); }); m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "glyph/x-fonteditor"); GUI::Clipboard::the().on_change = [this](DeprecatedString const& data_type) { m_paste_action->set_enabled(data_type == "glyph/x-fonteditor"); }; m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { delete_selected_glyphs(); }); m_undo_action = GUI::CommonActions::make_undo_action([this](auto&) { undo(); }); m_undo_action->set_enabled(false); m_redo_action = GUI::CommonActions::make_redo_action([this](auto&) { redo(); }); m_redo_action->set_enabled(false); m_select_all_action = GUI::CommonActions::make_select_all_action([this](auto&) { m_glyph_map_widget->set_selection(m_range.first, m_range.last - m_range.first + 1); m_glyph_map_widget->update(); auto selection = m_glyph_map_widget->selection().normalized(); m_undo_selection->set_start(selection.start()); m_undo_selection->set_size(selection.size()); update_statusbar(); }); m_open_preview_action = GUI::Action::create("&Preview Font", { Mod_Ctrl, Key_P }, g_resources.preview_font, [this](auto&) { if (!m_font_preview_window) { if (auto maybe_window = create_preview_window(); maybe_window.is_error()) show_error(maybe_window.release_error(), "Creating preview window failed"sv); else m_font_preview_window = maybe_window.release_value(); } if (m_font_preview_window) m_font_preview_window->show(); }); m_open_preview_action->set_status_tip("Preview the current font"_string); bool show_metadata = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowMetadata"sv, true); m_font_metadata_groupbox->set_visible(show_metadata); m_show_metadata_action = GUI::Action::create_checkable("Font &Metadata", { Mod_Ctrl, Key_M }, [this](auto& action) { m_font_metadata_groupbox->set_visible(action.is_checked()); Config::write_bool("FontEditor"sv, "Layout"sv, "ShowMetadata"sv, action.is_checked()); }); m_show_metadata_action->set_checked(show_metadata); m_show_metadata_action->set_status_tip("Show or hide metadata about the current font"_string); bool show_unicode_blocks = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowUnicodeBlocks"sv, true); m_unicode_block_container->set_visible(show_unicode_blocks); m_show_unicode_blocks_action = GUI::Action::create_checkable("&Unicode Blocks", { Mod_Ctrl, Key_U }, [this](auto& action) { m_unicode_block_container->set_visible(action.is_checked()); if (action.is_checked()) m_search_textbox->set_focus(m_initialized); else m_glyph_map_widget->set_focus(m_initialized); Config::write_bool("FontEditor"sv, "Layout"sv, "ShowUnicodeBlocks"sv, action.is_checked()); }); m_show_unicode_blocks_action->set_checked(show_unicode_blocks); m_show_unicode_blocks_action->set_status_tip("Show or hide the Unicode block list"_string); bool show_toolbar = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowToolbar"sv, true); m_toolbar_container->set_visible(show_toolbar); m_show_toolbar_action = GUI::Action::create_checkable("&Toolbar", [this](auto& action) { m_toolbar_container->set_visible(action.is_checked()); Config::write_bool("FontEditor"sv, "Layout"sv, "ShowToolbar"sv, action.is_checked()); }); m_show_toolbar_action->set_checked(show_toolbar); m_show_toolbar_action->set_status_tip("Show or hide the toolbar"_string); bool show_statusbar = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowStatusbar"sv, true); m_statusbar->set_visible(show_statusbar); m_show_statusbar_action = GUI::Action::create_checkable("&Status Bar", [this](auto& action) { m_statusbar->set_visible(action.is_checked()); update_statusbar(); Config::write_bool("FontEditor"sv, "Layout"sv, "ShowStatusbar"sv, action.is_checked()); }); m_show_statusbar_action->set_checked(show_statusbar); m_show_statusbar_action->set_status_tip("Show or hide the status bar"_string); bool highlight_modifications = Config::read_bool("FontEditor"sv, "GlyphMap"sv, "HighlightModifications"sv, true); m_glyph_map_widget->set_highlight_modifications(highlight_modifications); m_highlight_modifications_action = GUI::Action::create_checkable("&Highlight Modifications", { Mod_Ctrl, Key_H }, [this](auto& action) { m_glyph_map_widget->set_highlight_modifications(action.is_checked()); Config::write_bool("FontEditor"sv, "GlyphMap"sv, "HighlightModifications"sv, action.is_checked()); }); m_highlight_modifications_action->set_checked(highlight_modifications); m_highlight_modifications_action->set_status_tip("Show or hide highlights on modified glyphs"_string); bool show_system_emoji = Config::read_bool("FontEditor"sv, "GlyphMap"sv, "ShowSystemEmoji"sv, true); m_glyph_map_widget->set_show_system_emoji(show_system_emoji); m_show_system_emoji_action = GUI::Action::create_checkable("System &Emoji", { Mod_Ctrl, Key_E }, [this](auto& action) { m_glyph_map_widget->set_show_system_emoji(action.is_checked()); Config::write_bool("FontEditor"sv, "GlyphMap"sv, "ShowSystemEmoji"sv, action.is_checked()); }); m_show_system_emoji_action->set_checked(show_system_emoji); m_show_system_emoji_action->set_status_tip("Show or hide system emoji"_string); m_go_to_glyph_action = GUI::Action::create("&Go to Glyph...", { Mod_Ctrl, Key_G }, g_resources.go_to_glyph, [this](auto&) { String input; auto result = GUI::InputBox::try_show(window(), input, {}, "Go to Glyph"sv, GUI::InputType::NonemptyText, "Hexadecimal"sv); if (!result.is_error() && result.value() == GUI::InputBox::ExecResult::OK) { auto maybe_code_point = AK::StringUtils::convert_to_uint_from_hex(input); if (!maybe_code_point.has_value()) return; auto code_point = maybe_code_point.value(); code_point = clamp(code_point, m_range.first, m_range.last); m_glyph_map_widget->set_focus(true); m_glyph_map_widget->set_active_glyph(code_point); m_glyph_map_widget->scroll_to_glyph(code_point); } }); m_go_to_glyph_action->set_status_tip("Go to the specified code point"_string); m_previous_glyph_action = GUI::Action::create("Pre&vious Glyph", { Mod_Alt, Key_Left }, g_resources.previous_glyph, [this](auto&) { m_glyph_map_widget->select_previous_existing_glyph(); }); m_previous_glyph_action->set_status_tip("Seek the previous visible glyph"_string); m_next_glyph_action = GUI::Action::create("&Next Glyph", { Mod_Alt, Key_Right }, g_resources.next_glyph, [this](auto&) { m_glyph_map_widget->select_next_existing_glyph(); }); m_next_glyph_action->set_status_tip("Seek the next visible glyph"_string); i32 scale = Config::read_i32("FontEditor"sv, "GlyphEditor"sv, "Scale"sv, 10); m_glyph_editor_widget->set_scale(scale); m_scale_five_action = GUI::Action::create_checkable("500%", { Mod_Ctrl, Key_1 }, [this](auto&) { set_scale_and_save(5); }); m_scale_five_action->set_checked(scale == 5); m_scale_five_action->set_status_tip("Scale the editor in proportion to the current font"_string); m_scale_ten_action = GUI::Action::create_checkable("1000%", { Mod_Ctrl, Key_2 }, [this](auto&) { set_scale_and_save(10); }); m_scale_ten_action->set_checked(scale == 10); m_scale_ten_action->set_status_tip("Scale the editor in proportion to the current font"_string); m_scale_fifteen_action = GUI::Action::create_checkable("1500%", { Mod_Ctrl, Key_3 }, [this](auto&) { set_scale_and_save(15); }); m_scale_fifteen_action->set_checked(scale == 15); m_scale_fifteen_action->set_status_tip("Scale the editor in proportion to the current font"_string); m_glyph_editor_scale_actions.add_action(*m_scale_five_action); m_glyph_editor_scale_actions.add_action(*m_scale_ten_action); m_glyph_editor_scale_actions.add_action(*m_scale_fifteen_action); m_glyph_editor_scale_actions.set_exclusive(true); m_paint_glyph_action = GUI::Action::create_checkable("Paint Glyph", { Mod_Ctrl, KeyCode::Key_J }, g_resources.paint_glyph, [this](auto&) { m_glyph_editor_widget->set_mode(GlyphEditorWidget::Paint); }); m_paint_glyph_action->set_checked(true); m_move_glyph_action = GUI::Action::create_checkable("Move Glyph", { Mod_Ctrl, KeyCode::Key_K }, g_resources.move_glyph, [this](auto&) { m_glyph_editor_widget->set_mode(GlyphEditorWidget::Move); }); m_glyph_tool_actions.add_action(*m_paint_glyph_action); m_glyph_tool_actions.add_action(*m_move_glyph_action); m_glyph_tool_actions.set_exclusive(true); m_rotate_counterclockwise_action = GUI::CommonActions::make_rotate_counterclockwise_action([this](auto&) { m_glyph_editor_widget->rotate_90(Gfx::RotationDirection::CounterClockwise); }); m_rotate_clockwise_action = GUI::CommonActions::make_rotate_clockwise_action([this](auto&) { m_glyph_editor_widget->rotate_90(Gfx::RotationDirection::Clockwise); }); m_flip_horizontal_action = GUI::Action::create("Flip Horizontally", { Mod_Ctrl | Mod_Shift, Key_Q }, g_resources.flip_horizontally, [this](auto&) { m_glyph_editor_widget->flip(Gfx::Orientation::Horizontal); }); m_flip_vertical_action = GUI::Action::create("Flip Vertically", { Mod_Ctrl | Mod_Shift, Key_W }, g_resources.flip_vertically, [this](auto&) { m_glyph_editor_widget->flip(Gfx::Orientation::Vertical); }); m_copy_text_action = GUI::Action::create("Copy as Te&xt", { Mod_Ctrl, Key_T }, g_resources.copy_as_text, [this](auto&) { StringBuilder builder; auto selection = m_glyph_map_widget->selection().normalized(); for (auto code_point = selection.start(); code_point < selection.start() + selection.size(); ++code_point) { if (!m_glyph_map_widget->font().contains_glyph(code_point)) continue; builder.append_code_point(code_point); } GUI::Clipboard::the().set_plain_text(builder.to_deprecated_string()); }); m_copy_text_action->set_status_tip("Copy to clipboard as text"_string); return {}; } ErrorOr MainWidget::create_toolbars() { auto& toolbar = *find_descendant_of_type_named("toolbar"); (void)TRY(toolbar.try_add_action(*m_new_action)); (void)TRY(toolbar.try_add_action(*m_open_action)); (void)TRY(toolbar.try_add_action(*m_save_action)); TRY(toolbar.try_add_separator()); (void)TRY(toolbar.try_add_action(*m_cut_action)); (void)TRY(toolbar.try_add_action(*m_copy_action)); (void)TRY(toolbar.try_add_action(*m_paste_action)); (void)TRY(toolbar.try_add_action(*m_delete_action)); TRY(toolbar.try_add_separator()); (void)TRY(toolbar.try_add_action(*m_undo_action)); (void)TRY(toolbar.try_add_action(*m_redo_action)); TRY(toolbar.try_add_separator()); (void)TRY(toolbar.try_add_action(*m_open_preview_action)); TRY(toolbar.try_add_separator()); (void)TRY(toolbar.try_add_action(*m_previous_glyph_action)); (void)TRY(toolbar.try_add_action(*m_next_glyph_action)); (void)TRY(toolbar.try_add_action(*m_go_to_glyph_action)); auto& glyph_transform_toolbar = *find_descendant_of_type_named("glyph_transform_toolbar"); (void)TRY(glyph_transform_toolbar.try_add_action(*m_flip_horizontal_action)); (void)TRY(glyph_transform_toolbar.try_add_action(*m_flip_vertical_action)); (void)TRY(glyph_transform_toolbar.try_add_action(*m_rotate_counterclockwise_action)); (void)TRY(glyph_transform_toolbar.try_add_action(*m_rotate_clockwise_action)); auto& glyph_mode_toolbar = *find_descendant_of_type_named("glyph_mode_toolbar"); (void)TRY(glyph_mode_toolbar.try_add_action(*m_paint_glyph_action)); (void)TRY(glyph_mode_toolbar.try_add_action(*m_move_glyph_action)); return {}; } ErrorOr MainWidget::create_models() { TRY(m_font_slope_list.try_ensure_capacity(Gfx::font_slope_names.size())); for (auto& it : Gfx::font_slope_names) m_font_slope_list.unchecked_append(TRY(String::from_utf8(it.name))); m_slope_combobox->set_model(TRY(GUI::ItemListModel::try_create(m_font_slope_list))); TRY(m_font_weight_list.try_ensure_capacity(Gfx::font_weight_names.size())); for (auto& it : Gfx::font_weight_names) m_font_weight_list.unchecked_append(TRY(String::from_utf8(it.name))); m_weight_combobox->set_model(TRY(GUI::ItemListModel::try_create(m_font_weight_list))); auto unicode_blocks = Unicode::block_display_names(); TRY(m_unicode_block_list.try_ensure_capacity(unicode_blocks.size() + 1)); m_unicode_block_list.unchecked_append(TRY(String::from_utf8("Show All"sv))); for (auto& block : unicode_blocks) m_unicode_block_list.unchecked_append(TRY(String::from_utf8(block.display_name))); m_unicode_block_model = TRY(GUI::ItemListModel::try_create(m_unicode_block_list)); m_filter_model = TRY(GUI::FilteringProxyModel::create(*m_unicode_block_model)); m_filter_model->set_filter_term(""sv); m_unicode_block_listview = find_descendant_of_type_named("unicode_block_listview"); m_unicode_block_listview->on_selection_change = [this, unicode_blocks] { auto index = m_unicode_block_listview->selection().first(); auto mapped_index = m_filter_model->map(index); if (mapped_index.row() > 0) m_range = unicode_blocks[mapped_index.row() - 1].code_point_range; else m_range = { 0x0000, 0x10FFFF }; m_glyph_map_widget->set_active_range(m_range); }; m_unicode_block_listview->set_model(m_filter_model); m_unicode_block_listview->set_activates_on_selection(true); m_unicode_block_listview->horizontal_scrollbar().set_visible(false); m_unicode_block_listview->set_cursor(m_unicode_block_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set); m_unicode_block_listview->set_focus_proxy(m_search_textbox); return {}; } ErrorOr MainWidget::create_undo_stack() { m_undo_stack = TRY(try_make()); m_undo_stack->on_state_change = [this] { m_undo_action->set_enabled(m_undo_stack->can_undo()); m_redo_action->set_enabled(m_undo_stack->can_redo()); update_action_text(); if (m_undo_stack->is_current_modified()) did_modify_font(); }; return {}; } void MainWidget::update_action_text() { auto text_or_error = [](auto prefix, auto suffix) -> ErrorOr { StringBuilder builder; TRY(builder.try_append(prefix)); if (suffix.has_value()) { TRY(builder.try_append(' ')); TRY(builder.try_append(suffix.value())); } return builder.to_string(); }; if (auto maybe_text = text_or_error("&Undo"sv, m_undo_stack->undo_action_text()); !maybe_text.is_error()) m_undo_action->set_text(maybe_text.release_value().to_deprecated_string()); if (auto maybe_text = text_or_error("&Redo"sv, m_undo_stack->redo_action_text()); !maybe_text.is_error()) m_redo_action->set_text(maybe_text.release_value().to_deprecated_string()); } ErrorOr MainWidget::create_widgets() { TRY(load_from_gml(font_editor_window_gml)); m_font_metadata_groupbox = find_descendant_of_type_named("font_metadata_groupbox"); m_unicode_block_container = find_descendant_of_type_named("unicode_block_container"); m_toolbar_container = find_descendant_of_type_named("toolbar_container"); m_width_control_container = find_descendant_of_type_named("width_control_container"); m_glyph_map_widget = find_descendant_of_type_named("glyph_map_widget"); m_glyph_editor_widget = find_descendant_of_type_named("glyph_editor_widget"); m_glyph_editor_widget->on_glyph_altered = [this](int glyph) { m_glyph_map_widget->update_glyph(glyph); update_preview(); did_modify_font(); }; m_glyph_editor_widget->on_undo_event = [this](auto action_text) { reset_selection(); push_undo(action_text); }; m_glyph_editor_width_spinbox = find_descendant_of_type_named("glyph_editor_width_spinbox"); m_glyph_editor_present_checkbox = find_descendant_of_type_named("glyph_editor_present_checkbox"); m_glyph_map_widget->on_active_glyph_changed = [this](int glyph) { if (m_undo_selection) { auto selection = m_glyph_map_widget->selection().normalized(); m_undo_selection->set_start(selection.start()); m_undo_selection->set_size(selection.size()); m_undo_selection->set_active_glyph(glyph); } m_glyph_editor_widget->set_glyph(glyph); auto glyph_width = m_font->raw_glyph_width(glyph); if (m_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No); else m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No); update_statusbar(); }; m_glyph_map_widget->on_context_menu_request = [this](auto& event) { m_context_menu->popup(event.screen_position()); }; m_glyph_map_widget->on_escape_pressed = [this]() { update_statusbar(); }; m_name_textbox = find_descendant_of_type_named("name_textbox"); m_name_textbox->on_change = [this] { m_font->set_name(m_name_textbox->text()); did_modify_font(); }; m_family_textbox = find_descendant_of_type_named("family_textbox"); m_family_textbox->on_change = [this] { m_font->set_family(m_family_textbox->text()); did_modify_font(); }; m_fixed_width_checkbox = find_descendant_of_type_named("fixed_width_checkbox"); m_fixed_width_checkbox->on_checked = [this](bool checked) { m_font->set_fixed_width(checked); auto glyph_width = m_font->raw_glyph_width(m_glyph_map_widget->active_glyph()); m_glyph_editor_width_spinbox->set_visible(!checked); m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No); m_glyph_editor_present_checkbox->set_visible(checked); m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No); m_glyph_editor_widget->update(); update_preview(); did_modify_font(); }; m_glyph_editor_width_spinbox->on_change = [this](int value) { reset_selection(); push_undo("Resize Glyph"sv); m_font->set_glyph_width(m_glyph_map_widget->active_glyph(), value); m_glyph_editor_widget->update(); m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph()); update_preview(); update_statusbar(); did_modify_font(); }; m_glyph_editor_present_checkbox->on_checked = [this](bool checked) { reset_selection(); push_undo("Resize Glyph"sv); m_font->set_glyph_width(m_glyph_map_widget->active_glyph(), checked ? m_font->glyph_fixed_width() : 0); m_glyph_editor_widget->update(); m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph()); update_preview(); update_statusbar(); did_modify_font(); }; m_weight_combobox = find_descendant_of_type_named("weight_combobox"); m_weight_combobox->on_change = [this](auto&, auto&) { m_font->set_weight(Gfx::name_to_weight(m_weight_combobox->text())); did_modify_font(); }; m_slope_combobox = find_descendant_of_type_named("slope_combobox"); m_slope_combobox->on_change = [this](auto&, auto&) { m_font->set_slope(Gfx::name_to_slope(m_slope_combobox->text())); did_modify_font(); }; m_presentation_spinbox = find_descendant_of_type_named("presentation_spinbox"); m_presentation_spinbox->on_change = [this](int value) { m_font->set_presentation_size(value); update_preview(); did_modify_font(); }; m_spacing_spinbox = find_descendant_of_type_named("spacing_spinbox"); m_spacing_spinbox->on_change = [this](int value) { m_font->set_glyph_spacing(value); update_preview(); did_modify_font(); }; m_baseline_spinbox = find_descendant_of_type_named("baseline_spinbox"); m_baseline_spinbox->on_change = [this](int value) { m_font->set_baseline(value); m_glyph_editor_widget->update(); update_preview(); did_modify_font(); }; m_mean_line_spinbox = find_descendant_of_type_named("mean_line_spinbox"); m_mean_line_spinbox->on_change = [this](int value) { m_font->set_mean_line(value); m_glyph_editor_widget->update(); update_preview(); did_modify_font(); }; m_search_textbox = find_descendant_of_type_named("search_textbox"); m_search_textbox->on_return_pressed = [this] { if (!m_unicode_block_listview->selection().is_empty()) m_unicode_block_listview->activate_selected(); }; m_search_textbox->on_down_pressed = [this] { m_unicode_block_listview->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); }; m_search_textbox->on_up_pressed = [this] { m_unicode_block_listview->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set); }; m_search_textbox->on_change = [this] { m_filter_model->set_filter_term(m_search_textbox->text()); if (m_filter_model->row_count() != 0) m_unicode_block_listview->set_cursor(m_filter_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set); }; m_statusbar = find_descendant_of_type_named("statusbar"); m_statusbar->segment(1).set_mode(GUI::Statusbar::Segment::Mode::Auto); m_statusbar->segment(1).set_clickable(true); m_statusbar->segment(1).on_click = [this](auto) { m_show_unicode_blocks_action->activate(); }; GUI::Application::the()->on_action_enter = [this](GUI::Action& action) { m_statusbar->set_override_text(action.status_tip()); }; GUI::Application::the()->on_action_leave = [this](GUI::Action&) { m_statusbar->set_override_text({}); }; return {}; } ErrorOr MainWidget::initialize(StringView path, RefPtr&& mutable_font) { VERIFY(window()); if (m_font == mutable_font) return {}; ScopeGuard reset_on_error([&] { if (!m_initialized) reset(); }); m_initialized = false; m_path = TRY(String::from_utf8(path)); m_font = move(mutable_font); TRY(m_glyph_map_widget->initialize(m_font)); auto active_glyph = m_glyph_map_widget->active_glyph(); m_glyph_map_widget->set_focus(true); m_glyph_map_widget->scroll_to_glyph(active_glyph); auto selection = m_glyph_map_widget->selection().normalized(); m_undo_selection = TRY(try_make_ref_counted(selection.start(), selection.size(), active_glyph, *m_font, *m_glyph_map_widget)); m_undo_stack->clear(); if (m_preview_label) m_preview_label->set_font(*m_font); m_glyph_editor_widget->initialize(m_font); m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height()); m_glyph_editor_widget->set_glyph(active_glyph); m_glyph_editor_width_spinbox->set_visible(!m_font->is_fixed_width()); m_glyph_editor_width_spinbox->set_max(m_font->max_glyph_width(), GUI::AllowCallback::No); m_glyph_editor_width_spinbox->set_value(m_font->raw_glyph_width(active_glyph), GUI::AllowCallback::No); m_glyph_editor_present_checkbox->set_visible(m_font->is_fixed_width()); m_glyph_editor_present_checkbox->set_checked(m_font->contains_raw_glyph(active_glyph), GUI::AllowCallback::No); m_fixed_width_checkbox->set_checked(m_font->is_fixed_width(), GUI::AllowCallback::No); m_name_textbox->set_text(m_font->name(), GUI::AllowCallback::No); m_family_textbox->set_text(m_font->family(), GUI::AllowCallback::No); m_presentation_spinbox->set_value(m_font->presentation_size(), GUI::AllowCallback::No); m_spacing_spinbox->set_value(m_font->glyph_spacing(), GUI::AllowCallback::No); m_mean_line_spinbox->set_range(0, max(m_font->glyph_height() - 2, 0), GUI::AllowCallback::No); m_baseline_spinbox->set_range(0, max(m_font->glyph_height() - 2, 0), GUI::AllowCallback::No); m_mean_line_spinbox->set_value(m_font->mean_line(), GUI::AllowCallback::No); m_baseline_spinbox->set_value(m_font->baseline(), GUI::AllowCallback::No); for (size_t i = 0; i < Gfx::font_weight_names.size(); ++i) { if (Gfx::font_weight_names[i].style == m_font->weight()) { m_weight_combobox->set_selected_index(i, GUI::AllowCallback::No); break; } } for (size_t i = 0; i < Gfx::font_slope_names.size(); ++i) { if (Gfx::font_slope_names[i].style == m_font->slope()) { m_slope_combobox->set_selected_index(i, GUI::AllowCallback::No); break; } } window()->set_modified(false); update_title(); update_statusbar(); set_actions_enabled(true); set_widgets_enabled(true); m_initialized = true; return {}; } ErrorOr MainWidget::initialize_menubar(GUI::Window& window) { auto file_menu = TRY(window.try_add_menu("&File"_string)); TRY(file_menu->try_add_action(*m_new_action)); TRY(file_menu->try_add_action(*m_open_action)); TRY(file_menu->try_add_action(*m_save_action)); TRY(file_menu->try_add_action(*m_save_as_action)); file_menu->add_separator(); TRY(file_menu->add_recent_files_list([this](auto& action) { if (!request_close()) return; auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(this->window(), action.text()); if (response.is_error()) return; auto file = response.release_value(); if (auto result = open_file(file.filename(), file.release_stream()); result.is_error()) show_error(result.release_error(), "Opening"sv, file.filename()); })); TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([this](auto&) { if (!request_close()) return; GUI::Application::the()->quit(); }))); auto edit_menu = TRY(window.try_add_menu("&Edit"_string)); TRY(edit_menu->try_add_action(*m_undo_action)); TRY(edit_menu->try_add_action(*m_redo_action)); edit_menu->add_separator(); TRY(edit_menu->try_add_action(*m_cut_action)); TRY(edit_menu->try_add_action(*m_copy_action)); TRY(edit_menu->try_add_action(*m_paste_action)); TRY(edit_menu->try_add_action(*m_delete_action)); edit_menu->add_separator(); TRY(edit_menu->try_add_action(*m_select_all_action)); edit_menu->add_separator(); TRY(edit_menu->try_add_action(*m_copy_text_action)); m_context_menu = edit_menu; auto go_menu = TRY(window.try_add_menu("&Go"_string)); TRY(go_menu->try_add_action(*m_previous_glyph_action)); TRY(go_menu->try_add_action(*m_next_glyph_action)); TRY(go_menu->try_add_action(*m_go_to_glyph_action)); auto view_menu = TRY(window.try_add_menu("&View"_string)); auto layout_menu = view_menu->add_submenu("&Layout"_string); TRY(layout_menu->try_add_action(*m_show_toolbar_action)); TRY(layout_menu->try_add_action(*m_show_statusbar_action)); TRY(layout_menu->try_add_action(*m_show_metadata_action)); TRY(layout_menu->try_add_action(*m_show_unicode_blocks_action)); view_menu->add_separator(); TRY(view_menu->try_add_action(*m_open_preview_action)); view_menu->add_separator(); TRY(view_menu->try_add_action(*m_highlight_modifications_action)); TRY(view_menu->try_add_action(*m_show_system_emoji_action)); view_menu->add_separator(); auto scale_menu = view_menu->add_submenu("&Scale"_string); scale_menu->set_icon(g_resources.scale_editor); TRY(scale_menu->try_add_action(*m_scale_five_action)); TRY(scale_menu->try_add_action(*m_scale_ten_action)); TRY(scale_menu->try_add_action(*m_scale_fifteen_action)); auto help_menu = TRY(window.try_add_menu("&Help"_string)); TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(&window))); TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) { Desktop::Launcher::open(URL::create_with_file_scheme("/usr/share/man/man1/Applications/FontEditor.md"), "/bin/Help"); }))); TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Font Editor", TRY(GUI::Icon::try_create_default_icon("app-font-editor"sv)), &window))); return {}; } ErrorOr MainWidget::save_file(StringView path, NonnullOwnPtr file) { auto masked_font = TRY(m_font->masked_character_set()); TRY(masked_font->write_to_file(move(file))); m_path = TRY(String::from_utf8(path)); m_undo_stack->set_current_unmodified(); window()->set_modified(false); update_title(); return {}; } ErrorOr MainWidget::open_file(StringView path, NonnullOwnPtr file) { auto mapped_file = TRY(Core::MappedFile::map_from_file(move(file), path)); auto unmasked_font = TRY(TRY(Gfx::BitmapFont::try_load_from_mapped_file(mapped_file))->unmasked_character_set()); TRY(initialize(path, move(unmasked_font))); if (!path.is_empty()) GUI::Application::the()->set_most_recently_open_file(TRY(String::from_utf8(path))); return {}; } void MainWidget::push_undo(StringView action_text) { auto maybe_state = m_undo_selection->save_state(); if (maybe_state.is_error()) return show_error(maybe_state.release_error(), "Saving undo state failed"sv); auto maybe_text = String::from_utf8(action_text); if (maybe_text.is_error()) return show_error(maybe_text.release_error(), "Creating action text failed"sv); auto maybe_command = try_make(*m_undo_selection, move(maybe_state.value()), move(maybe_text.value())); if (maybe_command.is_error()) return show_error(maybe_command.release_error(), "Making undo command failed"sv); if (auto maybe_push = m_undo_stack->try_push(move(maybe_command.value())); maybe_push.is_error()) show_error(maybe_push.release_error(), "Pushing undo stack failed"sv); } void MainWidget::reset_selection() { auto selection = m_glyph_map_widget->selection().normalized(); if (selection.size() == 1) return; auto start = m_glyph_map_widget->active_glyph(); m_undo_selection->set_start(start); m_undo_selection->set_size(1); m_glyph_map_widget->set_selection(start, 1); m_glyph_map_widget->update(); } void MainWidget::restore_state() { auto glyph = m_undo_selection->restored_active_glyph(); auto glyph_width = m_font->raw_glyph_width(glyph); if (glyph < m_range.first || glyph > m_range.last) m_search_textbox->set_text(""sv); auto start = m_undo_selection->restored_start(); auto size = m_undo_selection->restored_size(); m_glyph_map_widget->restore_selection(start, size, glyph); m_glyph_map_widget->scroll_to_glyph(glyph); m_glyph_map_widget->set_focus(true); if (m_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No); else m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No); m_glyph_editor_widget->update(); m_glyph_map_widget->update(); update_preview(); update_statusbar(); } void MainWidget::undo() { if (!m_undo_stack->can_undo()) return; m_undo_stack->undo(); restore_state(); } void MainWidget::redo() { if (!m_undo_stack->can_redo()) return; m_undo_stack->redo(); restore_state(); } bool MainWidget::request_close() { if (!window()->is_modified()) return true; auto result = GUI::MessageBox::try_ask_about_unsaved_changes(window(), m_path, m_undo_stack->last_unmodified_timestamp()); if (result.is_error()) return false; if (result.value() == GUI::MessageBox::ExecResult::Yes) { m_save_action->activate(); if (!window()->is_modified()) return true; } if (result.value() == GUI::MessageBox::ExecResult::No) return true; return false; } void MainWidget::update_title() { StringBuilder title; if (m_path.is_empty()) title.append("Untitled"sv); else title.append(m_path); title.append("[*] - Font Editor"sv); window()->set_title(title.to_deprecated_string()); } void MainWidget::did_modify_font() { if (!window() || window()->is_modified()) return; window()->set_modified(true); update_title(); } void MainWidget::update_statusbar() { if (!m_font) return; if (!m_statusbar->is_visible()) return; auto format_statusbar = [this]() -> ErrorOr { auto glyph = m_glyph_map_widget->active_glyph(); StringBuilder builder; TRY(builder.try_appendff("U+{:04X} (", glyph)); if (auto abbreviation = Unicode::code_point_abbreviation(glyph); abbreviation.has_value()) TRY(builder.try_append(*abbreviation)); // FIXME: Bidirectional text cannot currently be isolated; for now, replace RTL glyphs with U+FFFD. else if (Gfx::get_char_bidi_class(glyph) == Gfx::BidirectionalClass::STRONG_RTL) TRY(builder.try_append_code_point(0xFFFD)); else TRY(builder.try_append_code_point(glyph)); builder.append(')'); auto glyph_name = Unicode::code_point_display_name(glyph); if (glyph_name.has_value()) TRY(builder.try_appendff(" {}", glyph_name.value())); if (m_font->contains_raw_glyph(glyph)) TRY(builder.try_appendff(" [{}x{}]", m_font->raw_glyph_width(glyph), m_font->glyph_height())); else if (Gfx::Emoji::emoji_for_code_point(glyph)) TRY(builder.try_appendff(" [emoji]")); m_statusbar->set_text(0, TRY(builder.to_string())); builder.clear(); auto selection = m_glyph_map_widget->selection().normalized(); if (selection.size() > 1) TRY(builder.try_appendff("{} glyphs selected", selection.size())); else TRY(builder.try_appendff("U+{:04X}-U+{:04X}", m_range.first, m_range.last)); m_statusbar->set_text(1, TRY(builder.to_string())); return {}; }(); if (format_statusbar.is_error()) warnln("Formatting status bar failed"); } void MainWidget::update_preview() { if (m_font_preview_window) m_font_preview_window->update(); } void MainWidget::drag_enter_event(GUI::DragEvent& event) { auto const& mime_types = event.mime_types(); if (mime_types.contains_slow("text/uri-list")) event.accept(); } void MainWidget::drop_event(GUI::DropEvent& event) { event.accept(); if (event.mime_data().has_urls()) { auto urls = event.mime_data().urls(); if (urls.is_empty()) return; window()->move_to_front(); if (!request_close()) return; auto file_path = urls.first().serialize_path(); auto result = FileSystemAccessClient::Client::the().request_file_read_only_approved(window(), file_path); if (result.is_error()) return; auto file = result.release_value(); if (auto result = open_file(file.filename(), file.release_stream()); result.is_error()) show_error(result.release_error(), "Opening"sv, file.filename()); } } void MainWidget::set_scale_and_save(i32 scale) { Config::write_i32("FontEditor"sv, "GlyphEditor"sv, "Scale"sv, scale); m_glyph_editor_widget->set_scale(scale); m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height()); } ErrorOr MainWidget::copy_selected_glyphs() { size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * m_font->glyph_height(); auto selection = m_glyph_map_widget->selection().normalized(); auto* rows = m_font->rows() + selection.start() * bytes_per_glyph; auto* widths = m_font->widths() + selection.start(); ByteBuffer buffer; TRY(buffer.try_append(rows, bytes_per_glyph * selection.size())); TRY(buffer.try_append(widths, selection.size())); HashMap metadata; metadata.set("start", DeprecatedString::number(selection.start())); metadata.set("count", DeprecatedString::number(selection.size())); metadata.set("width", DeprecatedString::number(m_font->max_glyph_width())); metadata.set("height", DeprecatedString::number(m_font->glyph_height())); GUI::Clipboard::the().set_data(buffer.bytes(), "glyph/x-fonteditor", metadata); return {}; } ErrorOr MainWidget::cut_selected_glyphs() { TRY(copy_selected_glyphs()); delete_selected_glyphs(); return {}; } void MainWidget::paste_glyphs() { auto [data, mime_type, metadata] = GUI::Clipboard::the().fetch_data_and_type(); if (!mime_type.starts_with("glyph/x-fonteditor"sv)) return; auto glyph_count = metadata.get("count").value().to_uint().value_or(0); if (!glyph_count) return; auto height = metadata.get("height").value().to_uint().value_or(0); if (!height) return; auto selection = m_glyph_map_widget->selection().normalized(); auto range_bound_glyph_count = min(glyph_count, 1 + m_range.last - selection.start()); m_undo_selection->set_size(range_bound_glyph_count); auto action_text = range_bound_glyph_count == 1 ? "Paste Glyph"sv : "Paste Glyphs"sv; push_undo(action_text); size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * m_font->glyph_height(); size_t bytes_per_copied_glyph = Gfx::GlyphBitmap::bytes_per_row() * height; size_t copyable_bytes_per_glyph = min(bytes_per_glyph, bytes_per_copied_glyph); auto* rows = m_font->rows() + selection.start() * bytes_per_glyph; auto* widths = m_font->widths() + selection.start(); for (size_t i = 0; i < range_bound_glyph_count; ++i) { auto copyable_width = m_font->is_fixed_width() ? data[bytes_per_copied_glyph * glyph_count + i] ? m_font->glyph_fixed_width() : 0 : min(m_font->max_glyph_width(), data[bytes_per_copied_glyph * glyph_count + i]); memcpy(&rows[i * bytes_per_glyph], &data[i * bytes_per_copied_glyph], copyable_bytes_per_glyph); memset(&widths[i], copyable_width, sizeof(u8)); m_glyph_map_widget->set_glyph_modified(selection.start() + i, true); } m_glyph_map_widget->set_selection(selection.start() + range_bound_glyph_count - 1, -range_bound_glyph_count + 1); if (m_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(m_font->contains_raw_glyph(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No); else m_glyph_editor_width_spinbox->set_value(m_font->raw_glyph_width(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No); m_glyph_editor_widget->update(); m_glyph_map_widget->update(); update_preview(); update_statusbar(); } void MainWidget::delete_selected_glyphs() { auto selection = m_glyph_map_widget->selection().normalized(); auto action_text = selection.size() == 1 ? "Delete Glyph"sv : "Delete Glyphs"sv; push_undo(action_text); size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * m_font->glyph_height(); auto* rows = m_font->rows() + selection.start() * bytes_per_glyph; auto* widths = m_font->widths() + selection.start(); memset(rows, 0, bytes_per_glyph * selection.size()); memset(widths, 0, selection.size()); if (m_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No); else m_glyph_editor_width_spinbox->set_value(0, GUI::AllowCallback::No); m_glyph_editor_widget->update(); m_glyph_map_widget->update(); update_preview(); update_statusbar(); } void MainWidget::show_error(Error error, StringView action, StringView filename) { auto format = filename.is_null() ? "{}{}: {}"sv : "{} \"{}\" failed: {}"sv; auto file = filename.is_null() ? StringView {} : filename; warnln(format, action, file, error); auto maybe_message = String::formatted(format, action, file, error); if (!maybe_message.is_error()) (void)GUI::MessageBox::try_show_error(window(), maybe_message.release_value()); } void MainWidget::reset() { VERIFY(window()); m_initialized = false; m_font = nullptr; m_path = {}; m_undo_selection = nullptr; m_undo_stack->clear(); (void)m_glyph_map_widget->initialize(nullptr); m_glyph_editor_widget->initialize(nullptr); if (m_font_preview_window) m_font_preview_window->close(); if (m_preview_label) m_preview_label->set_font(nullptr); m_name_textbox->set_text({}, GUI::AllowCallback::No); m_family_textbox->set_text({}, GUI::AllowCallback::No); m_slope_combobox->set_text({}, GUI::AllowCallback::No); m_weight_combobox->set_text({}, GUI::AllowCallback::No); m_presentation_spinbox->set_text({}, GUI::AllowCallback::No); m_baseline_spinbox->set_text({}, GUI::AllowCallback::No); m_mean_line_spinbox->set_text({}, GUI::AllowCallback::No); m_spacing_spinbox->set_text({}, GUI::AllowCallback::No); m_fixed_width_checkbox->set_checked(false, GUI::AllowCallback::No); m_statusbar->set_text(0, {}); m_statusbar->set_text(1, {}); window()->set_modified(false); window()->set_title("Font Editor"); set_actions_enabled(false); set_widgets_enabled(false); set_focus(true); } void MainWidget::set_actions_enabled(bool enabled) { m_save_action->set_enabled(enabled); m_save_as_action->set_enabled(enabled); m_cut_action->set_enabled(enabled); m_copy_action->set_enabled(enabled); m_paste_action->set_enabled(enabled && GUI::Clipboard::the().fetch_mime_type() == "glyph/x-fonteditor"); m_delete_action->set_enabled(enabled); m_copy_text_action->set_enabled(enabled); m_select_all_action->set_enabled(enabled); m_go_to_glyph_action->set_enabled(enabled); m_previous_glyph_action->set_enabled(enabled); m_next_glyph_action->set_enabled(enabled); m_move_glyph_action->set_enabled(enabled); m_paint_glyph_action->set_enabled(enabled); m_flip_horizontal_action->set_enabled(enabled); m_flip_vertical_action->set_enabled(enabled); m_rotate_clockwise_action->set_enabled(enabled); m_rotate_counterclockwise_action->set_enabled(enabled); m_open_preview_action->set_enabled(enabled); m_highlight_modifications_action->set_enabled(enabled); m_show_system_emoji_action->set_enabled(enabled); m_scale_five_action->set_enabled(enabled); m_scale_ten_action->set_enabled(enabled); m_scale_fifteen_action->set_enabled(enabled); } void MainWidget::set_widgets_enabled(bool enabled) { m_font_metadata_groupbox->set_enabled(enabled); m_unicode_block_container->set_enabled(enabled); m_width_control_container->set_enabled(enabled); m_width_control_container->set_visible(enabled); m_glyph_map_widget->set_enabled(enabled); m_glyph_editor_widget->set_enabled(enabled); m_glyph_editor_widget->set_visible(enabled); m_statusbar->segment(1).set_visible(enabled); } }