Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "MainWidget.h"
9#include "GlyphEditorWidget.h"
10#include "NewFontDialog.h"
11#include <AK/Array.h>
12#include <AK/StringBuilder.h>
13#include <AK/StringUtils.h>
14#include <Applications/FontEditor/FontEditorWindowGML.h>
15#include <Applications/FontEditor/FontPreviewWindowGML.h>
16#include <LibConfig/Client.h>
17#include <LibDesktop/Launcher.h>
18#include <LibGUI/Action.h>
19#include <LibGUI/Application.h>
20#include <LibGUI/Button.h>
21#include <LibGUI/CheckBox.h>
22#include <LibGUI/Clipboard.h>
23#include <LibGUI/ComboBox.h>
24#include <LibGUI/FilePicker.h>
25#include <LibGUI/GlyphMapWidget.h>
26#include <LibGUI/GroupBox.h>
27#include <LibGUI/InputBox.h>
28#include <LibGUI/ItemListModel.h>
29#include <LibGUI/Label.h>
30#include <LibGUI/ListView.h>
31#include <LibGUI/Menu.h>
32#include <LibGUI/Menubar.h>
33#include <LibGUI/MessageBox.h>
34#include <LibGUI/SpinBox.h>
35#include <LibGUI/Statusbar.h>
36#include <LibGUI/TextBox.h>
37#include <LibGUI/ToolbarContainer.h>
38#include <LibGUI/Window.h>
39#include <LibGfx/Font/BitmapFont.h>
40#include <LibGfx/Font/Emoji.h>
41#include <LibGfx/Font/FontStyleMapping.h>
42#include <LibGfx/TextDirection.h>
43#include <LibUnicode/CharacterTypes.h>
44
45namespace FontEditor {
46
47static constexpr Array pangrams = {
48 "quick fox jumps nightly above wizard"sv,
49 "five quacking zephyrs jolt my wax bed"sv,
50 "pack my box with five dozen liquor jugs"sv,
51 "quick brown fox jumps over the lazy dog"sv,
52 "waxy and quivering jocks fumble the pizza"sv,
53 "~#:[@_1%]*{$2.3}/4^(5'6\")-&|7+8!=<9,0\\>?;"sv,
54 "byxfjärmat föl gick på duvshowen"sv,
55 " "sv,
56 "float Fox.quick(h){ is_brown && it_jumps_over(doges.lazy) }"sv,
57 "<fox color=\"brown\" speed=\"quick\" jumps=\"over\">lazy dog</fox>"sv
58};
59
60ErrorOr<RefPtr<GUI::Window>> MainWidget::create_preview_window()
61{
62 auto window = TRY(GUI::Window::try_create(this));
63 window->set_window_mode(GUI::WindowMode::RenderAbove);
64 window->set_title("Preview");
65 window->resize(400, 150);
66 window->center_within(*this->window());
67
68 auto main_widget = TRY(window->set_main_widget<GUI::Widget>());
69 TRY(main_widget->load_from_gml(font_preview_window_gml));
70
71 m_preview_label = find_descendant_of_type_named<GUI::Label>("preview_label");
72 m_preview_label->set_font(edited_font());
73
74 m_preview_textbox = find_descendant_of_type_named<GUI::TextBox>("preview_textbox");
75 m_preview_textbox->on_change = [&] {
76 auto preview = DeprecatedString::formatted("{}\n{}", m_preview_textbox->text(), Unicode::to_unicode_uppercase_full(m_preview_textbox->text()).release_value_but_fixme_should_propagate_errors());
77 m_preview_label->set_text(preview);
78 };
79 m_preview_textbox->set_text(pangrams[0]);
80
81 auto& reload_button = *find_descendant_of_type_named<GUI::Button>("reload_button");
82 reload_button.on_click = [&](auto) {
83 static size_t i = 1;
84 if (i >= pangrams.size())
85 i = 0;
86 m_preview_textbox->set_text(pangrams[i]);
87 i++;
88 };
89
90 return window;
91}
92
93ErrorOr<void> MainWidget::create_actions()
94{
95 m_new_action = GUI::Action::create("&New Font...", { Mod_Ctrl, Key_N }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-font.png"sv)), [&](auto&) {
96 if (!request_close())
97 return;
98 auto new_font_wizard = NewFontDialog::construct(window());
99 if (new_font_wizard->exec() != GUI::Dialog::ExecResult::OK)
100 return;
101 new_font_wizard->hide();
102 auto maybe_font = new_font_wizard->create_font();
103 if (maybe_font.is_error())
104 return show_error(maybe_font.release_error(), "Creating new font failed"sv);
105 if (auto result = initialize({}, move(maybe_font.value())); result.is_error())
106 show_error(result.release_error(), "Initializing new font failed"sv);
107 });
108 m_new_action->set_status_tip("Create a new font");
109
110 m_open_action = GUI::CommonActions::make_open_action([&](auto&) {
111 if (!request_close())
112 return;
113 Optional<DeprecatedString> open_path = GUI::FilePicker::get_open_filepath(window(), {}, "/res/fonts/"sv);
114 if (!open_path.has_value())
115 return;
116 if (auto result = open_file(open_path.value()); result.is_error())
117 show_error(result.release_error(), "Opening"sv, LexicalPath { open_path.value() }.basename());
118 });
119
120 m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
121 if (m_path.is_empty())
122 return m_save_as_action->activate();
123 if (auto result = save_file(m_path); result.is_error())
124 show_error(result.release_error(), "Saving"sv, LexicalPath { m_path }.basename());
125 });
126
127 m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
128 LexicalPath lexical_path(m_path.is_empty() ? "Untitled.font" : m_path);
129 Optional<DeprecatedString> save_path = GUI::FilePicker::get_save_filepath(window(), lexical_path.title(), lexical_path.extension());
130 if (!save_path.has_value())
131 return;
132 if (auto result = save_file(save_path.value()); result.is_error())
133 show_error(result.release_error(), "Saving"sv, lexical_path.basename());
134 });
135
136 m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) {
137 if (auto result = cut_selected_glyphs(); result.is_error())
138 show_error(result.release_error(), "Cutting selection failed"sv);
139 });
140
141 m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) {
142 if (auto result = copy_selected_glyphs(); result.is_error())
143 show_error(result.release_error(), "Copying selection failed"sv);
144 });
145
146 m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
147 paste_glyphs();
148 });
149 m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "glyph/x-fonteditor");
150
151 GUI::Clipboard::the().on_change = [&](DeprecatedString const& data_type) {
152 m_paste_action->set_enabled(data_type == "glyph/x-fonteditor");
153 };
154
155 m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) {
156 delete_selected_glyphs();
157 });
158
159 m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
160 undo();
161 });
162 m_undo_action->set_enabled(false);
163
164 m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
165 redo();
166 });
167 m_redo_action->set_enabled(false);
168
169 m_select_all_action = GUI::CommonActions::make_select_all_action([this](auto&) {
170 m_glyph_map_widget->set_selection(m_range.first, m_range.last - m_range.first + 1);
171 m_glyph_map_widget->update();
172 auto selection = m_glyph_map_widget->selection().normalized();
173 m_undo_selection->set_start(selection.start());
174 m_undo_selection->set_size(selection.size());
175 update_statusbar();
176 });
177
178 m_open_preview_action = GUI::Action::create("&Preview Font", { Mod_Ctrl, Key_P }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"sv)), [&](auto&) {
179 if (!m_font_preview_window) {
180 if (auto maybe_window = create_preview_window(); maybe_window.is_error())
181 show_error(maybe_window.release_error(), "Creating preview window failed"sv);
182 else
183 m_font_preview_window = maybe_window.release_value();
184 }
185 if (m_font_preview_window)
186 m_font_preview_window->show();
187 });
188 m_open_preview_action->set_status_tip("Preview the current font");
189
190 bool show_metadata = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowMetadata"sv, true);
191 set_show_font_metadata(show_metadata);
192 m_show_metadata_action = GUI::Action::create_checkable("Font &Metadata", { Mod_Ctrl, Key_M }, [&](auto& action) {
193 set_show_font_metadata(action.is_checked());
194 Config::write_bool("FontEditor"sv, "Layout"sv, "ShowMetadata"sv, action.is_checked());
195 });
196 m_show_metadata_action->set_checked(show_metadata);
197 m_show_metadata_action->set_status_tip("Show or hide metadata about the current font");
198
199 bool show_unicode_blocks = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowUnicodeBlocks"sv, true);
200 set_show_unicode_blocks(show_unicode_blocks);
201 m_show_unicode_blocks_action = GUI::Action::create_checkable("&Unicode Blocks", { Mod_Ctrl, Key_U }, [&](auto& action) {
202 set_show_unicode_blocks(action.is_checked());
203 Config::write_bool("FontEditor"sv, "Layout"sv, "ShowUnicodeBlocks"sv, action.is_checked());
204 });
205 m_show_unicode_blocks_action->set_checked(show_unicode_blocks);
206 m_show_unicode_blocks_action->set_status_tip("Show or hide the Unicode block list");
207
208 bool show_toolbar = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowToolbar"sv, true);
209 set_show_toolbar(show_toolbar);
210 m_show_toolbar_action = GUI::Action::create_checkable("&Toolbar", [&](auto& action) {
211 set_show_toolbar(action.is_checked());
212 Config::write_bool("FontEditor"sv, "Layout"sv, "ShowToolbar"sv, action.is_checked());
213 });
214 m_show_toolbar_action->set_checked(show_toolbar);
215 m_show_toolbar_action->set_status_tip("Show or hide the toolbar");
216
217 bool show_statusbar = Config::read_bool("FontEditor"sv, "Layout"sv, "ShowStatusbar"sv, true);
218 set_show_statusbar(show_statusbar);
219 m_show_statusbar_action = GUI::Action::create_checkable("&Status Bar", [&](auto& action) {
220 set_show_statusbar(action.is_checked());
221 Config::write_bool("FontEditor"sv, "Layout"sv, "ShowStatusbar"sv, action.is_checked());
222 });
223 m_show_statusbar_action->set_checked(show_statusbar);
224 m_show_statusbar_action->set_status_tip("Show or hide the status bar");
225
226 bool highlight_modifications = Config::read_bool("FontEditor"sv, "GlyphMap"sv, "HighlightModifications"sv, true);
227 set_highlight_modifications(highlight_modifications);
228 m_highlight_modifications_action = GUI::Action::create_checkable("&Highlight Modifications", { Mod_Ctrl, Key_H }, [&](auto& action) {
229 set_highlight_modifications(action.is_checked());
230 Config::write_bool("FontEditor"sv, "GlyphMap"sv, "HighlightModifications"sv, action.is_checked());
231 });
232 m_highlight_modifications_action->set_checked(highlight_modifications);
233 m_highlight_modifications_action->set_status_tip("Show or hide highlights on modified glyphs. (Green = New, Blue = Modified, Red = Deleted)");
234
235 bool show_system_emoji = Config::read_bool("FontEditor"sv, "GlyphMap"sv, "ShowSystemEmoji"sv, true);
236 set_show_system_emoji(show_system_emoji);
237 m_show_system_emoji_action = GUI::Action::create_checkable("System &Emoji", { Mod_Ctrl, Key_E }, [&](auto& action) {
238 set_show_system_emoji(action.is_checked());
239 Config::write_bool("FontEditor"sv, "GlyphMap"sv, "ShowSystemEmoji"sv, action.is_checked());
240 });
241 m_show_system_emoji_action->set_checked(show_system_emoji);
242 m_show_system_emoji_action->set_status_tip("Show or hide system emoji");
243
244 m_go_to_glyph_action = GUI::Action::create("&Go to Glyph...", { Mod_Ctrl, Key_G }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-to.png"sv)), [&](auto&) {
245 DeprecatedString input;
246 if (GUI::InputBox::show(window(), input, "Hexadecimal:"sv, "Go to glyph"sv, GUI::InputType::NonemptyText) == GUI::InputBox::ExecResult::OK) {
247 auto maybe_code_point = AK::StringUtils::convert_to_uint_from_hex(input);
248 if (!maybe_code_point.has_value())
249 return;
250 auto code_point = maybe_code_point.value();
251 code_point = clamp(code_point, m_range.first, m_range.last);
252 m_glyph_map_widget->set_focus(true);
253 m_glyph_map_widget->set_active_glyph(code_point);
254 m_glyph_map_widget->scroll_to_glyph(code_point);
255 }
256 });
257 m_go_to_glyph_action->set_status_tip("Go to the specified code point");
258
259 m_previous_glyph_action = GUI::Action::create("Pre&vious Glyph", { Mod_Alt, Key_Left }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"sv)), [&](auto&) {
260 m_glyph_map_widget->select_previous_existing_glyph();
261 });
262 m_previous_glyph_action->set_status_tip("Seek the previous visible glyph");
263
264 m_next_glyph_action = GUI::Action::create("&Next Glyph", { Mod_Alt, Key_Right }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"sv)), [&](auto&) {
265 m_glyph_map_widget->select_next_existing_glyph();
266 });
267 m_next_glyph_action->set_status_tip("Seek the next visible glyph");
268
269 i32 scale = Config::read_i32("FontEditor"sv, "GlyphEditor"sv, "Scale"sv, 10);
270 m_glyph_editor_widget->set_scale(scale);
271 m_scale_five_action = GUI::Action::create_checkable("500%", { Mod_Ctrl, Key_1 }, [this](auto&) {
272 set_scale_and_save(5);
273 });
274 m_scale_five_action->set_checked(scale == 5);
275 m_scale_five_action->set_status_tip("Scale the editor in proportion to the current font");
276 m_scale_ten_action = GUI::Action::create_checkable("1000%", { Mod_Ctrl, Key_2 }, [this](auto&) {
277 set_scale_and_save(10);
278 });
279 m_scale_ten_action->set_checked(scale == 10);
280 m_scale_ten_action->set_status_tip("Scale the editor in proportion to the current font");
281 m_scale_fifteen_action = GUI::Action::create_checkable("1500%", { Mod_Ctrl, Key_3 }, [this](auto&) {
282 set_scale_and_save(15);
283 });
284 m_scale_fifteen_action->set_checked(scale == 15);
285 m_scale_fifteen_action->set_status_tip("Scale the editor in proportion to the current font");
286
287 m_glyph_editor_scale_actions.add_action(*m_scale_five_action);
288 m_glyph_editor_scale_actions.add_action(*m_scale_ten_action);
289 m_glyph_editor_scale_actions.add_action(*m_scale_fifteen_action);
290 m_glyph_editor_scale_actions.set_exclusive(true);
291
292 m_paint_glyph_action = GUI::Action::create_checkable("Paint Glyph", { Mod_Ctrl, KeyCode::Key_J }, TRY(Gfx::Bitmap::load_from_file("/res/icons/pixelpaint/pen.png"sv)), [&](auto&) {
293 m_glyph_editor_widget->set_mode(GlyphEditorWidget::Paint);
294 });
295 m_paint_glyph_action->set_checked(true);
296
297 m_move_glyph_action = GUI::Action::create_checkable("Move Glyph", { Mod_Ctrl, KeyCode::Key_K }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/selection-move.png"sv)), [&](auto&) {
298 m_glyph_editor_widget->set_mode(GlyphEditorWidget::Move);
299 });
300
301 m_glyph_tool_actions.add_action(*m_paint_glyph_action);
302 m_glyph_tool_actions.add_action(*m_move_glyph_action);
303 m_glyph_tool_actions.set_exclusive(true);
304
305 m_rotate_counterclockwise_action = GUI::CommonActions::make_rotate_counterclockwise_action([&](auto&) {
306 m_glyph_editor_widget->rotate_90(Gfx::RotationDirection::CounterClockwise);
307 });
308
309 m_rotate_clockwise_action = GUI::CommonActions::make_rotate_clockwise_action([&](auto&) {
310 m_glyph_editor_widget->rotate_90(Gfx::RotationDirection::Clockwise);
311 });
312
313 m_flip_horizontal_action = GUI::Action::create("Flip Horizontally", { Mod_Ctrl | Mod_Shift, Key_Q }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-flip-horizontal.png"sv)), [&](auto&) {
314 m_glyph_editor_widget->flip(Gfx::Orientation::Horizontal);
315 });
316
317 m_flip_vertical_action = GUI::Action::create("Flip Vertically", { Mod_Ctrl | Mod_Shift, Key_W }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-flip-vertical.png"sv)), [&](auto&) {
318 m_glyph_editor_widget->flip(Gfx::Orientation::Vertical);
319 });
320
321 m_copy_text_action = GUI::Action::create("Copy as Te&xt", { Mod_Ctrl, Key_T }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"sv)), [&](auto&) {
322 StringBuilder builder;
323 auto selection = m_glyph_map_widget->selection().normalized();
324 for (auto code_point = selection.start(); code_point < selection.start() + selection.size(); ++code_point) {
325 if (!m_glyph_map_widget->font().contains_glyph(code_point))
326 continue;
327 builder.append_code_point(code_point);
328 }
329 GUI::Clipboard::the().set_plain_text(builder.to_deprecated_string());
330 });
331 m_copy_text_action->set_status_tip("Copy to clipboard as text");
332
333 return {};
334}
335
336ErrorOr<void> MainWidget::create_toolbars()
337{
338 auto& toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar");
339 (void)TRY(toolbar.try_add_action(*m_new_action));
340 (void)TRY(toolbar.try_add_action(*m_open_action));
341 (void)TRY(toolbar.try_add_action(*m_save_action));
342 TRY(toolbar.try_add_separator());
343 (void)TRY(toolbar.try_add_action(*m_cut_action));
344 (void)TRY(toolbar.try_add_action(*m_copy_action));
345 (void)TRY(toolbar.try_add_action(*m_paste_action));
346 (void)TRY(toolbar.try_add_action(*m_delete_action));
347 TRY(toolbar.try_add_separator());
348 (void)TRY(toolbar.try_add_action(*m_undo_action));
349 (void)TRY(toolbar.try_add_action(*m_redo_action));
350 TRY(toolbar.try_add_separator());
351 (void)TRY(toolbar.try_add_action(*m_open_preview_action));
352 TRY(toolbar.try_add_separator());
353 (void)TRY(toolbar.try_add_action(*m_previous_glyph_action));
354 (void)TRY(toolbar.try_add_action(*m_next_glyph_action));
355 (void)TRY(toolbar.try_add_action(*m_go_to_glyph_action));
356
357 auto& glyph_transform_toolbar = *find_descendant_of_type_named<GUI::Toolbar>("glyph_transform_toolbar");
358 (void)TRY(glyph_transform_toolbar.try_add_action(*m_flip_horizontal_action));
359 (void)TRY(glyph_transform_toolbar.try_add_action(*m_flip_vertical_action));
360 (void)TRY(glyph_transform_toolbar.try_add_action(*m_rotate_counterclockwise_action));
361 (void)TRY(glyph_transform_toolbar.try_add_action(*m_rotate_clockwise_action));
362
363 auto& glyph_mode_toolbar = *find_descendant_of_type_named<GUI::Toolbar>("glyph_mode_toolbar");
364 (void)TRY(glyph_mode_toolbar.try_add_action(*m_paint_glyph_action));
365 (void)TRY(glyph_mode_toolbar.try_add_action(*m_move_glyph_action));
366
367 return {};
368}
369
370ErrorOr<void> MainWidget::create_models()
371{
372 for (auto& it : Gfx::font_slope_names)
373 TRY(m_font_slope_list.try_append(it.name));
374 m_slope_combobox->set_model(GUI::ItemListModel<DeprecatedString>::create(m_font_slope_list));
375
376 for (auto& it : Gfx::font_weight_names)
377 TRY(m_font_weight_list.try_append(it.name));
378 m_weight_combobox->set_model(GUI::ItemListModel<DeprecatedString>::create(m_font_weight_list));
379
380 auto unicode_blocks = Unicode::block_display_names();
381 TRY(m_unicode_block_list.try_append("Show All"));
382 for (auto& block : unicode_blocks)
383 TRY(m_unicode_block_list.try_append(block.display_name));
384
385 m_unicode_block_model = GUI::ItemListModel<DeprecatedString>::create(m_unicode_block_list);
386 m_filter_model = TRY(GUI::FilteringProxyModel::create(*m_unicode_block_model));
387 m_filter_model->set_filter_term(""sv);
388
389 m_unicode_block_listview = find_descendant_of_type_named<GUI::ListView>("unicode_block_listview");
390 m_unicode_block_listview->on_selection_change = [this, unicode_blocks] {
391 auto index = m_unicode_block_listview->selection().first();
392 auto mapped_index = m_filter_model->map(index);
393 if (mapped_index.row() > 0)
394 m_range = unicode_blocks[mapped_index.row() - 1].code_point_range;
395 else
396 m_range = { 0x0000, 0x10FFFF };
397 m_glyph_map_widget->set_active_range(m_range);
398 };
399 m_unicode_block_listview->set_model(m_filter_model);
400 m_unicode_block_listview->set_activates_on_selection(true);
401 m_unicode_block_listview->horizontal_scrollbar().set_visible(false);
402 m_unicode_block_listview->set_cursor(m_unicode_block_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set);
403 m_unicode_block_listview->set_focus_proxy(m_search_textbox);
404
405 return {};
406}
407
408ErrorOr<void> MainWidget::create_undo_stack()
409{
410 m_undo_stack = TRY(try_make<GUI::UndoStack>());
411 m_undo_stack->on_state_change = [this] {
412 m_undo_action->set_enabled(m_undo_stack->can_undo());
413 m_redo_action->set_enabled(m_undo_stack->can_redo());
414 if (m_undo_stack->is_current_modified())
415 did_modify_font();
416 };
417
418 return {};
419}
420
421MainWidget::MainWidget()
422{
423 load_from_gml(font_editor_window_gml).release_value_but_fixme_should_propagate_errors();
424
425 m_font_metadata_groupbox = find_descendant_of_type_named<GUI::GroupBox>("font_metadata_groupbox");
426 m_unicode_block_container = find_descendant_of_type_named<GUI::Widget>("unicode_block_container");
427 m_toolbar_container = find_descendant_of_type_named<GUI::ToolbarContainer>("toolbar_container");
428
429 m_glyph_map_widget = find_descendant_of_type_named<GUI::GlyphMapWidget>("glyph_map_widget");
430 m_glyph_editor_widget = find_descendant_of_type_named<GlyphEditorWidget>("glyph_editor_widget");
431 m_glyph_editor_widget->on_glyph_altered = [this](int glyph) {
432 m_glyph_map_widget->update_glyph(glyph);
433 update_preview();
434 did_modify_font();
435 };
436
437 m_glyph_editor_widget->on_undo_event = [this] {
438 reset_selection_and_push_undo();
439 };
440
441 m_glyph_editor_width_spinbox = find_descendant_of_type_named<GUI::SpinBox>("glyph_editor_width_spinbox");
442 m_glyph_editor_present_checkbox = find_descendant_of_type_named<GUI::CheckBox>("glyph_editor_present_checkbox");
443 m_glyph_map_widget->on_active_glyph_changed = [this](int glyph) {
444 if (m_undo_selection) {
445 auto selection = m_glyph_map_widget->selection().normalized();
446 m_undo_selection->set_start(selection.start());
447 m_undo_selection->set_size(selection.size());
448 m_undo_selection->set_active_glyph(glyph);
449 }
450 m_glyph_editor_widget->set_glyph(glyph);
451 auto glyph_width = m_edited_font->raw_glyph_width(glyph);
452 if (m_edited_font->is_fixed_width())
453 m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
454 else
455 m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
456 update_statusbar();
457 };
458
459 m_glyph_map_widget->on_context_menu_request = [this](auto& event) {
460 m_context_menu->popup(event.screen_position());
461 };
462
463 m_glyph_map_widget->on_escape_pressed = [this]() {
464 update_statusbar();
465 };
466
467 m_name_textbox = find_descendant_of_type_named<GUI::TextBox>("name_textbox");
468 m_name_textbox->on_change = [&] {
469 m_edited_font->set_name(m_name_textbox->text());
470 did_modify_font();
471 };
472
473 m_family_textbox = find_descendant_of_type_named<GUI::TextBox>("family_textbox");
474 m_family_textbox->on_change = [&] {
475 m_edited_font->set_family(m_family_textbox->text());
476 did_modify_font();
477 };
478
479 m_fixed_width_checkbox = find_descendant_of_type_named<GUI::CheckBox>("fixed_width_checkbox");
480 m_fixed_width_checkbox->on_checked = [this](bool checked) {
481 m_edited_font->set_fixed_width(checked);
482 auto glyph_width = m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph());
483 m_glyph_editor_width_spinbox->set_visible(!checked);
484 m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
485 m_glyph_editor_present_checkbox->set_visible(checked);
486 m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
487 m_glyph_editor_widget->update();
488 update_preview();
489 did_modify_font();
490 };
491
492 m_glyph_editor_width_spinbox->on_change = [this](int value) {
493 reset_selection_and_push_undo();
494 m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), value);
495 m_glyph_editor_widget->update();
496 m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph());
497 update_preview();
498 update_statusbar();
499 did_modify_font();
500 };
501
502 m_glyph_editor_present_checkbox->on_checked = [this](bool checked) {
503 reset_selection_and_push_undo();
504 m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), checked ? m_edited_font->glyph_fixed_width() : 0);
505 m_glyph_editor_widget->update();
506 m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph());
507 update_preview();
508 update_statusbar();
509 did_modify_font();
510 };
511
512 m_weight_combobox = find_descendant_of_type_named<GUI::ComboBox>("weight_combobox");
513 m_weight_combobox->on_change = [this](auto&, auto&) {
514 m_edited_font->set_weight(Gfx::name_to_weight(m_weight_combobox->text()));
515 did_modify_font();
516 };
517
518 m_slope_combobox = find_descendant_of_type_named<GUI::ComboBox>("slope_combobox");
519 m_slope_combobox->on_change = [this](auto&, auto&) {
520 m_edited_font->set_slope(Gfx::name_to_slope(m_slope_combobox->text()));
521 did_modify_font();
522 };
523
524 m_presentation_spinbox = find_descendant_of_type_named<GUI::SpinBox>("presentation_spinbox");
525 m_presentation_spinbox->on_change = [this](int value) {
526 m_edited_font->set_presentation_size(value);
527 update_preview();
528 did_modify_font();
529 };
530
531 m_spacing_spinbox = find_descendant_of_type_named<GUI::SpinBox>("spacing_spinbox");
532 m_spacing_spinbox->on_change = [this](int value) {
533 m_edited_font->set_glyph_spacing(value);
534 update_preview();
535 did_modify_font();
536 };
537
538 m_baseline_spinbox = find_descendant_of_type_named<GUI::SpinBox>("baseline_spinbox");
539 m_baseline_spinbox->on_change = [this](int value) {
540 m_edited_font->set_baseline(value);
541 m_glyph_editor_widget->update();
542 update_preview();
543 did_modify_font();
544 };
545
546 m_mean_line_spinbox = find_descendant_of_type_named<GUI::SpinBox>("mean_line_spinbox");
547 m_mean_line_spinbox->on_change = [this](int value) {
548 m_edited_font->set_mean_line(value);
549 m_glyph_editor_widget->update();
550 update_preview();
551 did_modify_font();
552 };
553
554 m_search_textbox = find_descendant_of_type_named<GUI::TextBox>("search_textbox");
555 m_search_textbox->on_return_pressed = [this] {
556 if (!m_unicode_block_listview->selection().is_empty())
557 m_unicode_block_listview->activate_selected();
558 };
559
560 m_search_textbox->on_down_pressed = [this] {
561 m_unicode_block_listview->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set);
562 };
563
564 m_search_textbox->on_up_pressed = [this] {
565 m_unicode_block_listview->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set);
566 };
567
568 m_search_textbox->on_change = [this] {
569 m_filter_model->set_filter_term(m_search_textbox->text());
570 if (m_filter_model->row_count() != 0)
571 m_unicode_block_listview->set_cursor(m_filter_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set);
572 };
573
574 m_statusbar = find_descendant_of_type_named<GUI::Statusbar>("statusbar");
575 m_statusbar->segment(1).set_mode(GUI::Statusbar::Segment::Mode::Auto);
576 m_statusbar->segment(1).set_clickable(true);
577 m_statusbar->segment(1).on_click = [&](auto) {
578 m_show_unicode_blocks_action->activate();
579 };
580
581 GUI::Application::the()->on_action_enter = [this](GUI::Action& action) {
582 auto text = action.status_tip();
583 if (text.is_empty())
584 text = Gfx::parse_ampersand_string(action.text());
585 m_statusbar->set_override_text(move(text));
586 };
587
588 GUI::Application::the()->on_action_leave = [this](GUI::Action&) {
589 m_statusbar->set_override_text({});
590 };
591}
592
593ErrorOr<void> MainWidget::initialize(DeprecatedString const& path, RefPtr<Gfx::BitmapFont>&& edited_font)
594{
595 if (m_edited_font == edited_font)
596 return {};
597
598 TRY(m_glyph_map_widget->set_font(*edited_font));
599
600 auto selection = m_glyph_map_widget->selection().normalized();
601 m_undo_selection = TRY(try_make_ref_counted<UndoSelection>(selection.start(), selection.size(), m_glyph_map_widget->active_glyph(), *edited_font, *m_glyph_map_widget));
602 m_undo_stack->clear();
603
604 m_path = path;
605 m_edited_font = edited_font;
606
607 if (m_preview_label)
608 m_preview_label->set_font(*m_edited_font);
609
610 m_glyph_editor_widget->set_font(*m_edited_font);
611 m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height());
612
613 m_glyph_editor_width_spinbox->set_visible(!m_edited_font->is_fixed_width());
614 m_glyph_editor_width_spinbox->set_max(m_edited_font->max_glyph_width(), GUI::AllowCallback::No);
615 m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
616
617 m_glyph_editor_present_checkbox->set_visible(m_edited_font->is_fixed_width());
618 m_glyph_editor_present_checkbox->set_checked(m_edited_font->contains_raw_glyph(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
619 m_fixed_width_checkbox->set_checked(m_edited_font->is_fixed_width(), GUI::AllowCallback::No);
620
621 m_name_textbox->set_text(m_edited_font->name(), GUI::AllowCallback::No);
622 m_family_textbox->set_text(m_edited_font->family(), GUI::AllowCallback::No);
623
624 m_presentation_spinbox->set_value(m_edited_font->presentation_size(), GUI::AllowCallback::No);
625 m_spacing_spinbox->set_value(m_edited_font->glyph_spacing(), GUI::AllowCallback::No);
626
627 m_mean_line_spinbox->set_range(0, max(m_edited_font->glyph_height() - 2, 0), GUI::AllowCallback::No);
628 m_baseline_spinbox->set_range(0, max(m_edited_font->glyph_height() - 2, 0), GUI::AllowCallback::No);
629 m_mean_line_spinbox->set_value(m_edited_font->mean_line(), GUI::AllowCallback::No);
630 m_baseline_spinbox->set_value(m_edited_font->baseline(), GUI::AllowCallback::No);
631
632 int i = 0;
633 for (auto& it : Gfx::font_weight_names) {
634 if (it.style == m_edited_font->weight()) {
635 m_weight_combobox->set_selected_index(i, GUI::AllowCallback::No);
636 break;
637 }
638 i++;
639 }
640 i = 0;
641 for (auto& it : Gfx::font_slope_names) {
642 if (it.style == m_edited_font->slope()) {
643 m_slope_combobox->set_selected_index(i, GUI::AllowCallback::No);
644 break;
645 }
646 i++;
647 }
648
649 update_statusbar();
650
651 deferred_invoke([this] {
652 auto glyph = m_glyph_map_widget->active_glyph();
653 m_glyph_map_widget->set_focus(true);
654 m_glyph_map_widget->scroll_to_glyph(glyph);
655 m_glyph_editor_widget->set_glyph(glyph);
656
657 VERIFY(window());
658 window()->set_modified(false);
659 update_title();
660 });
661
662 return {};
663}
664
665ErrorOr<void> MainWidget::initialize_menubar(GUI::Window& window)
666{
667 auto file_menu = TRY(window.try_add_menu("&File"));
668 TRY(file_menu->try_add_action(*m_new_action));
669 TRY(file_menu->try_add_action(*m_open_action));
670 TRY(file_menu->try_add_action(*m_save_action));
671 TRY(file_menu->try_add_action(*m_save_as_action));
672 TRY(file_menu->try_add_separator());
673 TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([this](auto&) {
674 if (!request_close())
675 return;
676 GUI::Application::the()->quit();
677 })));
678
679 auto edit_menu = TRY(window.try_add_menu("&Edit"));
680 TRY(edit_menu->try_add_action(*m_undo_action));
681 TRY(edit_menu->try_add_action(*m_redo_action));
682 TRY(edit_menu->try_add_separator());
683 TRY(edit_menu->try_add_action(*m_cut_action));
684 TRY(edit_menu->try_add_action(*m_copy_action));
685 TRY(edit_menu->try_add_action(*m_paste_action));
686 TRY(edit_menu->try_add_action(*m_delete_action));
687 TRY(edit_menu->try_add_separator());
688 TRY(edit_menu->try_add_action(*m_select_all_action));
689 TRY(edit_menu->try_add_separator());
690 TRY(edit_menu->try_add_action(*m_copy_text_action));
691
692 m_context_menu = edit_menu;
693
694 auto go_menu = TRY(window.try_add_menu("&Go"));
695 TRY(go_menu->try_add_action(*m_previous_glyph_action));
696 TRY(go_menu->try_add_action(*m_next_glyph_action));
697 TRY(go_menu->try_add_action(*m_go_to_glyph_action));
698
699 auto view_menu = TRY(window.try_add_menu("&View"));
700 auto layout_menu = TRY(view_menu->try_add_submenu("&Layout"));
701 TRY(layout_menu->try_add_action(*m_show_toolbar_action));
702 TRY(layout_menu->try_add_action(*m_show_statusbar_action));
703 TRY(layout_menu->try_add_action(*m_show_metadata_action));
704 TRY(layout_menu->try_add_action(*m_show_unicode_blocks_action));
705 TRY(view_menu->try_add_separator());
706 TRY(view_menu->try_add_action(*m_open_preview_action));
707 TRY(view_menu->try_add_separator());
708 TRY(view_menu->try_add_action(*m_highlight_modifications_action));
709 TRY(view_menu->try_add_action(*m_show_system_emoji_action));
710 TRY(view_menu->try_add_separator());
711 auto scale_menu = TRY(view_menu->try_add_submenu("&Scale"));
712 scale_menu->set_icon(TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/scale.png"sv)));
713 TRY(scale_menu->try_add_action(*m_scale_five_action));
714 TRY(scale_menu->try_add_action(*m_scale_ten_action));
715 TRY(scale_menu->try_add_action(*m_scale_fifteen_action));
716
717 auto help_menu = TRY(window.try_add_menu("&Help"));
718 TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(&window)));
719 TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) {
720 Desktop::Launcher::open(URL::create_with_file_scheme("/usr/share/man/man1/FontEditor.md"), "/bin/Help");
721 })));
722 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)));
723
724 return {};
725}
726
727ErrorOr<void> MainWidget::save_file(DeprecatedString const& path)
728{
729 auto masked_font = TRY(m_edited_font->masked_character_set());
730 TRY(masked_font->write_to_file(path));
731 m_path = path;
732 m_undo_stack->set_current_unmodified();
733 window()->set_modified(false);
734 update_title();
735 return {};
736}
737
738void MainWidget::set_show_toolbar(bool show)
739{
740 if (m_toolbar_container->is_visible() == show)
741 return;
742 m_toolbar_container->set_visible(show);
743}
744
745void MainWidget::set_show_statusbar(bool show)
746{
747 if (m_statusbar->is_visible() == show)
748 return;
749 m_statusbar->set_visible(show);
750 if (show)
751 update_statusbar();
752}
753
754void MainWidget::set_show_font_metadata(bool show)
755{
756 if (m_font_metadata == show)
757 return;
758 m_font_metadata = show;
759 m_font_metadata_groupbox->set_visible(m_font_metadata);
760}
761
762void MainWidget::set_show_unicode_blocks(bool show)
763{
764 if (m_unicode_blocks == show)
765 return;
766 m_unicode_blocks = show;
767 m_unicode_block_container->set_visible(m_unicode_blocks);
768 if (show)
769 m_search_textbox->set_focus(true);
770 else
771 m_glyph_map_widget->set_focus(true);
772}
773
774void MainWidget::set_highlight_modifications(bool highlight_modifications)
775{
776 m_glyph_map_widget->set_highlight_modifications(highlight_modifications);
777}
778
779void MainWidget::set_show_system_emoji(bool show)
780{
781 m_glyph_map_widget->set_show_system_emoji(show);
782}
783
784ErrorOr<void> MainWidget::open_file(DeprecatedString const& path)
785{
786 auto unmasked_font = TRY(TRY(Gfx::BitmapFont::try_load_from_file(path))->unmasked_character_set());
787 TRY(initialize(path, move(unmasked_font)));
788 return {};
789}
790
791void MainWidget::push_undo()
792{
793 auto maybe_state = m_undo_selection->save_state();
794 if (maybe_state.is_error())
795 return show_error(maybe_state.release_error(), "Saving undo state failed"sv);
796 auto maybe_command = try_make<SelectionUndoCommand>(*m_undo_selection, move(maybe_state.value()));
797 if (maybe_command.is_error())
798 return show_error(maybe_command.release_error(), "Making undo command failed"sv);
799 if (auto maybe_push = m_undo_stack->try_push(move(maybe_command.value())); maybe_push.is_error())
800 show_error(maybe_push.release_error(), "Pushing undo stack failed"sv);
801}
802
803void MainWidget::reset_selection_and_push_undo()
804{
805 auto selection = m_glyph_map_widget->selection().normalized();
806 if (selection.size() != 1) {
807 auto start = m_glyph_map_widget->active_glyph();
808 m_undo_selection->set_start(start);
809 m_undo_selection->set_size(1);
810 m_glyph_map_widget->set_selection(start, 1);
811 m_glyph_map_widget->update();
812 }
813 push_undo();
814}
815
816void MainWidget::undo()
817{
818 if (!m_undo_stack->can_undo())
819 return;
820 m_undo_stack->undo();
821
822 auto glyph = m_undo_selection->restored_active_glyph();
823 auto glyph_width = edited_font().raw_glyph_width(glyph);
824 if (glyph < m_range.first || glyph > m_range.last)
825 m_search_textbox->set_text(""sv);
826
827 deferred_invoke([this, glyph] {
828 auto start = m_undo_selection->restored_start();
829 auto size = m_undo_selection->restored_size();
830 m_glyph_map_widget->restore_selection(start, size, glyph);
831 m_glyph_map_widget->scroll_to_glyph(glyph);
832 m_glyph_map_widget->set_focus(true);
833 });
834
835 if (m_edited_font->is_fixed_width()) {
836 m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
837 } else {
838 m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
839 }
840 m_glyph_editor_widget->update();
841 m_glyph_map_widget->update();
842 update_preview();
843 update_statusbar();
844}
845
846void MainWidget::redo()
847{
848 if (!m_undo_stack->can_redo())
849 return;
850 m_undo_stack->redo();
851
852 auto glyph = m_undo_selection->restored_active_glyph();
853 auto glyph_width = edited_font().raw_glyph_width(glyph);
854 if (glyph < m_range.first || glyph > m_range.last)
855 m_search_textbox->set_text(""sv);
856
857 deferred_invoke([this, glyph] {
858 auto start = m_undo_selection->restored_start();
859 auto size = m_undo_selection->restored_size();
860 m_glyph_map_widget->restore_selection(start, size, glyph);
861 m_glyph_map_widget->scroll_to_glyph(glyph);
862 m_glyph_map_widget->set_focus(true);
863 });
864
865 if (m_edited_font->is_fixed_width()) {
866 m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
867 } else {
868 m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
869 }
870 m_glyph_editor_widget->update();
871 m_glyph_map_widget->update();
872 update_preview();
873 update_statusbar();
874}
875
876bool MainWidget::request_close()
877{
878 if (!window()->is_modified())
879 return true;
880 auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path, m_undo_stack->last_unmodified_timestamp());
881 if (result == GUI::MessageBox::ExecResult::Yes) {
882 m_save_action->activate();
883 if (!window()->is_modified())
884 return true;
885 }
886 if (result == GUI::MessageBox::ExecResult::No)
887 return true;
888 return false;
889}
890
891void MainWidget::update_title()
892{
893 StringBuilder title;
894 if (m_path.is_empty())
895 title.append("Untitled"sv);
896 else
897 title.append(m_path);
898 title.append("[*] - Font Editor"sv);
899 window()->set_title(title.to_deprecated_string());
900}
901
902void MainWidget::did_modify_font()
903{
904 if (!window() || window()->is_modified())
905 return;
906 window()->set_modified(true);
907 update_title();
908}
909
910void MainWidget::update_statusbar()
911{
912 if (!m_statusbar->is_visible())
913 return;
914
915 auto glyph = m_glyph_map_widget->active_glyph();
916 StringBuilder builder;
917 builder.appendff("U+{:04X} (", glyph);
918
919 if (auto abbreviation = Unicode::code_point_abbreviation(glyph); abbreviation.has_value()) {
920 builder.append(*abbreviation);
921 } else if (Gfx::get_char_bidi_class(glyph) == Gfx::BidirectionalClass::STRONG_RTL) {
922 // FIXME: This is a necessary hack, as RTL text will mess up the painting of the statusbar text.
923 // For now, replace RTL glyphs with U+FFFD, the replacement character.
924 builder.append_code_point(0xFFFD);
925 } else {
926 builder.append_code_point(glyph);
927 }
928
929 builder.append(')');
930
931 auto glyph_name = Unicode::code_point_display_name(glyph);
932 if (glyph_name.has_value()) {
933 builder.appendff(" {}", glyph_name.value());
934 }
935
936 if (m_edited_font->contains_raw_glyph(glyph))
937 builder.appendff(" [{}x{}]", m_edited_font->raw_glyph_width(glyph), m_edited_font->glyph_height());
938 else if (Gfx::Emoji::emoji_for_code_point(glyph))
939 builder.appendff(" [emoji]");
940 m_statusbar->set_text(builder.to_deprecated_string());
941
942 builder.clear();
943
944 auto selection = m_glyph_map_widget->selection().normalized();
945 if (selection.size() > 1)
946 builder.appendff("{} glyphs selected", selection.size());
947 else
948 builder.appendff("U+{:04X}-U+{:04X}", m_range.first, m_range.last);
949 m_statusbar->set_text(1, builder.to_deprecated_string());
950}
951
952void MainWidget::update_preview()
953{
954 if (m_font_preview_window)
955 m_font_preview_window->update();
956}
957
958void MainWidget::drag_enter_event(GUI::DragEvent& event)
959{
960 auto const& mime_types = event.mime_types();
961 if (mime_types.contains_slow("text/uri-list"))
962 event.accept();
963}
964
965void MainWidget::drop_event(GUI::DropEvent& event)
966{
967 event.accept();
968
969 if (event.mime_data().has_urls()) {
970 auto urls = event.mime_data().urls();
971 if (urls.is_empty())
972 return;
973
974 window()->move_to_front();
975 if (!request_close())
976 return;
977
978 if (auto result = open_file(urls.first().path()); result.is_error())
979 show_error(result.release_error(), "Opening"sv, LexicalPath { urls.first().path() }.basename());
980 }
981}
982
983void MainWidget::set_scale_and_save(i32 scale)
984{
985 Config::write_i32("FontEditor"sv, "GlyphEditor"sv, "Scale"sv, scale);
986 m_glyph_editor_widget->set_scale(scale);
987 m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height());
988}
989
990ErrorOr<void> MainWidget::copy_selected_glyphs()
991{
992 size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * edited_font().glyph_height();
993 auto selection = m_glyph_map_widget->selection().normalized();
994 auto* rows = m_edited_font->rows() + selection.start() * bytes_per_glyph;
995 auto* widths = m_edited_font->widths() + selection.start();
996
997 ByteBuffer buffer;
998 TRY(buffer.try_append(rows, bytes_per_glyph * selection.size()));
999 TRY(buffer.try_append(widths, selection.size()));
1000
1001 HashMap<DeprecatedString, DeprecatedString> metadata;
1002 metadata.set("start", DeprecatedString::number(selection.start()));
1003 metadata.set("count", DeprecatedString::number(selection.size()));
1004 metadata.set("width", DeprecatedString::number(edited_font().max_glyph_width()));
1005 metadata.set("height", DeprecatedString::number(edited_font().glyph_height()));
1006 GUI::Clipboard::the().set_data(buffer.bytes(), "glyph/x-fonteditor", metadata);
1007
1008 return {};
1009}
1010
1011ErrorOr<void> MainWidget::cut_selected_glyphs()
1012{
1013 TRY(copy_selected_glyphs());
1014 delete_selected_glyphs();
1015 return {};
1016}
1017
1018void MainWidget::paste_glyphs()
1019{
1020 auto [data, mime_type, metadata] = GUI::Clipboard::the().fetch_data_and_type();
1021 if (!mime_type.starts_with("glyph/"sv))
1022 return;
1023
1024 auto glyph_count = metadata.get("count").value().to_uint().value_or(0);
1025 if (!glyph_count)
1026 return;
1027
1028 auto height = metadata.get("height").value().to_uint().value_or(0);
1029 if (!height)
1030 return;
1031
1032 auto selection = m_glyph_map_widget->selection().normalized();
1033 auto range_bound_glyph_count = min(glyph_count, 1 + m_range.last - selection.start());
1034 m_undo_selection->set_size(range_bound_glyph_count);
1035 push_undo();
1036
1037 size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * edited_font().glyph_height();
1038 size_t bytes_per_copied_glyph = Gfx::GlyphBitmap::bytes_per_row() * height;
1039 size_t copyable_bytes_per_glyph = min(bytes_per_glyph, bytes_per_copied_glyph);
1040 auto* rows = m_edited_font->rows() + selection.start() * bytes_per_glyph;
1041 auto* widths = m_edited_font->widths() + selection.start();
1042
1043 for (size_t i = 0; i < range_bound_glyph_count; ++i) {
1044 auto copyable_width = edited_font().is_fixed_width()
1045 ? data[bytes_per_copied_glyph * glyph_count + i] ? edited_font().glyph_fixed_width() : 0
1046 : min(edited_font().max_glyph_width(), data[bytes_per_copied_glyph * glyph_count + i]);
1047 memcpy(&rows[i * bytes_per_glyph], &data[i * bytes_per_copied_glyph], copyable_bytes_per_glyph);
1048 memset(&widths[i], copyable_width, sizeof(u8));
1049 m_glyph_map_widget->set_glyph_modified(selection.start() + i, true);
1050 }
1051
1052 m_glyph_map_widget->set_selection(selection.start() + range_bound_glyph_count - 1, -range_bound_glyph_count + 1);
1053
1054 if (m_edited_font->is_fixed_width())
1055 m_glyph_editor_present_checkbox->set_checked(m_edited_font->contains_raw_glyph(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
1056 else
1057 m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
1058
1059 m_glyph_editor_widget->update();
1060 m_glyph_map_widget->update();
1061 update_preview();
1062 update_statusbar();
1063}
1064
1065void MainWidget::delete_selected_glyphs()
1066{
1067 push_undo();
1068
1069 auto selection = m_glyph_map_widget->selection().normalized();
1070 size_t bytes_per_glyph = Gfx::GlyphBitmap::bytes_per_row() * m_edited_font->glyph_height();
1071 auto* rows = m_edited_font->rows() + selection.start() * bytes_per_glyph;
1072 auto* widths = m_edited_font->widths() + selection.start();
1073 memset(rows, 0, bytes_per_glyph * selection.size());
1074 memset(widths, 0, selection.size());
1075
1076 if (m_edited_font->is_fixed_width())
1077 m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No);
1078 else
1079 m_glyph_editor_width_spinbox->set_value(0, GUI::AllowCallback::No);
1080
1081 m_glyph_editor_widget->update();
1082 m_glyph_map_widget->update();
1083 update_preview();
1084 update_statusbar();
1085}
1086
1087void MainWidget::show_error(Error error, StringView preface, StringView basename)
1088{
1089 DeprecatedString formatted_error;
1090 if (basename.is_empty())
1091 formatted_error = DeprecatedString::formatted("{}: {}", preface, error);
1092 else
1093 formatted_error = DeprecatedString::formatted("{} \"{}\" failed: {}", preface, basename, error);
1094 GUI::MessageBox::show_error(window(), formatted_error);
1095 warnln(formatted_error);
1096}
1097
1098}