Files
Megapixels/main.c
Michal Ciesielski 29ad14c78b Read slider value when changing from auto to manual
The user might have moved the slider while in Auto. When exiting Auto
mode the value and the slider will be desynced. This change syncs the
value with the slider position when entering Manual mode.
2020-12-22 15:19:22 +01:00

604 lines
15 KiB
C

#include "main.h"
#include <errno.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <linux/media.h>
#include <linux/v4l2-subdev.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <assert.h>
#include <limits.h>
#include <linux/kdev_t.h>
#include <sys/sysmacros.h>
#include <asm/errno.h>
#include <wordexp.h>
#include <gtk/gtk.h>
#include <locale.h>
#include "camera_config.h"
#include "quickpreview.h"
#include "io_pipeline.h"
enum user_control { USER_CONTROL_ISO, USER_CONTROL_SHUTTER };
static bool camera_is_initialized = false;
static const struct mp_camera_config *camera = NULL;
static MPCameraMode mode;
static int preview_width = -1;
static int preview_height = -1;
static bool gain_is_manual = false;
static int gain;
static int gain_max;
static bool exposure_is_manual = false;
static int exposure;
static bool has_auto_focus_continuous;
static bool has_auto_focus_start;
static cairo_surface_t *surface = NULL;
static cairo_surface_t *status_surface = NULL;
static char last_path[260] = "";
static int burst_length = 3;
static enum user_control current_control;
// Widgets
GtkWidget *preview;
GtkWidget *error_box;
GtkWidget *error_message;
GtkWidget *main_stack;
GtkWidget *thumb_last;
GtkWidget *control_box;
GtkWidget *control_name;
GtkAdjustment *control_slider;
GtkWidget *control_auto;
int
remap(int value, int input_min, int input_max, int output_min, int output_max)
{
const long long factor = 1000000000;
long long output_spread = output_max - output_min;
long long input_spread = input_max - input_min;
long long zero_value = value - input_min;
zero_value *= factor;
long long percentage = zero_value / input_spread;
long long zero_output = percentage * output_spread / factor;
long long result = output_min + zero_output;
return (int)result;
}
static void
update_io_pipeline()
{
struct mp_io_pipeline_state io_state = {
.camera = camera,
.burst_length = burst_length,
.preview_width = preview_width,
.preview_height = preview_height,
.gain_is_manual = gain_is_manual,
.gain = gain,
.exposure_is_manual = exposure_is_manual,
.exposure = exposure,
};
mp_io_pipeline_update_state(&io_state);
}
static bool
update_state(const struct mp_main_state *state)
{
if (!camera_is_initialized) {
camera_is_initialized = true;
}
if (camera == state->camera) {
mode = state->mode;
if (!gain_is_manual) {
gain = state->gain;
}
gain_max = state->gain_max;
if (!exposure_is_manual) {
exposure = state->exposure;
}
has_auto_focus_continuous = state->has_auto_focus_continuous;
has_auto_focus_start = state->has_auto_focus_start;
}
return false;
}
void
mp_main_update_state(const struct mp_main_state *state)
{
struct mp_main_state *state_copy = malloc(sizeof(struct mp_main_state));
*state_copy = *state;
g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE,
(GSourceFunc)update_state, state_copy, free);
}
static bool
set_preview(cairo_surface_t *image)
{
if (surface) {
cairo_surface_destroy(surface);
}
surface = image;
gtk_widget_queue_draw(preview);
return false;
}
void
mp_main_set_preview(cairo_surface_t *image)
{
g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE,
(GSourceFunc)set_preview, image, NULL);
}
static void
draw_surface_scaled_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height,
cairo_surface_t *surface)
{
cairo_save(cr);
cairo_translate(cr, dst_width / 2, dst_height / 2);
int width = cairo_image_surface_get_width(surface);
int height = cairo_image_surface_get_height(surface);
double scale = MIN(dst_width / (double)width, dst_height / (double)height);
cairo_scale(cr, scale, scale);
cairo_translate(cr, -width / 2, -height / 2);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_paint(cr);
cairo_restore(cr);
}
static bool
capture_completed(const char *fname)
{
strncpy(last_path, fname, 260);
// Create a thumbnail from the current surface
cairo_surface_t *thumb =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 24, 24);
cairo_t *cr = cairo_create(thumb);
draw_surface_scaled_centered(cr, 24, 24, surface);
cairo_destroy(cr);
gtk_image_set_from_surface(GTK_IMAGE(thumb_last), thumb);
cairo_surface_destroy(thumb);
return false;
}
void
mp_main_capture_completed(const char *fname)
{
gchar *name = g_strdup(fname);
g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE,
(GSourceFunc)capture_completed, name, g_free);
}
static void
draw_controls()
{
cairo_t *cr;
char iso[6];
int temp;
char shutterangle[6];
if (exposure_is_manual) {
temp = (int)((float)exposure / (float)camera->capture_mode.height *
360);
sprintf(shutterangle, "%d\u00b0", temp);
} else {
sprintf(shutterangle, "auto");
}
if (gain_is_manual) {
temp = remap(gain - 1, 0, gain_max, camera->iso_min,
camera->iso_max);
sprintf(iso, "%d", temp);
} else {
sprintf(iso, "auto");
}
if (status_surface)
cairo_surface_destroy(status_surface);
// Make a service to show status of controls, 32px high
if (gtk_widget_get_window(preview) == NULL) {
return;
}
status_surface =
gdk_window_create_similar_surface(gtk_widget_get_window(preview),
CAIRO_CONTENT_COLOR_ALPHA,
preview_width, 32);
cr = cairo_create(status_surface);
cairo_set_source_rgba(cr, 0, 0, 0, 0.0);
cairo_paint(cr);
// Draw the outlines for the headings
cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size(cr, 9);
cairo_set_source_rgba(cr, 0, 0, 0, 1);
cairo_move_to(cr, 16, 16);
cairo_text_path(cr, "ISO");
cairo_stroke(cr);
cairo_move_to(cr, 60, 16);
cairo_text_path(cr, "Shutter");
cairo_stroke(cr);
// Draw the fill for the headings
cairo_set_source_rgba(cr, 1, 1, 1, 1);
cairo_move_to(cr, 16, 16);
cairo_show_text(cr, "ISO");
cairo_move_to(cr, 60, 16);
cairo_show_text(cr, "Shutter");
// Draw the outlines for the values
cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_NORMAL);
cairo_set_font_size(cr, 11);
cairo_set_source_rgba(cr, 0, 0, 0, 1);
cairo_move_to(cr, 16, 26);
cairo_text_path(cr, iso);
cairo_stroke(cr);
cairo_move_to(cr, 60, 26);
cairo_text_path(cr, shutterangle);
cairo_stroke(cr);
// Draw the fill for the values
cairo_set_source_rgba(cr, 1, 1, 1, 1);
cairo_move_to(cr, 16, 26);
cairo_show_text(cr, iso);
cairo_move_to(cr, 60, 26);
cairo_show_text(cr, shutterangle);
cairo_destroy(cr);
gtk_widget_queue_draw_area(preview, 0, 0, preview_width, 32);
}
static gboolean
preview_draw(GtkWidget *widget, cairo_t *cr, gpointer data)
{
if (!camera_is_initialized) {
return FALSE;
}
// Clear preview area with black
cairo_paint(cr);
// Draw camera preview
if (surface) {
draw_surface_scaled_centered(cr, preview_width, preview_height,
surface);
}
// Draw control overlay
cairo_set_source_surface(cr, status_surface, 0, 0);
cairo_paint(cr);
return FALSE;
}
static gboolean
preview_configure(GtkWidget *widget, GdkEventConfigure *event)
{
int new_preview_width = gtk_widget_get_allocated_width(widget);
int new_preview_height = gtk_widget_get_allocated_height(widget);
if (preview_width != new_preview_width ||
preview_height != new_preview_height) {
preview_width = new_preview_width;
preview_height = new_preview_height;
update_io_pipeline();
}
draw_controls();
return TRUE;
}
void
on_open_last_clicked(GtkWidget *widget, gpointer user_data)
{
char uri[275];
GError *error = NULL;
if (strlen(last_path) == 0) {
return;
}
sprintf(uri, "file://%s.tiff", last_path);
if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) {
g_printerr("Could not launch image viewer: %s\n", error->message);
}
}
void
on_open_directory_clicked(GtkWidget *widget, gpointer user_data)
{
char uri[270];
GError *error = NULL;
sprintf(uri, "file://%s/Pictures", getenv("HOME"));
if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) {
g_printerr("Could not launch image viewer: %s\n", error->message);
}
}
void
on_shutter_clicked(GtkWidget *widget, gpointer user_data)
{
mp_io_pipeline_capture();
}
void
on_preview_tap(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
if (event->type != GDK_BUTTON_PRESS)
return;
// Handle taps on the controls
if (event->y < 32) {
if (gtk_widget_is_visible(control_box)) {
gtk_widget_hide(control_box);
return;
} else {
gtk_widget_show(control_box);
}
if (event->x < 60) {
// ISO
current_control = USER_CONTROL_ISO;
gtk_label_set_text(GTK_LABEL(control_name), "ISO");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(control_auto),
!gain_is_manual);
gtk_adjustment_set_lower(control_slider, 0.0);
gtk_adjustment_set_upper(control_slider, (float)gain_max);
gtk_adjustment_set_value(control_slider, (double)gain);
} else if (event->x > 60 && event->x < 120) {
// Shutter angle
current_control = USER_CONTROL_SHUTTER;
gtk_label_set_text(GTK_LABEL(control_name), "Shutter");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(control_auto),
!exposure_is_manual);
gtk_adjustment_set_lower(control_slider, 1.0);
gtk_adjustment_set_upper(control_slider, 360.0);
gtk_adjustment_set_value(control_slider, (double)exposure);
}
return;
}
// Tapped preview image itself, try focussing
if (has_auto_focus_start) {
mp_io_pipeline_focus();
}
}
void
on_error_close_clicked(GtkWidget *widget, gpointer user_data)
{
gtk_widget_hide(error_box);
}
void
on_camera_switch_clicked(GtkWidget *widget, gpointer user_data)
{
size_t next_index = camera->index + 1;
const struct mp_camera_config *next_camera =
mp_get_camera_config(next_index);
if (!next_camera) {
next_index = 0;
next_camera = mp_get_camera_config(next_index);
}
camera = next_camera;
update_io_pipeline();
}
void
on_settings_btn_clicked(GtkWidget *widget, gpointer user_data)
{
gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "settings");
}
void
on_back_clicked(GtkWidget *widget, gpointer user_data)
{
gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "main");
}
void
on_control_auto_toggled(GtkToggleButton *widget, gpointer user_data)
{
bool is_manual = gtk_toggle_button_get_active(widget) ? false : true;
bool has_changed;
switch (current_control) {
case USER_CONTROL_ISO:
if (gain_is_manual != is_manual) {
gain_is_manual = is_manual;
has_changed = true;
}
break;
case USER_CONTROL_SHUTTER:
if (exposure_is_manual != is_manual) {
exposure_is_manual = is_manual;
has_changed = true;
}
break;
}
if (has_changed) {
// The slider might have been moved while Auto mode is active. When entering
// Manual mode, first read the slider value to sync with those changes.
double value = gtk_adjustment_get_value(control_slider);
switch (current_control) {
case USER_CONTROL_ISO:
if (value != gain) {
gain = (int)value;
}
break;
case USER_CONTROL_SHUTTER: {
// So far all sensors use exposure time in number of sensor rows
int new_exposure =
(int)(value / 360.0 * camera->capture_mode.height);
if (new_exposure != exposure) {
exposure = new_exposure;
}
break;
}
}
update_io_pipeline();
draw_controls();
}
}
void
on_control_slider_changed(GtkAdjustment *widget, gpointer user_data)
{
double value = gtk_adjustment_get_value(widget);
bool has_changed = false;
switch (current_control) {
case USER_CONTROL_ISO:
if (value != gain) {
gain = (int)value;
has_changed = true;
}
break;
case USER_CONTROL_SHUTTER: {
// So far all sensors use exposure time in number of sensor rows
int new_exposure =
(int)(value / 360.0 * camera->capture_mode.height);
if (new_exposure != exposure) {
exposure = new_exposure;
has_changed = true;
}
break;
}
}
if (has_changed) {
update_io_pipeline();
draw_controls();
}
}
int
main(int argc, char *argv[])
{
if (!mp_load_config())
return 1;
setenv("LC_NUMERIC", "C", 1);
gtk_init(&argc, &argv);
g_object_set(gtk_settings_get_default(), "gtk-application-prefer-dark-theme",
TRUE, NULL);
GtkBuilder *builder = gtk_builder_new_from_resource(
"/org/postmarketos/Megapixels/camera.glade");
GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window"));
GtkWidget *shutter = GTK_WIDGET(gtk_builder_get_object(builder, "shutter"));
GtkWidget *switch_btn =
GTK_WIDGET(gtk_builder_get_object(builder, "switch_camera"));
GtkWidget *settings_btn =
GTK_WIDGET(gtk_builder_get_object(builder, "settings"));
GtkWidget *settings_back =
GTK_WIDGET(gtk_builder_get_object(builder, "settings_back"));
GtkWidget *error_close =
GTK_WIDGET(gtk_builder_get_object(builder, "error_close"));
GtkWidget *open_last =
GTK_WIDGET(gtk_builder_get_object(builder, "open_last"));
GtkWidget *open_directory =
GTK_WIDGET(gtk_builder_get_object(builder, "open_directory"));
preview = GTK_WIDGET(gtk_builder_get_object(builder, "preview"));
error_box = GTK_WIDGET(gtk_builder_get_object(builder, "error_box"));
error_message = GTK_WIDGET(gtk_builder_get_object(builder, "error_message"));
main_stack = GTK_WIDGET(gtk_builder_get_object(builder, "main_stack"));
thumb_last = GTK_WIDGET(gtk_builder_get_object(builder, "thumb_last"));
control_box = GTK_WIDGET(gtk_builder_get_object(builder, "control_box"));
control_name = GTK_WIDGET(gtk_builder_get_object(builder, "control_name"));
control_slider =
GTK_ADJUSTMENT(gtk_builder_get_object(builder, "control_adj"));
control_auto = GTK_WIDGET(gtk_builder_get_object(builder, "control_auto"));
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
g_signal_connect(shutter, "clicked", G_CALLBACK(on_shutter_clicked), NULL);
g_signal_connect(error_close, "clicked", G_CALLBACK(on_error_close_clicked),
NULL);
g_signal_connect(switch_btn, "clicked", G_CALLBACK(on_camera_switch_clicked),
NULL);
g_signal_connect(settings_btn, "clicked",
G_CALLBACK(on_settings_btn_clicked), NULL);
g_signal_connect(settings_back, "clicked", G_CALLBACK(on_back_clicked),
NULL);
g_signal_connect(open_last, "clicked", G_CALLBACK(on_open_last_clicked),
NULL);
g_signal_connect(open_directory, "clicked",
G_CALLBACK(on_open_directory_clicked), NULL);
g_signal_connect(preview, "draw", G_CALLBACK(preview_draw), NULL);
g_signal_connect(preview, "configure-event", G_CALLBACK(preview_configure),
NULL);
gtk_widget_set_events(preview, gtk_widget_get_events(preview) |
GDK_BUTTON_PRESS_MASK |
GDK_POINTER_MOTION_MASK);
g_signal_connect(preview, "button-press-event", G_CALLBACK(on_preview_tap),
NULL);
g_signal_connect(control_auto, "toggled",
G_CALLBACK(on_control_auto_toggled), NULL);
g_signal_connect(control_slider, "value-changed",
G_CALLBACK(on_control_slider_changed), NULL);
GtkCssProvider *provider = gtk_css_provider_new();
if (access("camera.css", F_OK) != -1) {
gtk_css_provider_load_from_path(provider, "camera.css", NULL);
} else {
gtk_css_provider_load_from_resource(
provider, "/org/postmarketos/Megapixels/camera.css");
}
GtkStyleContext *context = gtk_widget_get_style_context(error_box);
gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_USER);
context = gtk_widget_get_style_context(control_box);
gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_USER);
mp_io_pipeline_start();
camera = mp_get_camera_config(0);
update_io_pipeline();
gtk_widget_show(window);
gtk_main();
mp_io_pipeline_stop();
return 0;
}