FontEditor.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "FontEditor.h"
  7. #include "GlyphEditorWidget.h"
  8. #include "NewFontDialog.h"
  9. #include <AK/StringBuilder.h>
  10. #include <AK/UnicodeUtils.h>
  11. #include <Applications/FontEditor/FontEditorWindowGML.h>
  12. #include <LibConfig/Client.h>
  13. #include <LibDesktop/Launcher.h>
  14. #include <LibGUI/Action.h>
  15. #include <LibGUI/Application.h>
  16. #include <LibGUI/BoxLayout.h>
  17. #include <LibGUI/Button.h>
  18. #include <LibGUI/CheckBox.h>
  19. #include <LibGUI/Clipboard.h>
  20. #include <LibGUI/ComboBox.h>
  21. #include <LibGUI/FilePicker.h>
  22. #include <LibGUI/GroupBox.h>
  23. #include <LibGUI/InputBox.h>
  24. #include <LibGUI/ItemListModel.h>
  25. #include <LibGUI/Label.h>
  26. #include <LibGUI/Menu.h>
  27. #include <LibGUI/Menubar.h>
  28. #include <LibGUI/MessageBox.h>
  29. #include <LibGUI/Painter.h>
  30. #include <LibGUI/SpinBox.h>
  31. #include <LibGUI/Statusbar.h>
  32. #include <LibGUI/TextBox.h>
  33. #include <LibGUI/ToolbarContainer.h>
  34. #include <LibGUI/Window.h>
  35. #include <LibGfx/BitmapFont.h>
  36. #include <LibGfx/Emoji.h>
  37. #include <LibGfx/FontStyleMapping.h>
  38. #include <LibGfx/Palette.h>
  39. #include <LibGfx/TextDirection.h>
  40. #include <LibUnicode/CharacterTypes.h>
  41. #include <stdlib.h>
  42. static constexpr int s_pangram_count = 8;
  43. static char const* pangrams[s_pangram_count] = {
  44. "quick fox jumps nightly above wizard",
  45. "five quacking zephyrs jolt my wax bed",
  46. "pack my box with five dozen liquor jugs",
  47. "quick brown fox jumps over the lazy dog",
  48. "waxy and quivering jocks fumble the pizza",
  49. "~#:[@_1%]*{$2.3}/4^(5'6\")-&|7+8!=<9,0\\>?;",
  50. "byxfjärmat föl gick på duvshowen",
  51. "         "
  52. };
  53. static RefPtr<GUI::Window> create_font_preview_window(FontEditorWidget& editor)
  54. {
  55. auto window = GUI::Window::construct(&editor);
  56. window->set_window_type(GUI::WindowType::ToolWindow);
  57. window->set_title("Preview");
  58. window->resize(400, 150);
  59. window->set_minimum_size(200, 100);
  60. window->center_within(*editor.window());
  61. auto& main_widget = window->set_main_widget<GUI::Widget>();
  62. main_widget.set_fill_with_background_color(true);
  63. main_widget.set_layout<GUI::VerticalBoxLayout>();
  64. main_widget.layout()->set_margins(2);
  65. main_widget.layout()->set_spacing(4);
  66. auto& preview_box = main_widget.add<GUI::GroupBox>();
  67. preview_box.set_layout<GUI::VerticalBoxLayout>();
  68. preview_box.layout()->set_margins(8);
  69. auto& preview_label = preview_box.add<GUI::Label>();
  70. preview_label.set_font(editor.edited_font());
  71. editor.on_initialize = [&] {
  72. preview_label.set_font(editor.edited_font());
  73. };
  74. auto& textbox_button_container = main_widget.add<GUI::Widget>();
  75. textbox_button_container.set_layout<GUI::HorizontalBoxLayout>();
  76. textbox_button_container.set_fixed_height(22);
  77. auto& preview_textbox = textbox_button_container.add<GUI::TextBox>();
  78. preview_textbox.set_text(pangrams[0]);
  79. preview_textbox.set_placeholder("Preview text");
  80. preview_textbox.on_change = [&] {
  81. auto preview = String::formatted("{}\n{}",
  82. preview_textbox.text(),
  83. Unicode::to_unicode_uppercase_full(preview_textbox.text()));
  84. preview_label.set_text(preview);
  85. };
  86. auto& reload_button = textbox_button_container.add<GUI::Button>();
  87. reload_button.set_icon(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/reload.png").release_value_but_fixme_should_propagate_errors());
  88. reload_button.set_fixed_width(22);
  89. reload_button.on_click = [&](auto) {
  90. static int i = 1;
  91. if (i >= s_pangram_count)
  92. i = 0;
  93. preview_textbox.set_text(pangrams[i]);
  94. i++;
  95. };
  96. return window;
  97. }
  98. FontEditorWidget::FontEditorWidget()
  99. {
  100. load_from_gml(font_editor_window_gml);
  101. auto& toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar");
  102. auto& glyph_mode_toolbar = *find_descendant_of_type_named<GUI::Toolbar>("glyph_mode_toolbar");
  103. auto& glyph_transform_toolbar = *find_descendant_of_type_named<GUI::Toolbar>("glyph_transform_toolbar");
  104. auto& glyph_map_container = *find_descendant_of_type_named<GUI::Widget>("glyph_map_container");
  105. m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar");
  106. m_glyph_editor_container = *find_descendant_of_type_named<GUI::Widget>("glyph_editor_container");
  107. m_left_column_container = *find_descendant_of_type_named<GUI::Widget>("left_column_container");
  108. m_glyph_editor_width_spinbox = *find_descendant_of_type_named<GUI::SpinBox>("glyph_editor_width_spinbox");
  109. m_glyph_editor_present_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("glyph_editor_present_checkbox");
  110. m_name_textbox = *find_descendant_of_type_named<GUI::TextBox>("name_textbox");
  111. m_family_textbox = *find_descendant_of_type_named<GUI::TextBox>("family_textbox");
  112. m_presentation_spinbox = *find_descendant_of_type_named<GUI::SpinBox>("presentation_spinbox");
  113. m_weight_combobox = *find_descendant_of_type_named<GUI::ComboBox>("weight_combobox");
  114. m_slope_combobox = *find_descendant_of_type_named<GUI::ComboBox>("slope_combobox");
  115. m_spacing_spinbox = *find_descendant_of_type_named<GUI::SpinBox>("spacing_spinbox");
  116. m_mean_line_spinbox = *find_descendant_of_type_named<GUI::SpinBox>("mean_line_spinbox");
  117. m_baseline_spinbox = *find_descendant_of_type_named<GUI::SpinBox>("baseline_spinbox");
  118. m_fixed_width_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("fixed_width_checkbox");
  119. m_font_metadata_groupbox = *find_descendant_of_type_named<GUI::GroupBox>("font_metadata_groupbox");
  120. m_glyph_editor_widget = m_glyph_editor_container->add<GlyphEditorWidget>();
  121. m_glyph_map_widget = glyph_map_container.add<GUI::GlyphMapWidget>();
  122. m_new_action = GUI::Action::create("&New Font...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/filetype-font.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  123. if (!request_close())
  124. return;
  125. auto new_font_wizard = NewFontDialog::construct(window());
  126. if (new_font_wizard->exec() == GUI::Dialog::ExecOK) {
  127. auto metadata = new_font_wizard->new_font_metadata();
  128. auto new_font = Gfx::BitmapFont::create(metadata.glyph_height, metadata.glyph_width, metadata.is_fixed_width, 0x110000);
  129. new_font->set_name(metadata.name);
  130. new_font->set_family(metadata.family);
  131. new_font->set_presentation_size(metadata.presentation_size);
  132. new_font->set_weight(metadata.weight);
  133. new_font->set_slope(metadata.slope);
  134. new_font->set_baseline(metadata.baseline);
  135. new_font->set_mean_line(metadata.mean_line);
  136. window()->set_modified(true);
  137. initialize({}, move(new_font));
  138. }
  139. });
  140. m_new_action->set_status_tip("Create a new font");
  141. m_open_action = GUI::CommonActions::make_open_action([&](auto&) {
  142. if (!request_close())
  143. return;
  144. Optional<String> open_path = GUI::FilePicker::get_open_filepath(window(), {}, "/res/fonts/");
  145. if (!open_path.has_value())
  146. return;
  147. open_file(open_path.value());
  148. });
  149. m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
  150. if (m_path.is_empty())
  151. m_save_as_action->activate();
  152. else
  153. save_as(m_path);
  154. });
  155. m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
  156. LexicalPath lexical_path(m_path.is_empty() ? "Untitled.font" : m_path);
  157. Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), lexical_path.title(), lexical_path.extension());
  158. if (!save_path.has_value())
  159. return;
  160. save_as(save_path.value());
  161. });
  162. m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) {
  163. cut_selected_glyphs();
  164. if (m_edited_font->is_fixed_width())
  165. m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No);
  166. else
  167. m_glyph_editor_width_spinbox->set_value(0, GUI::AllowCallback::No);
  168. update_statusbar();
  169. });
  170. m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) {
  171. copy_selected_glyphs();
  172. });
  173. m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
  174. paste_glyphs();
  175. update_statusbar();
  176. });
  177. m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "glyph/x-fonteditor");
  178. m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) {
  179. delete_selected_glyphs();
  180. if (m_edited_font->is_fixed_width())
  181. m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No);
  182. else
  183. m_glyph_editor_width_spinbox->set_value(0, GUI::AllowCallback::No);
  184. update_statusbar();
  185. });
  186. m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
  187. undo();
  188. });
  189. m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
  190. redo();
  191. });
  192. m_open_preview_action = GUI::Action::create("&Preview Font", { Mod_Ctrl, Key_P }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/find.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  193. if (!m_font_preview_window)
  194. m_font_preview_window = create_font_preview_window(*this);
  195. m_font_preview_window->show();
  196. m_font_preview_window->move_to_front();
  197. });
  198. m_open_preview_action->set_checked(false);
  199. m_open_preview_action->set_status_tip("Preview the current font");
  200. m_show_metadata_action = GUI::Action::create_checkable("Font &Metadata", { Mod_Ctrl, Key_M }, [&](auto& action) {
  201. set_show_font_metadata(action.is_checked());
  202. });
  203. m_show_metadata_action->set_checked(true);
  204. m_show_metadata_action->set_status_tip("Show or hide metadata about the current font");
  205. m_go_to_glyph_action = GUI::Action::create("&Go to Glyph...", { Mod_Ctrl, Key_G }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-to.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  206. String input;
  207. if (GUI::InputBox::show(window(), input, "Hexadecimal:", "Go to Glyph") == GUI::InputBox::ExecOK && !input.is_empty()) {
  208. int code_point = strtoul(&input[0], nullptr, 16);
  209. code_point = clamp(code_point, 0x0000, 0x10FFFF);
  210. m_glyph_map_widget->set_focus(true);
  211. m_glyph_map_widget->set_active_glyph(code_point);
  212. m_glyph_map_widget->scroll_to_glyph(code_point);
  213. }
  214. });
  215. m_go_to_glyph_action->set_status_tip("Go to the specified code point");
  216. m_previous_glyph_action = GUI::Action::create("Pre&vious Glyph", { Mod_Alt, Key_Left }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-back.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  217. bool search_wrapped = false;
  218. for (int i = m_glyph_map_widget->active_glyph() - 1;; --i) {
  219. if (i < 0 && !search_wrapped) {
  220. i = 0x10FFFF;
  221. search_wrapped = true;
  222. } else if (i < 0 && search_wrapped) {
  223. break;
  224. }
  225. if (m_edited_font->contains_raw_glyph(i)) {
  226. m_glyph_map_widget->set_focus(true);
  227. m_glyph_map_widget->set_active_glyph(i);
  228. m_glyph_map_widget->scroll_to_glyph(i);
  229. break;
  230. }
  231. }
  232. });
  233. m_previous_glyph_action->set_status_tip("Seek the previous visible glyph");
  234. m_next_glyph_action = GUI::Action::create("&Next Glyph", { Mod_Alt, Key_Right }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/go-forward.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  235. bool search_wrapped = false;
  236. for (int i = m_glyph_map_widget->active_glyph() + 1;; ++i) {
  237. if (i > 0x10FFFF && !search_wrapped) {
  238. i = 0;
  239. search_wrapped = true;
  240. } else if (i > 0x10FFFF && search_wrapped) {
  241. break;
  242. }
  243. if (m_edited_font->contains_raw_glyph(i)) {
  244. m_glyph_map_widget->set_focus(true);
  245. m_glyph_map_widget->set_active_glyph(i);
  246. m_glyph_map_widget->scroll_to_glyph(i);
  247. break;
  248. }
  249. }
  250. });
  251. m_next_glyph_action->set_status_tip("Seek the next visible glyph");
  252. toolbar.add_action(*m_new_action);
  253. toolbar.add_action(*m_open_action);
  254. toolbar.add_action(*m_save_action);
  255. toolbar.add_separator();
  256. toolbar.add_action(*m_cut_action);
  257. toolbar.add_action(*m_copy_action);
  258. toolbar.add_action(*m_paste_action);
  259. toolbar.add_action(*m_delete_action);
  260. toolbar.add_separator();
  261. toolbar.add_action(*m_undo_action);
  262. toolbar.add_action(*m_redo_action);
  263. toolbar.add_separator();
  264. toolbar.add_action(*m_open_preview_action);
  265. toolbar.add_separator();
  266. toolbar.add_action(*m_previous_glyph_action);
  267. toolbar.add_action(*m_next_glyph_action);
  268. toolbar.add_action(*m_go_to_glyph_action);
  269. i32 scale = Config::read_i32("FontEditor", "GlyphEditor", "Scale", 10);
  270. m_scale_five_action = GUI::Action::create_checkable("500%", { Mod_Ctrl, Key_1 }, [this](auto&) {
  271. set_scale_and_save(5);
  272. });
  273. m_scale_five_action->set_checked(scale == 5);
  274. m_scale_five_action->set_status_tip("Scale the editor in proportion to the current font");
  275. m_scale_ten_action = GUI::Action::create_checkable("1000%", { Mod_Ctrl, Key_2 }, [this](auto&) {
  276. set_scale_and_save(10);
  277. });
  278. m_scale_ten_action->set_checked(scale == 10);
  279. m_scale_ten_action->set_status_tip("Scale the editor in proportion to the current font");
  280. m_scale_fifteen_action = GUI::Action::create_checkable("1500%", { Mod_Ctrl, Key_3 }, [this](auto&) {
  281. set_scale_and_save(15);
  282. });
  283. m_scale_fifteen_action->set_checked(scale == 15);
  284. m_scale_fifteen_action->set_status_tip("Scale the editor in proportion to the current font");
  285. m_glyph_editor_scale_actions.add_action(*m_scale_five_action);
  286. m_glyph_editor_scale_actions.add_action(*m_scale_ten_action);
  287. m_glyph_editor_scale_actions.add_action(*m_scale_fifteen_action);
  288. m_glyph_editor_scale_actions.set_exclusive(true);
  289. m_paint_glyph_action = GUI::Action::create_checkable("Paint Glyph", { Mod_Ctrl, KeyCode::Key_J }, Gfx::Bitmap::try_load_from_file("/res/icons/pixelpaint/pen.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  290. m_glyph_editor_widget->set_mode(GlyphEditorWidget::Paint);
  291. });
  292. m_paint_glyph_action->set_checked(true);
  293. m_move_glyph_action = GUI::Action::create_checkable("Move Glyph", { Mod_Ctrl, KeyCode::Key_K }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/selection-move.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  294. m_glyph_editor_widget->set_mode(GlyphEditorWidget::Move);
  295. });
  296. m_glyph_tool_actions.add_action(*m_move_glyph_action);
  297. m_glyph_tool_actions.add_action(*m_paint_glyph_action);
  298. m_glyph_tool_actions.set_exclusive(true);
  299. glyph_mode_toolbar.add_action(*m_paint_glyph_action);
  300. glyph_mode_toolbar.add_action(*m_move_glyph_action);
  301. m_rotate_counterclockwise_action = GUI::CommonActions::make_rotate_counterclockwise_action([&](auto&) {
  302. m_glyph_editor_widget->rotate_90(GlyphEditorWidget::Counterclockwise);
  303. });
  304. m_rotate_clockwise_action = GUI::CommonActions::make_rotate_clockwise_action([&](auto&) {
  305. m_glyph_editor_widget->rotate_90(GlyphEditorWidget::Clockwise);
  306. });
  307. m_flip_horizontal_action = GUI::Action::create("Flip Horizontally", { Mod_Ctrl | Mod_Shift, Key_A }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-flip-horizontal.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  308. m_glyph_editor_widget->flip_horizontally();
  309. });
  310. m_flip_vertical_action = GUI::Action::create("Flip Vertically", { Mod_Ctrl | Mod_Shift, Key_S }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-flip-vertical.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  311. m_glyph_editor_widget->flip_vertically();
  312. });
  313. m_copy_character_action = GUI::Action::create("Cop&y as Character", { Mod_Ctrl | Mod_Shift, Key_C }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-copy.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  314. StringBuilder glyph_builder;
  315. glyph_builder.append_code_point(m_glyph_editor_widget->glyph());
  316. GUI::Clipboard::the().set_plain_text(glyph_builder.build());
  317. });
  318. glyph_transform_toolbar.add_action(*m_flip_horizontal_action);
  319. glyph_transform_toolbar.add_action(*m_flip_vertical_action);
  320. glyph_transform_toolbar.add_action(*m_rotate_counterclockwise_action);
  321. glyph_transform_toolbar.add_action(*m_rotate_clockwise_action);
  322. GUI::Clipboard::the().on_change = [&](String const& data_type) {
  323. m_paste_action->set_enabled(data_type == "glyph/x-fonteditor");
  324. };
  325. m_glyph_editor_widget->on_glyph_altered = [this](int glyph) {
  326. m_glyph_map_widget->update_glyph(glyph);
  327. update_preview();
  328. did_modify_font();
  329. };
  330. m_glyph_editor_widget->on_undo_event = [this] {
  331. m_undo_stack->push(make<GlyphUndoCommand>(*m_undo_glyph));
  332. };
  333. m_glyph_map_widget->on_active_glyph_changed = [this](int glyph) {
  334. if (m_undo_glyph)
  335. m_undo_glyph->set_code_point(glyph);
  336. m_glyph_editor_widget->set_glyph(glyph);
  337. auto glyph_width = m_edited_font->raw_glyph_width(glyph);
  338. if (m_edited_font->is_fixed_width())
  339. m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
  340. else
  341. m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
  342. update_statusbar();
  343. };
  344. m_name_textbox->on_change = [&] {
  345. m_edited_font->set_name(m_name_textbox->text());
  346. did_modify_font();
  347. };
  348. m_family_textbox->on_change = [&] {
  349. m_edited_font->set_family(m_family_textbox->text());
  350. did_modify_font();
  351. };
  352. m_fixed_width_checkbox->on_checked = [this](bool checked) {
  353. m_edited_font->set_fixed_width(checked);
  354. auto glyph_width = m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph());
  355. m_glyph_editor_width_spinbox->set_visible(!checked);
  356. m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
  357. m_glyph_editor_present_checkbox->set_visible(checked);
  358. m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
  359. m_glyph_editor_widget->update();
  360. update_preview();
  361. did_modify_font();
  362. };
  363. m_glyph_editor_width_spinbox->on_change = [this](int value) {
  364. m_undo_stack->push(make<GlyphUndoCommand>(*m_undo_glyph));
  365. m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), value);
  366. m_glyph_editor_widget->update();
  367. m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph());
  368. update_preview();
  369. update_statusbar();
  370. did_modify_font();
  371. };
  372. m_glyph_editor_present_checkbox->on_checked = [this](bool checked) {
  373. m_undo_stack->push(make<GlyphUndoCommand>(*m_undo_glyph));
  374. m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), checked ? m_edited_font->glyph_fixed_width() : 0);
  375. m_glyph_editor_widget->update();
  376. m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph());
  377. update_preview();
  378. update_statusbar();
  379. did_modify_font();
  380. };
  381. m_weight_combobox->on_change = [this](auto&, auto&) {
  382. m_edited_font->set_weight(Gfx::name_to_weight(m_weight_combobox->text()));
  383. did_modify_font();
  384. };
  385. for (auto& it : Gfx::font_weight_names)
  386. m_font_weight_list.append(it.name);
  387. m_weight_combobox->set_model(*GUI::ItemListModel<String>::create(m_font_weight_list));
  388. m_slope_combobox->on_change = [this](auto&, auto&) {
  389. m_edited_font->set_slope(Gfx::name_to_slope(m_slope_combobox->text()));
  390. did_modify_font();
  391. };
  392. for (auto& it : Gfx::font_slope_names)
  393. m_font_slope_list.append(it.name);
  394. m_slope_combobox->set_model(*GUI::ItemListModel<String>::create(m_font_slope_list));
  395. m_presentation_spinbox->on_change = [this](int value) {
  396. m_edited_font->set_presentation_size(value);
  397. update_preview();
  398. did_modify_font();
  399. };
  400. m_spacing_spinbox->on_change = [this](int value) {
  401. m_edited_font->set_glyph_spacing(value);
  402. update_preview();
  403. did_modify_font();
  404. };
  405. m_baseline_spinbox->on_change = [this](int value) {
  406. m_edited_font->set_baseline(value);
  407. m_glyph_editor_widget->update();
  408. update_preview();
  409. did_modify_font();
  410. };
  411. m_mean_line_spinbox->on_change = [this](int value) {
  412. m_edited_font->set_mean_line(value);
  413. m_glyph_editor_widget->update();
  414. update_preview();
  415. did_modify_font();
  416. };
  417. GUI::Application::the()->on_action_enter = [this](GUI::Action& action) {
  418. auto text = action.status_tip();
  419. if (text.is_empty())
  420. text = Gfx::parse_ampersand_string(action.text());
  421. m_statusbar->set_override_text(move(text));
  422. };
  423. GUI::Application::the()->on_action_leave = [this](GUI::Action&) {
  424. m_statusbar->set_override_text({});
  425. };
  426. set_scale(scale);
  427. }
  428. FontEditorWidget::~FontEditorWidget()
  429. {
  430. }
  431. void FontEditorWidget::initialize(String const& path, RefPtr<Gfx::BitmapFont>&& edited_font)
  432. {
  433. if (m_edited_font == edited_font)
  434. return;
  435. m_path = path;
  436. m_edited_font = edited_font;
  437. m_glyph_map_widget->set_font(*m_edited_font);
  438. m_glyph_editor_widget->initialize(*m_edited_font);
  439. did_resize_glyph_editor();
  440. m_glyph_editor_width_spinbox->set_visible(!m_edited_font->is_fixed_width());
  441. m_glyph_editor_width_spinbox->set_max(m_edited_font->max_glyph_width(), GUI::AllowCallback::No);
  442. m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
  443. m_glyph_editor_present_checkbox->set_visible(m_edited_font->is_fixed_width());
  444. m_glyph_editor_present_checkbox->set_checked(m_edited_font->contains_raw_glyph(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No);
  445. m_fixed_width_checkbox->set_checked(m_edited_font->is_fixed_width(), GUI::AllowCallback::No);
  446. m_name_textbox->set_text(m_edited_font->name(), GUI::AllowCallback::No);
  447. m_family_textbox->set_text(m_edited_font->family(), GUI::AllowCallback::No);
  448. m_presentation_spinbox->set_value(m_edited_font->presentation_size(), GUI::AllowCallback::No);
  449. m_spacing_spinbox->set_value(m_edited_font->glyph_spacing(), GUI::AllowCallback::No);
  450. m_mean_line_spinbox->set_range(0, max(m_edited_font->glyph_height() - 2, 0), GUI::AllowCallback::No);
  451. m_baseline_spinbox->set_range(0, max(m_edited_font->glyph_height() - 2, 0), GUI::AllowCallback::No);
  452. m_mean_line_spinbox->set_value(m_edited_font->mean_line(), GUI::AllowCallback::No);
  453. m_baseline_spinbox->set_value(m_edited_font->baseline(), GUI::AllowCallback::No);
  454. int i = 0;
  455. for (auto& it : Gfx::font_weight_names) {
  456. if (it.style == m_edited_font->weight()) {
  457. m_weight_combobox->set_selected_index(i, GUI::AllowCallback::No);
  458. break;
  459. }
  460. i++;
  461. }
  462. i = 0;
  463. for (auto& it : Gfx::font_slope_names) {
  464. if (it.style == m_edited_font->slope()) {
  465. m_slope_combobox->set_selected_index(i, GUI::AllowCallback::No);
  466. break;
  467. }
  468. i++;
  469. }
  470. deferred_invoke([this] {
  471. m_glyph_map_widget->set_focus(true);
  472. m_glyph_map_widget->scroll_to_glyph(m_glyph_map_widget->active_glyph());
  473. update_title();
  474. });
  475. m_undo_stack = make<GUI::UndoStack>();
  476. m_undo_glyph = adopt_ref(*new UndoGlyph(m_glyph_map_widget->active_glyph(), *m_edited_font));
  477. m_undo_stack->on_state_change = [this] {
  478. m_undo_action->set_enabled(m_undo_stack->can_undo());
  479. m_redo_action->set_enabled(m_undo_stack->can_redo());
  480. did_modify_font();
  481. };
  482. m_undo_action->set_enabled(false);
  483. m_redo_action->set_enabled(false);
  484. update_statusbar();
  485. if (on_initialize)
  486. on_initialize();
  487. }
  488. void FontEditorWidget::initialize_menubar(GUI::Window& window)
  489. {
  490. auto& file_menu = window.add_menu("&File");
  491. file_menu.add_action(*m_new_action);
  492. file_menu.add_action(*m_open_action);
  493. file_menu.add_action(*m_save_action);
  494. file_menu.add_action(*m_save_as_action);
  495. file_menu.add_separator();
  496. file_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) {
  497. if (!request_close())
  498. return;
  499. GUI::Application::the()->quit();
  500. }));
  501. auto& edit_menu = window.add_menu("&Edit");
  502. edit_menu.add_action(*m_undo_action);
  503. edit_menu.add_action(*m_redo_action);
  504. edit_menu.add_separator();
  505. edit_menu.add_action(*m_cut_action);
  506. edit_menu.add_action(*m_copy_action);
  507. edit_menu.add_action(*m_paste_action);
  508. edit_menu.add_action(*m_delete_action);
  509. edit_menu.add_separator();
  510. edit_menu.add_action(*m_copy_character_action);
  511. edit_menu.add_separator();
  512. edit_menu.add_action(*m_previous_glyph_action);
  513. edit_menu.add_action(*m_next_glyph_action);
  514. edit_menu.add_action(*m_go_to_glyph_action);
  515. auto& view_menu = window.add_menu("&View");
  516. view_menu.add_action(*m_open_preview_action);
  517. view_menu.add_separator();
  518. view_menu.add_action(*m_show_metadata_action);
  519. view_menu.add_separator();
  520. auto& scale_menu = view_menu.add_submenu("&Scale");
  521. scale_menu.add_action(*m_scale_five_action);
  522. scale_menu.add_action(*m_scale_ten_action);
  523. scale_menu.add_action(*m_scale_fifteen_action);
  524. auto& help_menu = window.add_menu("&Help");
  525. help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) {
  526. Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/FontEditor.md"), "/bin/Help");
  527. }));
  528. help_menu.add_action(GUI::CommonActions::make_about_action("Font Editor", GUI::Icon::default_icon("app-font-editor"), &window));
  529. }
  530. bool FontEditorWidget::save_as(String const& path)
  531. {
  532. auto saved_font = m_edited_font->masked_character_set();
  533. auto ret_val = saved_font->write_to_file(path);
  534. if (!ret_val) {
  535. GUI::MessageBox::show(window(), "The font file could not be saved.", "Save failed", GUI::MessageBox::Type::Error);
  536. return false;
  537. }
  538. m_path = path;
  539. m_undo_stack->set_current_unmodified();
  540. window()->set_modified(false);
  541. update_title();
  542. return true;
  543. }
  544. void FontEditorWidget::set_show_font_metadata(bool show)
  545. {
  546. if (m_font_metadata == show)
  547. return;
  548. m_font_metadata = show;
  549. m_font_metadata_groupbox->set_visible(m_font_metadata);
  550. }
  551. bool FontEditorWidget::open_file(String const& path)
  552. {
  553. auto bitmap_font = Gfx::BitmapFont::load_from_file(path);
  554. if (!bitmap_font) {
  555. String message = String::formatted("Couldn't load font: {}\n", path);
  556. GUI::MessageBox::show(window(), message, "Font Editor", GUI::MessageBox::Type::Error);
  557. return false;
  558. }
  559. auto new_font = bitmap_font->unmasked_character_set();
  560. window()->set_modified(false);
  561. initialize(path, move(new_font));
  562. return true;
  563. }
  564. void FontEditorWidget::undo()
  565. {
  566. if (!m_undo_stack->can_undo())
  567. return;
  568. m_undo_stack->undo();
  569. auto glyph = m_undo_glyph->restored_code_point();
  570. auto glyph_width = m_undo_glyph->restored_width();
  571. m_glyph_map_widget->set_active_glyph(glyph);
  572. m_glyph_map_widget->scroll_to_glyph(glyph);
  573. if (m_edited_font->is_fixed_width()) {
  574. m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
  575. } else {
  576. m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
  577. }
  578. m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), glyph_width);
  579. m_glyph_editor_widget->update();
  580. m_glyph_map_widget->update_glyph(glyph);
  581. update_preview();
  582. update_statusbar();
  583. }
  584. void FontEditorWidget::redo()
  585. {
  586. if (!m_undo_stack->can_redo())
  587. return;
  588. m_undo_stack->redo();
  589. auto glyph = m_undo_glyph->restored_code_point();
  590. auto glyph_width = m_undo_glyph->restored_width();
  591. m_glyph_map_widget->set_active_glyph(glyph);
  592. m_glyph_map_widget->scroll_to_glyph(glyph);
  593. if (m_edited_font->is_fixed_width()) {
  594. m_glyph_editor_present_checkbox->set_checked(glyph_width > 0, GUI::AllowCallback::No);
  595. } else {
  596. m_glyph_editor_width_spinbox->set_value(glyph_width, GUI::AllowCallback::No);
  597. }
  598. m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), glyph_width);
  599. m_glyph_editor_widget->update();
  600. m_glyph_map_widget->update_glyph(glyph);
  601. update_preview();
  602. update_statusbar();
  603. }
  604. bool FontEditorWidget::request_close()
  605. {
  606. if (!window()->is_modified())
  607. return true;
  608. auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path, m_undo_stack->last_unmodified_timestamp());
  609. if (result == GUI::MessageBox::ExecYes) {
  610. m_save_action->activate();
  611. if (!window()->is_modified())
  612. return true;
  613. }
  614. if (result == GUI::MessageBox::ExecNo)
  615. return true;
  616. return false;
  617. }
  618. void FontEditorWidget::update_title()
  619. {
  620. StringBuilder title;
  621. if (m_path.is_empty())
  622. title.append("Untitled");
  623. else
  624. title.append(m_path);
  625. title.append("[*] - Font Editor");
  626. window()->set_title(title.to_string());
  627. }
  628. void FontEditorWidget::did_modify_font()
  629. {
  630. if (!window() || window()->is_modified())
  631. return;
  632. window()->set_modified(true);
  633. update_title();
  634. }
  635. void FontEditorWidget::update_statusbar()
  636. {
  637. auto glyph = m_glyph_map_widget->active_glyph();
  638. StringBuilder builder;
  639. builder.appendff("U+{:04X} (", glyph);
  640. if (AK::UnicodeUtils::is_unicode_control_code_point(glyph)) {
  641. builder.append(AK::UnicodeUtils::get_unicode_control_code_point_alias(glyph).value());
  642. } else if (Gfx::get_char_bidi_class(glyph) == Gfx::BidirectionalClass::STRONG_RTL) {
  643. // FIXME: This is a necessary hack, as RTL text will mess up the painting of the statusbar text.
  644. // For now, replace RTL glyphs with U+FFFD, the replacement character.
  645. builder.append_code_point(0xFFFD);
  646. } else {
  647. builder.append_code_point(glyph);
  648. }
  649. builder.append(")");
  650. auto glyph_name = Unicode::code_point_display_name(glyph);
  651. if (glyph_name.has_value()) {
  652. builder.appendff(" {}", glyph_name.value());
  653. }
  654. if (m_edited_font->contains_raw_glyph(glyph))
  655. builder.appendff(" [{}x{}]", m_edited_font->raw_glyph_width(glyph), m_edited_font->glyph_height());
  656. else if (Gfx::Emoji::emoji_for_code_point(glyph))
  657. builder.appendff(" [emoji]");
  658. m_statusbar->set_text(builder.to_string());
  659. }
  660. void FontEditorWidget::update_preview()
  661. {
  662. if (m_font_preview_window)
  663. m_font_preview_window->update();
  664. }
  665. void FontEditorWidget::drop_event(GUI::DropEvent& event)
  666. {
  667. event.accept();
  668. if (event.mime_data().has_urls()) {
  669. auto urls = event.mime_data().urls();
  670. if (urls.is_empty())
  671. return;
  672. window()->move_to_front();
  673. if (!request_close())
  674. return;
  675. open_file(urls.first().path());
  676. }
  677. }
  678. void FontEditorWidget::did_resize_glyph_editor()
  679. {
  680. constexpr int glyph_toolbars_width = 100;
  681. m_glyph_editor_container->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height());
  682. m_left_column_container->set_fixed_width(max(m_glyph_editor_widget->preferred_width(), glyph_toolbars_width));
  683. }
  684. void FontEditorWidget::config_i32_did_change(String const& domain, String const& group, String const& key, i32 value)
  685. {
  686. if (domain == "FontEditor"sv && group == "GlyphEditor"sv && key == "Scale"sv) {
  687. set_scale(value);
  688. }
  689. }
  690. void FontEditorWidget::config_string_did_change(String const& domain, String const& group, String const& key, String const& value)
  691. {
  692. config_i32_did_change(domain, group, key, value.to_int().value_or(10));
  693. }
  694. void FontEditorWidget::set_scale(i32 scale)
  695. {
  696. m_glyph_editor_widget->set_scale(scale);
  697. }
  698. void FontEditorWidget::set_scale_and_save(i32 scale)
  699. {
  700. set_scale(scale);
  701. Config::write_i32("FontEditor", "GlyphEditor", "Scale", scale);
  702. did_resize_glyph_editor();
  703. }
  704. void FontEditorWidget::copy_selected_glyphs()
  705. {
  706. ByteBuffer buffer;
  707. int first_glyph = -1;
  708. size_t glyph_count = 0;
  709. auto append_glyph_to_buffer = [&](int glyph) {
  710. if (!edited_font().contains_glyph(glyph))
  711. return;
  712. if (first_glyph == -1 || glyph < first_glyph)
  713. first_glyph = glyph;
  714. buffer.append({ (char*)&glyph, sizeof(int) });
  715. auto bitmap = edited_font().raw_glyph(glyph).glyph_bitmap();
  716. buffer.append((u8)bitmap.width());
  717. buffer.append((u8)bitmap.height());
  718. for (int x = 0; x < bitmap.width(); x++) {
  719. for (int y = 0; y < bitmap.height(); y++)
  720. buffer.append(bitmap.bit_at(x, y));
  721. }
  722. glyph_count++;
  723. };
  724. auto selection = m_glyph_map_widget->selection().normalized();
  725. for (int i = selection.start(); i < selection.start() + selection.size(); i++)
  726. append_glyph_to_buffer(i);
  727. HashMap<String, String> metadata;
  728. metadata.set("first_glyph", String::number(first_glyph));
  729. metadata.set("count", String::number(glyph_count));
  730. GUI::Clipboard::the().set_data(buffer.bytes(), "glyph/x-fonteditor", metadata);
  731. }
  732. void FontEditorWidget::cut_selected_glyphs()
  733. {
  734. copy_selected_glyphs();
  735. delete_selected_glyphs();
  736. }
  737. void FontEditorWidget::paste_glyphs()
  738. {
  739. auto [data, mime_type, metadata] = GUI::Clipboard::the().fetch_data_and_type();
  740. if (!mime_type.starts_with("glyph/"))
  741. return;
  742. ;
  743. auto glyph_count = metadata.get("count").value_or("0").to_uint().value_or(0);
  744. if (glyph_count == 0)
  745. return;
  746. // FIXME: This is a hack to avoid regression and still doesn't support
  747. // multiple glyphs. It should have done proper undo stack integration.
  748. if (glyph_count == 1) {
  749. if (m_glyph_editor_widget->on_undo_event)
  750. m_glyph_editor_widget->on_undo_event();
  751. }
  752. auto first_glyph = metadata.get("first_glyph").value_or("0").to_uint().value_or(0);
  753. InputMemoryStream stream(data.bytes());
  754. for (size_t s = 0; s < glyph_count; s++) {
  755. int copied_glyph {};
  756. char width {};
  757. char height {};
  758. stream >> Bytes { (char*)&copied_glyph, sizeof(int) } >> width >> height;
  759. if (stream.has_any_error()) {
  760. dbgln("Failed to read glyph from clipboard, aborting!");
  761. return;
  762. }
  763. int glyph = m_glyph_map_widget->active_glyph() + (copied_glyph - first_glyph);
  764. auto bitmap = edited_font().raw_glyph(glyph).glyph_bitmap();
  765. m_edited_font->set_glyph_width(glyph, min(width, edited_font().max_glyph_width()));
  766. for (int x = 0; x < min(width, edited_font().max_glyph_width()); x++) {
  767. for (int y = 0; y < min(height, edited_font().glyph_height()); y++) {
  768. char byte;
  769. stream >> byte;
  770. if (stream.has_any_error()) {
  771. dbgln("Failed to read glyph from clipboard, aborting!");
  772. return;
  773. }
  774. bitmap.set_bit_at(x, y, byte);
  775. }
  776. }
  777. if (m_glyph_editor_widget->on_glyph_altered)
  778. m_glyph_editor_widget->on_glyph_altered(glyph);
  779. }
  780. m_glyph_editor_widget->update();
  781. }
  782. void FontEditorWidget::delete_selected_glyphs()
  783. {
  784. auto delete_glyph = [&](int glyph) {
  785. auto bitmap = m_edited_font->raw_glyph(glyph).glyph_bitmap();
  786. m_edited_font->set_glyph_width(glyph, 0);
  787. for (int x = 0; x < m_edited_font->max_glyph_width(); x++)
  788. for (int y = 0; y < m_edited_font->glyph_height(); y++)
  789. bitmap.set_bit_at(x, y, false);
  790. if (m_glyph_editor_widget->on_glyph_altered)
  791. m_glyph_editor_widget->on_glyph_altered(glyph);
  792. };
  793. auto selection = m_glyph_map_widget->selection().normalized();
  794. // FIXME: This is a hack to avoid regression and still doesn't support
  795. // multiple glyphs. It should have done proper undo stack integration.
  796. if (selection.size() == 1) {
  797. if (m_glyph_editor_widget->on_undo_event)
  798. m_glyph_editor_widget->on_undo_event();
  799. }
  800. for (int i = selection.start(); i < selection.start() + selection.size(); i++)
  801. delete_glyph(i);
  802. }