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 ();