diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36955a833..8f4048b14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,11 @@ jobs: with: platforms: arm64 + - name: Install Rust SDK extension + run: | + flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y --arch=${{matrix.arch}} org.freedesktop.Sdk.Extension.rust-stable//23.08 + - name: Build uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 48eff261e..ab64f12fd 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -31,6 +31,11 @@ jobs: with: platforms: arm64 + - name: Install Rust SDK extension + run: | + flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y --arch=${{matrix.arch}} org.freedesktop.Sdk.Extension.rust-stable//23.08 + - name: Build uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f99ee792..f029fb901 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,11 @@ jobs: with: platforms: arm64 + - name: Install Rust SDK extension + run: | + flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y --arch=${{matrix.arch}} org.freedesktop.Sdk.Extension.rust-stable//23.08 + - name: Build uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: diff --git a/data/Application.css b/data/Application.css index d03a7d059..c890d66b3 100644 --- a/data/Application.css +++ b/data/Application.css @@ -16,3 +16,7 @@ border-right-width: 0; padding-right: 0; } + +picture { + background: black; +} diff --git a/data/io.elementary.camera.gschema.xml b/data/io.elementary.camera.gschema.xml index 35763ecc5..040649249 100644 --- a/data/io.elementary.camera.gschema.xml +++ b/data/io.elementary.camera.gschema.xml @@ -20,15 +20,20 @@ The mode of the camera, photo or video Photo mode is represented by 0, video mode by 1 - - false - Whether the window was maximized on last run - Whether the window was maximized on last run + + 768 + Most recent window height + Most recent window height + + + 918 + Most recent window width + Most recent window width - - (918, 768) - Most recent window size (height, width) - Most recent window size (height, width) + + false + Open window maximized + Whether the main window of the application should open maximized or not. diff --git a/io.elementary.camera.yml b/io.elementary.camera.yml index 9b9e92744..b0ec1293e 100644 --- a/io.elementary.camera.yml +++ b/io.elementary.camera.yml @@ -3,6 +3,12 @@ runtime: io.elementary.Platform runtime-version: '8' sdk: io.elementary.Sdk command: io.elementary.camera +sdk-extensions: + - "org.freedesktop.Sdk.Extension.rust-stable" +build-options: + env: + CARGO_HOME: /run/build/cargo-c/cargo + append-path: /usr/lib/sdk/rust-stable/bin finish-args: - '--filesystem=xdg-pictures' - '--filesystem=xdg-videos' @@ -11,13 +17,37 @@ finish-args: - '--socket=fallback-x11' - '--socket=wayland' - '--socket=pulseaudio' + - '--device=dri' - '--device=all' + - '--env=GST_PLUGIN_PATH_1_0=/app/lib64/gstreamer-1.0' - '--metadata=X-DConf=migrate-path=/io/elementary/camera/' cleanup: - '*.a' - '*.la' modules: + - name: cargo-c + buildsystem: simple + build-commands: + - "cargo install cargo-c --root /app" + build-options: + build-args: + - "--share=network" + cleanup: + - "*" + + - name: gst-plugins-rs + buildsystem: simple + sources: + - type: git + url: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs + branch: '0.12' + build-options: + build-args: + - "--share=network" + build-commands: + - "cargo cinstall -p gst-plugin-gtk4 --prefix=/app" + - name: canberra config-opts: - '--enable-gstreamer' diff --git a/meson.build b/meson.build index e5cdd2a45..7b9efbe5d 100644 --- a/meson.build +++ b/meson.build @@ -39,10 +39,9 @@ executable( dependency('gee-0.8'), dependency('gio-2.0'), dependency('glib-2.0'), - dependency('granite', version: '>=6.0.0'), - dependency('gtk+-3.0'), + dependency('granite-7', version: '>=7.2.0'), + dependency('gtk4'), dependency('libcanberra'), - dependency('libhandy-1', version: '>=0.90.0'), meson.get_compiler('vala').find_library('posix') ], install : true diff --git a/src/Application.vala b/src/Application.vala index 53e56e4bb..891edd6d0 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -28,7 +28,7 @@ public class Camera.Application : Gtk.Application { public override void startup () { base.startup (); - Hdy.init (); + Granite.init (); var quit_action = new SimpleAction ("quit", null); quit_action.activate.connect (quit); @@ -36,23 +36,15 @@ public class Camera.Application : Gtk.Application { set_accels_for_action ("app.quit", {"q"}); - var application_provider = new Gtk.CssProvider (); - application_provider.load_from_resource (resource_base_path + "/Application.css"); - Gtk.StyleContext.add_provider_for_screen ( - Gdk.Screen.get_default (), - application_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); - var granite_settings = Granite.Settings.get_default (); var gtk_settings = Gtk.Settings.get_default (); gtk_settings.gtk_application_prefer_dark_theme = - granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.prefers_color_scheme == DARK; granite_settings.notify["prefers-color-scheme"].connect (() => { gtk_settings.gtk_application_prefer_dark_theme = - granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.prefers_color_scheme == DARK; }); } @@ -60,16 +52,19 @@ public class Camera.Application : Gtk.Application { if (active_window == null) { var main_window = new MainWindow (this); - int width, height; - settings.get ("window-size", "(ii)", out width, out height); - - main_window.resize (width, height); + /* + * This is very finicky. Bind size after present else set_titlebar gives us bad sizes + * Set maximize after height/width else window is min size on unmaximize + * Bind maximize as SET else get get bad sizes + */ + settings.bind ("window-height", main_window, "default-height", DEFAULT); + settings.bind ("window-width", main_window, "default-width", DEFAULT); if (settings.get_boolean ("window-maximized")) { main_window.maximize (); } - settings.bind ("window-maximized", main_window, "is-maximized", SET); + settings.bind ("window-maximized", main_window, "maximized", SET); } active_window.present (); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 0f5dc56c8..318114c07 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -19,7 +19,7 @@ * Authored by: Marcus Wichelmann */ -public class Camera.MainWindow : Hdy.ApplicationWindow { +public class Camera.MainWindow : Gtk.ApplicationWindow { public const string ACTION_PREFIX = "win."; public const string ACTION_FULLSCREEN = "fullscreen"; public const string ACTION_TAKE_PHOTO = "take_photo"; @@ -37,8 +37,6 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { private const string VIDEO_ICON_SYMBOLIC = "view-list-video-symbolic"; private const string STOP_ICON_SYMBOLIC = "media-playback-stop-symbolic"; - private uint configure_id = 0; - private Widgets.CameraView camera_view; private Menu camera_options; private Gtk.Button take_button; @@ -69,7 +67,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { camera_view.camera_added.connect (add_camera_option); camera_view.camera_removed.connect (remove_camera_option); - var recording_finished_toast = new Granite.Widgets.Toast (_("Saved to Videos")); + var recording_finished_toast = new Granite.Toast (_("Saved to Videos")); recording_finished_toast.set_default_action (_("View File")); recording_finished_toast.set_data ("location", ""); recording_finished_toast.default_action.connect (() => { @@ -84,7 +82,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } }); - var recording_finished_fail_toast = new Granite.Widgets.Toast (_("Recording failed")); + var recording_finished_fail_toast = new Granite.Toast (_("Recording failed")); var overlay = new Gtk.Overlay () { child = camera_view @@ -92,15 +90,12 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { overlay.add_overlay (recording_finished_toast); overlay.add_overlay (recording_finished_fail_toast); - var grid = new Gtk.Grid (); - grid.attach (construct_headerbar (), 0, 0); - grid.attach (overlay, 0, 1); - - var window_handle = new Hdy.WindowHandle () { - child = grid + var window_handle = new Gtk.WindowHandle () { + child = overlay }; child = window_handle; + titlebar = construct_headerbar (); camera_view.recording_finished.connect ((file_path) => { if (file_path == "") { @@ -111,7 +106,6 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } }); - window_handle.show_all (); camera_view.start (); } @@ -120,7 +114,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { timer_button = new Widgets.TimerButton (); /* Construct take photo/video tool */ - take_image = new Gtk.Image.from_icon_name (PHOTO_ICON_SYMBOLIC, BUTTON); + take_image = new Gtk.Image.from_icon_name (PHOTO_ICON_SYMBOLIC); take_timer_label = new Gtk.Label (null); video_timer_revealer = new Gtk.Revealer () { @@ -131,17 +125,16 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { var take_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { halign = Gtk.Align.CENTER }; - take_box.pack_start (take_image); - take_box.pack_start (video_timer_revealer); + take_box.append (take_image); + take_box.append (video_timer_revealer); take_button = new Gtk.Button () { action_name = Camera.MainWindow.ACTION_PREFIX + Camera.MainWindow.ACTION_TAKE_PHOTO, child = take_box, width_request = 54 }; - unowned Gtk.StyleContext take_button_style_context = take_button.get_style_context (); - take_button_style_context.add_class ("take-button"); - take_button_style_context.add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + take_button.add_css_class ("take-button"); + take_button.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); /* Construct mode switch */ mode_switch = new Granite.ModeSwitch.from_icon_name (PHOTO_ICON_SYMBOLIC, VIDEO_ICON_SYMBOLIC) { @@ -171,7 +164,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { "active", camera_view, "horizontal-flip", GLib.BindingFlags.BIDIRECTIONAL ); - var brightness_image = new Gtk.Image.from_icon_name ("display-brightness-symbolic", Gtk.IconSize.MENU); + var brightness_image = new Gtk.Image.from_icon_name ("display-brightness-symbolic"); var brightness_label = new Gtk.Label (_("Brightness")) { hexpand = true, xalign = 0 @@ -184,7 +177,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { brightness_scale.set_value (0); brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, ""); - var contrast_image = new Gtk.Image.from_icon_name ("color-contrast-symbolic", Gtk.IconSize.MENU); + var contrast_image = new Gtk.Image.from_icon_name ("color-contrast-symbolic"); var contrast_label = new Gtk.Label (_("Contrast")) { hexpand = true, xalign = 0 @@ -224,14 +217,13 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { }; menu_popover_grid.attach (image_settings, 0, 0); menu_popover_grid.attach (mirror_switch, 0, 1); - menu_popover_grid.show_all (); - var popover = new Gtk.Popover (null) { + var popover = new Gtk.Popover () { child = menu_popover_grid }; menu_button = new Gtk.MenuButton () { - image = new Gtk.Image.from_icon_name ("open-menu-symbolic", Gtk.IconSize.MENU), + icon_name = "open-menu-symbolic", popover = popover, tooltip_text = _("Settings") }; @@ -239,29 +231,30 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { /* Construct menu for multiple cameras */ camera_options = new Menu (); var camera_menu_button = new Gtk.MenuButton () { - menu_model = camera_options, - use_popover = false + menu_model = camera_options }; - unowned Gtk.StyleContext camera_menu_button_style_context = camera_menu_button.get_style_context (); - camera_menu_button_style_context.add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - camera_menu_button_style_context.add_class ("camera-menu"); + camera_menu_button.popover.has_arrow = false; + + var menubutton_child = camera_menu_button.get_first_child (); + menubutton_child.add_css_class (Granite.STYLE_CLASS_DESTRUCTIVE_ACTION); + menubutton_child.add_css_class ("camera-menu"); + menubutton_child.remove_css_class ("image-button"); camera_menu_revealer = new Gtk.Revealer () { child = camera_menu_button, + overflow = VISIBLE, transition_duration = 250, transition_type = SLIDE_RIGHT }; linked_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); - linked_box.pack_start (take_button); - linked_box.pack_start (camera_menu_revealer); + linked_box.append (take_button); + linked_box.append (camera_menu_revealer); - /* Pack tools into HeaderBar */ var header_widget = new Gtk.HeaderBar () { - show_close_button = true, // Gtk4 -> show_title_buttons = true, - custom_title = linked_box // Gtk4 -> title_widget = linked_box + show_title_buttons = true, + title_widget = linked_box }; - header_widget.get_style_context ().add_class (Gtk.STYLE_CLASS_TITLEBAR); header_widget.pack_start (timer_button); header_widget.pack_end (menu_button); header_widget.pack_end (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); @@ -284,7 +277,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } private void on_fullscreen () { - if (Gdk.WindowState.FULLSCREEN in get_window ().get_state ()) { + if (fullscreened) { unfullscreen (); } else { fullscreen (); @@ -319,26 +312,6 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } } - public override bool configure_event (Gdk.EventConfigure event) { - if (configure_id != 0) { - GLib.Source.remove (configure_id); - } - - configure_id = Timeout.add (100, () => { - configure_id = 0; - - if (!is_maximized) { - int width, height; - get_size (out width, out height); - Application.settings.set ("window-size", "(ii)", width, height); - } - - return false; - }); - - return base.configure_event (event); - } - /** Header bar tools management functions from Camera.Widgets.HeaderBar **/ private void enable_header (bool enable) { @@ -382,13 +355,12 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } private void update_take_button () { - unowned Gtk.StyleContext take_button_style_context = take_button.get_style_context (); if (camera_options.get_n_items () > 1) { camera_menu_revealer.reveal_child = true; - take_button_style_context.add_class ("multiple"); + take_button.add_css_class ("multiple"); } else { camera_menu_revealer.reveal_child = false; - take_button_style_context.remove_class ("multiple"); + take_button.remove_css_class ("multiple"); } } diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 2d731a002..b25951571 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -26,9 +26,9 @@ public class Camera.Widgets.CameraView : Gtk.Box { private Gtk.Stack stack; private Gtk.Box status_box; - private Granite.Widgets.AlertView no_device_view; + private Granite.Placeholder no_device_view; private Gtk.Label status_label; - Gtk.Widget gst_video_widget; + private Gtk.Picture picture; private Gst.Pipeline pipeline; private Gst.Element tee; @@ -74,8 +74,9 @@ public class Camera.Widgets.CameraView : Gtk.Box { public signal void camera_removed (Gst.Device camera); construct { - var spinner = new Gtk.Spinner (); - spinner.active = true; + var spinner = new Gtk.Spinner () { + spinning = true + }; status_label = new Gtk.Label (null); @@ -83,21 +84,27 @@ public class Camera.Widgets.CameraView : Gtk.Box { halign = Gtk.Align.CENTER, valign = Gtk.Align.CENTER }; - status_box.pack_start (spinner); - status_box.pack_start (status_label); + status_box.append (spinner); + status_box.append (status_label); - no_device_view = new Granite.Widgets.AlertView ( - _("No Supported Camera Found"), - _("Connect a webcam or other supported video device to take photos and video."), - "" - ); + no_device_view = new Granite.Placeholder (_("No Supported Camera Found")) { + description = _("Connect a webcam or other supported video device to take photos and video.") + }; + + picture = new Gtk.Picture () { + content_fit = CONTAIN, + hexpand = true, + vexpand = true + }; stack = new Gtk.Stack (); - stack.add (status_box); // must be add_child for GTK4 - stack.add (no_device_view); // must be add_child for GTK4 + stack.add_child (status_box); + stack.add_child (no_device_view); + stack.add_child (picture); + monitor.get_bus ().add_watch (GLib.Priority.DEFAULT, on_bus_message); - add (stack); + append (stack); var caps = new Gst.Caps.empty_simple ("video/x-raw"); caps.append (new Gst.Caps.empty_simple ("image/jpeg")); @@ -189,7 +196,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { create_pipeline (camera); current_device = camera; - ((Camera.MainWindow) this.get_toplevel ()).change_action_state ( + ((Camera.MainWindow) this.get_root ()).change_action_state ( Camera.MainWindow.ACTION_CHANGE_CAMERA, new Variant.string (camera.name) ); @@ -234,32 +241,21 @@ public class Camera.Widgets.CameraView : Gtk.Box { hflip = (pipeline.get_by_name ("hflip") as Gst.Video.Direction); color_balance = (pipeline.get_by_name ("balance") as Gst.Video.ColorBalance); - if (gst_video_widget != null) { - stack.remove (gst_video_widget); - } - dynamic Gst.Element videorate = pipeline.get_by_name ("videorate"); videorate.max_rate = 30; videorate.drop_only = true; - dynamic Gst.Element gtksink = Gst.ElementFactory.make ("gtkglsink", null); - if (gtksink != null) { - dynamic Gst.Element glsinkbin = Gst.ElementFactory.make ("glsinkbin", null); - glsinkbin.sink = gtksink; - pipeline.add (glsinkbin); - pipeline.get_by_name ("videoscale").link (glsinkbin); - } else { - gtksink = Gst.ElementFactory.make ("gtksink", null); - pipeline.add (gtksink); - pipeline.get_by_name ("videoscale").link (gtksink); - } + dynamic Gst.Element gtksink = Gst.ElementFactory.make ("gtk4paintablesink", "sink"); + + pipeline.add (gtksink); + pipeline.get_by_name ("videoscale").link (gtksink); - gst_video_widget = gtksink.widget; + Gdk.Paintable gst_video_widget; + gtksink.get ("paintable", out gst_video_widget); - stack.add (gst_video_widget); // must be add_child for GTK4 - gst_video_widget.show (); + picture.paintable = gst_video_widget; - stack.visible_child = gst_video_widget; + stack.visible_child = picture; pipeline.set_state (Gst.State.PLAYING); } catch (Error e) { // It is possible that there is another camera present that could selected so do not show diff --git a/src/Widgets/TimerButton.vala b/src/Widgets/TimerButton.vala index 0a3e7d1cd..c76e1ce9b 100644 --- a/src/Widgets/TimerButton.vala +++ b/src/Widgets/TimerButton.vala @@ -57,12 +57,12 @@ public class Camera.Widgets.TimerButton : Gtk.Button { }; var box = new Gtk.Box (HORIZONTAL, 3); - box.add (new Gtk.Image.from_icon_name ("timer-symbolic", BUTTON)); - box.add (label_widget); + box.append (new Gtk.Image.from_icon_name ("timer-symbolic")); + box.append (label_widget); child = box; + has_frame = false; tooltip_text = _("Delay before photo is taken"); - get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); clicked.connect (() => { delay = delay.next ();