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 }; };