├── po ├── LINGUAS ├── extra │ ├── LINGUAS │ ├── POTFILES │ ├── meson.build │ ├── extra.pot │ ├── pt.po │ ├── fr.po │ └── de.po ├── meson.build ├── POTFILES ├── com.github.brain_child.moobo.pot ├── de.po ├── pt.po └── fr.po ├── .gitignore ├── preview ├── moobo_dark.png └── moobo_light.png ├── data ├── styles │ ├── ColorButton.css │ ├── SourceRow.css │ └── Style.css ├── com.github.brain_child.moobo.desktop.in ├── com.github.brain_child.moobo.gresource.xml ├── com.github.brain_child.moobo.gschema.xml ├── meson.build ├── icons │ ├── 16 │ │ └── com.github.brain_child.moobo.svg │ ├── 24 │ │ └── com.github.brain_child.moobo.svg │ ├── 32 │ │ └── com.github.brain_child.moobo.svg │ └── symbolic.svg └── com.github.brain_child.moobo.appdata.xml.in ├── src ├── Models │ ├── BaseModel.vala │ ├── TextModel.vala │ ├── ImageModel.vala │ ├── LabelModel.vala │ └── BoardModel.vala ├── Helper │ ├── Constants.vala │ ├── DemoBoard.vala │ ├── Colors.vala │ ├── Serializer.vala │ ├── ErrorHandler.vala │ ├── LazyLoader.vala │ ├── WidgetFactory.vala │ ├── ImageCache.vala │ ├── MemoryManager.vala │ └── Deserializer.vala ├── Widgets │ ├── ColorButton.vala │ ├── TextWidget.vala │ ├── LabelWidget.vala │ ├── FloatingButton.vala │ ├── Row.vala │ ├── MenuItemColor.vala │ ├── Board.vala │ └── ImageWidget.vala ├── Controllers │ ├── WidgetController.vala │ ├── FloatingButtonController.vala │ ├── TextController.vala │ ├── LabelController.vala │ ├── RowController.vala │ ├── ImageController.vala │ └── BoardController.vala ├── meson.build ├── Application.vala └── Movable.vala ├── .editorconfig ├── com.github.brain-child.moobo.yml ├── meson └── post_install.py ├── README.md ├── meson.build └── .github └── workflows └── main.yml /po/LINGUAS: -------------------------------------------------------------------------------- 1 | de 2 | fr 3 | pt 4 | -------------------------------------------------------------------------------- /po/extra/LINGUAS: -------------------------------------------------------------------------------- 1 | de 2 | fr 3 | pt 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | builddir/ 4 | .flatpak-builder/ 5 | *~ 6 | -------------------------------------------------------------------------------- /preview/moobo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brain-child/moobo/HEAD/preview/moobo_dark.png -------------------------------------------------------------------------------- /preview/moobo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brain-child/moobo/HEAD/preview/moobo_light.png -------------------------------------------------------------------------------- /po/extra/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.github.brain_child.moobo.desktop.in 2 | data/com.github.brain_child.moobo.appdata.xml.in 3 | -------------------------------------------------------------------------------- /po/extra/meson.build: -------------------------------------------------------------------------------- 1 | # Install metadata translations 2 | i18n.gettext ('extra', 3 | args: [ 4 | '--directory=' + meson.source_root (), 5 | '--from-code=UTF-8' 6 | ], 7 | install: false 8 | ) 9 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | # Install main translations 2 | i18n.gettext (meson.project_name (), 3 | args: [ 4 | '--directory=' + meson.source_root (), 5 | '--from-code=UTF-8', 6 | '-cTRANSLATORS' 7 | ] 8 | ) 9 | 10 | subdir ('extra') 11 | -------------------------------------------------------------------------------- /data/styles/ColorButton.css: -------------------------------------------------------------------------------- 1 | .color-button.none check { 2 | background: @base_color; 3 | color: mix (@text_color, @base_color, 0.5); 4 | min-height: 14px; 5 | min-width: 14px; 6 | -gtk-icon-source: -gtk-icontheme("close-symbolic"); 7 | } 8 | -------------------------------------------------------------------------------- /src/Models/BaseModel.vala: -------------------------------------------------------------------------------- 1 | public abstract class BaseModel : GLib.Object { 2 | 3 | public string model { set; get; } 4 | public int x { set; get; } 5 | public int y { set; get; } 6 | public int z_index { set; get; default = 0; } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | src/Application.vala 2 | src/Window.vala 3 | src/Movable.vala 4 | 5 | src/Controllers/RowController.vala 6 | src/Controllers/ImageController.vala 7 | 8 | #src/Helper/DemoBoard.vala 9 | 10 | src/Widgets/FloatingButton.vala 11 | src/Widgets/Row.vala 12 | -------------------------------------------------------------------------------- /src/Models/TextModel.vala: -------------------------------------------------------------------------------- 1 | public class TextModel : BaseModel { 2 | 3 | public int font_size { set; get; } 4 | public string content { set; get; } 5 | 6 | public TextModel () { 7 | model = "TextWidget"; 8 | font_size = 150; 9 | content = ""; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/ImageModel.vala: -------------------------------------------------------------------------------- 1 | public class ImageModel : BaseModel { 2 | 3 | public string path { set; get; } 4 | public double scale_factor { set; get; } 5 | 6 | public ImageModel () { 7 | model = "ImageWidget"; 8 | path = ""; 9 | scale_factor = 1.0; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /data/com.github.brain_child.moobo.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Moobo 3 | GenericName=Moodboard Application 4 | Comment=Visualize your ideas 5 | Categories=GTK;Office;Utility; 6 | Exec=com.github.brain_child.moobo 7 | Icon=com.github.brain_child.moobo 8 | Terminal=false 9 | Type=Application 10 | Keywords=notes;moobo;moodboard;idea; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | root = true 3 | 4 | # elementary defaults 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = tab 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | tab_width = 4 13 | 14 | # Markup files 15 | [{*.html,*.xml,*.xml.in,*.yml}] 16 | tab_width = 2 17 | -------------------------------------------------------------------------------- /data/com.github.brain_child.moobo.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | styles/Style.css 5 | styles/SourceRow.css 6 | styles/ColorButton.css 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Models/LabelModel.vala: -------------------------------------------------------------------------------- 1 | public class LabelModel : BaseModel { 2 | 3 | public int font_size { set; get; } 4 | public string content { set; get; } 5 | public string color { set; get; } 6 | 7 | public LabelModel () { 8 | model = "LabelWidget"; 9 | font_size = 150; 10 | content = ""; 11 | color = ""; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /data/styles/SourceRow.css: -------------------------------------------------------------------------------- 1 | .source-color { 2 | background: @colorAccent; 3 | border: 1px solid @borders; 4 | border-radius: 50%; 5 | box-shadow: 6 | inset 0 1px 0 0 alpha (@inset_dark_color, 0.7), 7 | inset 0 0 0 1px alpha (@inset_dark_color, 0.3), 8 | 0 1px 0 0 alpha (@bg_highlight_color, 0.3); 9 | min-height: 14px; 10 | min-width: 14px; 11 | } 12 | -------------------------------------------------------------------------------- /com.github.brain-child.moobo.yml: -------------------------------------------------------------------------------- 1 | app-id: com.github.brain_child.moobo 2 | 3 | runtime: io.elementary.Platform 4 | runtime-version: '6.1' 5 | sdk: io.elementary.Sdk 6 | 7 | command: com.github.brain_child.moobo 8 | 9 | finish-args: 10 | - '--share=ipc' 11 | - '--socket=fallback-x11' 12 | - '--socket=wayland' 13 | 14 | modules: 15 | - name: Moobo 16 | buildsystem: meson 17 | sources: 18 | - type: dir 19 | path: . 20 | -------------------------------------------------------------------------------- /src/Helper/Constants.vala: -------------------------------------------------------------------------------- 1 | namespace Const { 2 | public const string APP_NAME = "Moobo"; 3 | public const string APP_ID = "com.github.brain_child.moobo"; 4 | public const string APP_PATH = "/" + APP_ID; 5 | 6 | public const int WIN_RATIO_X = 14; 7 | public const int WIN_RATIO_Y = 9; 8 | public const int IMG_MAX_WIDTH = 1200; 9 | public const int IMG_MAX_HEIGHT= 800; 10 | public const int WIN_MIN_WIDTH = 640; 11 | public const int WIN_MIN_HEIGHT = 480; 12 | 13 | public const double WIN_SCALE_X = 0.75; 14 | public const double WIN_SCALE_Y = 0.833; 15 | public const double MAX_SCALE = 3.0; 16 | } 17 | -------------------------------------------------------------------------------- /src/Models/BoardModel.vala: -------------------------------------------------------------------------------- 1 | public class BoardModel : GLib.Object { 2 | 3 | public signal void changed_event (Gee.ArrayList widgets, string color, string title); 4 | 5 | public Gee.ArrayList widgets { set; get; } 6 | public string color { set; get; } 7 | public string title { set; get; } 8 | public bool is_active { set; get; } 9 | 10 | public BoardModel () { 11 | widgets = new Gee.ArrayList (); 12 | color = "transparent"; 13 | title = _("Board"); 14 | is_active = false; 15 | } 16 | 17 | public void changed_trigger () { 18 | changed_event (widgets, color, title); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /data/styles/Style.css: -------------------------------------------------------------------------------- 1 | .fab { 2 | background: #3689e6; 3 | border-radius: 50%; 4 | } 5 | 6 | .white_label { 7 | color: #fafafa; 8 | } 9 | 10 | .black_label { 11 | color: black; 12 | } 13 | 14 | .note { 15 | background: #fff394; 16 | } 17 | 18 | .rounded_corners { 19 | border-radius: 5px; 20 | } 21 | 22 | .transparent { 23 | background: transparent; 24 | } 25 | 26 | .editable-label { 27 | background: transparent; 28 | border: none; 29 | box-shadow: none; 30 | color: inherit; 31 | padding: 0; 32 | } 33 | 34 | .drag-indicator { 35 | background: rgba(54, 137, 230, 0.1); 36 | border: 2px dashed #3689e6; 37 | border-radius: 8px; 38 | color: #3689e6; 39 | font-weight: bold; 40 | font-size: 18px; 41 | } 42 | -------------------------------------------------------------------------------- /meson/post_install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import path, environ 4 | import subprocess 5 | 6 | prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') 7 | schemadir = path.join(environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas') 8 | datadir = path.join(prefix, 'share') 9 | desktop_database_dir = path.join(datadir, 'applications') 10 | 11 | if not environ.get('DESTDIR'): 12 | print('Compiling gsettings schemas…') 13 | subprocess.call(['glib-compile-schemas', schemadir]) 14 | print('Updating desktop database…') 15 | subprocess.call(['update-desktop-database', '-q', desktop_database_dir]) 16 | print('Updating icon cache…') 17 | subprocess.call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) 18 | -------------------------------------------------------------------------------- /src/Widgets/ColorButton.vala: -------------------------------------------------------------------------------- 1 | private class ColorButton : Gtk.CheckButton { 2 | private static Gtk.CssProvider css_provider; 3 | public string color_name { get; construct; } 4 | 5 | static construct { 6 | css_provider = new Gtk.CssProvider (); 7 | css_provider.load_from_resource ("com/github/brain_child/moobo/styles/ColorButton.css"); 8 | } 9 | 10 | public ColorButton (string color_name) { 11 | Object (color_name: color_name); 12 | } 13 | 14 | construct { 15 | var style_context = get_style_context (); 16 | style_context.add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 17 | style_context.add_class (Granite.STYLE_CLASS_COLOR_BUTTON); 18 | style_context.add_class (color_name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Controllers/WidgetController.vala: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-3.0-or-later 3 | * SPDX-FileCopyrightText: 2021 Pierre Fabarius 4 | */ 5 | 6 | public abstract class WidgetController { 7 | 8 | public abstract Movable get_movable (); 9 | public abstract BaseModel get_model (); 10 | 11 | protected BoardController board_controller; 12 | protected int x; 13 | protected int y; 14 | 15 | protected WidgetController (BoardController board_controller, int x, int y) { 16 | this.board_controller = board_controller; 17 | this.x = x; 18 | this.y = y; 19 | } 20 | 21 | public abstract void update_widget (); 22 | public abstract void handle_key_press (Gdk.EventKey event); 23 | public abstract void handle_key_release (); 24 | } 25 | -------------------------------------------------------------------------------- /data/com.github.brain_child.moobo.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | first time running flag 7 | flag to determine if app is running for the first time 8 | 9 | 10 | 100 11 | x-position 12 | window position on x axis 13 | 14 | 15 | 100 16 | y-position 17 | window position on y axis 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Widgets/TextWidget.vala: -------------------------------------------------------------------------------- 1 | public class TextWidget : Movable { 2 | 3 | public int font_size { set; get; } 4 | public Gtk.TextBuffer buffer { private set; get; } 5 | public Gtk.TextView textview { private set; get; } 6 | public TextController controller { private set; get; } 7 | 8 | public TextWidget (TextController controller) { 9 | this.controller = controller; 10 | init (); 11 | } 12 | 13 | private void init () { 14 | textview = new Gtk.TextView () { 15 | wrap_mode = Gtk.WrapMode.NONE, 16 | vexpand = true, 17 | margin = 10 18 | }; 19 | 20 | var style_context = textview.get_style_context (); 21 | style_context.add_class ("transparent"); 22 | style_context.add_class ("font-size"); 23 | 24 | buffer = textview.get_buffer (); 25 | 26 | add (textview); 27 | show_all (); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Widgets/LabelWidget.vala: -------------------------------------------------------------------------------- 1 | public class LabelWidget : Movable { 2 | 3 | public int font_size { set; get; } 4 | public Gtk.TextBuffer buffer { private set; get; } 5 | public Gtk.TextView textview { private set; get; } 6 | public LabelController controller { private set; get; } 7 | public Gtk.Grid card { private set; get; } 8 | 9 | public LabelWidget (LabelController controller) { 10 | this.controller = controller; 11 | init (); 12 | } 13 | 14 | private void init () { 15 | textview = new Gtk.TextView () { 16 | wrap_mode = Gtk.WrapMode.NONE, 17 | vexpand = true, 18 | margin = 10, 19 | }; 20 | buffer = textview.get_buffer (); 21 | 22 | card = new Gtk.Grid () { 23 | margin = 5 24 | }; 25 | var card_context = card.get_style_context (); 26 | card_context.add_class (Granite.STYLE_CLASS_CARD); 27 | card_context.add_class (Granite.STYLE_CLASS_ROUNDED); 28 | 29 | card.add (textview); 30 | add (card); 31 | 32 | show_all (); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moobo 2 | 3 | ## Visualize your ideas on moodboards 4 | 5 | Use different widgets to visualize your thoughts. 6 | 7 | ⚠ NOTE: Moobo was developed as part of my bachelors degree. At this point, consider Moobo as proof of concept. 8 | 9 | [![Get it on AppCenter](https://appcenter.elementary.io/badge.svg)](https://appcenter.elementary.io/com.github.brain_child.moobo) 10 | 11 | ![moobo light](/preview/moobo_light.png) 12 | 13 | ![moobo dark](/preview/moobo_dark.png) 14 | 15 | ### Credits 16 | 17 | Moobo is developed after a concept by [Heru Setiawan](https://hrstwn.github.io/). Heru Setiawan is an illustrator who came up with the idea of the app. The original mockup can be found on [reddit](https://www.reddit.com/r/elementaryos/comments/kg5uiw/moobo_is_a_moodboarding_and_notetaking_app/). 18 | 19 | Run `meson build` to configure the build environment. Change to the build directory and run `ninja` to build 20 | 21 | meson build --prefix=/usr 22 | cd build 23 | ninja 24 | 25 | To install, use `ninja install`, then execute with `io.elementary.photos` 26 | 27 | sudo ninja install 28 | io.elementary.photos 29 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # Project name, programming language and version 2 | project ( 3 | 'com.github.brain_child.moobo', 4 | 'c', 'vala', 5 | version: '0.1.3' 6 | ) 7 | 8 | # GNOME module 9 | gnome = import ('gnome') 10 | 11 | # Translation module 12 | i18n = import ('i18n') 13 | 14 | # Project arguments 15 | add_project_arguments ( 16 | '-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name ()), 17 | language: 'c' 18 | ) 19 | 20 | # Compiling resources 21 | asresources = gnome.compile_resources ( 22 | 'as-resources', 23 | join_paths ('data', meson.project_name ()) + '.gresource.xml', 24 | source_dir: 'data', 25 | c_name: 'as' 26 | ) 27 | 28 | # Listing dependencies 29 | dependencies = [ 30 | dependency ('gtk+-3.0'), 31 | dependency ('granite'), 32 | dependency ('libhandy-1'), 33 | dependency ('json-glib-1.0') 34 | ] 35 | 36 | 37 | subdir ('src') 38 | 39 | # Define executable 40 | executable( 41 | meson.project_name (), 42 | sources, 43 | asresources, 44 | dependencies: dependencies, 45 | install: true 46 | ) 47 | 48 | subdir ('data') 49 | subdir ('po') 50 | 51 | meson.add_install_script ('meson/post_install.py') 52 | -------------------------------------------------------------------------------- /src/Controllers/FloatingButtonController.vala: -------------------------------------------------------------------------------- 1 | public class FloatingButtonController { 2 | 3 | public signal void selection (Type type); 4 | 5 | private FloatingButton floating_button; 6 | 7 | public FloatingButtonController (FloatingButton floating_button) { 8 | this.floating_button = floating_button; 9 | floating_button.mode_button.mode_changed.connect ( () => { 10 | var index = floating_button.mode_button.selected; 11 | WidgetFactory.WidgetType widget_type; 12 | switch (index) { 13 | case 0: 14 | widget_type = WidgetFactory.WidgetType.TEXT; 15 | break; 16 | case 1: 17 | widget_type = WidgetFactory.WidgetType.LABEL; 18 | break; 19 | case 2: 20 | widget_type = WidgetFactory.WidgetType.IMAGE; 21 | break; 22 | default: 23 | widget_type = WidgetFactory.WidgetType.TEXT; 24 | break; 25 | } 26 | selection (WidgetFactory.get_class_type_from_widget_type (widget_type)); 27 | }); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | sources = files( 2 | 'Application.vala', 3 | 'Window.vala', 4 | 'Movable.vala', 5 | 6 | 'Controllers/BoardController.vala', 7 | 'Controllers/RowController.vala', 8 | 'Controllers/WidgetController.vala', 9 | 'Controllers/TextController.vala', 10 | 'Controllers/LabelController.vala', 11 | 'Controllers/ImageController.vala', 12 | 'Controllers/FloatingButtonController.vala', 13 | 14 | 'Helper/Colors.vala', 15 | 'Helper/Constants.vala', 16 | 'Helper/ErrorHandler.vala', 17 | 'Helper/ImageCache.vala', 18 | 'Helper/LazyLoader.vala', 19 | 'Helper/MemoryManager.vala', 20 | 'Helper/Serializer.vala', 21 | 'Helper/Deserializer.vala', 22 | 'Helper/WidgetFactory.vala', 23 | 'Helper/DemoBoard.vala', 24 | 25 | 'Models/BoardModel.vala', 26 | 'Models/BaseModel.vala', 27 | 'Models/TextModel.vala', 28 | 'Models/LabelModel.vala', 29 | 'Models/ImageModel.vala', 30 | 31 | 'Widgets/Board.vala', 32 | 'Widgets/Row.vala', 33 | 'Widgets/MenuItemColor.vala', 34 | 'Widgets/ColorButton.vala', 35 | 'Widgets/FloatingButton.vala', 36 | 'Widgets/TextWidget.vala', 37 | 'Widgets/LabelWidget.vala', 38 | 'Widgets/ImageWidget.vala' 39 | ) 40 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | # Install icons 2 | icon_sizes = ['16', '24', '32', '48', '64', '128'] 3 | 4 | foreach i : icon_sizes 5 | install_data ( 6 | join_paths ('icons', i, meson.project_name () + '.svg'), 7 | install_dir: join_paths (get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i, 'apps') 8 | ) 9 | install_data ( 10 | join_paths ('icons', i, meson.project_name() + '.svg'), 11 | install_dir: join_paths (get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i + '@2', 'apps') 12 | ) 13 | endforeach 14 | 15 | # Translate and install our .desktop file so the Applications Menu will see it 16 | i18n.merge_file ( 17 | input: meson.project_name () + '.desktop.in', 18 | output: meson.project_name () + '.desktop', 19 | po_dir: join_paths (meson.source_root (), 'po', 'extra'), 20 | type: 'desktop', 21 | install: true, 22 | install_dir: join_paths (get_option ('datadir'), 'applications') 23 | ) 24 | 25 | # Translate and install our .appdata.xml file so AppCenter will see it 26 | i18n.merge_file ( 27 | input: meson.project_name () + '.appdata.xml.in', 28 | output: meson.project_name () + '.appdata.xml', 29 | po_dir: join_paths (meson.source_root (), 'po', 'extra'), 30 | install: true, 31 | install_dir: join_paths (get_option ('datadir'), 'metainfo') 32 | ) 33 | 34 | install_data ( 35 | meson.project_name () + '.gschema.xml', 36 | install_dir: join_paths (get_option ('datadir'), 'glib-2.0', 'schemas') 37 | ) 38 | -------------------------------------------------------------------------------- /src/Widgets/FloatingButton.vala: -------------------------------------------------------------------------------- 1 | public class FloatingButton : Gtk.MenuButton { 2 | 3 | public Granite.Widgets.ModeButton mode_button { private set; get; } 4 | 5 | public FloatingButton () { 6 | mode_button = create_mode_button (); 7 | var popover = create_popover (); 8 | 9 | var button = new Gtk.Button.from_icon_name ( 10 | "list-add-symbolic", 11 | Gtk.IconSize.SMALL_TOOLBAR 12 | ); 13 | button.get_style_context ().add_class ("white_label"); 14 | add (button); 15 | 16 | get_style_context ().add_class ("fab"); 17 | this.popover = popover; 18 | } 19 | 20 | construct { 21 | direction = Gtk.ArrowType.NONE; 22 | valign = Gtk.Align.END; 23 | halign = Gtk.Align.END; 24 | height_request = 40; 25 | width_request = 40; 26 | margin = 30; 27 | } 28 | 29 | private Granite.Widgets.ModeButton create_mode_button () { 30 | mode_button = new Granite.Widgets.ModeButton () { 31 | orientation = Gtk.Orientation.VERTICAL, 32 | homogeneous = false, 33 | margin = 5 34 | }; 35 | mode_button.append_text (_("Text")); 36 | mode_button.append_text (_("Label")); 37 | mode_button.append_text (_("Image")); 38 | mode_button.set_active (0); 39 | mode_button.show_all (); 40 | return mode_button; 41 | } 42 | 43 | private Gtk.Popover create_popover () { 44 | var popover = new Gtk.Popover (null); 45 | popover.add (mode_button); 46 | return popover; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # This workflow will run for any pull request or pushed commit 4 | on: [push, pull_request] 5 | 6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 7 | jobs: 8 | # This workflow contains a single job called "flatpak" 9 | flatpak: 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-latest 12 | 13 | # This job runs in a special container designed for building Flatpaks for AppCenter 14 | container: 15 | image: ghcr.io/elementary/flatpak-platform/runtime:6 16 | options: --privileged 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it 21 | - uses: actions/checkout@v2 22 | 23 | # Builds your flatpak manifest using the Flatpak Builder action 24 | - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v3 25 | with: 26 | # This is the name of the Bundle file we're building and can be anything you like 27 | bundle: Moobo.flatpak 28 | # This uses your app's RDNN ID 29 | manifest-path: com.github.brain-child.moobo.yml 30 | 31 | # You can automatically run any of the tests you've created as part of this workflow 32 | run-tests: true 33 | 34 | # These lines specify the location of the elementary Runtime and Sdk 35 | repository-name: appcenter 36 | repository-url: https://flatpak.elementary.io/repo.flatpakrepo 37 | cache-key: "flatpak-builder-${{ github.sha }}" 38 | -------------------------------------------------------------------------------- /src/Helper/DemoBoard.vala: -------------------------------------------------------------------------------- 1 | namespace DemoBoard { 2 | 3 | private const string TITLE = _("Demo"); 4 | private const string TEXT_A = _("Widget selection"); 5 | private const string TEXT_B = _("Double click anywhere to add a widget"); 6 | private const string TEXT_C = _("Change font size: ctrl +/-\nsave and close app: ctrl q/w"); 7 | private const string TEXT_D = _("Right click on widget for\ncontext menu.\n(when 'grabbing cursor' appears)"); 8 | private const string TEXT_E = _("Create new board"); 9 | private const string TEXT_F = _("Right click to edit"); 10 | 11 | public string get_demo_board () { 12 | return """ 13 | [ 14 | { 15 | "title" : "%s", 16 | "color" : "mint", 17 | "active" : true, 18 | "widgets" : [ 19 | { 20 | "model" : "TextWidget", 21 | "x" : 915, 22 | "y" : 695, 23 | "font-size" : 150, 24 | "content" : "%s -->" 25 | }, 26 | { 27 | "model" : "LabelWidget", 28 | "x" : 149, 29 | "y" : 136, 30 | "font-size" : 150, 31 | "content" : "%s", 32 | "color" : "rgb(32,74,135)" 33 | }, 34 | { 35 | "model" : "LabelWidget", 36 | "x" : 645, 37 | "y" : 261, 38 | "font-size" : 150, 39 | "content" : "%s", 40 | "color" : "rgb(255,255,255)" 41 | }, 42 | { 43 | "model" : "TextWidget", 44 | "x" : 407, 45 | "y" : 441, 46 | "font-size" : 170, 47 | "content" : "%s" 48 | }, 49 | { 50 | "model" : "TextWidget", 51 | "x" : 1, 52 | "y" : 728, 53 | "font-size" : 120, 54 | "content" : "<-- %s" 55 | }, 56 | { 57 | "model" : "TextWidget", 58 | "x" : 1, 59 | "y" : 25, 60 | "font-size" : 130, 61 | "content" : "<-- %s" 62 | }, 63 | { 64 | "model" : "TextWidget", 65 | "x" : 474, 66 | "y" : 117, 67 | "font-size" : 210, 68 | "content" : "🙃️" 69 | } 70 | ] 71 | } 72 | ] 73 | """.printf (TITLE, TEXT_A, TEXT_B, TEXT_C, TEXT_D, TEXT_E, TEXT_F); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /po/com.github.brain_child.moobo.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.brain_child.moobo package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: com.github.brain_child.moobo\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/Application.vala:104 21 | msgid "Shortcuts" 22 | msgstr "" 23 | 24 | #: src/Application.vala:123 25 | msgid "Application" 26 | msgstr "" 27 | 28 | #: src/Application.vala:124 29 | msgid "Show shortcuts:" 30 | msgstr "" 31 | 32 | #: src/Application.vala:126 33 | msgid "Save and Quit:" 34 | msgstr "" 35 | 36 | #: src/Application.vala:129 src/Window.vala:71 37 | msgid "Boards" 38 | msgstr "" 39 | 40 | #: src/Application.vala:130 41 | msgid "Rename:" 42 | msgstr "" 43 | 44 | #: src/Application.vala:132 45 | msgid "Widgets" 46 | msgstr "" 47 | 48 | #: src/Application.vala:133 49 | msgid "Increase font size:" 50 | msgstr "" 51 | 52 | #: src/Application.vala:135 53 | msgid "Decrease font size:" 54 | msgstr "" 55 | 56 | #: src/Window.vala:56 57 | msgid "New Board" 58 | msgstr "" 59 | 60 | #: src/Movable.vala:67 61 | msgid "Change Color" 62 | msgstr "" 63 | 64 | #: src/Movable.vala:86 65 | msgid "Change Font" 66 | msgstr "" 67 | 68 | #: src/Movable.vala:100 69 | msgid "Scale" 70 | msgstr "" 71 | 72 | #: src/Movable.vala:125 src/Controllers/RowController.vala:127 73 | msgid "Delete" 74 | msgstr "" 75 | 76 | #: src/Controllers/RowController.vala:89 src/Widgets/Row.vala:7 77 | msgid "Board" 78 | msgstr "" 79 | 80 | #: src/Controllers/RowController.vala:120 81 | #, c-format 82 | msgid "Do you want to delete \"%s\"?" 83 | msgstr "" 84 | 85 | #: src/Controllers/RowController.vala:121 86 | msgid "" 87 | "The content of this board will be deleted completely and cannot be restored." 88 | msgstr "" 89 | 90 | #: src/Controllers/ImageController.vala:44 91 | msgid "Open File" 92 | msgstr "" 93 | 94 | #: src/Controllers/ImageController.vala:47 95 | msgid "Open" 96 | msgstr "" 97 | 98 | #: src/Controllers/ImageController.vala:48 99 | msgid "Cancel" 100 | msgstr "" 101 | 102 | #: src/Widgets/FloatingButton.vala:35 103 | msgid "Text" 104 | msgstr "" 105 | 106 | #: src/Widgets/FloatingButton.vala:36 107 | msgid "Label" 108 | msgstr "" 109 | 110 | #: src/Widgets/FloatingButton.vala:37 111 | msgid "Image" 112 | msgstr "" 113 | 114 | #: src/Widgets/Row.vala:77 115 | msgid "Rename" 116 | msgstr "" 117 | -------------------------------------------------------------------------------- /src/Controllers/TextController.vala: -------------------------------------------------------------------------------- 1 | public class TextController : WidgetController { 2 | 3 | public TextWidget movable { private set; get; } 4 | public TextModel model { private set; get; } 5 | private Gtk.Overlay overlay; 6 | 7 | public TextController (BoardController board_controller, int x, int y, TextModel model = new TextModel ()) { 8 | base (board_controller, x, y); 9 | this.overlay = board_controller.overlay; 10 | this.movable = new TextWidget (this); 11 | this.model = model; 12 | model.x = x; 13 | model.y = y; 14 | 15 | movable.textview.key_press_event.connect (on_key_press); 16 | movable.textview.key_release_event.connect (on_key_release); 17 | 18 | update_widget (); 19 | 20 | set_font_size (movable.font_size); 21 | movable.handle_events (board_controller); 22 | } 23 | 24 | public override Movable get_movable () { 25 | return movable; 26 | } 27 | 28 | public override BaseModel get_model () { 29 | return model; 30 | } 31 | 32 | public override void handle_key_press (Gdk.EventKey event) { 33 | on_key_press (event); 34 | } 35 | 36 | public override void handle_key_release () { 37 | on_key_release (); 38 | } 39 | 40 | private bool on_key_press (Gdk.EventKey event) { 41 | var key_name = event.state.to_string (); 42 | 43 | if (key_name == "GDK_CONTROL_MASK" && (event.keyval == Gdk.Key.plus || event.keyval == Gdk.Key.equal)) { 44 | movable.font_size += 10; 45 | set_font_size (movable.font_size); 46 | } 47 | if (key_name == "GDK_CONTROL_MASK" && event.keyval == Gdk.Key.minus) { 48 | movable.font_size -= 10; 49 | set_font_size (movable.font_size); 50 | } 51 | return false; 52 | } 53 | 54 | public bool on_key_release () { 55 | model.content = movable.buffer.text; 56 | return false; 57 | } 58 | 59 | public override void update_widget () { 60 | movable.rel_pos_x = movable.margin_start = model.x; 61 | movable.rel_pos_y = movable.margin_top = model.y; 62 | movable.buffer.text = model.content; 63 | movable.font_size = model.font_size; 64 | } 65 | 66 | private void set_font_size (int font_size) { 67 | model.font_size = font_size; 68 | string style = "textview { 69 | font-size: %d%; 70 | }".printf (font_size); 71 | 72 | var style_provider = new Gtk.CssProvider (); 73 | try { 74 | style_provider.load_from_data (style, style.length); 75 | var style_context = movable.textview.get_style_context (); 76 | style_context.add_provider (style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 77 | } catch (Error e) { 78 | warning ("%s\n", e.message); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /data/icons/symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 51 | 55 | 66 | 67 | 77 | 87 | 88 | -------------------------------------------------------------------------------- /po/extra/extra.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: extra\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: data/com.github.brain_child.moobo.desktop.in:3 21 | #: data/com.github.brain_child.moobo.appdata.xml.in:6 22 | msgid "Moobo" 23 | msgstr "" 24 | 25 | #: data/com.github.brain_child.moobo.desktop.in:4 26 | msgid "Moodboard Application" 27 | msgstr "" 28 | 29 | #: data/com.github.brain_child.moobo.desktop.in:5 30 | msgid "Visualize your ideas" 31 | msgstr "" 32 | 33 | #: data/com.github.brain_child.moobo.desktop.in:8 34 | msgid "com.github.brain_child.moobo" 35 | msgstr "" 36 | 37 | #: data/com.github.brain_child.moobo.desktop.in:11 38 | msgid "notes;moobo;moodboard;idea;" 39 | msgstr "" 40 | 41 | #: data/com.github.brain_child.moobo.appdata.xml.in:7 42 | msgid "Visualize your ideas on moodboards" 43 | msgstr "" 44 | 45 | #: data/com.github.brain_child.moobo.appdata.xml.in:10 46 | msgid "Use different widgets to visualize your thoughts." 47 | msgstr "" 48 | 49 | #: data/com.github.brain_child.moobo.appdata.xml.in:13 50 | msgid "" 51 | "⚠ NOTE: The app is not finished. At this point, consider Moobo as proof of " 52 | "concept." 53 | msgstr "" 54 | 55 | #: data/com.github.brain_child.moobo.appdata.xml.in:22 56 | msgid "brain·​child" 57 | msgstr "" 58 | 59 | #: data/com.github.brain_child.moobo.appdata.xml.in:42 60 | msgid "Add shortcuts for QWERTY and AZERTY keyboard layout" 61 | msgstr "" 62 | 63 | #: data/com.github.brain_child.moobo.appdata.xml.in:49 64 | msgid "Window size is calculated dynamically" 65 | msgstr "" 66 | 67 | #: data/com.github.brain_child.moobo.appdata.xml.in:50 68 | msgid "Show shortcuts by pressing ESC" 69 | msgstr "" 70 | 71 | #: data/com.github.brain_child.moobo.appdata.xml.in:51 72 | msgid "Rename board by pressing F2" 73 | msgstr "" 74 | 75 | #: data/com.github.brain_child.moobo.appdata.xml.in:52 76 | msgid "Show placeholder when adding an image" 77 | msgstr "" 78 | 79 | #: data/com.github.brain_child.moobo.appdata.xml.in:53 80 | msgid "Add French translation" 81 | msgstr "" 82 | 83 | #: data/com.github.brain_child.moobo.appdata.xml.in:54 84 | msgid "Add Portuguese translation" 85 | msgstr "" 86 | 87 | #: data/com.github.brain_child.moobo.appdata.xml.in:55 88 | msgid "Visual improvements" 89 | msgstr "" 90 | 91 | #: data/com.github.brain_child.moobo.appdata.xml.in:62 92 | msgid "Fix productpage in AppCenter" 93 | msgstr "" 94 | 95 | #: data/com.github.brain_child.moobo.appdata.xml.in:69 96 | msgid "First release!" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /po/extra/pt.po: -------------------------------------------------------------------------------- 1 | # Portuguese translations for extra package. 2 | # Copyright (C) 2021 THE extra'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # Automatically generated, 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: extra\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-19 16:27+0100\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: pt\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: data/com.github.brain_child.moobo.desktop.in:3 21 | #: data/com.github.brain_child.moobo.appdata.xml.in:6 22 | msgid "Moobo" 23 | msgstr "Moobo" 24 | 25 | #: data/com.github.brain_child.moobo.desktop.in:4 26 | msgid "Moodboard Application" 27 | msgstr "aplicação Moodboard" 28 | 29 | #: data/com.github.brain_child.moobo.desktop.in:5 30 | msgid "Visualize your ideas" 31 | msgstr "Visualize as suas ideas" 32 | 33 | #: data/com.github.brain_child.moobo.desktop.in:8 34 | msgid "com.github.brain_child.moobo" 35 | msgstr "com.github.brain_child.moobo" 36 | 37 | #: data/com.github.brain_child.moobo.desktop.in:11 38 | msgid "notes;moobo;moodboard;idea;" 39 | msgstr "notas;moobo;moodboard;ideas" 40 | 41 | #: data/com.github.brain_child.moobo.appdata.xml.in:7 42 | msgid "Visualize your ideas on moodboards" 43 | msgstr "Visualize as suas ideas num moodboard" 44 | 45 | #: data/com.github.brain_child.moobo.appdata.xml.in:10 46 | msgid "Use different widgets to visualize your thoughts." 47 | msgstr "" 48 | 49 | #: data/com.github.brain_child.moobo.appdata.xml.in:13 50 | msgid "" 51 | "⚠ NOTE: The app is not finished. At this point, consider Moobo as proof of " 52 | "concept." 53 | msgstr "" 54 | 55 | #: data/com.github.brain_child.moobo.appdata.xml.in:22 56 | msgid "brain·​child" 57 | msgstr "brain·​child" 58 | 59 | #: data/com.github.brain_child.moobo.appdata.xml.in:42 60 | msgid "Add shortcuts for QWERTY and AZERTY keyboard layout" 61 | msgstr "" 62 | 63 | #: data/com.github.brain_child.moobo.appdata.xml.in:49 64 | msgid "Window size is calculated dynamically" 65 | msgstr "" 66 | 67 | #: data/com.github.brain_child.moobo.appdata.xml.in:50 68 | msgid "Show shortcuts by pressing ESC" 69 | msgstr "" 70 | 71 | #: data/com.github.brain_child.moobo.appdata.xml.in:51 72 | msgid "Rename board by pressing F2" 73 | msgstr "" 74 | 75 | #: data/com.github.brain_child.moobo.appdata.xml.in:52 76 | msgid "Show placeholder when adding an image" 77 | msgstr "" 78 | 79 | #: data/com.github.brain_child.moobo.appdata.xml.in:53 80 | msgid "Add French translation" 81 | msgstr "" 82 | 83 | #: data/com.github.brain_child.moobo.appdata.xml.in:54 84 | msgid "Add Portuguese translation" 85 | msgstr "" 86 | 87 | #: data/com.github.brain_child.moobo.appdata.xml.in:55 88 | msgid "Visual improvements" 89 | msgstr "" 90 | 91 | #: data/com.github.brain_child.moobo.appdata.xml.in:62 92 | msgid "Fix productpage in AppCenter" 93 | msgstr "Consertado pela site do produto no AppCenter" 94 | 95 | #: data/com.github.brain_child.moobo.appdata.xml.in:69 96 | msgid "First release!" 97 | msgstr "Publicada pela primeira vez!" 98 | -------------------------------------------------------------------------------- /src/Widgets/Row.vala: -------------------------------------------------------------------------------- 1 | public class Row : Gtk.ListBoxRow { 2 | 3 | public signal void delete_row (int index); 4 | public signal void renamed (string title); 5 | 6 | public Gtk.Grid source_color { private set; get; } 7 | public string label { set; get; default = _("Board"); } 8 | public Gtk.Label display_name_label { set; get; } 9 | public Gtk.EventBox delete_eventbox { private set; get; } 10 | public Gtk.Grid grid { private set; get; } 11 | public Gtk.EventBox grid_eventbox { private set; get; } 12 | public Gtk.Image delete_button { private set; get; } 13 | public Gtk.Menu menu { private set; get; } 14 | public Gtk.MenuItem rename_item { private set; get; } 15 | public MenuItemColor menu_item { private set; get; } 16 | 17 | private BoardController board_controller; 18 | private Gtk.CssProvider listrow_provider; 19 | private string color; 20 | 21 | public Row (BoardController board_controller, string name, string color) { 22 | this.board_controller = board_controller; 23 | label = name; 24 | this.display_name_label.label = label; 25 | this.display_name_label.label = label; 26 | this.color = color; 27 | create_menu (); 28 | } 29 | 30 | construct { 31 | listrow_provider = new Gtk.CssProvider (); 32 | listrow_provider.load_from_resource ("com/github/brain_child/moobo/styles/SourceRow.css"); 33 | 34 | source_color = new Gtk.Grid () { 35 | halign = Gtk.Align.START, 36 | }; 37 | 38 | var source_color_context = source_color.get_style_context (); 39 | source_color_context.add_class ("source-color"); 40 | source_color_context.add_provider (listrow_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 41 | 42 | display_name_label = new Gtk.Label ("") { 43 | halign = Gtk.Align.START, 44 | hexpand = true, 45 | }; 46 | 47 | delete_button = new Gtk.Image.from_icon_name ("", Gtk.IconSize.BUTTON) { 48 | halign = Gtk.Align.END, 49 | margin = 0 50 | }; 51 | 52 | delete_eventbox = new Gtk.EventBox () { 53 | halign = Gtk.Align.END 54 | }; 55 | delete_eventbox.add (delete_button); 56 | 57 | grid = new Gtk.Grid () { 58 | column_spacing = 6, 59 | margin_start = 12, 60 | margin_end = 6 61 | }; 62 | grid.attach (source_color, 0, 0); 63 | grid.attach (display_name_label, 1, 0); 64 | grid.attach (delete_eventbox, 2, 0); 65 | 66 | grid_eventbox = new Gtk.EventBox (); 67 | grid_eventbox.add (grid); 68 | 69 | add (grid_eventbox); 70 | show_all (); 71 | } 72 | 73 | private void create_menu () { 74 | 75 | rename_item = new Gtk.MenuItem (); 76 | rename_item.add (new Granite.AccelLabel ( 77 | _("Rename"), 78 | "F2" 79 | )); 80 | 81 | menu_item = new MenuItemColor (); 82 | menu_item.check_color (Colors.from_name_to_index (color)); 83 | 84 | menu = new Gtk.Menu (); 85 | menu.add (rename_item); 86 | menu.add (new Gtk.SeparatorMenuItem ()); 87 | menu.add (menu_item); 88 | menu.show_all (); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /po/extra/fr.po: -------------------------------------------------------------------------------- 1 | # French translations for extra package. 2 | # Copyright (C) 2021 THE extra'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # Nathan Bonnemains (@NathanBnm), 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: extra\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-19 21:06+0100\n" 12 | "Last-Translator: Nathan Bonnemains (@NathanBnm)\n" 13 | "Language-Team: none\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: data/com.github.brain_child.moobo.desktop.in:3 21 | #: data/com.github.brain_child.moobo.appdata.xml.in:6 22 | msgid "Moobo" 23 | msgstr "Moobo" 24 | 25 | #: data/com.github.brain_child.moobo.desktop.in:4 26 | msgid "Moodboard Application" 27 | msgstr "Application de moodboard" 28 | 29 | #: data/com.github.brain_child.moobo.desktop.in:5 30 | msgid "Visualize your ideas" 31 | msgstr "Visualisez vos idées" 32 | 33 | #: data/com.github.brain_child.moobo.desktop.in:8 34 | msgid "com.github.brain_child.moobo" 35 | msgstr "com.github.brain_child.moobo" 36 | 37 | #: data/com.github.brain_child.moobo.desktop.in:11 38 | msgid "notes;moobo;moodboard;idea;" 39 | msgstr "notes;moobo;moodboard;idée;" 40 | 41 | #: data/com.github.brain_child.moobo.appdata.xml.in:7 42 | msgid "Visualize your ideas on moodboards" 43 | msgstr "Visualisez vos idées sur des moodboards" 44 | 45 | #: data/com.github.brain_child.moobo.appdata.xml.in:10 46 | msgid "Use different widgets to visualize your thoughts." 47 | msgstr "" 48 | 49 | #: data/com.github.brain_child.moobo.appdata.xml.in:13 50 | msgid "" 51 | "⚠ NOTE: The app is not finished. At this point, consider Moobo as proof of " 52 | "concept." 53 | msgstr "" 54 | 55 | #: data/com.github.brain_child.moobo.appdata.xml.in:22 56 | msgid "brain·​child" 57 | msgstr "brain·​child" 58 | 59 | #: data/com.github.brain_child.moobo.appdata.xml.in:42 60 | msgid "Add shortcuts for QWERTY and AZERTY keyboard layout" 61 | msgstr "" 62 | 63 | #: data/com.github.brain_child.moobo.appdata.xml.in:49 64 | msgid "Window size is calculated dynamically" 65 | msgstr "" 66 | 67 | #: data/com.github.brain_child.moobo.appdata.xml.in:50 68 | msgid "Show shortcuts by pressing ESC" 69 | msgstr "" 70 | 71 | #: data/com.github.brain_child.moobo.appdata.xml.in:51 72 | msgid "Rename board by pressing F2" 73 | msgstr "" 74 | 75 | #: data/com.github.brain_child.moobo.appdata.xml.in:52 76 | msgid "Show placeholder when adding an image" 77 | msgstr "" 78 | 79 | #: data/com.github.brain_child.moobo.appdata.xml.in:53 80 | msgid "Add French translation" 81 | msgstr "" 82 | 83 | #: data/com.github.brain_child.moobo.appdata.xml.in:54 84 | msgid "Add Portuguese translation" 85 | msgstr "" 86 | 87 | #: data/com.github.brain_child.moobo.appdata.xml.in:55 88 | msgid "Visual improvements" 89 | msgstr "" 90 | 91 | #: data/com.github.brain_child.moobo.appdata.xml.in:62 92 | msgid "Fix productpage in AppCenter" 93 | msgstr "Correction de la page de l'application dans le Centre d'Applications" 94 | 95 | #: data/com.github.brain_child.moobo.appdata.xml.in:69 96 | msgid "First release!" 97 | msgstr "Première version !" 98 | -------------------------------------------------------------------------------- /data/com.github.brain_child.moobo.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.brain_child.moobo 5 | CC0 6 | Moobo 7 | Visualize your ideas on moodboards 8 | 9 | 10 |

11 | Use different widgets to visualize your thoughts. 12 |

13 |

14 | ⚠ NOTE: The app is not finished. At this point, consider Moobo as proof of concept. 15 |

16 |
17 | 18 | 19 | 20 | 21 | 22 | brain·​child 23 | http://github.com/brain-child/moobo 24 | http://github.com/brain-child/moobo/issues 25 | http://github.com/brain-child/moobo/issues 26 | 27 | 28 | 29 | https://raw.githubusercontent.com/brain-child/moobo/main/preview/moobo_light.png 30 | 31 | 32 | https://raw.githubusercontent.com/brain-child/moobo/main/preview/moobo_dark.png 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
    42 |
  • Add shortcuts for QWERTY and AZERTY keyboard layout
  • 43 |
44 |
45 |
46 | 47 | 48 |
    49 |
  • Window size is calculated dynamically
  • 50 |
  • Show shortcuts by pressing ESC
  • 51 |
  • Rename board by pressing F2
  • 52 |
  • Show placeholder when adding an image
  • 53 |
  • Add French translation
  • 54 |
  • Add Portuguese translation
  • 55 |
  • Visual improvements
  • 56 |
57 |
58 |
59 | 60 | 61 |
    62 |
  • Fix productpage in AppCenter
  • 63 |
64 |
65 |
66 | 67 | 68 |
    69 |
  • First release!
  • 70 |
71 |
72 |
73 |
74 | 75 | 76 | #3689E6 77 | #FFFFFF 78 | 1 79 | pk_live_51Jn7RuIIljArhmepVPJdZfHeDKFox4HDQN60QFkowrkHtfNuxuM74dHrXeYU75IoYsNUGXmkZVvVz9YywwFEtQ8700QubEbk62 80 | 81 | 82 |
83 | 84 | -------------------------------------------------------------------------------- /po/extra/de.po: -------------------------------------------------------------------------------- 1 | # German translations for extra package. 2 | # Copyright (C) 2021 THE extra'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # Automatically generated, 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: extra\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-10 11:25+0100\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: data/com.github.brain_child.moobo.desktop.in:3 21 | #: data/com.github.brain_child.moobo.appdata.xml.in:6 22 | msgid "Moobo" 23 | msgstr "Moobo" 24 | 25 | #: data/com.github.brain_child.moobo.desktop.in:4 26 | msgid "Moodboard Application" 27 | msgstr "Moodboard Applikation" 28 | 29 | #: data/com.github.brain_child.moobo.desktop.in:5 30 | msgid "Visualize your ideas" 31 | msgstr "Visualisiere deine Ideen" 32 | 33 | #: data/com.github.brain_child.moobo.desktop.in:8 34 | msgid "com.github.brain_child.moobo" 35 | msgstr "com.github.brain_child.moobo" 36 | 37 | #: data/com.github.brain_child.moobo.desktop.in:11 38 | msgid "notes;moobo;moodboard;idea;" 39 | msgstr "notizen;moobo;moodbord;idee" 40 | 41 | #: data/com.github.brain_child.moobo.appdata.xml.in:7 42 | msgid "Visualize your ideas on moodboards" 43 | msgstr "Visualisiere deine Ideen auf Moodboards" 44 | 45 | #: data/com.github.brain_child.moobo.appdata.xml.in:10 46 | msgid "Use different widgets to visualize your thoughts." 47 | msgstr "Benutze verschiedene Widgets um deine Gedanken zu visualisieren." 48 | 49 | #: data/com.github.brain_child.moobo.appdata.xml.in:13 50 | msgid "" 51 | "⚠ NOTE: The app is not finished. At this point, consider Moobo as proof of " 52 | "concept." 53 | msgstr "Die App ist noch nicht fertig. Betrachte sie im Moment als Nachweis der Machbarkeit." 54 | 55 | #: data/com.github.brain_child.moobo.appdata.xml.in:22 56 | msgid "brain·​child" 57 | msgstr "brain·​child" 58 | 59 | #: data/com.github.brain_child.moobo.appdata.xml.in:42 60 | msgid "Add shortcuts for QWERTY and AZERTY keyboard layout" 61 | msgstr "Shortcuts für QWERTY und AZERTY Tastaturen" 62 | 63 | #: data/com.github.brain_child.moobo.appdata.xml.in:49 64 | msgid "Window size is calculated dynamically" 65 | msgstr "Fenstergöße wird dynamisch berechnet" 66 | 67 | #: data/com.github.brain_child.moobo.appdata.xml.in:50 68 | msgid "Show shortcuts by pressing ESC" 69 | msgstr "Anzeigen von Tastenkürzel mit ESC" 70 | 71 | #: data/com.github.brain_child.moobo.appdata.xml.in:51 72 | msgid "Rename board by pressing F2" 73 | msgstr "Boards mit F2 umbenennen" 74 | 75 | #: data/com.github.brain_child.moobo.appdata.xml.in:52 76 | msgid "Show placeholder when adding an image" 77 | msgstr "Platzhalter wird angezeigt beim Hinzufügen eines Bildes" 78 | 79 | #: data/com.github.brain_child.moobo.appdata.xml.in:53 80 | msgid "Add French translation" 81 | msgstr "Französische Übersetzung" 82 | 83 | #: data/com.github.brain_child.moobo.appdata.xml.in:54 84 | msgid "Add Portuguese translation" 85 | msgstr "Portugiesische Übersetzung" 86 | 87 | #: data/com.github.brain_child.moobo.appdata.xml.in:55 88 | msgid "Visual improvements" 89 | msgstr "Optische Verbesserungen" 90 | 91 | #: data/com.github.brain_child.moobo.appdata.xml.in:62 92 | msgid "Fix productpage in AppCenter" 93 | msgstr "Fix für die Produktseite im AppCenter" 94 | 95 | #: data/com.github.brain_child.moobo.appdata.xml.in:69 96 | msgid "First release!" 97 | msgstr "Erste Veröffentlichung!" 98 | -------------------------------------------------------------------------------- /src/Helper/Colors.vala: -------------------------------------------------------------------------------- 1 | namespace Colors { 2 | 3 | public string from_index (int index) { 4 | string color = "transparent"; 5 | switch (index) { 6 | case 0: 7 | break; 8 | case 1: 9 | color = "strawberry"; 10 | break; 11 | case 2: 12 | color = "orange"; 13 | break; 14 | case 3: 15 | color = "banana"; 16 | break; 17 | case 4: 18 | color = "lime"; 19 | break; 20 | case 5: 21 | color = "mint"; 22 | break; 23 | case 6: 24 | color = "blueberry"; 25 | break; 26 | case 7: 27 | color = "grape"; 28 | break; 29 | case 8: 30 | color = "bubblegum"; 31 | break; 32 | case 9: 33 | color = "cocoa"; 34 | break; 35 | case 10: 36 | color = "slate"; 37 | break; 38 | } 39 | return color; 40 | } 41 | 42 | public string from_name (string color_name) { 43 | var color = "transparent"; 44 | switch (color_name) { 45 | case "transparent": 46 | break; 47 | case "strawberry": 48 | color = "#c6262e"; 49 | break; 50 | case "orange": 51 | color = "#f37329"; 52 | break; 53 | case "banana": 54 | color = "#e6a92a"; 55 | break; 56 | case "lime": 57 | color = "#68b723"; 58 | break; 59 | case "mint": 60 | color = "#0e9a83"; 61 | break; 62 | case "blueberry": 63 | color = "#3689e6"; 64 | break; 65 | case "grape": 66 | color = "#a56de2"; 67 | break; 68 | case "bubblegum": 69 | color = "#de3e80"; 70 | break; 71 | case "cocoa": 72 | color = "#8a715e"; 73 | break; 74 | case "slate": 75 | color = "#667885"; 76 | break; 77 | } 78 | return color; 79 | } 80 | 81 | public int from_name_to_index (string color_name) { 82 | var index = 0; 83 | switch (color_name) { 84 | case "transparent": 85 | break; 86 | case "strawberry": 87 | index = 1; 88 | break; 89 | case "orange": 90 | index = 2; 91 | break; 92 | case "banana": 93 | index = 3; 94 | break; 95 | case "lime": 96 | index = 4; 97 | break; 98 | case "mint": 99 | index = 5; 100 | break; 101 | case "blueberry": 102 | index = 6; 103 | break; 104 | case "grape": 105 | index = 7; 106 | break; 107 | case "bubblegum": 108 | index = 8; 109 | break; 110 | case "cocoa": 111 | index = 9; 112 | break; 113 | case "slate": 114 | index = 10; 115 | break; 116 | } 117 | return index; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/Widgets/MenuItemColor.vala: -------------------------------------------------------------------------------- 1 | public class MenuItemColor : Gtk.MenuItem { 2 | public signal void color_changed (string color); 3 | private Gee.ArrayList color_buttons; 4 | private const int COLORBOX_SPACING = 3; 5 | 6 | construct { 7 | var color_button_remove = new ColorButton ("none"); 8 | color_buttons = new Gee.ArrayList (); 9 | color_buttons.add (new ColorButton ("red")); 10 | color_buttons.add (new ColorButton ("orange")); 11 | color_buttons.add (new ColorButton ("yellow")); 12 | color_buttons.add (new ColorButton ("green")); 13 | color_buttons.add (new ColorButton ("mint")); 14 | color_buttons.add (new ColorButton ("blue")); 15 | color_buttons.add (new ColorButton ("purple")); 16 | color_buttons.add (new ColorButton ("pink")); 17 | color_buttons.add (new ColorButton ("brown")); 18 | color_buttons.add (new ColorButton ("slate")); 19 | 20 | var colorbox = new Gtk.Grid () { 21 | column_spacing = COLORBOX_SPACING, 22 | margin_start = 3, 23 | halign = Gtk.Align.START 24 | }; 25 | 26 | colorbox.add (color_button_remove); 27 | 28 | for (int i = 0; i < color_buttons.size; i++) { 29 | colorbox.add (color_buttons[i]); 30 | } 31 | 32 | add (colorbox); 33 | 34 | try { 35 | string css = ".nohover { background: none; }"; 36 | 37 | var css_provider = new Gtk.CssProvider (); 38 | css_provider.load_from_data (css, -1); 39 | 40 | var style_context = get_style_context (); 41 | style_context.add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 42 | style_context.add_class ("nohover"); 43 | } catch (GLib.Error e) { 44 | warning ("Failed to parse css style : %s", e.message); 45 | } 46 | 47 | show_all (); 48 | 49 | button_press_event.connect (button_pressed_cb); 50 | 51 | } 52 | 53 | private void clear_checks () { 54 | color_buttons.foreach ((b) => { b.active = false; return true;}); 55 | } 56 | 57 | public void check_color (int color) { 58 | if (color == 0 || color > color_buttons.size) { 59 | return; 60 | } 61 | color_buttons[color - 1].active = true; 62 | } 63 | 64 | private bool button_pressed_cb (Gtk.Widget widget, Gdk.EventButton event) { 65 | 66 | var color_button_width = color_buttons[0].get_allocated_width (); 67 | 68 | int y0 = (get_allocated_height () - color_button_width) / 2; 69 | int x0 = COLORBOX_SPACING + color_button_width; 70 | 71 | if (event.y < y0 || event.y > y0 + color_button_width) { 72 | return true; 73 | } 74 | 75 | if (Gtk.StateFlags.DIR_RTL in get_style_context ().get_state ()) { 76 | var width = get_allocated_width (); 77 | int x = width - 27; 78 | for (int i = 0; i < 10; i++) { 79 | if (event.x <= x && event.x >= x - color_button_width) { 80 | color_changed (Colors.from_index (i)); 81 | clear_checks (); 82 | check_color (i); 83 | break; 84 | } 85 | 86 | x -= x0; 87 | } 88 | } else { 89 | int x = 27; 90 | for (int i = 0; i <= 10; i++) { 91 | if (event.x >= x && event.x <= x + color_button_width) { 92 | color_changed (Colors.from_index (i)); 93 | clear_checks (); 94 | check_color (i); 95 | break; 96 | } 97 | 98 | x += x0; 99 | } 100 | } 101 | 102 | return true; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # German translations for com.github.brain_child.moobo package. 2 | # Copyright (C) 2021 THE com.github.brain_child.moobo'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.brain_child.moobo package. 4 | # Automatically generated, 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: com.github.brain_child.moobo\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-06 16:35+0100\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: src/Application.vala:104 21 | msgid "Shortcuts" 22 | msgstr "Tastenkürzel" 23 | 24 | #: src/Application.vala:123 25 | msgid "Application" 26 | msgstr "Anwendung" 27 | 28 | #: src/Application.vala:124 29 | msgid "Show shortcuts:" 30 | msgstr "Zeige Tastenkürzel" 31 | 32 | #: src/Application.vala:126 33 | msgid "Save and Quit:" 34 | msgstr "Speichern und schließen" 35 | 36 | #: src/Application.vala:129 src/Window.vala:71 37 | msgid "Boards" 38 | msgstr "" 39 | 40 | #: src/Application.vala:130 41 | msgid "Rename:" 42 | msgstr "Umbenennen:" 43 | 44 | #: src/Application.vala:132 45 | msgid "Widgets" 46 | msgstr "" 47 | 48 | #: src/Application.vala:133 49 | msgid "Increase font size:" 50 | msgstr "Schrift vergrößern" 51 | 52 | #: src/Application.vala:135 53 | msgid "Decrease font size:" 54 | msgstr "Schrift verkleinern" 55 | 56 | #: src/Window.vala:56 57 | msgid "New Board" 58 | msgstr "Neues Board" 59 | 60 | #: src/Movable.vala:67 61 | msgid "Change Color" 62 | msgstr "Farbe ändern" 63 | 64 | #: src/Movable.vala:86 65 | msgid "Change Font" 66 | msgstr "Schriftart ändern" 67 | 68 | #: src/Movable.vala:100 69 | msgid "Scale" 70 | msgstr "Skalieren" 71 | 72 | #: src/Movable.vala:125 src/Controllers/RowController.vala:127 73 | msgid "Delete" 74 | msgstr "Löschen" 75 | 76 | #: src/Controllers/RowController.vala:89 src/Widgets/Row.vala:7 77 | msgid "Board" 78 | msgstr "" 79 | 80 | #: src/Controllers/RowController.vala:120 81 | #, c-format 82 | msgid "Do you want to delete \"%s\"?" 83 | msgstr "Möchtest du \"%s\" löschen?" 84 | 85 | #: src/Controllers/RowController.vala:121 86 | msgid "" 87 | "The content of this board will be deleted completely and cannot be restored." 88 | msgstr "" 89 | "Der Inhalt des Boards wird komplett gelöscht und kann nicht " 90 | "wiederhergestellt werden." 91 | 92 | #: src/Controllers/ImageController.vala:44 93 | msgid "Open File" 94 | msgstr "Datei Öffnen" 95 | 96 | #: src/Controllers/ImageController.vala:47 97 | msgid "Open" 98 | msgstr "Öffnen" 99 | 100 | #: src/Controllers/ImageController.vala:48 101 | msgid "Cancel" 102 | msgstr "Abbrechen" 103 | 104 | #: src/Widgets/FloatingButton.vala:35 105 | msgid "Text" 106 | msgstr "" 107 | 108 | #: src/Widgets/FloatingButton.vala:36 109 | msgid "Label" 110 | msgstr "" 111 | 112 | #: src/Widgets/FloatingButton.vala:37 113 | msgid "Image" 114 | msgstr "Bild" 115 | 116 | #: src/Widgets/Row.vala:77 117 | msgid "Rename" 118 | msgstr "Umbenennen" 119 | 120 | #~ msgid "Double click anywhere to add a widget" 121 | #~ msgstr "Mach irgendwo einen doppelklick um ein Widget zu erstellen" 122 | 123 | #~ msgid "" 124 | #~ "Change font size: ctrl +/-\n" 125 | #~ "save and close app: ctrl q/w" 126 | #~ msgstr "" 127 | #~ "Schriftgröße ändern: strg +/-\n" 128 | #~ "Speichern und schließen: strg q/w" 129 | 130 | #~ msgid "" 131 | #~ "Right click on widget for\n" 132 | #~ "context menu.\n" 133 | #~ "(when 'grabbing cursor' appears)" 134 | #~ msgstr "" 135 | #~ "Rechtsklick auf einem Widget\n" 136 | #~ "öffnet ein Kontextmenü.\n" 137 | #~ "(wenn 'Handcursor' erscheint)" 138 | 139 | #~ msgid "Create new board" 140 | #~ msgstr "Neues Board erstellen" 141 | 142 | #~ msgid "Right click to edit" 143 | #~ msgstr "Rechtsklick zum ändern" 144 | -------------------------------------------------------------------------------- /po/pt.po: -------------------------------------------------------------------------------- 1 | # Portuguese translations for com.github.brain_child.moobo package. 2 | # Copyright (C) 2021 THE com.github.brain_child.moobo'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.brain_child.moobo package. 4 | # Automatically generated, 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: com.github.brain_child.moobo\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-14 20:17+0100\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: pt\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: src/Application.vala:104 21 | msgid "Shortcuts" 22 | msgstr "" 23 | 24 | #: src/Application.vala:123 25 | msgid "Application" 26 | msgstr "" 27 | 28 | #: src/Application.vala:124 29 | msgid "Show shortcuts:" 30 | msgstr "" 31 | 32 | #: src/Application.vala:126 33 | msgid "Save and Quit:" 34 | msgstr "" 35 | 36 | #: src/Application.vala:129 src/Window.vala:71 37 | msgid "Boards" 38 | msgstr "" 39 | 40 | #: src/Application.vala:130 41 | msgid "Rename:" 42 | msgstr "mudar o nome:" 43 | 44 | #: src/Application.vala:132 45 | msgid "Widgets" 46 | msgstr "" 47 | 48 | #: src/Application.vala:133 49 | msgid "Increase font size:" 50 | msgstr "aumentar tamanho da fonte:" 51 | 52 | #: src/Application.vala:135 53 | msgid "Decrease font size:" 54 | msgstr "diminuir tamanho da fonte" 55 | 56 | #: src/Window.vala:56 57 | msgid "New Board" 58 | msgstr "Board novo" 59 | 60 | #: src/Movable.vala:67 61 | msgid "Change Color" 62 | msgstr "alterar cor" 63 | 64 | #: src/Movable.vala:86 65 | msgid "Change Font" 66 | msgstr "alterar fonte" 67 | 68 | #: src/Movable.vala:100 69 | msgid "Scale" 70 | msgstr "alterar escala" 71 | 72 | #: src/Movable.vala:125 src/Controllers/RowController.vala:127 73 | msgid "Delete" 74 | msgstr "deletar" 75 | 76 | #: src/Controllers/RowController.vala:89 src/Widgets/Row.vala:7 77 | msgid "Board" 78 | msgstr "Board" 79 | 80 | #: src/Controllers/RowController.vala:120 81 | #, c-format 82 | msgid "Do you want to delete \"%s\"?" 83 | msgstr "Você quer deletar \"%s\"?" 84 | 85 | #: src/Controllers/RowController.vala:121 86 | msgid "" 87 | "The content of this board will be deleted completely and cannot be restored." 88 | msgstr "" 89 | "O conteudo deste board vai ser deletado completamente sem possibilidade de " 90 | "ser restaurado depois." 91 | 92 | #: src/Controllers/ImageController.vala:44 93 | msgid "Open File" 94 | msgstr "abrir arquivo" 95 | 96 | #: src/Controllers/ImageController.vala:47 97 | msgid "Open" 98 | msgstr "abrir" 99 | 100 | #: src/Controllers/ImageController.vala:48 101 | msgid "Cancel" 102 | msgstr "cancelar" 103 | 104 | #: src/Widgets/FloatingButton.vala:35 105 | msgid "Text" 106 | msgstr "texto" 107 | 108 | #: src/Widgets/FloatingButton.vala:36 109 | msgid "Label" 110 | msgstr "" 111 | 112 | #: src/Widgets/FloatingButton.vala:37 113 | msgid "Image" 114 | msgstr "imagem" 115 | 116 | #: src/Widgets/Row.vala:77 117 | msgid "Rename" 118 | msgstr "mudar o nome" 119 | 120 | #~ msgid "Double click anywhere to add a widget" 121 | #~ msgstr "Faz um clique duplo em algum lugar para criar um widget" 122 | 123 | #~ msgid "" 124 | #~ "Change font size: ctrl +/-\n" 125 | #~ "save and close app: ctrl q/w" 126 | #~ msgstr "" 127 | #~ "alterar fonte strg: +/-\n" 128 | #~ "salvar e fechar: strg q/w" 129 | 130 | #~ msgid "" 131 | #~ "Right click on widget for\n" 132 | #~ "context menu.\n" 133 | #~ "(when 'grabbing cursor' appears)" 134 | #~ msgstr "" 135 | #~ "clique com botão direito em cima de um widget\n" 136 | #~ "abrir menu contextual.\n" 137 | #~ "( em caso ponteiro de mão aparece )" 138 | 139 | #~ msgid "Create new board" 140 | #~ msgstr "criar um Board novo" 141 | 142 | #~ msgid "Right click to edit" 143 | #~ msgstr "clique com botão direito para alterar" 144 | -------------------------------------------------------------------------------- /po/fr.po: -------------------------------------------------------------------------------- 1 | # French translations for com.github.brain_child.moobo package. 2 | # Copyright (C) 2021 THE com.github.brain_child.moobo'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.brain_child.moobo package. 4 | # Nathan Bonnemains (@NathanBnm), 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: com.github.brain_child.moobo\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-05-28 21:00+0200\n" 11 | "PO-Revision-Date: 2021-11-19 21:06+0100\n" 12 | "Last-Translator: Nathan Bonnemains (@NathanBnm)\n" 13 | "Language-Team: none\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: src/Application.vala:104 21 | msgid "Shortcuts" 22 | msgstr "" 23 | 24 | #: src/Application.vala:123 25 | msgid "Application" 26 | msgstr "" 27 | 28 | #: src/Application.vala:124 29 | msgid "Show shortcuts:" 30 | msgstr "" 31 | 32 | #: src/Application.vala:126 33 | msgid "Save and Quit:" 34 | msgstr "" 35 | 36 | #: src/Application.vala:129 src/Window.vala:71 37 | msgid "Boards" 38 | msgstr "Tableaux" 39 | 40 | #: src/Application.vala:130 41 | msgid "Rename:" 42 | msgstr "Renommer:" 43 | 44 | #: src/Application.vala:132 45 | msgid "Widgets" 46 | msgstr "" 47 | 48 | #: src/Application.vala:133 49 | msgid "Increase font size:" 50 | msgstr "" 51 | 52 | #: src/Application.vala:135 53 | msgid "Decrease font size:" 54 | msgstr "" 55 | 56 | #: src/Window.vala:56 57 | msgid "New Board" 58 | msgstr "Nouveau tableau" 59 | 60 | #: src/Movable.vala:67 61 | msgid "Change Color" 62 | msgstr "Modifier la couleur" 63 | 64 | #: src/Movable.vala:86 65 | msgid "Change Font" 66 | msgstr "Modifier la police" 67 | 68 | #: src/Movable.vala:100 69 | msgid "Scale" 70 | msgstr "Modifier la taille" 71 | 72 | #: src/Movable.vala:125 src/Controllers/RowController.vala:127 73 | msgid "Delete" 74 | msgstr "Supprimer" 75 | 76 | #: src/Controllers/RowController.vala:89 src/Widgets/Row.vala:7 77 | msgid "Board" 78 | msgstr "Tableau" 79 | 80 | #: src/Controllers/RowController.vala:120 81 | #, c-format 82 | msgid "Do you want to delete \"%s\"?" 83 | msgstr "Voulez-vous supprimer « %s » ?" 84 | 85 | #: src/Controllers/RowController.vala:121 86 | msgid "" 87 | "The content of this board will be deleted completely and cannot be restored." 88 | msgstr "" 89 | "Le contenu de ce tableau sera entièrement supprimé et ne pourra pas être " 90 | "restauré." 91 | 92 | #: src/Controllers/ImageController.vala:44 93 | msgid "Open File" 94 | msgstr "Ouvrir un fichier" 95 | 96 | #: src/Controllers/ImageController.vala:47 97 | msgid "Open" 98 | msgstr "Ouvrir" 99 | 100 | #: src/Controllers/ImageController.vala:48 101 | msgid "Cancel" 102 | msgstr "Annuler" 103 | 104 | #: src/Widgets/FloatingButton.vala:35 105 | msgid "Text" 106 | msgstr "Texte" 107 | 108 | #: src/Widgets/FloatingButton.vala:36 109 | msgid "Label" 110 | msgstr "Étiquette" 111 | 112 | #: src/Widgets/FloatingButton.vala:37 113 | msgid "Image" 114 | msgstr "Image" 115 | 116 | #: src/Widgets/Row.vala:77 117 | msgid "Rename" 118 | msgstr "Renommer" 119 | 120 | #~ msgid "Demo" 121 | #~ msgstr "Démo" 122 | 123 | #~ msgid "Double click anywhere to add a widget" 124 | #~ msgstr "Effectuez un double-clic où vous voulez pour ajouter un widget" 125 | 126 | #~ msgid "" 127 | #~ "Change font size: ctrl +/-\n" 128 | #~ "save and close app: ctrl q/w" 129 | #~ msgstr "" 130 | #~ "Modifier la taille de la police : Ctrl +/-\n" 131 | #~ "Enregistrer et fermer l'application : Ctrl Q/W" 132 | 133 | #~ msgid "" 134 | #~ "Right click on widget for\n" 135 | #~ "context menu.\n" 136 | #~ "(when 'grabbing cursor' appears)" 137 | #~ msgstr "" 138 | #~ "Effectuez un clic droit sur un widget\n" 139 | #~ "pour afficher le menu contextuel.\n" 140 | #~ "(lorsque le curseur « main » apparaît)" 141 | 142 | #~ msgid "Create new board" 143 | #~ msgstr "Créer un nouveau tableau" 144 | 145 | #~ msgid "Right click to edit" 146 | #~ msgstr "Effectuez un clic droit pour modifier" 147 | -------------------------------------------------------------------------------- /src/Controllers/LabelController.vala: -------------------------------------------------------------------------------- 1 | public class LabelController : WidgetController { 2 | 3 | public LabelWidget movable { private set; get; } 4 | public LabelModel model { private set; get; } 5 | private Gtk.Overlay overlay; 6 | private const string CSS = """ 7 | .color { 8 | background: %s; 9 | color: %s; 10 | text-shadow: none; 11 | } 12 | """; 13 | 14 | public LabelController (BoardController board_controller, int x, int y, LabelModel model = new LabelModel ()) { 15 | base (board_controller, x, y); 16 | this.overlay = board_controller.overlay; 17 | this.movable = new LabelWidget (this); 18 | this.model = model; 19 | model.x = x; 20 | model.y = y; 21 | 22 | movable.textview.key_press_event.connect (on_key_press); 23 | movable.textview.key_release_event.connect (on_key_release); 24 | 25 | update_widget (); 26 | 27 | set_font_size (movable.font_size); 28 | movable.handle_events (board_controller); 29 | } 30 | 31 | public override Movable get_movable () { 32 | return movable; 33 | } 34 | 35 | public override BaseModel get_model () { 36 | return model; 37 | } 38 | 39 | public override void handle_key_press (Gdk.EventKey event) { 40 | on_key_press (event); 41 | } 42 | 43 | public override void handle_key_release () { 44 | on_key_release (); 45 | } 46 | 47 | private bool on_key_press (Gdk.EventKey event) { 48 | var key_name = event.state.to_string (); 49 | 50 | if (key_name == "GDK_CONTROL_MASK" && (event.keyval == Gdk.Key.plus || event.keyval == Gdk.Key.equal)) { 51 | movable.font_size += 10; 52 | set_font_size (movable.font_size); 53 | } 54 | if (key_name == "GDK_CONTROL_MASK" && event.keyval == Gdk.Key.minus) { 55 | movable.font_size -= 10; 56 | set_font_size (movable.font_size); 57 | } 58 | return false; 59 | } 60 | 61 | public bool on_key_release () { 62 | model.content = movable.buffer.text; 63 | return false; 64 | } 65 | 66 | public override void update_widget () { 67 | movable.rel_pos_x = movable.margin_start = model.x; 68 | movable.rel_pos_y = movable.margin_top = model.y; 69 | movable.buffer.text = model.content; 70 | movable.font_size = model.font_size; 71 | var rgba = Gdk.RGBA (); 72 | if (rgba.parse (model.color)) { change_color (rgba); } 73 | } 74 | 75 | public void change_color (Gdk.RGBA rgba) { 76 | var css_provider = new Gtk.CssProvider (); 77 | 78 | var css = CSS.printf ( 79 | rgba.to_string (), 80 | Granite.contrasting_foreground_color (rgba).to_string () 81 | ); 82 | try { 83 | css_provider.load_from_data (css, css.length); 84 | } catch (Error e) { 85 | warning (e.message); 86 | } 87 | 88 | movable.textview.get_style_context ().add_provider ( 89 | css_provider, 90 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 91 | ); 92 | movable.card.get_style_context ().add_provider ( 93 | css_provider, 94 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 95 | ); 96 | 97 | movable.textview.get_style_context ().add_class ("color"); 98 | movable.card.get_style_context ().add_class ("color"); 99 | } 100 | 101 | private void set_font_size (int font_size) { 102 | model.font_size = font_size; 103 | string style = "textview { 104 | font-size: %d%; 105 | }".printf (font_size); 106 | 107 | var style_provider = new Gtk.CssProvider (); 108 | try { 109 | style_provider.load_from_data (style, style.length); 110 | var style_context = movable.textview.get_style_context (); 111 | style_context.add_provider (style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 112 | } catch (Error e) { 113 | warning (e.message); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Helper/Serializer.vala: -------------------------------------------------------------------------------- 1 | namespace Serializer { 2 | 3 | public void to_json (GLib.List widgets) { 4 | var node_array = new Json.Array (); 5 | 6 | foreach (var widget in widgets) { 7 | var board = (Board) widget; 8 | var node = serilize_board (board.controller.model); 9 | node_array.add_element (node); 10 | } 11 | 12 | var object = new Json.Object (); 13 | object.set_array_member ("boards", node_array); 14 | write_file (object.get_member ("boards")); 15 | } 16 | 17 | private Json.Node serilize_board (BoardModel model) { 18 | var widgets = model.widgets; 19 | var title = model.title; 20 | var color = model.color; 21 | var is_active = model.is_active; 22 | 23 | var builder = new Json.Builder (); 24 | builder.begin_object (); 25 | 26 | builder.set_member_name ("title"); 27 | builder.add_string_value (title); 28 | builder.set_member_name ("color"); 29 | builder.add_string_value (color); 30 | builder.set_member_name ("active"); 31 | builder.add_boolean_value (is_active); 32 | 33 | builder.set_member_name ("widgets"); 34 | builder.begin_array (); 35 | foreach (var widget in widgets) { 36 | BaseModel base_model = widget; 37 | var widget_type = WidgetFactory.get_widget_type_from_string (widget.model); 38 | 39 | switch (widget_type) { 40 | case WidgetFactory.WidgetType.TEXT: 41 | var text_model = base_model as TextModel; 42 | text_model.content = text_model.content.strip (); 43 | if (text_model.content == "") { continue; } 44 | break; 45 | case WidgetFactory.WidgetType.LABEL: 46 | var label_model = base_model as LabelModel; 47 | label_model.content = label_model.content.strip (); 48 | if (label_model.content == "") { continue; } 49 | break; 50 | case WidgetFactory.WidgetType.IMAGE: 51 | var image_model = base_model as ImageModel; 52 | if (image_model.path == "") { continue; } 53 | break; 54 | } 55 | var r = Json.gobject_serialize (base_model); 56 | builder.add_value (r); 57 | } 58 | builder.end_array (); 59 | builder.end_object (); 60 | 61 | return builder.get_root (); 62 | } 63 | 64 | private void write_file (Json.Node node) { 65 | var generator = new Json.Generator () { 66 | pretty = true, 67 | root = node 68 | }; 69 | 70 | var app_dir = Environment.get_user_data_dir () + Const.APP_PATH; 71 | var path = "%s/%s.json".printf (app_dir, "boards"); 72 | 73 | try { 74 | // Ensure directory exists 75 | var dir = File.new_for_path (app_dir); 76 | if (!dir.query_exists ()) { 77 | dir.make_directory_with_parents (); 78 | } 79 | 80 | // Create backup of existing file 81 | var file = File.new_for_path (path); 82 | if (file.query_exists ()) { 83 | var backup_path = "%s.backup".printf (path); 84 | var backup_file = File.new_for_path (backup_path); 85 | file.copy (backup_file, FileCopyFlags.OVERWRITE); 86 | } 87 | 88 | // Write to temporary file first 89 | var temp_path = "%s.tmp".printf (path); 90 | var temp_file = File.new_for_path (temp_path); 91 | generator.to_file (temp_path); 92 | 93 | // Move temp file to final location 94 | temp_file.move (file, FileCopyFlags.OVERWRITE); 95 | 96 | } catch (Error e) { 97 | warning ("Failed to save boards: %s", e.message); 98 | 99 | // Try to restore backup if it exists 100 | var backup_path = "%s.backup".printf (path); 101 | var backup_file = File.new_for_path (backup_path); 102 | if (backup_file.query_exists ()) { 103 | try { 104 | backup_file.copy (File.new_for_path (path), FileCopyFlags.OVERWRITE); 105 | info ("Restored backup file"); 106 | } catch (Error restore_error) { 107 | warning ("Failed to restore backup: %s", restore_error.message); 108 | } 109 | } 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Controllers/RowController.vala: -------------------------------------------------------------------------------- 1 | public class RowController { 2 | 3 | public Row row { private set; get; } 4 | private BoardController board_controller; 5 | private string name; 6 | private string color; 7 | 8 | public RowController (BoardController board_controller, string name, string color) { 9 | row = new Row (board_controller, name, color); 10 | this.board_controller = board_controller; 11 | this.name = name; 12 | this.color = color; 13 | 14 | row.state_flags_changed.connect (on_state_flags_changed); 15 | row.delete_eventbox.button_press_event.connect (on_delete_button_press); 16 | row.grid_eventbox.button_press_event.connect (on_button_press); 17 | row.rename_item.activate.connect (on_activate); 18 | row.menu_item.color_changed.connect (on_color_changed); 19 | } 20 | 21 | private void on_color_changed (string color) { 22 | change_color (color); 23 | } 24 | 25 | private void on_activate () { 26 | rename (); 27 | } 28 | 29 | private bool on_delete_button_press () { 30 | show_delete_dialog (); 31 | return false; 32 | } 33 | 34 | private bool on_button_press (Gdk.EventButton event) { 35 | if (event.button == 3) { 36 | row.menu.popup_at_pointer (); 37 | } 38 | return false; 39 | } 40 | 41 | private void on_state_flags_changed () { 42 | var flag_string = row.get_state_flags ().to_string (); 43 | if (flag_string == "GTK_STATE_FLAG_PRELIGHT") { 44 | var b = row.grid.get_child_at (2, 0); 45 | row.delete_button.set_from_icon_name ("application-exit-symbolic", Gtk.IconSize.BUTTON); 46 | if (b == null) { 47 | } 48 | } else { 49 | if (flag_string != "GTK_STATE_FLAG_ACTIVE") { 50 | row.delete_button.set_from_icon_name ("", Gtk.IconSize.BUTTON); 51 | } 52 | } 53 | } 54 | 55 | private void change_color (string color_name) { 56 | var color = Colors.from_name (color_name); 57 | string style = """ 58 | @define-color colorAccent %s; 59 | @define-color accent_color %s; 60 | """.printf (color, color); 61 | 62 | var style_provider = new Gtk.CssProvider (); 63 | try { 64 | style_provider.load_from_data (style, style.length); 65 | } catch (Error e) { 66 | warning (e.message); 67 | } 68 | unowned Gtk.StyleContext style_context = row.source_color.get_style_context (); 69 | style_context.add_provider (style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 70 | 71 | board_controller.model.color = color_name; 72 | } 73 | 74 | public void rename () { 75 | row.display_name_label.hide (); 76 | var textview = new Gtk.TextView () { 77 | wrap_mode = Gtk.WrapMode.NONE, 78 | halign = Gtk.Align.START, 79 | valign = Gtk.Align.CENTER, 80 | can_focus = true, 81 | hexpand = true, 82 | }; 83 | textview.grab_focus (); 84 | var textbuffer = textview.get_buffer (); 85 | textbuffer.set_text (row.display_name_label.label); 86 | 87 | textview.focus_out_event.connect (() => { 88 | if (textbuffer.text.strip () == "") { 89 | textbuffer.text = _("Board"); 90 | } 91 | textview.hide (); 92 | board_controller.model.title = textbuffer.text; 93 | row.display_name_label.label = textbuffer.text; 94 | row.display_name_label.show (); 95 | row.renamed (textbuffer.text); 96 | return false; 97 | }); 98 | 99 | textview.key_press_event.connect ((event) => { 100 | if (event.keyval == Gdk.Key.Return) { 101 | board_controller.board.grab_focus (); 102 | return true; 103 | } 104 | return false; 105 | }); 106 | 107 | Gtk.TextIter start_iter, end_iter; 108 | textview.buffer.get_start_iter (out start_iter); 109 | textview.buffer.get_end_iter (out end_iter); 110 | textview.buffer.select_range (start_iter, end_iter); 111 | 112 | textview.get_style_context ().add_class ("editable-label"); 113 | row.grid.attach (textview, 1, 0); 114 | textview.show (); 115 | textview.grab_focus (); 116 | } 117 | 118 | private void show_delete_dialog () { 119 | var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( 120 | _("Do you want to delete \"%s\"?").printf (row.display_name_label.label), 121 | _("The content of this board will be deleted completely and cannot be restored."), 122 | "dialog-warning", 123 | Gtk.ButtonsType.CANCEL 124 | ); 125 | message_dialog.transient_for = row.get_toplevel () as Gtk.Window; 126 | 127 | var suggested_button = new Gtk.Button.with_label (_("Delete")); 128 | suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); 129 | message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); 130 | 131 | message_dialog.show_all (); 132 | row.activate (); 133 | board_controller.window.sensitive = false; 134 | message_dialog.response.connect ((response_id) => { 135 | if (response_id == Gtk.ResponseType.ACCEPT) { 136 | row.delete_row (row.get_index ()); 137 | if (board_controller.window.listbox.get_children ().length () == 1) { 138 | board_controller.window.create_new_board (); 139 | } 140 | board_controller.board.destroy (); 141 | row.destroy (); 142 | } else { 143 | board_controller.window.sensitive = true; 144 | } 145 | message_dialog.close (); 146 | }); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/Helper/ErrorHandler.vala: -------------------------------------------------------------------------------- 1 | namespace ErrorHandler { 2 | 3 | public class ErrorDialog : Gtk.Dialog { 4 | private Gtk.Label message_label; 5 | private Gtk.TextView details_text; 6 | private Gtk.Expander details_expander; 7 | 8 | public ErrorDialog (Gtk.Window? parent, string title, string message, string? details = null) { 9 | this.title = title; 10 | transient_for = parent; 11 | modal = true; 12 | resizable = false; 13 | 14 | var content_area = get_content_area (); 15 | content_area.spacing = 12; 16 | content_area.margin = 20; 17 | 18 | // Main message 19 | message_label = new Gtk.Label (message) { 20 | wrap = true, 21 | max_width_chars = 50, 22 | justify = Gtk.Justification.CENTER 23 | }; 24 | content_area.add (message_label); 25 | 26 | // Details expander (if details provided) 27 | if (details != null && details != "") { 28 | details_text = new Gtk.TextView () { 29 | editable = false, 30 | wrap_mode = Gtk.WrapMode.WORD, 31 | height_request = 150 32 | }; 33 | details_text.buffer.text = details; 34 | 35 | details_expander = new Gtk.Expander.with_mnemonic (_("_Show Details")) { 36 | expanded = false 37 | }; 38 | details_expander.add (details_text); 39 | content_area.add (details_expander); 40 | } 41 | 42 | // Buttons 43 | add_button (_("_OK"), Gtk.ResponseType.OK); 44 | 45 | show_all (); 46 | } 47 | } 48 | 49 | public void show_error_dialog (Gtk.Window? parent, string title, string message, string? details = null) { 50 | var dialog = new ErrorDialog (parent, title, message, details); 51 | dialog.run (); 52 | dialog.destroy (); 53 | } 54 | 55 | public void show_warning_dialog (Gtk.Window? parent, string title, string message) { 56 | var dialog = new Gtk.MessageDialog ( 57 | parent, 58 | Gtk.DialogFlags.MODAL, 59 | Gtk.MessageType.WARNING, 60 | Gtk.ButtonsType.OK, 61 | "%s", message 62 | ); 63 | dialog.title = title; 64 | dialog.run (); 65 | dialog.destroy (); 66 | } 67 | 68 | public void show_info_dialog (Gtk.Window? parent, string title, string message) { 69 | var dialog = new Gtk.MessageDialog ( 70 | parent, 71 | Gtk.DialogFlags.MODAL, 72 | Gtk.MessageType.INFO, 73 | Gtk.ButtonsType.OK, 74 | "%s", message 75 | ); 76 | dialog.title = title; 77 | dialog.run (); 78 | dialog.destroy (); 79 | } 80 | 81 | public bool confirm_dialog (Gtk.Window? parent, string title, string message) { 82 | var dialog = new Gtk.MessageDialog ( 83 | parent, 84 | Gtk.DialogFlags.MODAL, 85 | Gtk.MessageType.QUESTION, 86 | Gtk.ButtonsType.YES_NO, 87 | "%s", message 88 | ); 89 | dialog.title = title; 90 | var response = dialog.run (); 91 | dialog.destroy (); 92 | return response == Gtk.ResponseType.YES; 93 | } 94 | 95 | public string get_user_friendly_error_message (Error error) { 96 | switch (error.code) { 97 | case FileError.NOENT: 98 | return _("File not found. The file may have been moved or deleted."); 99 | case FileError.ACCES: 100 | return _("Permission denied. You don't have permission to access this file."); 101 | case FileError.ISDIR: 102 | return _("Expected a file but found a directory."); 103 | case FileError.NOTDIR: 104 | return _("Expected a directory but found a file."); 105 | case FileError.NOSPC: 106 | return _("No space left on device."); 107 | case FileError.ROFS: 108 | return _("Read-only file system."); 109 | case FileError.EXIST: 110 | return _("File already exists."); 111 | case FileError.INVAL: 112 | return _("Invalid argument."); 113 | case FileError.IO: 114 | return _("Input/output error occurred."); 115 | default: 116 | return error.message; 117 | } 118 | } 119 | 120 | public bool validate_file_path (string path) { 121 | if (path == null || path == "") { 122 | return false; 123 | } 124 | 125 | var file = File.new_for_path (path); 126 | return file.query_exists (); 127 | } 128 | 129 | public bool validate_image_file (string path) { 130 | if (!validate_file_path (path)) { 131 | return false; 132 | } 133 | 134 | try { 135 | var file = File.new_for_path (path); 136 | var info = file.query_info ("standard::content-type", FileQueryInfoFlags.NONE); 137 | var content_type = info.get_content_type (); 138 | 139 | // Check if it's an image type 140 | return content_type.has_prefix ("image/"); 141 | } catch (Error e) { 142 | return false; 143 | } 144 | } 145 | 146 | public string? get_safe_filename (string original_path) { 147 | if (original_path == null || original_path == "") { 148 | return null; 149 | } 150 | 151 | var file = File.new_for_path (original_path); 152 | var basename = file.get_basename (); 153 | 154 | // Remove potentially dangerous characters 155 | var safe_name = basename.replace ("/", "_") 156 | .replace ("\\", "_") 157 | .replace (":", "_") 158 | .replace ("*", "_") 159 | .replace ("?", "_") 160 | .replace ("\"", "_") 161 | .replace ("<", "_") 162 | .replace (">", "_") 163 | .replace ("|", "_"); 164 | 165 | return safe_name; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Helper/LazyLoader.vala: -------------------------------------------------------------------------------- 1 | namespace LazyLoader { 2 | 3 | public class LazyWidget { 4 | public BaseModel model; 5 | public BoardController board_controller; 6 | public Movable? widget; 7 | public bool is_loaded; 8 | public bool is_visible; 9 | public int x; 10 | public int y; 11 | 12 | public LazyWidget (BaseModel model, BoardController board_controller, int x, int y) { 13 | this.model = model; 14 | this.board_controller = board_controller; 15 | this.x = x; 16 | this.y = y; 17 | this.is_loaded = false; 18 | this.is_visible = false; 19 | } 20 | 21 | public void load_widget () { 22 | if (is_loaded) { 23 | return; 24 | } 25 | 26 | widget = WidgetFactory.create_widget_from_string (model.model, board_controller, x, y, model); 27 | if (widget != null) { 28 | is_loaded = true; 29 | if (is_visible) { 30 | board_controller.overlay.add_overlay (widget); 31 | } 32 | } 33 | } 34 | 35 | public void unload_widget () { 36 | if (!is_loaded || widget == null) { 37 | return; 38 | } 39 | 40 | if (is_visible) { 41 | board_controller.overlay.remove (widget); 42 | } 43 | 44 | widget.destroy (); 45 | widget = null; 46 | is_loaded = false; 47 | } 48 | 49 | public void show_widget () { 50 | if (!is_loaded) { 51 | load_widget (); 52 | } 53 | 54 | if (widget != null && !is_visible) { 55 | board_controller.overlay.add_overlay (widget); 56 | is_visible = true; 57 | } 58 | } 59 | 60 | public void hide_widget () { 61 | if (widget != null && is_visible) { 62 | board_controller.overlay.remove (widget); 63 | is_visible = false; 64 | } 65 | } 66 | 67 | public bool is_in_viewport (int viewport_x, int viewport_y, int viewport_width, int viewport_height) { 68 | return (x >= viewport_x - 100 && x <= viewport_x + viewport_width + 100 && 69 | y >= viewport_y - 100 && y <= viewport_y + viewport_height + 100); 70 | } 71 | } 72 | 73 | public class LazyLoadingManager { 74 | private Gee.ArrayList lazy_widgets; 75 | private BoardController board_controller; 76 | private int viewport_x; 77 | private int viewport_y; 78 | private int viewport_width; 79 | private int viewport_height; 80 | private bool is_enabled; 81 | 82 | public LazyLoadingManager (BoardController board_controller) { 83 | this.board_controller = board_controller; 84 | this.lazy_widgets = new Gee.ArrayList (); 85 | this.is_enabled = true; 86 | this.viewport_x = 0; 87 | this.viewport_y = 0; 88 | this.viewport_width = 800; 89 | this.viewport_height = 600; 90 | } 91 | 92 | public void add_widget (BaseModel model, int x, int y) { 93 | var lazy_widget = new LazyWidget (model, board_controller, x, y); 94 | lazy_widgets.add (lazy_widget); 95 | 96 | if (is_enabled && lazy_widget.is_in_viewport (viewport_x, viewport_y, viewport_width, viewport_height)) { 97 | lazy_widget.show_widget (); 98 | } 99 | } 100 | 101 | public void remove_widget (BaseModel model) { 102 | for (int i = 0; i < lazy_widgets.size; i++) { 103 | if (lazy_widgets[i].model == model) { 104 | lazy_widgets[i].unload_widget (); 105 | lazy_widgets.remove_at (i); 106 | break; 107 | } 108 | } 109 | } 110 | 111 | public void update_viewport (int x, int y, int width, int height) { 112 | viewport_x = x; 113 | viewport_y = y; 114 | viewport_width = width; 115 | viewport_height = height; 116 | 117 | if (!is_enabled) { 118 | return; 119 | } 120 | 121 | foreach (var lazy_widget in lazy_widgets) { 122 | bool should_be_visible = lazy_widget.is_in_viewport (x, y, width, height); 123 | 124 | if (should_be_visible && !lazy_widget.is_visible) { 125 | lazy_widget.show_widget (); 126 | } else if (!should_be_visible && lazy_widget.is_visible) { 127 | lazy_widget.hide_widget (); 128 | } 129 | } 130 | } 131 | 132 | public void load_all_widgets () { 133 | foreach (var lazy_widget in lazy_widgets) { 134 | lazy_widget.show_widget (); 135 | } 136 | } 137 | 138 | public void unload_all_widgets () { 139 | foreach (var lazy_widget in lazy_widgets) { 140 | lazy_widget.unload_widget (); 141 | } 142 | } 143 | 144 | public void set_enabled (bool enabled) { 145 | is_enabled = enabled; 146 | 147 | if (enabled) { 148 | update_viewport (viewport_x, viewport_y, viewport_width, viewport_height); 149 | } else { 150 | load_all_widgets (); 151 | } 152 | } 153 | 154 | public int get_loaded_count () { 155 | int count = 0; 156 | foreach (var lazy_widget in lazy_widgets) { 157 | if (lazy_widget.is_loaded) { 158 | count++; 159 | } 160 | } 161 | return count; 162 | } 163 | 164 | public int get_total_count () { 165 | return lazy_widgets.size; 166 | } 167 | 168 | public void cleanup_unused_widgets () { 169 | foreach (var lazy_widget in lazy_widgets) { 170 | if (lazy_widget.is_loaded && !lazy_widget.is_visible) { 171 | // Unload widgets that haven't been visible for a while 172 | lazy_widget.unload_widget (); 173 | } 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Helper/WidgetFactory.vala: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-3.0-or-later 3 | * SPDX-FileCopyrightText: 2021 Pierre Fabarius 4 | */ 5 | 6 | public class WidgetFactory { 7 | 8 | public enum WidgetType { 9 | TEXT, 10 | LABEL, 11 | IMAGE 12 | } 13 | 14 | public static WidgetType get_widget_type_from_string (string type_name) { 15 | switch (type_name) { 16 | case "TextWidget": 17 | return WidgetType.TEXT; 18 | case "LabelWidget": 19 | return WidgetType.LABEL; 20 | case "ImageWidget": 21 | return WidgetType.IMAGE; 22 | default: 23 | warning ("Unknown widget type: %s", type_name); 24 | return WidgetType.TEXT; 25 | } 26 | } 27 | 28 | public static string get_string_from_widget_type (WidgetType type) { 29 | switch (type) { 30 | case WidgetType.TEXT: 31 | return "TextWidget"; 32 | case WidgetType.LABEL: 33 | return "LabelWidget"; 34 | case WidgetType.IMAGE: 35 | return "ImageWidget"; 36 | default: 37 | return "TextWidget"; 38 | } 39 | } 40 | 41 | public static WidgetType get_widget_type_from_class (Type class_type) { 42 | if (class_type == typeof (TextWidget)) { 43 | return WidgetType.TEXT; 44 | } else if (class_type == typeof (LabelWidget)) { 45 | return WidgetType.LABEL; 46 | } else if (class_type == typeof (ImageWidget)) { 47 | return WidgetType.IMAGE; 48 | } else { 49 | warning ("Unknown widget class type: %s", class_type.name ()); 50 | return WidgetType.TEXT; 51 | } 52 | } 53 | 54 | public static Type get_class_type_from_widget_type (WidgetType type) { 55 | switch (type) { 56 | case WidgetType.TEXT: 57 | return typeof (TextWidget); 58 | case WidgetType.LABEL: 59 | return typeof (LabelWidget); 60 | case WidgetType.IMAGE: 61 | return typeof (ImageWidget); 62 | default: 63 | return typeof (TextWidget); 64 | } 65 | } 66 | 67 | public static BaseModel create_model (WidgetType type) { 68 | switch (type) { 69 | case WidgetType.TEXT: 70 | return new TextModel (); 71 | case WidgetType.LABEL: 72 | return new LabelModel (); 73 | case WidgetType.IMAGE: 74 | return new ImageModel (); 75 | default: 76 | return new TextModel (); 77 | } 78 | } 79 | 80 | public static BaseModel create_model_from_string (string type_name) { 81 | return create_model (get_widget_type_from_string (type_name)); 82 | } 83 | 84 | public static BaseModel create_model_from_class (Type class_type) { 85 | return create_model (get_widget_type_from_class (class_type)); 86 | } 87 | 88 | public static WidgetController create_controller (WidgetType type, BoardController board_controller, int x, int y, BaseModel? model = null) { 89 | BaseModel actual_model = model ?? create_model (type); 90 | 91 | switch (type) { 92 | case WidgetType.TEXT: 93 | return new TextController (board_controller, x, y, (TextModel) actual_model); 94 | case WidgetType.LABEL: 95 | return new LabelController (board_controller, x, y, (LabelModel) actual_model); 96 | case WidgetType.IMAGE: 97 | return new ImageController (board_controller, x, y, (ImageModel) actual_model); 98 | default: 99 | return new TextController (board_controller, x, y, (TextModel) actual_model); 100 | } 101 | } 102 | 103 | public static WidgetController create_controller_from_string (string type_name, BoardController board_controller, int x, int y, BaseModel? model = null) { 104 | return create_controller (get_widget_type_from_string (type_name), board_controller, x, y, model); 105 | } 106 | 107 | public static WidgetController create_controller_from_class (Type class_type, BoardController board_controller, int x, int y, BaseModel? model = null) { 108 | return create_controller (get_widget_type_from_class (class_type), board_controller, x, y, model); 109 | } 110 | 111 | public static Movable create_widget (WidgetType type, BoardController board_controller, int x, int y, BaseModel? model = null) { 112 | var controller = create_controller (type, board_controller, x, y, model); 113 | return controller.get_movable (); 114 | } 115 | 116 | public static Movable create_widget_from_string (string type_name, BoardController board_controller, int x, int y, BaseModel? model = null) { 117 | return create_widget (get_widget_type_from_string (type_name), board_controller, x, y, model); 118 | } 119 | 120 | public static Movable create_widget_from_class (Type class_type, BoardController board_controller, int x, int y, BaseModel? model = null) { 121 | return create_widget (get_widget_type_from_class (class_type), board_controller, x, y, model); 122 | } 123 | 124 | public static void focus_widget (Movable widget) { 125 | if (widget is TextWidget) { 126 | var text_widget = widget as TextWidget; 127 | text_widget.textview.grab_focus (); 128 | } else if (widget is LabelWidget) { 129 | var label_widget = widget as LabelWidget; 130 | label_widget.textview.grab_focus (); 131 | } 132 | // ImageWidget doesn't need focus handling 133 | } 134 | 135 | public static void remove_widget_from_model (Movable widget, Gee.ArrayList widgets) { 136 | if (widget is TextWidget) { 137 | var text_widget = widget as TextWidget; 138 | widgets.remove (text_widget.controller.model); 139 | } else if (widget is LabelWidget) { 140 | var label_widget = widget as LabelWidget; 141 | widgets.remove (label_widget.controller.model); 142 | } else if (widget is ImageWidget) { 143 | var image_widget = widget as ImageWidget; 144 | widgets.remove (image_widget.controller.model); 145 | } 146 | } 147 | 148 | public static void add_widget_to_model (Movable widget, Gee.ArrayList widgets) { 149 | if (widget is TextWidget) { 150 | var text_widget = widget as TextWidget; 151 | widgets.add (text_widget.controller.model); 152 | } else if (widget is LabelWidget) { 153 | var label_widget = widget as LabelWidget; 154 | widgets.add (label_widget.controller.model); 155 | } else if (widget is ImageWidget) { 156 | var image_widget = widget as ImageWidget; 157 | widgets.add (image_widget.controller.model); 158 | } 159 | } 160 | 161 | public static void reorder_widget_in_model (Movable widget, Gee.ArrayList widgets) { 162 | remove_widget_from_model (widget, widgets); 163 | add_widget_to_model (widget, widgets); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /data/icons/16/com.github.brain_child.moobo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 39 | 43 | 50 | 51 | 53 | 55 | 59 | 63 | 64 | 73 | 75 | 79 | 83 | 87 | 91 | 92 | 94 | 98 | 102 | 103 | 113 | 122 | 124 | 128 | 132 | 136 | 140 | 141 | 142 | 144 | 145 | 147 | image/svg+xml 148 | 150 | 151 | 152 | 153 | 162 | 169 | 176 | 183 | 192 | 201 | 208 | 215 | 222 | 223 | -------------------------------------------------------------------------------- /src/Helper/ImageCache.vala: -------------------------------------------------------------------------------- 1 | namespace ImageCache { 2 | 3 | public class CacheEntry { 4 | public Gdk.Pixbuf original_pixbuf; 5 | public Gee.HashMap scaled_cache; 6 | public int64 last_accessed; 7 | public int access_count; 8 | 9 | public CacheEntry (Gdk.Pixbuf pixbuf) { 10 | this.original_pixbuf = pixbuf; 11 | this.scaled_cache = new Gee.HashMap (); 12 | this.last_accessed = get_monotonic_time (); 13 | this.access_count = 1; 14 | } 15 | 16 | public void touch () { 17 | this.last_accessed = get_monotonic_time (); 18 | this.access_count++; 19 | } 20 | 21 | public string get_cache_key (int width, int height) { 22 | return "%dx%d".printf (width, height); 23 | } 24 | 25 | public Gdk.Pixbuf? get_scaled (int width, int height) { 26 | var key = get_cache_key (width, height); 27 | return scaled_cache.get (key); 28 | } 29 | 30 | public void set_scaled (int width, int height, Gdk.Pixbuf pixbuf) { 31 | var key = get_cache_key (width, height); 32 | scaled_cache.set (key, pixbuf); 33 | } 34 | } 35 | 36 | public class ImageCacheManager { 37 | private static ImageCacheManager? instance = null; 38 | private Gee.HashMap cache; 39 | private int max_cache_size; 40 | private int64 max_cache_age; // in microseconds 41 | private Timer cleanup_timer; 42 | 43 | public static ImageCacheManager get_instance () { 44 | if (instance == null) { 45 | instance = new ImageCacheManager (); 46 | } 47 | return instance; 48 | } 49 | 50 | private ImageCacheManager () { 51 | cache = new Gee.HashMap (); 52 | max_cache_size = 50; // Maximum number of images to cache 53 | max_cache_age = 5 * 60 * 1000000; // 5 minutes in microseconds 54 | cleanup_timer = new Timer (); 55 | 56 | // Start periodic cleanup 57 | Timeout.add_seconds (60, cleanup_expired_entries); 58 | } 59 | 60 | public Gdk.Pixbuf? get_image (string path, int? width = null, int? height = null) { 61 | if (path == null || path == "") { 62 | return null; 63 | } 64 | 65 | var entry = cache.get (path); 66 | if (entry == null) { 67 | // Load image from file 68 | try { 69 | var pixbuf = new Gdk.Pixbuf.from_file (path); 70 | if (pixbuf == null) { 71 | return null; 72 | } 73 | entry = new CacheEntry (pixbuf); 74 | cache.set (path, entry); 75 | } catch (Error e) { 76 | warning ("Failed to load image %s: %s", path, e.message); 77 | return null; 78 | } 79 | } else { 80 | entry.touch (); 81 | } 82 | 83 | // Return scaled version if requested 84 | if (width != null && height != null) { 85 | var scaled = entry.get_scaled (width, height); 86 | if (scaled == null) { 87 | // Create scaled version 88 | try { 89 | scaled = entry.original_pixbuf.scale_simple (width, height, Gdk.InterpType.BILINEAR); 90 | entry.set_scaled (width, height, scaled); 91 | } catch (Error e) { 92 | warning ("Failed to scale image: %s", e.message); 93 | return entry.original_pixbuf; 94 | } 95 | } 96 | return scaled; 97 | } 98 | 99 | return entry.original_pixbuf; 100 | } 101 | 102 | public void preload_image (string path) { 103 | if (path == null || path == "" || cache.has_key (path)) { 104 | return; 105 | } 106 | 107 | try { 108 | var pixbuf = new Gdk.Pixbuf.from_file (path); 109 | if (pixbuf != null) { 110 | var entry = new CacheEntry (pixbuf); 111 | cache.set (path, entry); 112 | } 113 | } catch (Error e) { 114 | warning ("Failed to preload image %s: %s", path, e.message); 115 | } 116 | } 117 | 118 | public void remove_image (string path) { 119 | cache.unset (path); 120 | } 121 | 122 | public void clear_cache () { 123 | cache.clear (); 124 | } 125 | 126 | public int get_cache_size () { 127 | return cache.size; 128 | } 129 | 130 | public int64 get_cache_memory_usage () { 131 | int64 total_size = 0; 132 | foreach (var entry in cache.values) { 133 | // Estimate memory usage (rough calculation) 134 | total_size += entry.original_pixbuf.get_byte_length (); 135 | foreach (var scaled in entry.scaled_cache.values) { 136 | total_size += scaled.get_byte_length (); 137 | } 138 | } 139 | return total_size; 140 | } 141 | 142 | private bool cleanup_expired_entries () { 143 | var now = get_monotonic_time (); 144 | var to_remove = new Gee.ArrayList (); 145 | 146 | foreach (var entry in cache.entries) { 147 | if (now - entry.value.last_accessed > max_cache_age) { 148 | to_remove.add (entry.key); 149 | } 150 | } 151 | 152 | foreach (var key in to_remove) { 153 | cache.unset (key); 154 | } 155 | 156 | // If cache is still too large, remove least recently used entries 157 | if (cache.size > max_cache_size) { 158 | var entries_list = new Gee.ArrayList> (); 159 | foreach (var entry in cache.entries) { 160 | entries_list.add (entry); 161 | } 162 | 163 | // Sort by last_accessed (oldest first) 164 | entries_list.sort ((a, b) => { 165 | return (int) (a.value.last_accessed - b.value.last_accessed); 166 | }); 167 | 168 | var to_remove_lru = new Gee.ArrayList (); 169 | for (int i = 0; i < entries_list.size - max_cache_size; i++) { 170 | to_remove_lru.add (entries_list[i].key); 171 | } 172 | 173 | foreach (var key in to_remove_lru) { 174 | cache.unset (key); 175 | } 176 | } 177 | 178 | return true; // Continue the timeout 179 | } 180 | 181 | public void set_max_cache_size (int size) { 182 | max_cache_size = size; 183 | } 184 | 185 | public void set_max_cache_age (int64 age_microseconds) { 186 | max_cache_age = age_microseconds; 187 | } 188 | } 189 | } 190 | 191 | -------------------------------------------------------------------------------- /src/Widgets/Board.vala: -------------------------------------------------------------------------------- 1 | public class Board : Gtk.EventBox { 2 | 3 | public string title { set; get; default = _("Board"); } 4 | public Gtk.Overlay overlay { private set; get; } 5 | public BoardController controller { private set; get; } 6 | public Gtk.Revealer revealer { private set; get; } 7 | public Gtk.Scale scale { set; get; } 8 | private Gtk.Revealer drag_indicator { private set; get; } 9 | 10 | public Board (BoardController controller) { 11 | this.controller = controller; 12 | can_focus = true; 13 | 14 | // Enable drag and drop 15 | Gtk.drag_dest_set (this, Gtk.DestDefaults.ALL, {}, Gdk.DragAction.COPY); 16 | Gtk.drag_dest_add_uri_targets (this); 17 | drag_data_received.connect (on_drag_data_received); 18 | drag_motion.connect (on_drag_motion); 19 | drag_leave.connect (on_drag_leave); 20 | 21 | scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0.1, 2, 0.1) { 22 | value_pos = Gtk.PositionType.BOTTOM, 23 | halign = Gtk.Align.CENTER, 24 | valign = Gtk.Align.END, 25 | draw_value = false, 26 | }; 27 | scale.add_mark (1, Gtk.PositionType.BOTTOM, null); 28 | scale.set_value (1); 29 | scale.set_size_request (300, -1); 30 | 31 | revealer = new Gtk.Revealer () { 32 | valign = Gtk.Align.END, 33 | margin = 30, 34 | }; 35 | revealer.add (scale); 36 | 37 | overlay = new Gtk.Overlay (); 38 | overlay.add (draw_grid ()); 39 | overlay.add_overlay (revealer); 40 | 41 | // Create drag indicator 42 | var drag_label = new Gtk.Label (_("Drop image files here")) { 43 | halign = Gtk.Align.CENTER, 44 | valign = Gtk.Align.CENTER 45 | }; 46 | drag_label.get_style_context ().add_class ("drag-indicator"); 47 | 48 | drag_indicator = new Gtk.Revealer () { 49 | valign = Gtk.Align.FILL, 50 | halign = Gtk.Align.FILL, 51 | reveal_child = false 52 | }; 53 | drag_indicator.add (drag_label); 54 | overlay.add_overlay (drag_indicator); 55 | 56 | add (overlay); 57 | show_all (); 58 | } 59 | 60 | private Gtk.DrawingArea draw_grid () { 61 | var drawing_area = new Gtk.DrawingArea (); 62 | drawing_area.draw.connect ((widget, context) => { 63 | var width = get_allocated_width (); 64 | var height = get_allocated_height (); 65 | context.set_source_rgb (0.7, 0.7, 0.7); 66 | context.fill (); 67 | var spacing = 20; 68 | var y = 0; 69 | var x = spacing; 70 | context.set_line_width (2.0); 71 | while (x <= width ) { 72 | while (y <= height) { 73 | context.move_to (x, y); 74 | context.line_to (x, y + 2); 75 | y += spacing; 76 | } 77 | y = 0; 78 | x += spacing; 79 | } 80 | context.stroke (); 81 | return true; 82 | }); 83 | 84 | 85 | return drawing_area; 86 | } 87 | 88 | private void on_drag_data_received (Gtk.Widget widget, Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { 89 | // Hide drag indicator 90 | drag_indicator.reveal_child = false; 91 | 92 | if (selection_data.get_length () >= 0) { 93 | string[] uris = selection_data.get_uris (); 94 | 95 | foreach (string uri in uris) { 96 | // Convert URI to file path 97 | var file = File.new_for_uri (uri); 98 | string file_path = file.get_path (); 99 | 100 | if (file_path != null && is_image_file (file_path)) { 101 | create_image_widget_from_file (file_path, x, y); 102 | } 103 | } 104 | 105 | Gtk.drag_finish (context, true, false, time); 106 | } else { 107 | Gtk.drag_finish (context, false, false, time); 108 | } 109 | } 110 | 111 | private bool on_drag_motion (Gtk.Widget widget, Gdk.DragContext context, int x, int y, uint time) { 112 | // Show drag indicator 113 | drag_indicator.reveal_child = true; 114 | 115 | // Set visual feedback based on whether we can accept the drop 116 | Gdk.drag_status (context, Gdk.DragAction.COPY, time); 117 | 118 | return true; 119 | } 120 | 121 | private void on_drag_leave (Gtk.Widget widget, Gdk.DragContext context, uint time) { 122 | // Hide drag indicator 123 | drag_indicator.reveal_child = false; 124 | } 125 | 126 | private bool is_image_file (string file_path) { 127 | string[] image_extensions = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".svg", ".webp"}; 128 | string lower_path = file_path.down (); 129 | 130 | foreach (string ext in image_extensions) { 131 | if (lower_path.has_suffix (ext)) { 132 | return true; 133 | } 134 | } 135 | return false; 136 | } 137 | 138 | private void create_image_widget_from_file (string file_path, int x, int y) { 139 | try { 140 | // Validate the image file 141 | if (!ErrorHandler.validate_image_file (file_path)) { 142 | ErrorHandler.show_error_dialog ( 143 | controller.window, 144 | _("Invalid Image File"), 145 | _("The dropped file is not a valid image or cannot be read."), 146 | _("Please drop a valid image file (PNG, JPEG, GIF, etc.)") 147 | ); 148 | return; 149 | } 150 | 151 | // Create image model with the file path 152 | var image_model = new ImageModel () { 153 | path = file_path, 154 | x = x, 155 | y = y, 156 | scale_factor = 1.0 157 | }; 158 | 159 | // Create image controller and widget 160 | var image_controller = new ImageController (controller, x, y, image_model); 161 | var image_widget = image_controller.get_movable (); 162 | 163 | // Add to model 164 | WidgetFactory.add_widget_to_model (image_widget, controller.model.widgets); 165 | 166 | // Set z_index for new widget to be on top 167 | int max_z_index = 0; 168 | foreach (var model_widget in controller.model.widgets) { 169 | if (model_widget.z_index > max_z_index) { 170 | max_z_index = model_widget.z_index; 171 | } 172 | } 173 | image_model.z_index = max_z_index + 1; 174 | 175 | // Add to overlay 176 | overlay.add_overlay (image_widget); 177 | 178 | // Focus the new widget 179 | WidgetFactory.focus_widget (image_widget); 180 | 181 | } catch (Error e) { 182 | warning ("Failed to create image widget from dropped file: %s", e.message); 183 | ErrorHandler.show_error_dialog ( 184 | controller.window, 185 | _("Failed to Add Image"), 186 | _("Could not add the dropped image file."), 187 | _("Please check that the file is a valid image and try again.") 188 | ); 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/Helper/MemoryManager.vala: -------------------------------------------------------------------------------- 1 | namespace MemoryManager { 2 | 3 | public class MemoryMonitor { 4 | private static MemoryMonitor? instance = null; 5 | private Timer monitor_timer; 6 | private int64 last_memory_usage; 7 | private int64 peak_memory_usage; 8 | private bool is_monitoring; 9 | 10 | // Memory thresholds 11 | private int64 warning_threshold = 100 * 1024 * 1024; // 100MB 12 | private int64 critical_threshold = 200 * 1024 * 1024; // 200MB 13 | 14 | public signal void memory_warning (int64 current_usage, int64 threshold); 15 | public signal void memory_critical (int64 current_usage, int64 threshold); 16 | 17 | public static MemoryMonitor get_instance () { 18 | if (instance == null) { 19 | instance = new MemoryMonitor (); 20 | } 21 | return instance; 22 | } 23 | 24 | private MemoryMonitor () { 25 | is_monitoring = false; 26 | last_memory_usage = 0; 27 | peak_memory_usage = 0; 28 | } 29 | 30 | public void start_monitoring () { 31 | if (is_monitoring) { 32 | return; 33 | } 34 | 35 | is_monitoring = true; 36 | monitor_timer = new Timer (); 37 | 38 | // Monitor every 30 seconds 39 | Timeout.add_seconds (30, monitor_memory); 40 | } 41 | 42 | public void stop_monitoring () { 43 | is_monitoring = false; 44 | } 45 | 46 | private bool monitor_memory () { 47 | if (!is_monitoring) { 48 | return false; // Stop the timeout 49 | } 50 | 51 | var current_usage = get_current_memory_usage (); 52 | 53 | if (current_usage > peak_memory_usage) { 54 | peak_memory_usage = current_usage; 55 | } 56 | 57 | // Check thresholds 58 | if (current_usage > critical_threshold) { 59 | memory_critical (current_usage, critical_threshold); 60 | trigger_cleanup (); 61 | } else if (current_usage > warning_threshold) { 62 | memory_warning (current_usage, warning_threshold); 63 | } 64 | 65 | last_memory_usage = current_usage; 66 | return true; // Continue monitoring 67 | } 68 | 69 | private int64 get_current_memory_usage () { 70 | try { 71 | var file = File.new_for_path ("/proc/self/status"); 72 | if (!file.query_exists ()) { 73 | return 0; 74 | } 75 | 76 | var stream = file.read (); 77 | var data_stream = new DataInputStream (stream); 78 | string? line; 79 | 80 | while ((line = data_stream.read_line (null)) != null) { 81 | // Additional null check for safety 82 | if (line == null) { 83 | continue; 84 | } 85 | 86 | if (line.has_prefix ("VmRSS:")) { 87 | // Extract memory usage in KB - use more robust parsing 88 | var trimmed_line = line.strip (); 89 | var space_index = trimmed_line.index_of_char (' '); 90 | if (space_index > 0) { 91 | var memory_str = trimmed_line.substring (space_index + 1).strip (); 92 | if (memory_str != "") { 93 | return int64.parse (memory_str) * 1024; // Convert to bytes 94 | } 95 | } 96 | } 97 | } 98 | } catch (Error e) { 99 | warning ("Failed to read memory usage: %s", e.message); 100 | } 101 | 102 | return 0; 103 | } 104 | 105 | private void trigger_cleanup () { 106 | // Clean up image cache 107 | var image_cache = ImageCache.ImageCacheManager.get_instance (); 108 | image_cache.clear_cache (); 109 | 110 | // Force garbage collection 111 | Gtk.main_iteration (); 112 | } 113 | 114 | public int64 get_peak_memory_usage () { 115 | return peak_memory_usage; 116 | } 117 | 118 | public int64 get_last_memory_usage () { 119 | return last_memory_usage; 120 | } 121 | 122 | public void set_warning_threshold (int64 threshold) { 123 | warning_threshold = threshold; 124 | } 125 | 126 | public void set_critical_threshold (int64 threshold) { 127 | critical_threshold = threshold; 128 | } 129 | 130 | public string format_memory_size (int64 bytes) { 131 | if (bytes < 1024) { 132 | return "%lld B".printf (bytes); 133 | } else if (bytes < 1024 * 1024) { 134 | return "%.1f KB".printf (bytes / 1024.0); 135 | } else if (bytes < 1024 * 1024 * 1024) { 136 | return "%.1f MB".printf (bytes / (1024.0 * 1024.0)); 137 | } else { 138 | return "%.1f GB".printf (bytes / (1024.0 * 1024.0 * 1024.0)); 139 | } 140 | } 141 | } 142 | 143 | public class ResourceManager { 144 | private static ResourceManager? instance = null; 145 | private Gee.ArrayList tracked_objects; 146 | private bool is_cleanup_scheduled; 147 | 148 | public static ResourceManager get_instance () { 149 | if (instance == null) { 150 | instance = new ResourceManager (); 151 | } 152 | return instance; 153 | } 154 | 155 | private ResourceManager () { 156 | tracked_objects = new Gee.ArrayList (); 157 | is_cleanup_scheduled = false; 158 | } 159 | 160 | public void track_object (GLib.Object obj) { 161 | tracked_objects.add (obj); 162 | obj.weak_ref (on_object_destroyed); 163 | } 164 | 165 | private void on_object_destroyed (GLib.Object obj) { 166 | tracked_objects.remove (obj); 167 | } 168 | 169 | public void schedule_cleanup () { 170 | if (is_cleanup_scheduled) { 171 | return; 172 | } 173 | 174 | is_cleanup_scheduled = true; 175 | Idle.add (perform_cleanup); 176 | } 177 | 178 | private bool perform_cleanup () { 179 | // Clean up any null references 180 | var to_remove = new Gee.ArrayList (); 181 | foreach (var obj in tracked_objects) { 182 | if (obj == null) { 183 | to_remove.add (obj); 184 | } 185 | } 186 | 187 | foreach (var obj in to_remove) { 188 | tracked_objects.remove (obj); 189 | } 190 | 191 | // Force garbage collection 192 | Gtk.main_iteration (); 193 | 194 | is_cleanup_scheduled = false; 195 | return false; // Don't repeat 196 | } 197 | 198 | public int get_tracked_object_count () { 199 | return tracked_objects.size; 200 | } 201 | 202 | public void clear_all_tracked_objects () { 203 | tracked_objects.clear (); 204 | } 205 | } 206 | } 207 | 208 | -------------------------------------------------------------------------------- /src/Widgets/ImageWidget.vala: -------------------------------------------------------------------------------- 1 | public class ImageWidget : Movable { 2 | 3 | public ImageController controller { private set; get; } 4 | public Gtk.Image image { set; get; } 5 | public Gdk.Pixbuf pixbuf { set; get; } 6 | 7 | // Resize handle constants 8 | private const int RESIZE_HANDLE_SIZE = 8; 9 | private const int RESIZE_THRESHOLD = 12; 10 | 11 | public enum ResizeHandle { 12 | NONE, 13 | TOP_LEFT, 14 | TOP_RIGHT, 15 | BOTTOM_LEFT, 16 | BOTTOM_RIGHT, 17 | TOP, 18 | BOTTOM, 19 | LEFT, 20 | RIGHT 21 | } 22 | 23 | public ResizeHandle current_handle { private set; get; default = ResizeHandle.NONE; } 24 | private bool is_resizing = false; 25 | private int resize_start_x; 26 | private int resize_start_y; 27 | private int resize_start_width; 28 | private int resize_start_height; 29 | private double resize_start_scale_factor; 30 | 31 | public ImageWidget (ImageController controller) { 32 | this.controller = controller; 33 | image = new Gtk.Image (); 34 | } 35 | 36 | public void init () { 37 | var max_x = Const.IMG_MAX_WIDTH; 38 | var max_y = Const.IMG_MAX_HEIGHT; 39 | 40 | int width; 41 | int height; 42 | Gdk.Pixbuf.get_file_info (controller.model.path, out width, out height); 43 | var scale_factor = controller.model.scale_factor; 44 | 45 | if (width * scale_factor > max_x) { 46 | scale_factor = ((double) max_x / width); 47 | } 48 | if (height * scale_factor > max_y) { 49 | scale_factor = ((double) max_y / height); 50 | } 51 | width = (int) (scale_factor * width); 52 | height = (int) (scale_factor * height); 53 | 54 | // Use image cache for better performance 55 | var cache_manager = ImageCache.ImageCacheManager.get_instance (); 56 | pixbuf = cache_manager.get_image (controller.model.path, width, height); 57 | 58 | if (pixbuf == null) { 59 | warning ("Failed to load image from cache: %s", controller.model.path); 60 | // Create a placeholder 61 | pixbuf = new Gdk.Pixbuf (Gdk.Colorspace.RGB, false, 8, width, height); 62 | pixbuf.fill (0x80808080u); 63 | } 64 | 65 | image = new Gtk.Image.from_pixbuf (pixbuf); 66 | add (image); 67 | } 68 | 69 | public ResizeHandle get_resize_handle_at_position (double x, double y) { 70 | int width = get_allocated_width (); 71 | int height = get_allocated_height (); 72 | 73 | // Check corner handles first 74 | if (x <= RESIZE_THRESHOLD && y <= RESIZE_THRESHOLD) { 75 | return ResizeHandle.TOP_LEFT; 76 | } else if (x >= width - RESIZE_THRESHOLD && y <= RESIZE_THRESHOLD) { 77 | return ResizeHandle.TOP_RIGHT; 78 | } else if (x <= RESIZE_THRESHOLD && y >= height - RESIZE_THRESHOLD) { 79 | return ResizeHandle.BOTTOM_LEFT; 80 | } else if (x >= width - RESIZE_THRESHOLD && y >= height - RESIZE_THRESHOLD) { 81 | return ResizeHandle.BOTTOM_RIGHT; 82 | } 83 | // Check edge handles 84 | else if (y <= RESIZE_THRESHOLD) { 85 | return ResizeHandle.TOP; 86 | } else if (y >= height - RESIZE_THRESHOLD) { 87 | return ResizeHandle.BOTTOM; 88 | } else if (x <= RESIZE_THRESHOLD) { 89 | return ResizeHandle.LEFT; 90 | } else if (x >= width - RESIZE_THRESHOLD) { 91 | return ResizeHandle.RIGHT; 92 | } 93 | 94 | return ResizeHandle.NONE; 95 | } 96 | 97 | public string get_cursor_name_for_handle (ResizeHandle handle) { 98 | switch (handle) { 99 | case ResizeHandle.TOP_LEFT: 100 | case ResizeHandle.BOTTOM_RIGHT: 101 | return "nw-resize"; 102 | case ResizeHandle.TOP_RIGHT: 103 | case ResizeHandle.BOTTOM_LEFT: 104 | return "ne-resize"; 105 | case ResizeHandle.TOP: 106 | case ResizeHandle.BOTTOM: 107 | return "ns-resize"; 108 | case ResizeHandle.LEFT: 109 | case ResizeHandle.RIGHT: 110 | return "ew-resize"; 111 | default: 112 | return "grab"; 113 | } 114 | } 115 | 116 | public void start_resize (ResizeHandle handle, double x, double y) { 117 | current_handle = handle; 118 | is_resizing = true; 119 | resize_start_x = (int) x; 120 | resize_start_y = (int) y; 121 | resize_start_width = get_allocated_width (); 122 | resize_start_height = get_allocated_height (); 123 | resize_start_scale_factor = controller.model.scale_factor; 124 | } 125 | 126 | public void update_resize (double x, double y) { 127 | if (!is_resizing || current_handle == ResizeHandle.NONE) { 128 | return; 129 | } 130 | 131 | double dx = x - resize_start_x; 132 | double dy = y - resize_start_y; 133 | 134 | // Get original image dimensions 135 | int orig_width, orig_height; 136 | Gdk.Pixbuf.get_file_info (controller.model.path, out orig_width, out orig_height); 137 | 138 | double new_scale_factor = resize_start_scale_factor; 139 | 140 | // Calculate new scale factor based on handle 141 | switch (current_handle) { 142 | case ResizeHandle.BOTTOM_RIGHT: 143 | new_scale_factor = resize_start_scale_factor * (1.0 + dx / (orig_width * resize_start_scale_factor)); 144 | break; 145 | case ResizeHandle.BOTTOM_LEFT: 146 | new_scale_factor = resize_start_scale_factor * (1.0 - dx / (orig_width * resize_start_scale_factor)); 147 | break; 148 | case ResizeHandle.TOP_RIGHT: 149 | new_scale_factor = resize_start_scale_factor * (1.0 + dx / (orig_width * resize_start_scale_factor)); 150 | break; 151 | case ResizeHandle.TOP_LEFT: 152 | new_scale_factor = resize_start_scale_factor * (1.0 - dx / (orig_width * resize_start_scale_factor)); 153 | break; 154 | case ResizeHandle.RIGHT: 155 | new_scale_factor = resize_start_scale_factor * (1.0 + dx / (orig_width * resize_start_scale_factor)); 156 | break; 157 | case ResizeHandle.LEFT: 158 | new_scale_factor = resize_start_scale_factor * (1.0 - dx / (orig_width * resize_start_scale_factor)); 159 | break; 160 | case ResizeHandle.BOTTOM: 161 | new_scale_factor = resize_start_scale_factor * (1.0 + dy / (orig_height * resize_start_scale_factor)); 162 | break; 163 | case ResizeHandle.TOP: 164 | new_scale_factor = resize_start_scale_factor * (1.0 - dy / (orig_height * resize_start_scale_factor)); 165 | break; 166 | } 167 | 168 | // Apply constraints 169 | new_scale_factor = double.max (0.1, new_scale_factor); 170 | new_scale_factor = double.min (3.0, new_scale_factor); 171 | 172 | // Check maximum size constraints 173 | int new_width = (int) (orig_width * new_scale_factor); 174 | int new_height = (int) (orig_height * new_scale_factor); 175 | 176 | if (new_width > Const.IMG_MAX_WIDTH) { 177 | new_scale_factor = (double) Const.IMG_MAX_WIDTH / orig_width; 178 | } 179 | if (new_height > Const.IMG_MAX_HEIGHT) { 180 | new_scale_factor = double.min (new_scale_factor, (double) Const.IMG_MAX_HEIGHT / orig_height); 181 | } 182 | 183 | // Update the model and widget 184 | controller.model.scale_factor = new_scale_factor; 185 | controller.update_widget (); 186 | } 187 | 188 | public void end_resize () { 189 | is_resizing = false; 190 | current_handle = ResizeHandle.NONE; 191 | } 192 | 193 | public bool get_is_resizing () { 194 | return is_resizing; 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/Application.vala: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-3.0-or-later 3 | * SPDX-FileCopyrightText: 2021 Pierre Fabarius 4 | */ 5 | 6 | public class Application : Gtk.Application { 7 | 8 | public static int main (string[] args) { 9 | return new Application ().run (args); 10 | } 11 | 12 | protected override void activate () { 13 | 14 | var css_provider = new Gtk.CssProvider (); 15 | css_provider.load_from_resource ("com/github/brain_child/moobo/styles/Style.css"); 16 | Gtk.StyleContext.add_provider_for_screen ( 17 | Gdk.Screen.get_default (), 18 | css_provider, 19 | Gtk.STYLE_PROVIDER_PRIORITY_USER 20 | ); 21 | 22 | var gtk_settings = Gtk.Settings.get_default (); 23 | 24 | var granite_settings = Granite.Settings.get_default (); 25 | gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; 26 | 27 | 28 | granite_settings.notify["prefers-color-scheme"].connect (() => { 29 | gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; 30 | }); 31 | 32 | var window = new Window (this) { 33 | resizable = true, 34 | }; 35 | calc_window_size_from_screen_resolution (window); 36 | 37 | var quit_action = new SimpleAction ("quit", null); 38 | add_action (quit_action); 39 | set_accels_for_action ("app.quit", {"q", "w"}); 40 | 41 | var rename_action = new SimpleAction ("rename", null); 42 | add_action (rename_action); 43 | set_accels_for_action ("app.rename", {"F2"}); 44 | 45 | var help_action = new SimpleAction ("help", null); 46 | add_action (help_action); 47 | set_accels_for_action ("app.help", {"Escape"}); 48 | 49 | var move_to_foreground_action = new SimpleAction ("move-to-foreground", null); 50 | add_action (move_to_foreground_action); 51 | set_accels_for_action ("app.move-to-foreground", {"bracketright"}); 52 | 53 | var move_to_background_action = new SimpleAction ("move-to-background", null); 54 | add_action (move_to_background_action); 55 | set_accels_for_action ("app.move-to-background", {"bracketleft"}); 56 | 57 | var add_image_action = new SimpleAction ("add-image", null); 58 | add_action (add_image_action); 59 | set_accels_for_action ("app.add-image", {"o"}); 60 | 61 | var add_text_action = new SimpleAction ("add-text", null); 62 | add_action (add_text_action); 63 | set_accels_for_action ("app.add-text", {"t"}); 64 | 65 | var add_label_action = new SimpleAction ("add-label", null); 66 | add_action (add_label_action); 67 | set_accels_for_action ("app.add-label", {"l"}); 68 | 69 | var delete_widget_action = new SimpleAction ("delete-widget", null); 70 | add_action (delete_widget_action); 71 | set_accels_for_action ("app.delete-widget", {"Delete"}); 72 | 73 | add_window (window); 74 | window.show_all (); 75 | 76 | quit_action.activate.connect (() => { 77 | window.save (); 78 | window.destroy (); 79 | }); 80 | 81 | rename_action.activate.connect (() => { 82 | window.rename_selected_board (); 83 | }); 84 | 85 | help_action.activate.connect (() => { 86 | help_window (window).show_all (); 87 | }); 88 | 89 | move_to_foreground_action.activate.connect (() => { 90 | window.move_selected_widget_to_foreground (); 91 | }); 92 | 93 | move_to_background_action.activate.connect (() => { 94 | window.move_selected_widget_to_background (); 95 | }); 96 | 97 | add_image_action.activate.connect (() => { 98 | window.add_image_widget (); 99 | }); 100 | 101 | add_text_action.activate.connect (() => { 102 | window.add_text_widget (); 103 | }); 104 | 105 | add_label_action.activate.connect (() => { 106 | window.add_label_widget (); 107 | }); 108 | 109 | delete_widget_action.activate.connect (() => { 110 | window.delete_selected_widget (); 111 | }); 112 | } 113 | 114 | private void calc_window_size_from_screen_resolution (Window window) { 115 | var display = Gdk.Screen.get_default ().get_display (); 116 | var monitor = display.get_monitor_at_window (window.get_window ()); 117 | var rect = monitor.get_geometry (); 118 | 119 | var width = (int) (rect.width * Const.WIN_SCALE_X); 120 | var height = (int) (rect.height * Const.WIN_SCALE_Y); 121 | 122 | // Ensure minimum size 123 | width = int.max (width, Const.WIN_MIN_WIDTH); 124 | height = int.max (height, Const.WIN_MIN_HEIGHT); 125 | 126 | // Use set_default_size instead of set_size_request to allow resizing 127 | window.set_default_size (width, height); 128 | window.set_geometry_hints (null, Gdk.Geometry () { 129 | min_width = Const.WIN_MIN_WIDTH, 130 | min_height = Const.WIN_MIN_HEIGHT 131 | }, Gdk.WindowHints.MIN_SIZE); 132 | } 133 | 134 | private Hdy.Window help_window (Hdy.ApplicationWindow main_window) { 135 | 136 | var layout = new Gtk.Grid () { 137 | orientation = Gtk.Orientation.VERTICAL, 138 | }; 139 | 140 | var shortcut_window = new Hdy.Window () { 141 | window_position = Gtk.WindowPosition.CENTER_ON_PARENT, 142 | transient_for = main_window, 143 | skip_taskbar_hint = true, 144 | resizable = false, 145 | }; 146 | 147 | shortcut_window.set_keep_above (true); 148 | shortcut_window.focus_out_event.connect (() => { 149 | shortcut_window.destroy (); 150 | return true; 151 | }); 152 | 153 | shortcut_window.key_press_event.connect ((event) => { 154 | if (event.keyval == Gdk.Key.Escape) { 155 | shortcut_window.destroy (); 156 | } 157 | return true; 158 | }); 159 | 160 | var headerbar = new Gtk.HeaderBar () { 161 | title = _("Shortcuts"), 162 | has_subtitle = false, 163 | show_close_button = true 164 | }; 165 | unowned Gtk.StyleContext headerbar_context = headerbar.get_style_context (); 166 | headerbar_context.add_class ("default-decoration"); 167 | headerbar_context.add_class (Gtk.STYLE_CLASS_FLAT); 168 | headerbar_context.add_class (Gtk.STYLE_CLASS_TITLEBAR); 169 | 170 | layout.add (headerbar); 171 | 172 | var shortcuts = new Gtk.Grid () { 173 | orientation = Gtk.Orientation.VERTICAL, 174 | column_spacing = 12, 175 | row_spacing = 12, 176 | margin = 36, 177 | margin_top = 12 178 | }; 179 | 180 | shortcuts.attach (new Granite.HeaderLabel (_("Application")), 0, 0, 2); 181 | shortcuts.attach (new Gtk.Label (_("Show shortcuts:")){ halign = Gtk.Align.END }, 1, 1); 182 | shortcuts.attach (new Gtk.Label ("Escape"){ halign = Gtk.Align.START },2, 1); 183 | shortcuts.attach (new Gtk.Label (_("Save and Quit:")){ halign = Gtk.Align.END }, 1, 2); 184 | shortcuts.attach (new Gtk.Label ("W"){ halign = Gtk.Align.START },2, 2); 185 | shortcuts.attach (new Gtk.Label ("Q"){ halign = Gtk.Align.START }, 2, 3); 186 | shortcuts.attach (new Granite.HeaderLabel (_("Boards")), 0, 4, 2); 187 | shortcuts.attach (new Gtk.Label (_("Rename:")){ halign = Gtk.Align.END }, 1, 5); 188 | shortcuts.attach (new Gtk.Label ("F2"){ halign = Gtk.Align.START }, 2, 5); 189 | shortcuts.attach (new Granite.HeaderLabel (_("Widgets")), 0, 6, 2); 190 | shortcuts.attach (new Gtk.Label (_("Increase font size:")){ halign = Gtk.Align.END }, 1, 7); 191 | shortcuts.attach (new Gtk.Label ("plus"){ halign = Gtk.Align.START }, 2, 7); 192 | shortcuts.attach (new Gtk.Label (_("Decrease font size:")){ halign = Gtk.Align.END }, 1, 8); 193 | shortcuts.attach (new Gtk.Label ("minus"){ halign = Gtk.Align.START }, 2, 8); 194 | shortcuts.attach (new Gtk.Label (_("Move to foreground:")){ halign = Gtk.Align.END }, 1, 9); 195 | shortcuts.attach (new Gtk.Label ("]"){ halign = Gtk.Align.START }, 2, 9); 196 | shortcuts.attach (new Gtk.Label (_("Move to background:")){ halign = Gtk.Align.END }, 1, 10); 197 | shortcuts.attach (new Gtk.Label ("["){ halign = Gtk.Align.START }, 2, 10); 198 | shortcuts.attach (new Gtk.Label (_("Add image widget:")){ halign = Gtk.Align.END }, 1, 11); 199 | shortcuts.attach (new Gtk.Label ("O"){ halign = Gtk.Align.START }, 2, 11); 200 | shortcuts.attach (new Gtk.Label (_("Add text widget:")){ halign = Gtk.Align.END }, 1, 12); 201 | shortcuts.attach (new Gtk.Label ("T"){ halign = Gtk.Align.START }, 2, 12); 202 | shortcuts.attach (new Gtk.Label (_("Add label widget:")){ halign = Gtk.Align.END }, 1, 13); 203 | shortcuts.attach (new Gtk.Label ("L"){ halign = Gtk.Align.START }, 2, 13); 204 | shortcuts.attach (new Gtk.Label (_("Delete selected widget:")){ halign = Gtk.Align.END }, 1, 14); 205 | shortcuts.attach (new Gtk.Label ("Delete"){ halign = Gtk.Align.START }, 2, 14); 206 | 207 | layout.add (shortcuts); 208 | shortcut_window.add (layout); 209 | return shortcut_window; 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/Controllers/ImageController.vala: -------------------------------------------------------------------------------- 1 | public class ImageController : WidgetController { 2 | 3 | public ImageWidget movable { private set; get; } 4 | public ImageModel model { private set; get; } 5 | 6 | private string app_dir = "%s/com.github.brain_child.moobo".printf (Environment.get_user_data_dir ()); 7 | private int width; 8 | private int height; 9 | 10 | public ImageController (BoardController board_controller, int x, int y, ImageModel model = new ImageModel ()) { 11 | base (board_controller, x, y); 12 | this.movable = new ImageWidget (this); 13 | this.model = model; 14 | 15 | if (model.path == "") { 16 | var placeholder = new Gtk.Image.from_icon_name ("image-x-generic", Gtk.IconSize.DIALOG) { 17 | valign = Gtk.Align.START, 18 | halign = Gtk.Align.START, 19 | margin_start = x, 20 | margin_top = y 21 | }; 22 | board_controller.overlay.add_overlay (placeholder); 23 | placeholder.show (); 24 | model.path = get_file (); 25 | board_controller.overlay.remove (placeholder); 26 | } 27 | 28 | Gdk.Pixbuf.get_file_info (model.path, out width, out height); 29 | 30 | movable.init (); 31 | update_widget (); 32 | movable.handle_events (board_controller); 33 | board_controller.board.scale.value_changed.connect (on_value_changed); 34 | } 35 | 36 | public string get_file () { 37 | string file_path = ""; 38 | var file_chooser = new Gtk.FileChooserNative ( 39 | _("Select Image"), 40 | board_controller.window, 41 | Gtk.FileChooserAction.OPEN, 42 | _("Open"), 43 | _("Cancel") 44 | ); 45 | 46 | // Set up image file filter 47 | var filter = new Gtk.FileFilter (); 48 | filter.add_pixbuf_formats (); 49 | filter.set_name (_("Image Files")); 50 | file_chooser.filter = filter; 51 | 52 | // Add all files filter 53 | var all_filter = new Gtk.FileFilter (); 54 | all_filter.add_pattern ("*"); 55 | all_filter.set_name (_("All Files")); 56 | file_chooser.add_filter (all_filter); 57 | 58 | file_chooser.set_transient_for (board_controller.window); 59 | file_chooser.set_modal (true); 60 | 61 | var res = file_chooser.run (); 62 | if (res == Gtk.ResponseType.ACCEPT) { 63 | var source_file = file_chooser.get_file (); 64 | 65 | // Validate the selected file 66 | if (!ErrorHandler.validate_image_file (source_file.get_path ())) { 67 | ErrorHandler.show_error_dialog ( 68 | board_controller.window, 69 | _("Invalid Image File"), 70 | _("The selected file is not a valid image or cannot be read."), 71 | _("Please select a valid image file (PNG, JPEG, GIF, etc.)") 72 | ); 73 | movable.destroy (); 74 | return ""; 75 | } 76 | 77 | file_path = copy_file (source_file); 78 | if (file_path == "") { 79 | ErrorHandler.show_error_dialog ( 80 | board_controller.window, 81 | _("Failed to Add Image"), 82 | _("Could not copy the selected image file."), 83 | _("Please check that you have permission to access the file and sufficient disk space.") 84 | ); 85 | movable.destroy (); 86 | return ""; 87 | } 88 | } else { 89 | movable.destroy (); 90 | } 91 | return file_path; 92 | } 93 | 94 | public override Movable get_movable () { 95 | return movable; 96 | } 97 | 98 | public override BaseModel get_model () { 99 | return model; 100 | } 101 | 102 | public override void handle_key_press (Gdk.EventKey event) { 103 | // ImageController doesn't handle key events 104 | } 105 | 106 | public override void handle_key_release () { 107 | // ImageController doesn't handle key events 108 | } 109 | 110 | public override void update_widget () { 111 | movable.rel_pos_x = movable.margin_start = model.x; 112 | movable.rel_pos_y = movable.margin_top = model.y; 113 | 114 | var scaled_width = (int) (width * model.scale_factor); 115 | var scaled_height = (int) (height * model.scale_factor); 116 | 117 | // Validate file exists before loading 118 | if (!ErrorHandler.validate_file_path (model.path)) { 119 | debug ("Image file not found: %s", model.path); 120 | show_missing_image_placeholder (); 121 | return; 122 | } 123 | 124 | // Use image cache for better performance 125 | var cache_manager = ImageCache.ImageCacheManager.get_instance (); 126 | var pixbuf = cache_manager.get_image (model.path, scaled_width, scaled_height); 127 | 128 | if (pixbuf != null) { 129 | movable.pixbuf = pixbuf; 130 | movable.image.pixbuf = pixbuf; 131 | } else { 132 | warning ("Failed to load image from cache: %s", model.path); 133 | show_missing_image_placeholder (); 134 | } 135 | } 136 | 137 | private void show_missing_image_placeholder () { 138 | try { 139 | // Create a placeholder image 140 | var placeholder_pixbuf = new Gdk.Pixbuf (Gdk.Colorspace.RGB, false, 8, 200, 150); 141 | placeholder_pixbuf.fill (0x80808080u); // Gray background 142 | 143 | // Create a simple "missing image" icon using a basic pattern 144 | // Draw a simple "X" pattern to indicate missing image 145 | var pixels = placeholder_pixbuf.get_pixels (); 146 | var rowstride = placeholder_pixbuf.get_rowstride (); 147 | var n_channels = placeholder_pixbuf.get_n_channels (); 148 | 149 | // Draw a simple X pattern 150 | for (int y = 0; y < 150; y++) { 151 | for (int x = 0; x < 200; x++) { 152 | unowned uchar[] pixel = pixels[y * rowstride + x * n_channels:3]; 153 | 154 | // Draw X pattern 155 | if ((x - 100) * (x - 100) + (y - 75) * (y - 75) < 1000) { 156 | if ((x - y).abs () < 3 || (x + y - 200).abs () < 3) { 157 | pixel[0] = 0x00; // Red 158 | pixel[1] = 0x00; // Green 159 | pixel[2] = 0x00; // Blue 160 | } 161 | } 162 | } 163 | } 164 | 165 | movable.pixbuf = placeholder_pixbuf; 166 | movable.image.pixbuf = placeholder_pixbuf; 167 | } catch (Error e) { 168 | warning ("Failed to create placeholder image: %s", e.message); 169 | } 170 | } 171 | 172 | private string copy_file (File source_file) { 173 | try { 174 | // Validate source file exists and is readable 175 | if (!source_file.query_exists ()) { 176 | throw new FileError.NOENT ("Source file does not exist: %s".printf (source_file.get_path ())); 177 | } 178 | 179 | var source_info = source_file.query_info ("access::can-read", FileQueryInfoFlags.NONE); 180 | if (!source_info.get_attribute_boolean ("access::can-read")) { 181 | throw new FileError.ACCES ("Cannot read source file: %s".printf (source_file.get_path ())); 182 | } 183 | 184 | // Ensure app directory exists 185 | var app_dir_file = File.new_for_path (app_dir); 186 | if (!app_dir_file.query_exists ()) { 187 | app_dir_file.make_directory_with_parents (); 188 | } 189 | 190 | var target_file_path = "%s/%d".printf (app_dir, (int) source_file.hash ()); 191 | var target_file = File.new_for_path (target_file_path); 192 | 193 | // Check if file already exists 194 | if (target_file.query_exists ()) { 195 | // Verify the existing file is valid 196 | try { 197 | var test_pixbuf = new Gdk.Pixbuf.from_file (target_file_path); 198 | if (test_pixbuf != null) { 199 | return target_file_path; 200 | } 201 | } catch (Error e) { 202 | // Existing file is corrupted, remove it 203 | target_file.delete (); 204 | } 205 | } 206 | 207 | // Copy the file 208 | source_file.copy (target_file, FileCopyFlags.OVERWRITE); 209 | 210 | // Verify the copy was successful 211 | if (!target_file.query_exists ()) { 212 | throw new FileError.IO ("Failed to copy file"); 213 | } 214 | 215 | return target_file_path; 216 | } catch (Error e) { 217 | warning ("Failed to copy file %s: %s", source_file.get_path (), e.message); 218 | return ""; 219 | } 220 | } 221 | 222 | private void on_value_changed () { 223 | if (board_controller.selected_widget == movable) { 224 | var scale_val = board_controller.board.scale.get_value (); 225 | var new_width = (int) (scale_val * width); 226 | var new_height = (int) (scale_val * height); 227 | 228 | // Validate file exists before scaling 229 | if (!ErrorHandler.validate_file_path (model.path)) { 230 | debug ("Image file not found during scaling: %s", model.path); 231 | show_missing_image_placeholder (); 232 | return; 233 | } 234 | 235 | // Use image cache for better performance 236 | var cache_manager = ImageCache.ImageCacheManager.get_instance (); 237 | var scaled_pixbuf = cache_manager.get_image (model.path, new_width, new_height); 238 | 239 | if (scaled_pixbuf != null) { 240 | movable.image.pixbuf = scaled_pixbuf; 241 | model.scale_factor = scale_val; 242 | } else { 243 | warning ("Failed to get scaled image from cache"); 244 | show_missing_image_placeholder (); 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Helper/Deserializer.vala: -------------------------------------------------------------------------------- 1 | namespace Deserializer { 2 | 3 | public Gee.ArrayList from_json () { 4 | var app_dir = Environment.get_user_data_dir () + Const.APP_PATH; 5 | var path = "%s/%s".printf (app_dir, "boards.json"); 6 | 7 | var boards_list = new Gee.ArrayList (); 8 | 9 | try { 10 | // Check if file exists 11 | var file = File.new_for_path (path); 12 | if (!file.query_exists ()) { 13 | info ("Boards file does not exist, creating new board"); 14 | create_default_board (boards_list); 15 | return boards_list; 16 | } 17 | 18 | // Validate file is readable 19 | var file_info = file.query_info ("access::can-read", FileQueryInfoFlags.NONE); 20 | if (!file_info.get_attribute_boolean ("access::can-read")) { 21 | warning ("Cannot read boards file: %s", path); 22 | create_default_board (boards_list); 23 | return boards_list; 24 | } 25 | 26 | var parser = new Json.Parser (); 27 | parser.load_from_file (path); 28 | var root = parser.get_root (); 29 | 30 | if (root == null) { 31 | warning ("JSON file is empty or invalid"); 32 | create_default_board (boards_list); 33 | return boards_list; 34 | } 35 | 36 | parse_board (root, boards_list); 37 | 38 | } catch (Error e) { 39 | warning ("Failed to load boards: %s", e.message); 40 | 41 | var settings = new GLib.Settings (Const.APP_ID); 42 | var first_time_running = settings.get_boolean ("first-run"); 43 | 44 | if (first_time_running) { 45 | try { 46 | File dir = File.new_for_path (app_dir); 47 | if (!dir.query_exists ()) { 48 | dir.make_directory_with_parents (); 49 | } 50 | } catch (Error dir_error) { 51 | warning ("Failed to create app directory: %s", dir_error.message); 52 | } 53 | settings.set_boolean ("first-run", false); 54 | } 55 | 56 | create_default_board (boards_list); 57 | } 58 | return boards_list; 59 | } 60 | 61 | private void create_default_board (Gee.ArrayList boards_list) { 62 | var default_board = new BoardModel (); 63 | default_board.title = _("My Board"); 64 | default_board.color = "#ffffff"; 65 | default_board.is_active = true; 66 | boards_list.add (default_board); 67 | } 68 | 69 | private void parse_board (Json.Node node, Gee.ArrayList boards_list) { 70 | if (node.get_node_type () != Json.NodeType.ARRAY) { 71 | warning ("Expected JSON array for boards, got %s", node.get_node_type ().to_string ()); 72 | return; 73 | } 74 | 75 | var array = node.get_array (); 76 | array.foreach_element ((array, index, node) => { 77 | try { 78 | if (node.get_node_type () != Json.NodeType.OBJECT) { 79 | warning ("Board at index %u is not an object, skipping", index); 80 | return; 81 | } 82 | 83 | var board_model = new BoardModel (); 84 | var object = node.get_object (); 85 | 86 | // Parse title with fallback 87 | if (object.has_member ("title")) { 88 | board_model.title = object.get_string_member ("title"); 89 | } else { 90 | board_model.title = _("Untitled Board"); 91 | } 92 | 93 | // Parse color with fallback 94 | if (object.has_member ("color")) { 95 | board_model.color = object.get_string_member ("color"); 96 | } else { 97 | board_model.color = "#ffffff"; 98 | } 99 | 100 | // Parse active status with fallback 101 | if (object.has_member ("active")) { 102 | board_model.is_active = object.get_boolean_member ("active"); 103 | } else { 104 | board_model.is_active = false; 105 | } 106 | 107 | // Parse widgets with validation 108 | if (object.has_member ("widgets")) { 109 | var widgets_node = object.get_member ("widgets"); 110 | if (widgets_node.get_node_type () == Json.NodeType.ARRAY) { 111 | var widgets = widgets_node.get_array (); 112 | foreach (var item in widgets.get_elements ()) { 113 | if (item.get_node_type () == Json.NodeType.OBJECT) { 114 | var widget_model = parse_widgets (item.get_object ()); 115 | if (widget_model != null) { 116 | board_model.widgets.add (widget_model); 117 | } 118 | } else { 119 | warning ("Widget is not an object, skipping"); 120 | } 121 | } 122 | } else { 123 | warning ("Widgets member is not an array"); 124 | } 125 | } 126 | 127 | boards_list.add (board_model); 128 | } catch (Error e) { 129 | warning ("Failed to parse board at index %u: %s", index, e.message); 130 | } 131 | }); 132 | } 133 | 134 | private BaseModel? parse_widgets (Json.Object node) { 135 | try { 136 | // Validate model type 137 | if (!node.has_member ("model")) { 138 | warning ("Widget missing 'model' field, skipping"); 139 | return null; 140 | } 141 | 142 | var name = node.get_string_member ("model"); 143 | if (name == null || name == "") { 144 | warning ("Widget has empty model name, skipping"); 145 | return null; 146 | } 147 | 148 | var widget_type = WidgetFactory.get_widget_type_from_string (name); 149 | 150 | // Get z_index with default value of 0 if not present 151 | int z_index = 0; 152 | if (node.has_member ("z-index")) { 153 | z_index = (int) node.get_int_member ("z-index"); 154 | } 155 | 156 | // Get x, y coordinates with defaults 157 | int x = 0; 158 | int y = 0; 159 | if (node.has_member ("x")) { 160 | x = (int) node.get_int_member ("x"); 161 | } 162 | if (node.has_member ("y")) { 163 | y = (int) node.get_int_member ("y"); 164 | } 165 | 166 | BaseModel model = null; 167 | switch (widget_type) { 168 | case WidgetFactory.WidgetType.TEXT: 169 | int font_size = 12; 170 | if (node.has_member ("font-size")) { 171 | font_size = (int) node.get_int_member ("font-size"); 172 | } 173 | 174 | string content = ""; 175 | if (node.has_member ("content")) { 176 | content = node.get_string_member ("content"); 177 | } 178 | 179 | model = new TextModel () { 180 | x = x, 181 | y = y, 182 | z_index = z_index, 183 | font_size = font_size, 184 | content = content 185 | }; 186 | break; 187 | 188 | case WidgetFactory.WidgetType.LABEL: 189 | int font_size = 12; 190 | if (node.has_member ("font-size")) { 191 | font_size = (int) node.get_int_member ("font-size"); 192 | } 193 | 194 | string content = ""; 195 | if (node.has_member ("content")) { 196 | content = node.get_string_member ("content"); 197 | } 198 | 199 | string color = "#000000"; 200 | if (node.has_member ("color")) { 201 | color = node.get_string_member ("color"); 202 | } 203 | 204 | model = new LabelModel () { 205 | x = x, 206 | y = y, 207 | z_index = z_index, 208 | font_size = font_size, 209 | content = content, 210 | color = color 211 | }; 212 | break; 213 | 214 | case WidgetFactory.WidgetType.IMAGE: 215 | string path = ""; 216 | if (node.has_member ("path")) { 217 | path = node.get_string_member ("path"); 218 | } 219 | 220 | double scale_factor = 1.0; 221 | if (node.has_member ("scale-factor")) { 222 | scale_factor = node.get_double_member ("scale-factor"); 223 | } 224 | 225 | // Validate image file exists 226 | if (path != "" && !ErrorHandler.validate_file_path (path)) { 227 | debug ("Image file not found: %s, skipping widget", path); 228 | return null; 229 | } 230 | 231 | model = new ImageModel () { 232 | x = x, 233 | y = y, 234 | z_index = z_index, 235 | path = path, 236 | scale_factor = scale_factor 237 | }; 238 | break; 239 | 240 | default: 241 | warning ("Unknown widget type: %s, skipping", name); 242 | return null; 243 | } 244 | return model; 245 | } catch (Error e) { 246 | warning ("Failed to parse widget: %s", e.message); 247 | return null; 248 | } 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /src/Movable.vala: -------------------------------------------------------------------------------- 1 | public abstract class Movable : Gtk.EventBox { 2 | 3 | public int rel_pos_x { set; get; } 4 | public int rel_pos_y { set; get; } 5 | public int overlay_width { set; get; } 6 | public int overlay_height { set; get; } 7 | 8 | private BoardController controller; 9 | private Gee.ArrayList widgets; 10 | private Gtk.Revealer revealer; 11 | private Gtk.Scale scale; 12 | private Gtk.Overlay overlay; 13 | private int abs_pos_x; 14 | private int abs_pos_y; 15 | 16 | public void handle_events (BoardController controller) { 17 | this.controller = controller; 18 | this.widgets = controller.model.widgets; 19 | this.revealer = controller.board.revealer; 20 | this.scale = controller.board.scale; 21 | this.overlay = controller.overlay; 22 | 23 | valign = Gtk.Align.START; 24 | halign = Gtk.Align.START; 25 | 26 | button_press_event.connect (on_button_press); 27 | button_release_event.connect (on_button_release); 28 | enter_notify_event.connect (on_enter_notify); 29 | motion_notify_event.connect (on_motion_notify_hover); 30 | 31 | show_all (); 32 | } 33 | 34 | private bool on_button_press (Gtk.Widget widget, Gdk.EventButton event) { 35 | can_focus = false; 36 | 37 | if (widget is ImageWidget) { 38 | can_focus = true; 39 | grab_focus (); 40 | var image_widget = widget as ImageWidget; 41 | image_widget.grab_focus (); 42 | 43 | // Check if we're clicking on a resize handle 44 | if (event.button == 1) { 45 | var resize_handle = image_widget.get_resize_handle_at_position (event.x, event.y); 46 | if (resize_handle != ImageWidget.ResizeHandle.NONE) { 47 | image_widget.start_resize (resize_handle, event.x, event.y); 48 | motion_notify_event.connect (on_motion_notify); 49 | var display = get_display (); 50 | var cursor = new Gdk.Cursor.from_name (display, image_widget.get_cursor_name_for_handle (resize_handle)); 51 | get_window ().set_cursor (cursor); 52 | return true; 53 | } 54 | } 55 | } 56 | 57 | WidgetFactory.reorder_widget_in_model (this, widgets); 58 | 59 | if (event.button == 1) { 60 | motion_notify_event.connect (on_motion_notify); 61 | var display = get_display (); 62 | var cursor = new Gdk.Cursor.from_name (display, "grabbing"); 63 | get_window ().set_cursor (cursor); 64 | } 65 | 66 | if (event.button == 3) { 67 | Gtk.Menu menu = new Gtk.Menu (); 68 | Gtk.MenuItem color_item = new Gtk.MenuItem.with_label (_("Change Color")) { 69 | sensitive = false 70 | }; 71 | color_item.activate.connect (() => { 72 | var dialog = new Gtk.ColorChooserDialog (null, null); 73 | dialog.response.connect ((id) => { 74 | if (id == -5) { 75 | if (widget is LabelWidget) { 76 | var label_widget = (LabelWidget) widget; 77 | var rgba = dialog.get_rgba (); 78 | label_widget.controller.change_color (rgba); 79 | label_widget.controller.model.color = rgba.to_string (); 80 | } 81 | } 82 | dialog.close (); 83 | }); 84 | dialog.run (); 85 | }); 86 | 87 | var font_item = new Gtk.MenuItem.with_label (_("Change Font")) { 88 | sensitive = false 89 | }; 90 | font_item.activate.connect (() => { 91 | var dialog = new Gtk.FontChooserDialog (null, null); 92 | dialog.response.connect ((id) => { 93 | if (id == -5) { 94 | dialog.get_font (); 95 | } 96 | dialog.close (); 97 | }); 98 | dialog.run (); 99 | }); 100 | 101 | var resize_item = new Gtk.MenuItem.with_label (_("Scale")) { 102 | sensitive = false 103 | }; 104 | resize_item.activate.connect (() => { 105 | overlay.reorder_overlay (controller.board.revealer, -2); 106 | var image_widget = widget as ImageWidget; 107 | var scale_val = image_widget.controller.model.scale_factor; 108 | 109 | int width; 110 | int height; 111 | Gdk.Pixbuf.get_file_info (image_widget.controller.model.path, out width, out height); 112 | 113 | var max_x = (double) Const.IMG_MAX_WIDTH; 114 | var max_y = (double) Const.IMG_MAX_HEIGHT; 115 | var max_scale = Const.MAX_SCALE; 116 | 117 | max_scale = max_x / width; 118 | var tmp = max_y / height; 119 | if (tmp < max_scale) { max_scale = tmp; } 120 | scale.set_range (0.1, max_scale); 121 | 122 | scale.set_value (scale_val); 123 | revealer.reveal_child = true; 124 | }); 125 | 126 | Gtk.MenuItem delete_item = new Gtk.MenuItem.with_label (_("Delete")); 127 | delete_item.activate.connect (() => { 128 | WidgetFactory.remove_widget_from_model (this, widgets); 129 | this.destroy (); 130 | }); 131 | menu.attach_to_widget (widget, null); 132 | menu.add (color_item); 133 | // menu.add (font_item); 134 | menu.add (resize_item); 135 | menu.add (new Gtk.SeparatorMenuItem ()); 136 | menu.add (delete_item); 137 | menu.show_all (); 138 | menu.popup_at_pointer (event); 139 | 140 | if (widget is LabelWidget) { 141 | color_item.sensitive = true; 142 | font_item.sensitive = true; 143 | } 144 | if (widget is TextWidget) { 145 | font_item.sensitive = true; 146 | } 147 | if (widget is ImageWidget) { 148 | resize_item.sensitive = true; 149 | } 150 | } 151 | 152 | widget.grab_focus (); 153 | 154 | revealer.reveal_child = false; 155 | 156 | abs_pos_x = (int) event.x_root; 157 | abs_pos_y = (int) event.y_root; 158 | 159 | // Use the controller's selection system to avoid overlap 160 | controller.select_widget (this); 161 | return true; 162 | } 163 | 164 | private bool on_button_release (Gtk.Widget widget, Gdk.EventButton event) { 165 | motion_notify_event.disconnect (on_motion_notify); 166 | 167 | var display = get_display (); 168 | var cursor = new Gdk.Cursor.from_name (display, "grab"); 169 | get_window ().set_cursor (cursor); 170 | 171 | widget.set_opacity (1); 172 | 173 | // Handle resize end for image widgets 174 | if (widget is ImageWidget) { 175 | var image_widget = widget as ImageWidget; 176 | if (image_widget.get_is_resizing ()) { 177 | image_widget.end_resize (); 178 | return false; 179 | } 180 | } 181 | 182 | double dx = event.x_root - abs_pos_x; 183 | double dy = event.y_root - abs_pos_y; 184 | 185 | double x = rel_pos_x + dx; 186 | double y = rel_pos_y + dy; 187 | 188 | int dw = overlay.get_allocated_width () - get_allocated_width (); 189 | int dh = overlay.get_allocated_height () - get_allocated_height (); 190 | 191 | if (x > dw) { x = dw; } 192 | if (y > dh) { y = dh; } 193 | 194 | if (x < 1) { x = 1; } 195 | if (y < 1) { y = 1; } 196 | 197 | rel_pos_x = (int) x; 198 | rel_pos_y = (int) y; 199 | 200 | // Update model position for all widget types 201 | if (widget is TextWidget) { 202 | var text_widget = widget as TextWidget; 203 | text_widget.controller.model.x = rel_pos_x; 204 | text_widget.controller.model.y = rel_pos_y; 205 | } else if (widget is LabelWidget) { 206 | var label_widget = widget as LabelWidget; 207 | label_widget.controller.model.x = rel_pos_x; 208 | label_widget.controller.model.y = rel_pos_y; 209 | } else if (widget is ImageWidget) { 210 | var image_widget = widget as ImageWidget; 211 | image_widget.controller.model.x = rel_pos_x; 212 | image_widget.controller.model.y = rel_pos_y; 213 | } 214 | 215 | return false; 216 | } 217 | 218 | private bool on_motion_notify (Gtk.Widget widget, Gdk.EventMotion event) { 219 | widget.set_opacity (0.7); 220 | 221 | var display = get_display (); 222 | 223 | // Handle resize for image widgets 224 | if (widget is ImageWidget) { 225 | var image_widget = widget as ImageWidget; 226 | if (image_widget.get_is_resizing ()) { 227 | image_widget.update_resize (event.x, event.y); 228 | var cursor = new Gdk.Cursor.from_name (display, image_widget.get_cursor_name_for_handle (image_widget.current_handle)); 229 | get_window ().set_cursor (cursor); 230 | return true; 231 | } 232 | } 233 | 234 | var cursor = new Gdk.Cursor.from_name (display, "grabbing"); 235 | get_window ().set_cursor (cursor); 236 | 237 | var dx = event.x_root - abs_pos_x; 238 | var dy = event.y_root - abs_pos_y; 239 | 240 | int x = (int) (rel_pos_x + dx); 241 | int y = (int) (rel_pos_y + dy); 242 | 243 | int dw = overlay.get_allocated_width () - get_allocated_width (); 244 | int dh = overlay.get_allocated_height () - get_allocated_height (); 245 | 246 | if (x > dw) { x = dw; } 247 | if (y > dh) { y = dh; } 248 | 249 | if (x < 1) { x = 1; } 250 | if (y < 1) { y = 1; } 251 | 252 | margin_start = x; 253 | margin_top = y; 254 | 255 | return true; 256 | } 257 | 258 | private bool on_enter_notify () { 259 | var display = get_display (); 260 | var cursor = new Gdk.Cursor.from_name (display, "grab"); 261 | get_window ().set_cursor (cursor); 262 | return true; 263 | } 264 | 265 | private bool on_motion_notify_hover (Gtk.Widget widget, Gdk.EventMotion event) { 266 | if (widget is ImageWidget) { 267 | var image_widget = widget as ImageWidget; 268 | if (!image_widget.get_is_resizing ()) { 269 | var resize_handle = image_widget.get_resize_handle_at_position (event.x, event.y); 270 | var display = get_display (); 271 | var cursor = new Gdk.Cursor.from_name (display, image_widget.get_cursor_name_for_handle (resize_handle)); 272 | get_window ().set_cursor (cursor); 273 | } 274 | } 275 | return false; 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /data/icons/24/com.github.brain_child.moobo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 39 | 43 | 50 | 51 | 53 | 55 | 59 | 63 | 64 | 66 | 70 | 74 | 75 | 83 | 91 | 101 | 103 | 107 | 111 | 115 | 119 | 120 | 129 | 139 | 141 | 145 | 149 | 150 | 159 | 161 | 165 | 169 | 173 | 177 | 178 | 179 | 181 | 182 | 184 | image/svg+xml 185 | 187 | 188 | 189 | 190 | 193 | 200 | 208 | 215 | 216 | 225 | 232 | 239 | 246 | 253 | 260 | 267 | 274 | 281 | 288 | 295 | 302 | 309 | 316 | 323 | 330 | 339 | 348 | 349 | -------------------------------------------------------------------------------- /src/Controllers/BoardController.vala: -------------------------------------------------------------------------------- 1 | public class BoardController { 2 | 3 | public Board board { private set; get; } 4 | public Row row { private set; get; } 5 | public BoardModel model { private set; get; } 6 | public Gtk.Overlay overlay { private set; get; } 7 | public Movable selected_widget { set; get; } 8 | private int selection_z_index = 10000; // High value for selected widgets 9 | private int original_z_index = 0; // Store original z_index of selected widget 10 | public Window window { private set; get; } 11 | 12 | public RowController row_controller; 13 | private LazyLoader.LazyLoadingManager? lazy_loader; 14 | 15 | public BoardController (Window window, BoardModel model) { 16 | this.window = window; 17 | this.model = model; 18 | this.board = new Board (this); 19 | this.overlay = board.overlay; 20 | this.row_controller = new RowController (this, "Board", model.color); 21 | row = row_controller.row; 22 | 23 | // Initialize lazy loading for performance 24 | this.lazy_loader = new LazyLoader.LazyLoadingManager (this); 25 | 26 | row.focus_in_event.connect (on_focus_in); 27 | board.enter_notify_event.connect (on_enter_notify); 28 | row.activate.connect (on_activate); 29 | row.delete_row.connect (on_delete); 30 | row.renamed.connect (on_rename); 31 | board.button_press_event.connect (on_button_press); 32 | model.changed_event.connect (changed_handler); 33 | model.changed_trigger (); 34 | } 35 | 36 | public void changed_handler (Gee.ArrayList widgets, string color, string title) { 37 | board.title = title; 38 | 39 | // Clear existing lazy widgets 40 | if (lazy_loader != null) { 41 | lazy_loader.unload_all_widgets (); 42 | } 43 | 44 | // Add widgets using lazy loading for better performance 45 | foreach (var widget in widgets) { 46 | if (lazy_loader != null) { 47 | lazy_loader.add_widget (widget, widget.x, widget.y); 48 | } else { 49 | // Fallback to immediate loading if lazy loading is disabled 50 | var w = WidgetFactory.create_widget_from_string (widget.model, this, widget.x, widget.y, widget); 51 | overlay.add_overlay (w); 52 | } 53 | } 54 | 55 | // Apply proper layering based on z_index 56 | update_widget_layering (); 57 | 58 | row.display_name_label.label = title; 59 | set_color (color); 60 | } 61 | 62 | private bool on_focus_in () { 63 | row.activate (); 64 | return false; 65 | } 66 | 67 | private bool on_enter_notify () { 68 | if (window.listbox.get_selected_row () != row) { 69 | row.activate (); 70 | } 71 | return false; 72 | } 73 | 74 | private bool on_button_press (Gdk.EventButton event) { 75 | board.grab_focus (); 76 | board.revealer.reveal_child = false; 77 | 78 | // Deselect any previously selected widget 79 | deselect_widget (); 80 | 81 | if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS) { 82 | var selection = window.fab_selection; 83 | 84 | var x = (int) event.x; 85 | var y = (int) event.y; 86 | 87 | var widget = WidgetFactory.create_widget_from_class (selection, this, x, y); 88 | WidgetFactory.add_widget_to_model (widget, model.widgets); 89 | 90 | widget.rel_pos_x = widget.margin_start = x; 91 | widget.rel_pos_y = widget.margin_top = y; 92 | 93 | // Set z_index for new widget to be on top 94 | BaseModel widget_model = get_widget_model (widget); 95 | if (widget_model != null) { 96 | int max_z_index = 0; 97 | foreach (var model_widget in model.widgets) { 98 | if (model_widget.z_index > max_z_index) { 99 | max_z_index = model_widget.z_index; 100 | } 101 | } 102 | widget_model.z_index = max_z_index + 1; 103 | } 104 | 105 | overlay.add_overlay (widget); 106 | WidgetFactory.focus_widget (widget); 107 | } 108 | 109 | return true; 110 | } 111 | 112 | private void on_activate () { 113 | window.board_header.title = board.title; 114 | if (window.deck.get_visible_child () != board) { 115 | window.deck.set_visible_child (board); 116 | } 117 | 118 | // Update viewport for lazy loading when board becomes active 119 | if (lazy_loader != null) { 120 | int width, height; 121 | board.get_size_request (out width, out height); 122 | if (width <= 0) width = 800; 123 | if (height <= 0) height = 600; 124 | lazy_loader.update_viewport (0, 0, width, height); 125 | } 126 | } 127 | 128 | private void on_delete (int index) { 129 | var children = window.deck.get_children (); 130 | if (children.nth_data (0) != board) { 131 | if (window.deck.get_visible_child () == board) { 132 | window.deck.set_visible_child (children.nth_data (index - 1)); 133 | var board = children.nth_data (index - 1) as Board; 134 | board.controller.row.activate (); 135 | } 136 | } 137 | } 138 | 139 | private void on_rename (string name) { 140 | board.title = name; 141 | if (window.deck.get_visible_child () == board) { 142 | window.board_header.title = board.title; 143 | } 144 | } 145 | 146 | private void set_color (string color) { 147 | var hex_color = Colors.from_name (color); 148 | 149 | string style = """ 150 | @define-color colorAccent %s; 151 | @define-color accent_color %s; 152 | """.printf (hex_color, hex_color); 153 | 154 | var style_provider = new Gtk.CssProvider (); 155 | try { 156 | style_provider.load_from_data (style, style.length); 157 | } catch (Error e) { 158 | warning (e.message); 159 | } 160 | unowned Gtk.StyleContext style_context = row.source_color.get_style_context (); 161 | style_context.add_provider (style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 162 | } 163 | 164 | public void move_widget_to_foreground (Movable widget) { 165 | // Find the highest z_index among all widgets (excluding selection z_index) 166 | int max_z_index = 0; 167 | foreach (var model in model.widgets) { 168 | if (model.z_index > max_z_index && model.z_index < selection_z_index) { 169 | max_z_index = model.z_index; 170 | } 171 | } 172 | 173 | // Set the widget's z_index to be higher than all others 174 | BaseModel widget_model = get_widget_model (widget); 175 | if (widget_model != null) { 176 | widget_model.z_index = max_z_index + 1; 177 | 178 | // If this widget is currently selected, update the original_z_index too 179 | if (selected_widget == widget) { 180 | original_z_index = widget_model.z_index; 181 | } 182 | 183 | update_widget_layering (); 184 | } 185 | } 186 | 187 | public void move_widget_to_background (Movable widget) { 188 | // Find the lowest z_index among all widgets (excluding selection z_index) 189 | int min_z_index = 0; 190 | foreach (var model in model.widgets) { 191 | if (model.z_index < min_z_index && model.z_index < selection_z_index) { 192 | min_z_index = model.z_index; 193 | } 194 | } 195 | 196 | // Set the widget's z_index to be lower than all others 197 | BaseModel widget_model = get_widget_model (widget); 198 | if (widget_model != null) { 199 | widget_model.z_index = min_z_index - 1; 200 | 201 | // If this widget is currently selected, update the original_z_index too 202 | if (selected_widget == widget) { 203 | original_z_index = widget_model.z_index; 204 | } 205 | 206 | update_widget_layering (); 207 | } 208 | } 209 | 210 | public void delete_selected_widget () { 211 | if (selected_widget == null) { 212 | return; 213 | } 214 | 215 | // Get the widget model to determine the widget type for confirmation 216 | BaseModel? widget_model = get_widget_model (selected_widget); 217 | if (widget_model == null) { 218 | return; 219 | } 220 | 221 | // Show confirmation dialog 222 | var dialog = new Gtk.MessageDialog ( 223 | window, 224 | Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, 225 | Gtk.MessageType.QUESTION, 226 | Gtk.ButtonsType.YES_NO, 227 | _("Are you sure you want to delete this %s widget?").printf (get_widget_type_name (widget_model.model)) 228 | ); 229 | 230 | dialog.format_secondary_text (_("This action cannot be undone.")); 231 | dialog.set_transient_for (window); 232 | dialog.set_modal (true); 233 | 234 | var response = dialog.run (); 235 | dialog.destroy (); 236 | 237 | if (response == Gtk.ResponseType.YES) { 238 | // Remove widget from model 239 | model.widgets.remove (widget_model); 240 | 241 | // Remove widget from overlay 242 | overlay.remove (selected_widget); 243 | 244 | // Clear selection 245 | selected_widget = null; 246 | 247 | // Update the board to reflect changes 248 | model.changed_trigger (); 249 | } 250 | } 251 | 252 | private string get_widget_type_name (string model_type) { 253 | switch (model_type) { 254 | case "TextWidget": 255 | return _("text"); 256 | case "LabelWidget": 257 | return _("label"); 258 | case "ImageWidget": 259 | return _("image"); 260 | default: 261 | return _("widget"); 262 | } 263 | } 264 | 265 | private BaseModel? get_widget_model (Movable widget) { 266 | if (widget is TextWidget) { 267 | var text_widget = widget as TextWidget; 268 | return text_widget.controller.model; 269 | } else if (widget is LabelWidget) { 270 | var label_widget = widget as LabelWidget; 271 | return label_widget.controller.model; 272 | } else if (widget is ImageWidget) { 273 | var image_widget = widget as ImageWidget; 274 | return image_widget.controller.model; 275 | } 276 | return null; 277 | } 278 | 279 | public void select_widget (Movable widget) { 280 | // Deselect previous widget if any 281 | if (selected_widget != null && selected_widget != widget) { 282 | deselect_widget (); 283 | } 284 | 285 | // Select new widget 286 | selected_widget = widget; 287 | 288 | // Store original z_index and temporarily bring to foreground 289 | BaseModel widget_model = get_widget_model (widget); 290 | if (widget_model != null) { 291 | original_z_index = widget_model.z_index; 292 | widget_model.z_index = selection_z_index; 293 | update_widget_layering (); 294 | } 295 | } 296 | 297 | public void deselect_widget () { 298 | if (selected_widget != null) { 299 | // Restore original z_index for deselected widget 300 | BaseModel widget_model = get_widget_model (selected_widget); 301 | if (widget_model != null) { 302 | widget_model.z_index = original_z_index; 303 | update_widget_layering (); 304 | } 305 | selected_widget = null; 306 | } 307 | } 308 | 309 | private void update_widget_layering () { 310 | // Sort widgets by z_index and reorder them in the overlay 311 | var sorted_widgets = new Gee.ArrayList (); 312 | 313 | // Get all movable widgets from the overlay 314 | var children = overlay.get_children (); 315 | foreach (var child in children) { 316 | if (child is Movable) { 317 | sorted_widgets.add ((Movable) child); 318 | } 319 | } 320 | 321 | // Sort by z_index (ascending order - lower z_index first) 322 | sorted_widgets.sort ((a, b) => { 323 | BaseModel? model_a = get_widget_model (a); 324 | BaseModel? model_b = get_widget_model (b); 325 | if (model_a == null || model_b == null) return 0; 326 | return model_a.z_index - model_b.z_index; 327 | }); 328 | 329 | // Reorder widgets in the overlay 330 | foreach (var widget in sorted_widgets) { 331 | overlay.reorder_overlay (widget, -1); 332 | } 333 | } 334 | 335 | } 336 | -------------------------------------------------------------------------------- /data/icons/32/com.github.brain_child.moobo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 40 | 44 | 51 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | 61 | 62 | 63 | 65 | 67 | 71 | 75 | 76 | 78 | 82 | 86 | 87 | 89 | 93 | 97 | 98 | 100 | 104 | 108 | 112 | 113 | 122 | 132 | 142 | 144 | 148 | 152 | 156 | 160 | 161 | 170 | 180 | 182 | 186 | 190 | 194 | 198 | 199 | 208 | 210 | 214 | 218 | 222 | 226 | 227 | 236 | 237 | 240 | 247 | 252 | 257 | 258 | 267 | 276 | 285 | 292 | 299 | 306 | 313 | 320 | 327 | 334 | 341 | 348 | 355 | 362 | 369 | 376 | 383 | 390 | 397 | 398 | --------------------------------------------------------------------------------