From 1d9849923419b4d96dbbe1d6da86fa0fc02e1cd6 Mon Sep 17 00:00:00 2001 From: Maciej Date: Tue, 4 Jan 2022 18:24:06 +0100 Subject: [PATCH] FontEditor: Support selection ranges This makes editing much easier, e.g. you don't need longer to copy hundreds of glyphs one by one. It has some flaws, e.g. it's not integrated with undo stack, but we need to start with something! --- .../Applications/FontEditor/FontEditor.cpp | 173 ++++++++++++++---- Userland/Applications/FontEditor/FontEditor.h | 5 + .../FontEditor/GlyphEditorWidget.cpp | 92 ---------- .../FontEditor/GlyphEditorWidget.h | 5 - .../FontEditor/GlyphMapWidget.cpp | 123 ++++++++++--- .../Applications/FontEditor/GlyphMapWidget.h | 48 ++++- 6 files changed, 283 insertions(+), 163 deletions(-) diff --git a/Userland/Applications/FontEditor/FontEditor.cpp b/Userland/Applications/FontEditor/FontEditor.cpp index c962b97809..fa1e859f10 100644 --- a/Userland/Applications/FontEditor/FontEditor.cpp +++ b/Userland/Applications/FontEditor/FontEditor.cpp @@ -179,9 +179,7 @@ FontEditorWidget::FontEditorWidget() save_as(save_path.value()); }); m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) { - if (!m_edited_font->contains_raw_glyph(m_glyph_map_widget->selected_glyph())) - return; - m_glyph_editor_widget->cut_glyph(); + cut_selected_glyphs(); if (m_edited_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No); else @@ -189,23 +187,15 @@ FontEditorWidget::FontEditorWidget() update_statusbar(); }); m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) { - if (!m_edited_font->contains_raw_glyph(m_glyph_map_widget->selected_glyph())) - return; - m_glyph_editor_widget->copy_glyph(); + copy_selected_glyphs(); }); m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) { - m_glyph_editor_widget->paste_glyph(); - if (m_edited_font->is_fixed_width()) - m_glyph_editor_present_checkbox->set_checked(true, GUI::AllowCallback::No); - else - m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->selected_glyph()), GUI::AllowCallback::No); + paste_glyphs(); update_statusbar(); }); m_paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "glyph/x-fonteditor"); m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { - if (m_glyph_editor_widget->is_glyph_empty() && !m_edited_font->contains_raw_glyph(m_glyph_map_widget->selected_glyph())) - return; - m_glyph_editor_widget->delete_glyph(); + delete_selected_glyphs(); if (m_edited_font->is_fixed_width()) m_glyph_editor_present_checkbox->set_checked(false, GUI::AllowCallback::No); else @@ -237,14 +227,14 @@ FontEditorWidget::FontEditorWidget() int code_point = strtoul(&input[0], nullptr, 16); code_point = clamp(code_point, 0x0000, 0x10FFFF); m_glyph_map_widget->set_focus(true); - m_glyph_map_widget->set_selected_glyph(code_point); + 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"); 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&) { bool search_wrapped = false; - for (int i = m_glyph_map_widget->selected_glyph() - 1;; --i) { + for (int i = m_glyph_map_widget->active_glyph() - 1;; --i) { if (i < 0 && !search_wrapped) { i = 0x10FFFF; search_wrapped = true; @@ -253,7 +243,7 @@ FontEditorWidget::FontEditorWidget() } if (m_edited_font->contains_raw_glyph(i)) { m_glyph_map_widget->set_focus(true); - m_glyph_map_widget->set_selected_glyph(i); + m_glyph_map_widget->set_active_glyph(i); m_glyph_map_widget->scroll_to_glyph(i); break; } @@ -262,7 +252,7 @@ FontEditorWidget::FontEditorWidget() m_previous_glyph_action->set_status_tip("Seek the previous visible glyph"); 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&) { bool search_wrapped = false; - for (int i = m_glyph_map_widget->selected_glyph() + 1;; ++i) { + for (int i = m_glyph_map_widget->active_glyph() + 1;; ++i) { if (i > 0x10FFFF && !search_wrapped) { i = 0; search_wrapped = true; @@ -271,7 +261,7 @@ FontEditorWidget::FontEditorWidget() } if (m_edited_font->contains_raw_glyph(i)) { m_glyph_map_widget->set_focus(true); - m_glyph_map_widget->set_selected_glyph(i); + m_glyph_map_widget->set_active_glyph(i); m_glyph_map_widget->scroll_to_glyph(i); break; } @@ -377,7 +367,7 @@ FontEditorWidget::FontEditorWidget() m_undo_stack->push(make(*m_undo_glyph)); }; - m_glyph_map_widget->on_glyph_selected = [this](int glyph) { + m_glyph_map_widget->on_active_glyph_changed = [this](int glyph) { if (m_undo_glyph) m_undo_glyph->set_code_point(glyph); m_glyph_editor_widget->set_glyph(glyph); @@ -401,7 +391,7 @@ FontEditorWidget::FontEditorWidget() m_fixed_width_checkbox->on_checked = [this](bool checked) { m_edited_font->set_fixed_width(checked); - auto glyph_width = m_edited_font->raw_glyph_width(m_glyph_map_widget->selected_glyph()); + auto glyph_width = m_edited_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); @@ -413,9 +403,9 @@ FontEditorWidget::FontEditorWidget() m_glyph_editor_width_spinbox->on_change = [this](int value) { m_undo_stack->push(make(*m_undo_glyph)); - m_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), value); + m_edited_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->selected_glyph()); + m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph()); update_preview(); update_statusbar(); did_modify_font(); @@ -423,9 +413,9 @@ FontEditorWidget::FontEditorWidget() m_glyph_editor_present_checkbox->on_checked = [this](bool checked) { m_undo_stack->push(make(*m_undo_glyph)); - m_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), checked ? m_edited_font->glyph_fixed_width() : 0); + m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), checked ? m_edited_font->glyph_fixed_width() : 0); m_glyph_editor_widget->update(); - m_glyph_map_widget->update_glyph(m_glyph_map_widget->selected_glyph()); + m_glyph_map_widget->update_glyph(m_glyph_map_widget->active_glyph()); update_preview(); update_statusbar(); did_modify_font(); @@ -504,10 +494,10 @@ void FontEditorWidget::initialize(String const& path, RefPtr&& m_glyph_editor_width_spinbox->set_visible(!m_edited_font->is_fixed_width()); m_glyph_editor_width_spinbox->set_max(m_edited_font->max_glyph_width(), GUI::AllowCallback::No); - m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->selected_glyph()), GUI::AllowCallback::No); + m_glyph_editor_width_spinbox->set_value(m_edited_font->raw_glyph_width(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No); m_glyph_editor_present_checkbox->set_visible(m_edited_font->is_fixed_width()); - m_glyph_editor_present_checkbox->set_checked(m_edited_font->contains_raw_glyph(m_glyph_map_widget->selected_glyph()), GUI::AllowCallback::No); + m_glyph_editor_present_checkbox->set_checked(m_edited_font->contains_raw_glyph(m_glyph_map_widget->active_glyph()), GUI::AllowCallback::No); m_fixed_width_checkbox->set_checked(m_edited_font->is_fixed_width(), GUI::AllowCallback::No); m_name_textbox->set_text(m_edited_font->name(), GUI::AllowCallback::No); @@ -540,12 +530,12 @@ void FontEditorWidget::initialize(String const& path, RefPtr&& deferred_invoke([this] { m_glyph_map_widget->set_focus(true); - m_glyph_map_widget->scroll_to_glyph(m_glyph_map_widget->selected_glyph()); + m_glyph_map_widget->scroll_to_glyph(m_glyph_map_widget->active_glyph()); update_title(); }); m_undo_stack = make(); - m_undo_glyph = adopt_ref(*new UndoGlyph(m_glyph_map_widget->selected_glyph(), *m_edited_font)); + m_undo_glyph = adopt_ref(*new UndoGlyph(m_glyph_map_widget->active_glyph(), *m_edited_font)); m_undo_stack->on_state_change = [this] { m_undo_action->set_enabled(m_undo_stack->can_undo()); @@ -651,14 +641,14 @@ void FontEditorWidget::undo() m_undo_stack->undo(); auto glyph = m_undo_glyph->restored_code_point(); auto glyph_width = m_undo_glyph->restored_width(); - m_glyph_map_widget->set_selected_glyph(glyph); + m_glyph_map_widget->set_active_glyph(glyph); m_glyph_map_widget->scroll_to_glyph(glyph); if (m_edited_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_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), glyph_width); + m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), glyph_width); m_glyph_editor_widget->update(); m_glyph_map_widget->update_glyph(glyph); update_preview(); @@ -672,14 +662,14 @@ void FontEditorWidget::redo() m_undo_stack->redo(); auto glyph = m_undo_glyph->restored_code_point(); auto glyph_width = m_undo_glyph->restored_width(); - m_glyph_map_widget->set_selected_glyph(glyph); + m_glyph_map_widget->set_active_glyph(glyph); m_glyph_map_widget->scroll_to_glyph(glyph); if (m_edited_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_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), glyph_width); + m_edited_font->set_glyph_width(m_glyph_map_widget->active_glyph(), glyph_width); m_glyph_editor_widget->update(); m_glyph_map_widget->update_glyph(glyph); update_preview(); @@ -722,7 +712,7 @@ void FontEditorWidget::did_modify_font() void FontEditorWidget::update_statusbar() { - auto glyph = m_glyph_map_widget->selected_glyph(); + auto glyph = m_glyph_map_widget->active_glyph(); StringBuilder builder; builder.appendff("U+{:04X} (", glyph); @@ -803,3 +793,118 @@ void FontEditorWidget::set_scale_and_save(i32 scale) Config::write_i32("FontEditor", "GlyphEditor", "Scale", scale); did_resize_glyph_editor(); } +void FontEditorWidget::copy_selected_glyphs() +{ + ByteBuffer buffer; + + int first_glyph = -1; + size_t glyph_count = 0; + auto append_glyph_to_buffer = [&](int glyph) { + if (!edited_font().contains_glyph(glyph)) + return; + + if (first_glyph == -1 || glyph < first_glyph) + first_glyph = glyph; + buffer.append({ (char*)&glyph, sizeof(int) }); + + auto bitmap = edited_font().raw_glyph(glyph).glyph_bitmap(); + buffer.append((u8)bitmap.width()); + buffer.append((u8)bitmap.height()); + for (int x = 0; x < bitmap.width(); x++) { + for (int y = 0; y < bitmap.height(); y++) + buffer.append(bitmap.bit_at(x, y)); + } + glyph_count++; + }; + + auto selection = m_glyph_map_widget->selection().normalized(); + for (int i = selection.start(); i < selection.start() + selection.size(); i++) + append_glyph_to_buffer(i); + + HashMap metadata; + metadata.set("first_glyph", String::number(first_glyph)); + metadata.set("count", String::number(glyph_count)); + GUI::Clipboard::the().set_data(buffer.bytes(), "glyph/x-fonteditor", metadata); +} + +void FontEditorWidget::cut_selected_glyphs() +{ + copy_selected_glyphs(); + delete_selected_glyphs(); +} + +void FontEditorWidget::paste_glyphs() +{ + auto [data, mime_type, metadata] = GUI::Clipboard::the().fetch_data_and_type(); + if (!mime_type.starts_with("glyph/")) + return; + ; + auto glyph_count = metadata.get("count").value_or("0").to_uint().value_or(0); + if (glyph_count == 0) + return; + + // FIXME: This is a hack to avoid regression and still doesn't support + // multiple glyphs. It should have done proper undo stack integration. + if (glyph_count == 1) { + if (m_glyph_editor_widget->on_undo_event) + m_glyph_editor_widget->on_undo_event(); + } + + auto first_glyph = metadata.get("first_glyph").value_or("0").to_uint().value_or(0); + + InputMemoryStream stream(data.bytes()); + for (size_t s = 0; s < glyph_count; s++) { + int copied_glyph {}; + char width {}; + char height {}; + stream >> Bytes { (char*)&copied_glyph, sizeof(int) } >> width >> height; + if (stream.has_any_error()) { + dbgln("Failed to read glyph from clipboard, aborting!"); + return; + } + + int glyph = m_glyph_map_widget->active_glyph() + (copied_glyph - first_glyph); + auto bitmap = edited_font().raw_glyph(glyph).glyph_bitmap(); + m_edited_font->set_glyph_width(glyph, min(width, edited_font().max_glyph_width())); + for (int x = 0; x < min(width, edited_font().max_glyph_width()); x++) { + for (int y = 0; y < min(height, edited_font().glyph_height()); y++) { + char byte; + stream >> byte; + if (stream.has_any_error()) { + dbgln("Failed to read glyph from clipboard, aborting!"); + return; + } + bitmap.set_bit_at(x, y, byte); + } + } + if (m_glyph_editor_widget->on_glyph_altered) + m_glyph_editor_widget->on_glyph_altered(glyph); + } + + m_glyph_editor_widget->update(); +} + +void FontEditorWidget::delete_selected_glyphs() +{ + auto delete_glyph = [&](int glyph) { + auto bitmap = m_edited_font->raw_glyph(glyph).glyph_bitmap(); + m_edited_font->set_glyph_width(glyph, 0); + for (int x = 0; x < m_edited_font->max_glyph_width(); x++) + for (int y = 0; y < m_edited_font->glyph_height(); y++) + bitmap.set_bit_at(x, y, false); + if (m_glyph_editor_widget->on_glyph_altered) + m_glyph_editor_widget->on_glyph_altered(glyph); + }; + + auto selection = m_glyph_map_widget->selection().normalized(); + + // FIXME: This is a hack to avoid regression and still doesn't support + // multiple glyphs. It should have done proper undo stack integration. + if (selection.size() == 1) { + if (m_glyph_editor_widget->on_undo_event) + m_glyph_editor_widget->on_undo_event(); + } + + for (int i = selection.start(); i < selection.start() + selection.size(); i++) + delete_glyph(i); +} diff --git a/Userland/Applications/FontEditor/FontEditor.h b/Userland/Applications/FontEditor/FontEditor.h index f6d5b7270d..24efcc0f3c 100644 --- a/Userland/Applications/FontEditor/FontEditor.h +++ b/Userland/Applications/FontEditor/FontEditor.h @@ -55,6 +55,11 @@ private: void set_scale(i32); void set_scale_and_save(i32); + void copy_selected_glyphs(); + void cut_selected_glyphs(); + void paste_glyphs(); + void delete_selected_glyphs(); + RefPtr m_edited_font; RefPtr m_glyph_map_widget; diff --git a/Userland/Applications/FontEditor/GlyphEditorWidget.cpp b/Userland/Applications/FontEditor/GlyphEditorWidget.cpp index fce7991a36..a4c9d68da1 100644 --- a/Userland/Applications/FontEditor/GlyphEditorWidget.cpp +++ b/Userland/Applications/FontEditor/GlyphEditorWidget.cpp @@ -33,98 +33,6 @@ void GlyphEditorWidget::set_glyph(int glyph) update(); } -void GlyphEditorWidget::delete_glyph() -{ - if (on_undo_event) - on_undo_event(); - auto bitmap = font().raw_glyph(m_glyph).glyph_bitmap(); - for (int x = 0; x < font().max_glyph_width(); x++) - for (int y = 0; y < font().glyph_height(); y++) - bitmap.set_bit_at(x, y, false); - font().set_glyph_width(m_glyph, 0); - if (on_glyph_altered) - on_glyph_altered(m_glyph); - update(); -} - -void GlyphEditorWidget::cut_glyph() -{ - copy_glyph(); - delete_glyph(); -} - -void GlyphEditorWidget::copy_glyph() -{ - auto bitmap = font().raw_glyph(m_glyph).glyph_bitmap(); - u8 bits[bitmap.width()][bitmap.height()]; - for (int x = 0; x < bitmap.width(); x++) { - for (int y = 0; y < bitmap.height(); y++) { - bits[x][y] = bitmap.bit_at(x, y); - } - } - - StringBuilder glyph_builder; - if (AK::UnicodeUtils::is_unicode_control_code_point(m_glyph)) - glyph_builder.append(AK::UnicodeUtils::get_unicode_control_code_point_alias(m_glyph).value()); - else if (Gfx::get_char_bidi_class(m_glyph) == Gfx::BidirectionalClass::STRONG_RTL) - glyph_builder.append_code_point(0xFFFD); - else - glyph_builder.append_code_point(m_glyph); - - HashMap metadata; - metadata.set("char", glyph_builder.to_string()); - metadata.set("width", String::number(bitmap.width())); - metadata.set("height", String::number(bitmap.height())); - - GUI::Clipboard::the().set_data(ReadonlyBytes(&bits[0], bitmap.width() * bitmap.height()), "glyph/x-fonteditor", metadata); -} - -void GlyphEditorWidget::paste_glyph() -{ - auto [data, mime_type, metadata] = GUI::Clipboard::the().fetch_data_and_type(); - if (!mime_type.starts_with("glyph/")) - return; - - auto byte_buffer = data.data(); - auto buffer_height = metadata.get("height").value_or("0").to_int().value_or(0); - auto buffer_width = metadata.get("width").value_or("0").to_int().value_or(0); - - if (buffer_height <= 0 || buffer_width <= 0 || buffer_height > 128 || buffer_width > 128) { - dbgln("Refusing to receive glyph of dimensions {}x{}", buffer_width, buffer_height); - return; - } - if (data.size() != static_cast(buffer_width * buffer_height)) { - dbgln("Refusing to receive glyph with mismatching buffer sizes: Expected {}x{}={} bytes, received {} bytes.", - buffer_width, buffer_height, buffer_width * buffer_height, data.size()); - return; - } - - if (on_undo_event) - on_undo_event(); - - u8 bits[buffer_width][buffer_height]; - int i = 0; - for (int x = 0; x < buffer_width; x++) { - for (int y = 0; y < buffer_height; y++) { - bits[x][y] = byte_buffer[i]; - i++; - } - } - - auto bitmap = font().raw_glyph(m_glyph).glyph_bitmap(); - if (bitmap.width() < buffer_width) - font().set_glyph_width(m_glyph, min(buffer_width, font().max_glyph_width())); - for (int x = 0; x < min(buffer_width, font().max_glyph_width()); x++) { - for (int y = 0; y < min(buffer_height, font().glyph_height()); y++) { - bitmap.set_bit_at(x, y, bits[x][y]); - } - } - - if (on_glyph_altered) - on_glyph_altered(m_glyph); - update(); -} - void GlyphEditorWidget::paint_event(GUI::PaintEvent& event) { GUI::Frame::paint_event(event); diff --git a/Userland/Applications/FontEditor/GlyphEditorWidget.h b/Userland/Applications/FontEditor/GlyphEditorWidget.h index 94f2925e6e..08aebb4061 100644 --- a/Userland/Applications/FontEditor/GlyphEditorWidget.h +++ b/Userland/Applications/FontEditor/GlyphEditorWidget.h @@ -29,11 +29,6 @@ public: int glyph() const { return m_glyph; } void set_glyph(int); - - void cut_glyph(); - void copy_glyph(); - void paste_glyph(); - void delete_glyph(); bool is_glyph_empty(); void rotate_90(Direction); diff --git a/Userland/Applications/FontEditor/GlyphMapWidget.cpp b/Userland/Applications/FontEditor/GlyphMapWidget.cpp index 4e61d3f01f..91c518752a 100644 --- a/Userland/Applications/FontEditor/GlyphMapWidget.cpp +++ b/Userland/Applications/FontEditor/GlyphMapWidget.cpp @@ -6,11 +6,44 @@ */ #include "GlyphMapWidget.h" +#include +#include #include #include #include #include +GlyphMapWidget::Selection GlyphMapWidget::Selection::normalized() const +{ + if (m_size > 0) + return *this; + return { m_start + m_size, -m_size + 1 }; +} + +void GlyphMapWidget::Selection::resize_by(int i) +{ + m_size += i; + if (m_size == 0) { + if (i < 0) + m_size--; + else + m_size++; + } +} + +bool GlyphMapWidget::Selection::contains(int i) const +{ + auto this_normalized = normalized(); + return i >= this_normalized.m_start && i < this_normalized.m_start + this_normalized.m_size; +} + +void GlyphMapWidget::Selection::extend_to(int glyph) +{ + m_size = glyph - m_start; + if (m_size > 0) + m_size++; +} + GlyphMapWidget::GlyphMapWidget() { set_focus_policy(GUI::FocusPolicy::StrongFocus); @@ -27,7 +60,7 @@ void GlyphMapWidget::initialize(Gfx::BitmapFont& mutable_font) return; m_font = mutable_font; vertical_scrollbar().set_step(font().glyph_height() + m_vertical_spacing); - set_selected_glyph('A'); + set_active_glyph('A'); } void GlyphMapWidget::resize_event(GUI::ResizeEvent& event) @@ -45,18 +78,22 @@ void GlyphMapWidget::resize_event(GUI::ResizeEvent& event) int content_height = rows() * (font().glyph_height() + m_vertical_spacing) + frame_thickness(); set_content_size({ content_width, content_height }); - scroll_to_glyph(m_selected_glyph); + scroll_to_glyph(m_active_glyph); AbstractScrollableWidget::resize_event(event); } -void GlyphMapWidget::set_selected_glyph(int glyph) +void GlyphMapWidget::set_active_glyph(int glyph, ShouldResetSelection should_reset_selection) { - if (m_selected_glyph == glyph) + if (m_active_glyph == glyph) return; - m_selected_glyph = glyph; - if (on_glyph_selected) - on_glyph_selected(glyph); + m_active_glyph = glyph; + if (should_reset_selection == ShouldResetSelection::Yes) { + m_selection.set_start(glyph); + m_selection.set_size(1); + } + if (on_active_glyph_changed) + on_active_glyph_changed(glyph); update(); } @@ -99,7 +136,7 @@ void GlyphMapWidget::paint_event(GUI::PaintEvent& event) outer_rect.y() + m_vertical_spacing / 2, font().max_glyph_width(), font().glyph_height()); - if (glyph == m_selected_glyph) { + if (m_selection.contains(glyph)) { painter.fill_rect(outer_rect, is_focused() ? palette().selection() : palette().inactive_selection()); if (m_font->contains_raw_glyph(glyph)) painter.draw_glyph(inner_rect.location(), glyph, is_focused() ? palette().selection_text() : palette().inactive_selection_text()); @@ -113,6 +150,7 @@ void GlyphMapWidget::paint_event(GUI::PaintEvent& event) painter.draw_emoji(inner_rect.location(), *emoji, *m_font); } } + painter.draw_focus_rect(get_outer_rect(m_active_glyph), Gfx::Color::Black); } void GlyphMapWidget::mousedown_event(GUI::MouseEvent& event) @@ -125,7 +163,13 @@ void GlyphMapWidget::mousedown_event(GUI::MouseEvent& event) auto row = (map_position.y() - 1) / ((font().glyph_height() + m_vertical_spacing)); auto glyph = row * columns() + col; if (row >= 0 && row < rows() && col >= 0 && col < columns() && glyph < m_glyph_count) { - set_selected_glyph(glyph); + if (event.shift()) + m_selection.extend_to(glyph); + else { + m_selection.set_size(1); + m_selection.set_start(glyph); + } + set_active_glyph(glyph, ShouldResetSelection::No); } } @@ -133,53 +177,76 @@ void GlyphMapWidget::keydown_event(GUI::KeyEvent& event) { GUI::Frame::keydown_event(event); + if (!event.ctrl() && !event.shift()) { + m_selection.set_size(1); + m_selection.set_start(m_active_glyph); + } + if (event.key() == KeyCode::Key_Up) { - if (selected_glyph() >= m_columns) { - set_selected_glyph(selected_glyph() - m_columns); - scroll_to_glyph(selected_glyph()); + if (m_selection.start() >= m_columns) { + if (event.shift()) + m_selection.resize_by(-m_columns); + else + m_selection.set_start(m_selection.start() - m_columns); + set_active_glyph(m_active_glyph - m_columns, ShouldResetSelection::No); + scroll_to_glyph(m_active_glyph); return; } } if (event.key() == KeyCode::Key_Down) { - if (selected_glyph() < m_glyph_count - m_columns) { - set_selected_glyph(selected_glyph() + m_columns); - scroll_to_glyph(selected_glyph()); + if (m_selection.start() < m_glyph_count - m_columns) { + if (event.shift()) + m_selection.resize_by(m_columns); + else + m_selection.set_start(m_selection.start() + m_columns); + set_active_glyph(m_active_glyph + m_columns, ShouldResetSelection::No); + scroll_to_glyph(m_active_glyph); return; } } if (event.key() == KeyCode::Key_Left) { - if (selected_glyph() > 0) { - set_selected_glyph(selected_glyph() - 1); - scroll_to_glyph(selected_glyph()); + if (m_selection.start() > 0) { + if (event.shift()) + m_selection.resize_by(-1); + else + m_selection.set_start(m_selection.start() - 1); + set_active_glyph(m_active_glyph - 1, ShouldResetSelection::No); + scroll_to_glyph(m_active_glyph); return; } } if (event.key() == KeyCode::Key_Right) { - if (selected_glyph() < m_glyph_count - 1) { - set_selected_glyph(selected_glyph() + 1); - scroll_to_glyph(selected_glyph()); + if (m_selection.start() < m_glyph_count - 1) { + if (event.shift()) + m_selection.resize_by(1); + else + m_selection.set_start(m_selection.start() + 1); + set_active_glyph(m_active_glyph + 1, ShouldResetSelection::No); + scroll_to_glyph(m_active_glyph); return; } } + + // FIXME: Support selection for these. if (event.ctrl() && event.key() == KeyCode::Key_Home) { - set_selected_glyph(0); - scroll_to_glyph(selected_glyph()); + set_active_glyph(0); + scroll_to_glyph(m_active_glyph); return; } if (event.ctrl() && event.key() == KeyCode::Key_End) { - set_selected_glyph(m_glyph_count - 1); - scroll_to_glyph(selected_glyph()); + set_active_glyph(m_glyph_count - 1); + scroll_to_glyph(m_active_glyph); return; } if (!event.ctrl() && event.key() == KeyCode::Key_Home) { - set_selected_glyph(selected_glyph() / m_columns * m_columns); + set_active_glyph(m_active_glyph / m_columns * m_columns); return; } if (!event.ctrl() && event.key() == KeyCode::Key_End) { - int new_selection = selected_glyph() / m_columns * m_columns + (m_columns - 1); + int new_selection = m_active_glyph / m_columns * m_columns + (m_columns - 1); int max = m_glyph_count - 1; new_selection = clamp(new_selection, 0, max); - set_selected_glyph(new_selection); + set_active_glyph(new_selection); return; } } diff --git a/Userland/Applications/FontEditor/GlyphMapWidget.h b/Userland/Applications/FontEditor/GlyphMapWidget.h index 416a7a0d89..d6bfc5fcc7 100644 --- a/Userland/Applications/FontEditor/GlyphMapWidget.h +++ b/Userland/Applications/FontEditor/GlyphMapWidget.h @@ -6,7 +6,9 @@ #pragma once +#include #include +#include #include class GlyphMapWidget final : public GUI::AbstractScrollableWidget { @@ -16,8 +18,40 @@ public: void initialize(Gfx::BitmapFont&); - int selected_glyph() const { return m_selected_glyph; } - void set_selected_glyph(int); + class Selection { + public: + Selection() = default; + Selection(int start, int size) + : m_start(start) + , m_size(size) + { + } + + int size() const { return m_size; } + void set_size(int i) { m_size = i; } + int start() const { return m_start; } + void set_start(int i) { m_start = i; } + + Selection normalized() const; + bool contains(int) const; + void resize_by(int i); + void extend_to(int); + + private: + int m_start { 0 }; + int m_size { 1 }; + }; + + Selection selection() const { return m_selection; } + int active_glyph() const { return m_active_glyph; } + + enum class ShouldResetSelection { + Yes, + No + }; + + void set_active_glyph(int, ShouldResetSelection = ShouldResetSelection::Yes); + void clear_selection() { m_selection.set_size(0); } void scroll_to_glyph(int); void update_glyph(int); @@ -27,7 +61,7 @@ public: Gfx::BitmapFont& font() { return *m_font; } Gfx::BitmapFont const& font() const { return *m_font; } - Function on_glyph_selected; + Function on_active_glyph_changed; private: GlyphMapWidget(); @@ -38,12 +72,18 @@ private: Gfx::IntRect get_outer_rect(int glyph) const; + void cut_glyph(int glyph); + void copy_glyph(int glyph); + void paste_glyph(int glyph); + void delete_glyph(int glyph); + RefPtr m_font; int m_glyph_count { 0x110000 }; int m_columns { 0 }; int m_rows { 0 }; int m_horizontal_spacing { 2 }; int m_vertical_spacing { 2 }; - int m_selected_glyph { 0 }; + Selection m_selection; + int m_active_glyph { 0 }; int m_visible_glyphs { 0 }; };