diff --git a/data/icons/meson.build b/data/icons/meson.build index f8e01f72..b74bf65d 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build @@ -17,13 +17,13 @@ foreach i : icon_sizes icon_variant / 'hicolor' / i + '.png', install_dir: datadir / 'icons' / 'hicolor' / i + 'x' + i / 'apps', rename: app_id + '.png' - ) + ) install_data( icon_variant / 'hicolor@2' / i + '@2.png', install_dir: datadir / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps', rename: app_id + '.png' - ) + ) endforeach @@ -31,7 +31,4 @@ install_data( icon_variant / 'scalable.svg', install_dir: datadir / 'icons' / 'scalable' / 'apps', rename: app_id + '.svg' -) - - - + ) diff --git a/data/meson.build b/data/meson.build index 9fca466c..c97cd90d 100644 --- a/data/meson.build +++ b/data/meson.build @@ -9,13 +9,13 @@ gresource_file = configure_file( input: app_name + '.gresource.xml.in', output: app_id + '.gresource.xml', configuration: config_data, -) + ) gresource = gnome.compile_resources( 'gresource', gresource_file, source_dir: '.' -) + ) #======================== @@ -30,7 +30,7 @@ gschema_file = configure_file( install_data( gschema_file, install_dir: datadir / 'glib-2.0' / 'schemas' -) + ) #======================== @@ -62,7 +62,7 @@ desktop_file_in = configure_file( input: app_name + '.desktop.in.in', output: app_id + '.desktop.in', configuration: config_data, -) + ) desktop_file = i18n.merge_file( input: desktop_file_in, @@ -71,7 +71,7 @@ desktop_file = i18n.merge_file( type: 'desktop', install: true, install_dir: datadir / 'applications', -) + ) #======================== @@ -82,7 +82,7 @@ appstream_file_in = configure_file( input: app_name + '.metainfo.xml.in.in', output: app_id + '.metainfo.xml.in', configuration: config_data, -) + ) appstream_file = i18n.merge_file( input: appstream_file_in, @@ -91,5 +91,4 @@ appstream_file = i18n.merge_file( type: 'xml', install: true, install_dir: datadir / 'metainfo', -) - + ) diff --git a/io.github.ellie_commons.jorts.flathub.yml b/io.github.ellie_commons.jorts.flathub.yml index 338e823d..4d1bef08 100644 --- a/io.github.ellie_commons.jorts.flathub.yml +++ b/io.github.ellie_commons.jorts.flathub.yml @@ -20,7 +20,6 @@ cleanup: - /share/gir-1.0 - /share/vala -# Cleanup development-related files inherited from the BaseApp to reduce package size of your app. cleanup-commands: - /app/cleanup-BaseApp.sh @@ -29,8 +28,8 @@ modules: buildsystem: meson config-opts: - --buildtype=release - - -Dlegacy_rdnn=true - -Dicon_variant=default + - -Dlegacy_rdnn=true sources: - type: dir path: . diff --git a/io.github.elly_code.jorts.devel.yml b/io.github.elly_code.jorts.devel.yml index 22e80ad1..44a86105 100644 --- a/io.github.elly_code.jorts.devel.yml +++ b/io.github.elly_code.jorts.devel.yml @@ -24,8 +24,8 @@ modules: buildsystem: meson config-opts: - --buildtype=debug - - -Ddevelopment=true - -Dicon_variant=default + - -Ddevelopment=true sources: - type: dir path: . diff --git a/meson.build b/meson.build index b88d99e1..b3e4dc14 100644 --- a/meson.build +++ b/meson.build @@ -1,10 +1,10 @@ project( 'jorts', - ['c', 'vala'], # The floor is vala + ['c', 'vala'], # The floor is vala license: 'GPL-3.0-or-later', version: '4.3.0', # Versioning: first should follow Gtk version, second and third depending how proud you are of the update meson_version: '>= 1.1' # Use meson.option instead of meson_options.txt -) + ) app_name = meson.project_name() app_version = meson.project_version() @@ -48,7 +48,12 @@ endif gnome = import('gnome') i18n = import('i18n') -add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (app_id), language:'c') +add_project_arguments( + '-DGETTEXT_PACKAGE="@0@"'.format (app_id), # + '-DG_LOG_DOMAIN="@0@"'.format (app_id), # Dev builds add the app_id to G_MESSAGES_DEBUG + '-w', # Because of Vala we get ten thousand warnings we can do nothing about + language:'c' + ) # Play nice with clang @@ -84,7 +89,6 @@ build_machine_system = build_machine.system() if build_machine_system == 'windows' windows = true - endif # Here is to adjust the project accordingly to stuff @@ -116,8 +120,8 @@ config_data = configuration_data( 'PUBLISHER': publisher, 'ALTERNATE_APP_ID': alternate_app_id, 'LOCALEDIR': prefix / get_option('localedir') - } -) + } + ) subdir('po') subdir('data') @@ -134,4 +138,4 @@ gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true -) + ) diff --git a/po/POTFILES b/po/POTFILES index 2a789734..2228525f 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -13,6 +13,7 @@ src/Views/PreferencesView.vala src/Widgets/ActionBar.vala src/Widgets/EditableLabel.vala src/Widgets/EditableLabel.vala +src/Widgets/HyperTextView.vala src/Widgets/Popover.vala src/Widgets/PopoverWidgets/ColorBox.vala src/Widgets/PopoverWidgets/MonospaceBox.vala diff --git a/src/Application.vala b/src/Application.vala index 87b994bb..49aca57a 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -90,12 +90,6 @@ public class Jorts.Application : Gtk.Application { Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); Intl.textdomain (GETTEXT_PACKAGE); - -#if DEVEL - //GLib.Environment.set_variable ("LANGUAGE", "C", true); - GLib.Environment.set_variable ("GTK_DEBUG", "interactive", true); - //print (LOCALEDIR); -#endif } /*************************************************/ @@ -140,16 +134,6 @@ public class Jorts.Application : Gtk.Application { ); }); - print (""" -🎉✨ ACTIVATING: SUPER COOL JORTS 😎🔥❗🎶🤌 -Your Notes are all belong to us! - _ _ - (\o/) (\o/) <--- Tiny electric angels working in the background - /_\ /_\ - -Please wait while the app remembers all the things… -"""); - /* Quit if all sticky notes are closed and preferences arent shown */ window_removed.connect (check_if_quit); @@ -194,10 +178,6 @@ Please wait while the app remembers all the things… if (show_pref) {action_show_preferences (); show_pref = false;} } - public static int main (string[] args) { - return new Application ().run (args); - } - private void action_show_preferences () { debug ("Showing preferences!"); diff --git a/src/Interfaces/Zoomable.vala b/src/Interfaces/Zoomable.vala new file mode 100644 index 00000000..6dced7b0 --- /dev/null +++ b/src/Interfaces/Zoomable.vala @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/*************************************************/ +/** + * Implemented by childs of ZoomedWindow + */ +public interface Jorts.Zoomable : Gtk.Widget { + + /** + * Called by ZoomedWindow after changing zoom value. + * + * The default implementation does nothing. + * + * If you need to do some changes, such as display the new value, or refresh some displayed elements, override this. + */ + public virtual void on_zoom_changed (int new_zoom) { + debug ("Zoom changed: %i", new_zoom); + return; + } +} diff --git a/src/Services/ColorController.vala b/src/Services/ColorController.vala index 1017e380..d60affd7 100644 --- a/src/Services/ColorController.vala +++ b/src/Services/ColorController.vala @@ -12,7 +12,7 @@ */ public class Jorts.ColorController : Object { - private weak Jorts.StickyNoteWindow window; + private unowned Jorts.StickyNoteWindow window; private Jorts.Themes _theme; public Jorts.Themes theme { diff --git a/src/Services/NoteManager.vala b/src/Services/NoteManager.vala index 63722179..990e6779 100644 --- a/src/Services/NoteManager.vala +++ b/src/Services/NoteManager.vala @@ -15,8 +15,8 @@ public class Jorts.NoteManager : Object { private static bool saving_lock = true; private static NoteData? last_deleted = null; - private Jorts.Application application; - public Gee.ArrayList open_notes; + private unowned Jorts.Application application; + public Gee.ArrayList open_notes; public Jorts.Storage storage; public NoteManager (Jorts.Application app) { @@ -24,7 +24,7 @@ public class Jorts.NoteManager : Object { } construct { - open_notes = new Gee.ArrayList (); + open_notes = new Gee.ArrayList (); storage = new Jorts.Storage (); } @@ -40,6 +40,7 @@ public class Jorts.NoteManager : Object { Json.Array loaded_data = storage.load (); if (loaded_data.get_length () == 0) { + var note_data = new NoteData (); note_data.theme = DEFAULT_THEME; @@ -103,6 +104,9 @@ public class Jorts.NoteManager : Object { application.remove_window ((Gtk.Window)note); note.close (); + note.dispose (); + note.destroy (); + note = null; immediately_save (); } @@ -131,7 +135,7 @@ public class Jorts.NoteManager : Object { public void immediately_save () { var array = new Json.Array (); - foreach (Jorts.StickyNoteWindow note in open_notes) { + foreach (unowned Jorts.StickyNoteWindow note in open_notes) { var data = note.packaged (); var object = data.to_json (); array.add_object_element (object); diff --git a/src/Services/ScribblyController.vala b/src/Services/ScribblyController.vala index 7622331b..07e0e77a 100644 --- a/src/Services/ScribblyController.vala +++ b/src/Services/ScribblyController.vala @@ -13,7 +13,7 @@ public class Jorts.ScribblyController : Object { private const string STYLE_SCRIBBLED = "scribbled"; - private weak Jorts.StickyNoteWindow window; + private unowned Jorts.StickyNoteWindow window; private bool _scribble; public bool scribble { diff --git a/src/Views/NoteView.vala b/src/Views/NoteView.vala index 850c8d30..ddd6078c 100644 --- a/src/Views/NoteView.vala +++ b/src/Views/NoteView.vala @@ -5,7 +5,7 @@ * 2025-2026 Stella & Charlie (teamcons.carrd.co) */ - public class Jorts.NoteView : Gtk.Box { + public class Jorts.NoteView : Gtk.Box, Zoomable { public Gtk.HeaderBar headerbar; public Jorts.EditableLabel editablelabel; public Jorts.TextView textview; @@ -67,9 +67,6 @@ app.set_accels_for_action (ACTION_PREFIX + ACTION_SHOW_MENU, {"G", "O"}); app.set_accels_for_action (ACTION_PREFIX + ACTION_TOGGLE_MONO, {"m"}); - - - orientation = VERTICAL; spacing = 0; @@ -141,7 +138,23 @@ private void action_show_menu () {menu_button.activate ();} private void action_toggle_mono () {monospace = !monospace;} + public void on_zoom_changed (int new_zoom) { + textview.queue_refresh_indentation (); + + // Adapt headerbar size to avoid weird flickering + headerbar.height_request = Jorts.Zoom.from_int (new_zoom).to_ui_size (); + + // Reflect the number in the popover + popover.zoom = new_zoom; + + // Keep it for next new notes + NoteData.latest_zoom = new_zoom; + + //window.has_changed (); + } + ~NoteView () { + actionbar.destroy (); debug ("Destroyed"); } } diff --git a/src/Widgets/ActionBar.vala b/src/Widgets/ActionBar.vala index a35baf7c..0cd9da29 100644 --- a/src/Widgets/ActionBar.vala +++ b/src/Widgets/ActionBar.vala @@ -19,7 +19,8 @@ public Gtk.WindowHandle handle; public Jorts.Popover popover; - const int ICON_SIZE = 32; + const int ICON_SIZE = 32; // mobile needs bigger + const int REVEAL_DELAY = 250; // in ms construct { @@ -111,12 +112,46 @@ child = handle; + + /***************************************************/ + /* CONNECTS AND BINDS */ + /***************************************************/ + // Randomize-skip emoji icon emojichooser_popover.show.connect (on_emoji_popover); // Hide the list button if user has specified no list item symbol on_prefix_changed (); Application.settings.changed[KEY_LIST].connect (on_prefix_changed); + + // Respect animation settings for showing ui elements + if (Application.gtk_settings.gtk_enable_animations && !Application.settings.get_boolean (KEY_HIDEBAR)) { + actionbar.revealed = false; + realize.connect_after (delayed_show); + + } else { + bind_hidebar (); + } + } + + /** + * Show Actionbar shortly after the window is shown + * This is more for the Aesthetic + */ + private void delayed_show () { + debug ("delayed show"); + Timeout.add_once (REVEAL_DELAY, bind_hidebar); + realize.disconnect (delayed_show); + } + + private void bind_hidebar () { + debug ("Bind hidebar"); + + Application.settings.bind ( + KEY_HIDEBAR, + actionbar, + "revealed", + SettingsBindFlags.INVERT_BOOLEAN); } /** diff --git a/src/Widgets/EditableLabel.vala b/src/Widgets/EditableLabel.vala index 3bdac5fd..874c1d87 100644 --- a/src/Widgets/EditableLabel.vala +++ b/src/Widgets/EditableLabel.vala @@ -57,4 +57,8 @@ public class Jorts.EditableLabel : Granite.Bin { } remove_css_class (STYLE_MONOSPACE); } + + ~EditableLabel () { + debug ("Destroyed"); + } } diff --git a/src/Widgets/HyperTextView.vala b/src/Widgets/HyperTextView.vala new file mode 100644 index 00000000..24d4f408 --- /dev/null +++ b/src/Widgets/HyperTextView.vala @@ -0,0 +1,308 @@ +/* + * Copyright 2021 elementary, Inc. + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +/** + * This class enables navigatable URLs in Gtk.TextView + * @since 6.1.3 + */ +public class Jorts.HyperTextView : Gtk.TextView { + + private const int FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET = -1; + + private uint buffer_changed_debounce_timeout_id = 0; + private int buffer_cursor_position_when_change_started = 0; + + private GLib.HashTable uri_text_tags; + private Regex uri_regex; + private Gtk.EventControllerKey keypress_controller; + + private bool is_control_key_pressed = false; + + private int pointer_x; + private int pointer_y; + + construct { + var http_charset = "[\\w\\/\\-\\+\\.:@\\?&%=#]"; + var email_charset = "[\\w\\-\\.]"; + var email_tld_charset = "[\\w\\-]"; + + var http_match_str = @"https?:\\/\\/$(http_charset)+\\.$(http_charset)+"; + var email_match_str = @"(mailto:)?$(email_charset)+@$(email_charset)+\\.$(email_tld_charset)+"; + + var uri_regex_str = "(?:(" + + http_match_str + + ")|(" + + email_match_str + + "))"; + + uri_text_tags = new GLib.HashTable (str_hash, direct_equal); + try { + uri_regex = new Regex (uri_regex_str); + } catch (GLib.RegexError e) { + critical ("RegexError while constructing URI regex: %s", e.message); + } + + buffer_connect (buffer); + notify["buffer"].connect (on_buffer_changed); + + var motion_controller = new Gtk.EventControllerMotion (); + motion_controller.motion.connect (on_motion_notify_event); + add_controller (motion_controller); + + keypress_controller = new Gtk.EventControllerKey (); + keypress_controller.key_pressed.connect (on_key_press_event); + keypress_controller.key_released.connect (on_key_release_event); + + var click_controller = new Gtk.GestureClick (); + click_controller.pressed.connect (on_click_event); + add_controller (click_controller); + + var focus_controller = new Gtk.EventControllerFocus (); + focus_controller.leave.connect (on_focus_leave); + add_controller (focus_controller); + + realize.connect (on_realize); + } + + private void buffer_connect (Gtk.TextBuffer buffer) { + buffer.notify["cursor-position"].connect (on_buffer_cursor_position_changed); + buffer.paste_done.connect (on_paste_done); + buffer.changed.connect_after (on_after_buffer_changed); + } + + private void on_buffer_cursor_position_changed () { + if (buffer_cursor_position_when_change_started == 0) { + buffer_cursor_position_when_change_started = buffer.cursor_position; + } + } + + private void on_paste_done (Gdk.Clipboard clipboard) { + // force rescan of whole buffer: + buffer_cursor_position_when_change_started = FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET; + } + + private void on_after_buffer_changed () { + if (buffer_changed_debounce_timeout_id != 0) { + Source.remove (buffer_changed_debounce_timeout_id); + buffer_changed_debounce_timeout_id = 0; + } + + buffer_changed_debounce_timeout_id = GLib.Timeout.add (300, debouncer); + } + + private async void update_tags_in_buffer_for_range (int buffer_start_offset, int buffer_end_offset) { + if (buffer_start_offset == buffer_end_offset) { + return; + } + + Gtk.TextIter buffer_start_iter, buffer_end_iter; + buffer.get_iter_at_offset (out buffer_start_iter, buffer_start_offset); + buffer_start_iter.backward_line (); + buffer_start_offset = buffer_start_iter.get_offset (); + + buffer.get_iter_at_offset (out buffer_end_iter, buffer_end_offset); + buffer_end_iter.forward_line (); + buffer_end_offset = buffer_end_iter.get_offset (); + + // Delete all tags in buffer for range [start_iter.offset,end_iter.offset] + lock (uri_text_tags) { + foreach (var tag_key in uri_text_tags.get_keys ()) { + int tag_start_offset, tag_end_offset; + tag_key.scanf ("[%i,%i]", out tag_start_offset, out tag_end_offset); + + if ( + tag_start_offset > buffer_start_offset && tag_start_offset < buffer_end_offset + || + tag_end_offset > buffer_start_offset && tag_end_offset < buffer_end_offset + ) { + buffer.tag_table.remove (uri_text_tags.take (tag_key)); + } + } + } + + /* + Character counts are usually referred to as offsets, while byte counts are called indexes. + If you confuse these two, things will work fine with ASCII, but as soon as your + buffer contains multibyte characters, bad things will happen. + https://developer.gnome.org/gtk3/stable/TextWidget.html + */ + var buffer_start_index = buffer.text.index_of_nth_char (buffer_start_offset); + var buffer_end_index = buffer.text.index_of_nth_char (buffer_end_offset); + var buffer_substring = buffer.text.substring (buffer_start_index, buffer_end_index - buffer_start_index); + + if (buffer_substring.strip () == "") { + // if the substring is empty, we do not have anything to do... + return; + } + + // Add new tags in buffer for range [start_iter.offset,end_iter.offset] + GLib.MatchInfo match_info; + uri_regex.match (buffer_substring, 0, out match_info); + + while (match_info.matches ()) { + string match_text = match_info.fetch (0); + + int match_start_index, match_end_index; + match_info.fetch_pos (0, out match_start_index, out match_end_index); + + int match_start_offset, match_end_offset; + match_start_offset = buffer_substring.substring (0, match_start_index).char_count (); + match_end_offset = buffer_substring.substring (0, match_end_index).char_count (); + + var buffer_match_start_offset = buffer_start_offset + match_start_offset; + var buffer_match_end_offset = buffer_start_offset + match_end_offset; + + Gtk.TextIter buffer_match_start_iter, buffer_match_end_iter; + buffer.get_iter_at_offset (out buffer_match_start_iter, buffer_match_start_offset); + buffer.get_iter_at_offset (out buffer_match_end_iter, buffer_match_end_offset); + + var tag = buffer.create_tag (null, "underline", Pango.Underline.SINGLE); + if (!match_text.contains ("://") && match_text.contains ("@") && !match_text.has_prefix ("mailto:")) { + match_text = "mailto:" + match_text; + } + tag.set_data ("uri", match_text); + buffer.apply_tag (tag, buffer_match_start_iter, buffer_match_end_iter); + + lock (uri_text_tags) { + uri_text_tags.set ("[%i,%i]".printf (buffer_match_start_offset, buffer_match_end_offset), tag); + } + + try { + match_info.next (); + } catch (GLib.RegexError e) { + warning ("RegexError while scanning for the next URI match: %s", e.message); + } + } + } + + private bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); + if (uri_hovering_over != null) { + set_cursor (new Gdk.Cursor.from_name ("pointer", null)); + } + + is_control_key_pressed = true; + } + + return Gdk.EVENT_PROPAGATE; + } + + private void on_key_release_event (uint keyval, uint keycode, Gdk.ModifierType state) { + if (keyval == Gdk.Key.Control_L || keyval == Gdk.Key.Control_R) { + set_cursor (new Gdk.Cursor.from_name ("text", null)); + is_control_key_pressed = false; + } + } + + private void on_click_event (int n_press, double x, double y) { + if (!is_control_key_pressed) { + return; + } + + var uri = get_uri_at_location ((int)x, (int)y); + if (uri == null) { + return; + } + + Gtk.show_uri (null, uri, Gdk.CURRENT_TIME); + set_cursor (new Gdk.Cursor.from_name ("text", null)); + is_control_key_pressed = false; + } + + private void on_motion_notify_event (double x, double y) { + pointer_x = (int)x; + pointer_y = (int)y; + + var uri_hovering_over = get_uri_at_location (pointer_x, pointer_y); + + if (uri_hovering_over != null && !has_tooltip) { + has_tooltip = true; + tooltip_markup = string.joinv ("\n", { + _("Follow Link"), + Granite.TOOLTIP_SECONDARY_TEXT_MARKUP.printf (_("Control + Click")) + }); + + } else if (uri_hovering_over == null && has_tooltip) { + has_tooltip = false; + } + } + + private string? get_uri_at_location (int location_x, int location_y) { + string? uri = null; + + int x, y; + window_to_buffer_coords (Gtk.TextWindowType.TEXT, location_x, location_y, out x, out y); + + Gtk.TextIter text_iter; + if (get_iter_at_location (out text_iter, x, y)) { + var tags = text_iter.get_tags (); + + foreach (var tag in tags) { + if (tag.get_data ("uri") != null) { + uri = tag.get_data ("uri"); + break; + } + } + } + + return uri; + } + + private void on_buffer_changed () { + buffer_connect (buffer); + buffer.changed (); + } + + private void on_realize () { + // Attach the keypress controller to the root so we can see Ctrl key presses + // even when the widget isn't focused + get_root ().add_controller (keypress_controller); + } + + private void on_focus_leave () { + set_cursor (new Gdk.Cursor.from_name ("text", null)); + is_control_key_pressed = false; + } + + private bool debouncer () { + buffer_changed_debounce_timeout_id = 0; + + var change_start_offset = buffer_cursor_position_when_change_started; + var change_end_offset = buffer.cursor_position; + + buffer_cursor_position_when_change_started = 0; + + if (change_start_offset == FORCE_FULL_BUFFER_RESCAN_CHANGE_START_OFFSET || change_start_offset == change_end_offset) { + change_start_offset = 0; + change_end_offset = buffer.text.length; + } + + update_tags_in_buffer_for_range.begin ( + int.min (change_start_offset, change_end_offset), + int.max (change_start_offset, change_end_offset) + ); + + return GLib.Source.REMOVE; + } + + ~HyperTextView () { + debug ("Destroyed"); + } +} diff --git a/src/Widgets/Popover.vala b/src/Widgets/Popover.vala index 19ed1532..7f41ed75 100644 --- a/src/Widgets/Popover.vala +++ b/src/Widgets/Popover.vala @@ -11,9 +11,6 @@ */ public class Jorts.Popover : Gtk.Popover { - public Gtk.EventControllerKey keypress_controller; - public Gtk.EventControllerScroll scroll_controller; - private Jorts.ColorBox color_box; private Jorts.MonospaceBox monospace_box; private Jorts.ZoomBox font_size_box; @@ -39,10 +36,10 @@ public class Jorts.Popover : Gtk.Popover { } static construct { - add_binding_action (Gdk.Key.plus, Gdk.ModifierType.CONTROL_MASK, ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_IN, null); - add_binding_action (Gdk.Key.equal, Gdk.ModifierType.CONTROL_MASK, ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_DEFAULT, null); - add_binding_action (48, Gdk.ModifierType.CONTROL_MASK, ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_DEFAULT, null); - add_binding_action (Gdk.Key.minus, Gdk.ModifierType.CONTROL_MASK, ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_OUT, null); + add_binding_action (Gdk.Key.plus, Gdk.ModifierType.CONTROL_MASK, ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_IN, null); + add_binding_action (Gdk.Key.equal, Gdk.ModifierType.CONTROL_MASK, ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_DEFAULT, null); + add_binding_action (48, Gdk.ModifierType.CONTROL_MASK, ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_DEFAULT, null); + add_binding_action (Gdk.Key.minus, Gdk.ModifierType.CONTROL_MASK, ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_OUT, null); add_binding_action (Gdk.Key.n, Gdk.ModifierType.CONTROL_MASK, Application.ACTION_PREFIX + Application.ACTION_NEW, null); add_binding_action (Gdk.Key.w, Gdk.ModifierType.CONTROL_MASK, StickyNoteWindow.ACTION_PREFIX + StickyNoteWindow.ACTION_DELETE, null); @@ -70,17 +67,30 @@ public class Jorts.Popover : Gtk.Popover { child = view; - // Allow scrolling shenanigans from popover - keypress_controller = new Gtk.EventControllerKey (); - scroll_controller = new Gtk.EventControllerScroll (VERTICAL) { + // Propagate settings changes to the higher level + color_box.theme_changed.connect ((theme) => {theme_changed (theme);}); + + // Allow zooming shenanigans from popover + ((Gtk.Widget)this).realize.connect (on_realize); + } + + private void on_realize () { + var zoomed_window = (ZoomedWindow)(this.get_ancestor (typeof (ZoomedWindow))); + + var keypress_controller = new Gtk.EventControllerKey (); + var scroll_controller = new Gtk.EventControllerScroll (VERTICAL) { propagation_phase = Gtk.PropagationPhase.CAPTURE }; + var gesturezoom_controller = new Gtk.GestureZoom (); ((Gtk.Widget)this).add_controller (keypress_controller); ((Gtk.Widget)this).add_controller (scroll_controller); + ((Gtk.Widget)this).add_controller (gesturezoom_controller); - // Propagate settings changes to the higher level - color_box.theme_changed.connect ((theme) => {theme_changed (theme);}); + keypress_controller.key_pressed.connect (zoomed_window.on_key_press_event); + keypress_controller.key_released.connect (zoomed_window.on_key_release_event); + scroll_controller.scroll.connect (zoomed_window.on_scroll); + gesturezoom_controller.scale_changed.connect (zoomed_window.on_pinch); } /** diff --git a/src/Widgets/PopoverWidgets/ColorBox.vala b/src/Widgets/PopoverWidgets/ColorBox.vala index 725d51af..ebb017a2 100644 --- a/src/Widgets/PopoverWidgets/ColorBox.vala +++ b/src/Widgets/PopoverWidgets/ColorBox.vala @@ -79,4 +79,8 @@ public class Jorts.ColorBox : Gtk.Box { theme_changed ((Jorts.Themes)color); } } + + ~ColorBox () { + debug ("Destroyed"); + } } diff --git a/src/Widgets/PopoverWidgets/MonospaceBox.vala b/src/Widgets/PopoverWidgets/MonospaceBox.vala index 5cdd3df1..09b4f916 100644 --- a/src/Widgets/PopoverWidgets/MonospaceBox.vala +++ b/src/Widgets/PopoverWidgets/MonospaceBox.vala @@ -64,4 +64,8 @@ public class Jorts.MonospaceBox : Gtk.Box { GLib.BindingFlags.BIDIRECTIONAL | GLib.BindingFlags.INVERT_BOOLEAN ); } + + ~MonospaceBox () { + debug ("Destroyed"); + } } diff --git a/src/Widgets/PopoverWidgets/ZoomBox.vala b/src/Widgets/PopoverWidgets/ZoomBox.vala index 4d32e14f..8a50a00f 100644 --- a/src/Widgets/PopoverWidgets/ZoomBox.vala +++ b/src/Widgets/PopoverWidgets/ZoomBox.vala @@ -42,7 +42,7 @@ public class Jorts.ZoomBox : Gtk.Box { //TRANSLATORS: These are displayed on small linked buttons in a menu. User can click them to change zoom var zoom_out_button = new Gtk.Button.from_icon_name ("zoom-out-symbolic") { - action_name = ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_OUT, + action_name = ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_OUT, tooltip_markup = Granite.markup_accel_tooltip ( {"minus", "KP_Subtract"}, _("Zoom out") @@ -50,7 +50,7 @@ public class Jorts.ZoomBox : Gtk.Box { }; zoom_default_button = new Gtk.Button () { - action_name = ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_DEFAULT, + action_name = ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_DEFAULT, tooltip_markup = Granite.markup_accel_tooltip ( { "equal", "0", "KP_0" }, _("Default zoom level") @@ -58,7 +58,7 @@ public class Jorts.ZoomBox : Gtk.Box { }; var zoom_in_button = new Gtk.Button.from_icon_name ("zoom-in-symbolic") { - action_name = ZoomController.ACTION_PREFIX + ZoomController.ACTION_ZOOM_IN, + action_name = ZoomedWindow.ACTION_PREFIX + ZoomedWindow.ACTION_ZOOM_IN, tooltip_markup = Granite.markup_accel_tooltip ( {"plus", "KP_Add"}, _("Zoom in") @@ -69,4 +69,8 @@ public class Jorts.ZoomBox : Gtk.Box { append (zoom_default_button); append (zoom_in_button); } + + ~ZoomBox () { + debug ("Destroyed"); + } } diff --git a/src/Widgets/TextView.vala b/src/Widgets/TextView.vala index dee5be67..d08f52ca 100644 --- a/src/Widgets/TextView.vala +++ b/src/Widgets/TextView.vala @@ -10,7 +10,7 @@ * Inherits Hypertextview to detect links and emails * Adds a list feature which is a hot mess */ -public class Jorts.TextView : Granite.HyperTextView { +public class Jorts.TextView : Jorts.HyperTextView { // We subclass the buffer to manage the list feature at a lower level // We need to keep a reference to its "Extended version" @@ -228,8 +228,6 @@ public class Jorts.TextView : Granite.HyperTextView { } ~TextView () { - list_buffer.dispose (); - buffer.dispose (); debug ("Destroyed"); } } diff --git a/src/Services/ZoomController.vala b/src/Widgets/ZoomedWindow.vala similarity index 69% rename from src/Services/ZoomController.vala rename to src/Widgets/ZoomedWindow.vala index e70d960b..0dbdba19 100644 --- a/src/Services/ZoomController.vala +++ b/src/Widgets/ZoomedWindow.vala @@ -5,16 +5,17 @@ * 2025-2026 Stella & Charlie (teamcons.carrd.co) */ -/*************************************************/ /** -* Responsible to apply zoom appropriately to a window. -* Mainly, this abstracts zoom into an int and swap CSS classes -* As a treat it includes also the plumbing for ctrl+scroll zooming +* Copying the ScrolledWindow. This wraps a child and holds a zoom property */ -public class Jorts.ZoomController : Object { + public class Jorts.ZoomedWindow : Gtk.Widget { + // Scroll handers need that information to decide whether to act private static bool is_control_key_pressed = false; - private weak Jorts.StickyNoteWindow window {get; set;} + + private Gtk.EventControllerKey keypress_controller; + private Gtk.EventControllerScroll scroll_controller; + private Gtk.GestureZoom gesturezoom_controller; // Avoid setting this unless it is to restore a specific value, do_set_zoom does not check input private int _old_zoom; @@ -24,7 +25,7 @@ public class Jorts.ZoomController : Object { } public SimpleActionGroup actions { get; construct; } - public const string ACTION_PREFIX = "zoom_controller."; + public const string ACTION_PREFIX = "zoomed_window."; public const string ACTION_ZOOM_OUT = "action_zoom_out"; public const string ACTION_ZOOM_DEFAULT = "action_zoom_default"; public const string ACTION_ZOOM_IN = "action_zoom_in"; @@ -37,9 +38,32 @@ public class Jorts.ZoomController : Object { { ACTION_ZOOM_IN, zoom_in} }; + class construct { + set_layout_manager_type (typeof (Gtk.BinLayout)); + } + + private Zoomable? _child; + public Zoomable? child { + get { + return _child; + } + + set { + if (value != null && value.get_parent () != null) { + critical ("Tried to set a widget as child that already has a parent."); + return; + } - public ZoomController (Jorts.StickyNoteWindow window) { - this.window = window; + if (_child != null) { + _child.unparent (); + } + + _child = value; + + if (_child != null) { + _child.set_parent (this); + } + } } construct { @@ -50,6 +74,21 @@ public class Jorts.ZoomController : Object { app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_OUT, {"minus", "KP_Subtract"}); app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_DEFAULT, {"equal", "0", "KP_0"}); app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_IN, {"plus", "KP_Add"}); + + keypress_controller = new Gtk.EventControllerKey (); + scroll_controller = new Gtk.EventControllerScroll (VERTICAL) { + propagation_phase = Gtk.PropagationPhase.CAPTURE + }; + gesturezoom_controller = new Gtk.GestureZoom (); + + add_controller (keypress_controller); + add_controller (scroll_controller); + add_controller (gesturezoom_controller); + + keypress_controller.key_pressed.connect (on_key_press_event); + keypress_controller.key_released.connect (on_key_release_event); + scroll_controller.scroll.connect (on_scroll); + gesturezoom_controller.scale_changed.connect (on_pinch); } /** @@ -102,22 +141,11 @@ public class Jorts.ZoomController : Object { debug ("Setting zoom: " + zoom.to_string ()); // Switches the classes that control font size - window.remove_css_class (Jorts.Zoom.from_int ( _old_zoom).to_css_class ()); + remove_css_class (Jorts.Zoom.from_int ( _old_zoom).to_css_class ()); _old_zoom = new_zoom; - window.add_css_class (Jorts.Zoom.from_int ( new_zoom).to_css_class ()); - window.textview.queue_refresh_indentation (); - - - // Adapt headerbar size to avoid weird flickering - window.view.headerbar.height_request = Jorts.Zoom.from_int (new_zoom).to_ui_size (); + add_css_class (Jorts.Zoom.from_int ( new_zoom).to_css_class ()); - // Reflect the number in the popover - window.popover.zoom = new_zoom; - - // Keep it for next new notes - NoteData.latest_zoom = zoom; - - window.has_changed (); + _child.on_zoom_changed (new_zoom); } public bool on_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { @@ -161,8 +189,11 @@ public class Jorts.ZoomController : Object { //return Gdk.EVENT_STOP; } - ~ZoomController () { + ~ZoomedWindow () { + if (_child != null) { + _child.unparent (); + } + debug ("Destroyed"); - window = null; } } diff --git a/src/Windows/StickyNoteWindow.vala b/src/Windows/StickyNoteWindow.vala index 6d89ccf4..02525c68 100644 --- a/src/Windows/StickyNoteWindow.vala +++ b/src/Windows/StickyNoteWindow.vala @@ -19,12 +19,10 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { public Popover popover; public TextView textview; + private Jorts.ZoomedWindow zoomed_window; private Jorts.ColorController color_controller; - public Jorts.ZoomController zoom_controller; private Jorts.ScribblyController scribbly_controller; - private Gtk.EventControllerKey keypress_controller; - private Gtk.EventControllerScroll scroll_controller; - private Gtk.GestureZoom gesturezoom_controller; + public NoteData data { owned get {return packaged ();} @@ -50,40 +48,26 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { app.set_accels_for_action (ACTION_PREFIX + ACTION_DELETE, {"W"}); color_controller = new Jorts.ColorController (this); - zoom_controller = new Jorts.ZoomController (this); scribbly_controller = new Jorts.ScribblyController (this); - keypress_controller = new Gtk.EventControllerKey (); - scroll_controller = new Gtk.EventControllerScroll (VERTICAL) { - propagation_phase = Gtk.PropagationPhase.CAPTURE - }; - - gesturezoom_controller = new Gtk.GestureZoom (); - - - ((Gtk.Widget)this).add_controller (keypress_controller); - ((Gtk.Widget)this).add_controller (scroll_controller); - ((Gtk.Widget)this).add_controller (gesturezoom_controller); - // The view has its own titlebar titlebar = new Gtk.Grid () {visible = false}; view = new NoteView (); - textview = view.textview; - insert_action_group ("noteview", view.actions); - insert_action_group ("textview", textview.actions); - insert_action_group ("zoom_controller", zoom_controller.actions); - // Have shortcuts keep working this.destroy ()with the popover open. + zoomed_window = new ZoomedWindow () { + child = view + }; + + textview = view.textview; popover = view.popover; - view.popover.scroll_controller.scroll.connect (zoom_controller.on_scroll); - view.popover.keypress_controller.key_pressed.connect (zoom_controller.on_key_press_event); - view.popover.keypress_controller.key_released.connect (zoom_controller.on_key_release_event); - //zoom_controller.notify["zoom"].connect_after (textview.refresh_indentation); + insert_action_group ("noteview", view.actions); + insert_action_group ("textview", textview.actions); + insert_action_group ("zoomed_window", zoomed_window.actions); - set_child (view); - set_focus (view); + set_child (zoomed_window); + set_focus (zoomed_window); load_data (data); #if DEVEL @@ -96,49 +80,11 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { /* CONNECTS AND BINDS */ /***************************************************/ - // We need this for Ctr + Scroll. We delegate everything to zoomcontroller - keypress_controller.key_pressed.connect (zoom_controller.on_key_press_event); - keypress_controller.key_released.connect (zoom_controller.on_key_release_event); - scroll_controller.scroll.connect (zoom_controller.on_scroll); - gesturezoom_controller.scale_changed.connect (zoom_controller.on_pinch); - - - - debug ("Built UI. Lets do connects and binds"); - // Save when title or text have changed view.editablelabel.changed.connect (on_editable_changed); view.textview.buffer.changed.connect (has_changed); popover.theme_changed.connect (color_controller.on_color_changed); - - // Respect animation settings for showing ui elements - if (Application.gtk_settings.gtk_enable_animations && (!Application.settings.get_boolean (KEY_HIDEBAR))) { - show.connect_after (delayed_show); - - } else { - bind_hidebar (); - } - } - - /********************************************/ - /* METHODS */ - /********************************************/ - - /** - * Show Actionbar shortly after the window is shown - * This is more for the Aesthetic - */ - private void delayed_show () { - Timeout.add_once (250, bind_hidebar); - show.disconnect (delayed_show); - } - - private void bind_hidebar () { - Application.settings.bind ( - KEY_HIDEBAR, - view.actionbar.actionbar, - "revealed", - SettingsBindFlags.INVERT_BOOLEAN); + zoomed_window.notify ["zoom"].connect (has_changed); } /** @@ -171,7 +117,7 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { theme = popover.color, content = view.content, monospace = popover.monospace, - zoom = zoom_controller.zoom, + zoom = zoomed_window.zoom, width = this_width, height = this_height }; @@ -197,7 +143,7 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { view.content = data.content; color_controller.theme = data.theme; - zoom_controller.zoom = data.zoom; + zoomed_window.zoom = data.zoom; view.monospace = data.monospace; } @@ -211,18 +157,17 @@ public class Jorts.StickyNoteWindow : Gtk.ApplicationWindow { ~StickyNoteWindow () { debug ("Destroying %s", view.title); - - keypress_controller.key_pressed.disconnect (zoom_controller.on_key_press_event); - keypress_controller.key_released.disconnect (zoom_controller.on_key_release_event); - scroll_controller.scroll.disconnect (zoom_controller.on_scroll); - gesturezoom_controller.scale_changed.disconnect (zoom_controller.on_pinch); - view.editablelabel.changed.disconnect (on_editable_changed); view.textview.buffer.changed.disconnect (has_changed); popover.theme_changed.disconnect (color_controller.on_color_changed); - color_controller.dispose (); - zoom_controller.dispose (); - scribbly_controller.dispose (); + zoomed_window = null; + view = null; + popover = null; + textview = null; + + color_controller = null; + scribbly_controller = null; + application = null; } } diff --git a/src/main.vala b/src/main.vala new file mode 100644 index 00000000..4d3c06fd --- /dev/null +++ b/src/main.vala @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017-2024 Lains + * 2025 Contributions from the ellie_Commons community (github.com/ellie-commons/) + * 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +int main (string[] args) { + +#if DEVEL + + // Do not overwrite environment, just show our apps debugs by default on devel builds + var new_messages_domain = "%s %s".printf (Environment.get_variable ("G_MESSAGES_DEBUG"), APP_ID); + + //GLib.Environment.set_variable ("LANGUAGE", "C", true); + GLib.Environment.set_variable ("GTK_DEBUG", "interactive", true); + GLib.Environment.set_variable ("G_MESSAGES_DEBUG", new_messages_domain, true); + //print (LOCALEDIR); + + warning (""" + ---------------------------------------- + You are running a development version. + Here be dragons. + Tread carefully. + ---------------------------------------- + """); + +#endif + + print (""" +🎉✨ ACTIVATING: SUPER COOL JORTS 😎🔥❗🎶🤌 +Your Notes are all belong to us! + _ _ + (\o/) (\o/) <--- Tiny electric angels working in the background + /_\ /_\ + +Please wait while the app remembers all the things… +"""); + + return new Jorts.Application ().run (args); +} diff --git a/src/meson.build b/src/meson.build index 8695f126..c589361e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,6 +22,8 @@ dependencies = [ #================================ sources = files ( + 'Interfaces' / 'Zoomable.vala', + 'Objects' / 'ZoomType.vala', 'Objects' / 'Zoom.vala', 'Objects' / 'Themes.vala', @@ -32,14 +34,15 @@ sources = files ( 'Services' / 'Storage.vala', 'Services' / 'NoteManager.vala', - 'Services' / 'ZoomController.vala', 'Services' / 'ScribblyController.vala', 'Services' / 'ColorController.vala', 'Widgets' / 'EditableLabel.vala', + 'Widgets' / 'HyperTextView.vala', 'Widgets' / 'TextView.vala', 'Widgets' / 'ActionBar.vala', 'Widgets' / 'TextBuffer.vala', + 'Widgets' / 'ZoomedWindow.vala', 'Widgets' / 'PopoverWidgets' / 'ColorPill.vala', 'Widgets' / 'PopoverWidgets' / 'ColorBox.vala', @@ -55,6 +58,7 @@ sources = files ( 'Windows' / 'StickyNoteWindow.vala', 'Constants.vala', + 'main.vala', 'Application.vala', ) @@ -96,7 +100,6 @@ executable( config_file, sources, dependencies: dependencies, - c_args: '-w', # Because of Vala we get ten thousand warnings we can do nothing about win_subsystem: development ? 'console' : 'windows', # Show a console in windows dev builds. Seem to not bother standard builds install : true ) diff --git a/windows/meson.build b/windows/meson.build index d1f9ffb3..4d64f1d4 100644 --- a/windows/meson.build +++ b/windows/meson.build @@ -9,7 +9,7 @@ config_installer_script = configure_file( input: 'installer.nsi.in', output: 'installer.nsi', configuration: config_data -) + ) #======================== # DEPLOY @@ -18,4 +18,4 @@ config_deploy_script = configure_file( input: 'deploy.sh.in', output: 'deploy.sh', configuration: config_data -) + )