fixes these warnings: > ../src/main.c: In function 'capture_completed': > ../src/main.c:321:9: error: format not a string literal and no format arguments -Werror=format-security > 321 | snprintf(last_path, sizeof(last_path), args->fname); > | ^~~~~~~~ > ../src/process_pipeline.c: In function 'setup_capture': > ../src/process_pipeline.c:165:9: error: format not a string literal and no format arguments [-Werror=format-security] > 165 | snprintf(burst_dir, sizeof(burst_dir), tempdir); > | ^~~~~~~~ otherwise, `args->fname` and `tempdir` themself would be interpreted as the specifier.
1584 lines
56 KiB
C
1584 lines
56 KiB
C
#include "main.h"
|
|
|
|
#include "flash.h"
|
|
#include "gl_util.h"
|
|
#include "io_pipeline.h"
|
|
#include "process_pipeline.h"
|
|
#include "rotation.h"
|
|
#include "state.h"
|
|
#include <asm/errno.h>
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <gtk/gtk.h>
|
|
#define LIBFEEDBACK_USE_UNSTABLE_API
|
|
#include <libfeedback.h>
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
#include <gdk/wayland/gdkwayland.h>
|
|
#include <wayland-client.h>
|
|
#endif
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/Xrandr.h>
|
|
#include <gdk/x11/gdkx.h>
|
|
#endif
|
|
#include <libmegapixels.h>
|
|
#include <limits.h>
|
|
#include <linux/kdev_t.h>
|
|
#include <linux/media.h>
|
|
#include <linux/v4l2-subdev.h>
|
|
#include <linux/videodev2.h>
|
|
#include <locale.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/sysmacros.h>
|
|
#include <time.h>
|
|
#include <wordexp.h>
|
|
#include <zbar.h>
|
|
|
|
// #define RENDERDOC
|
|
|
|
#ifdef RENDERDOC
|
|
#include <dlfcn.h>
|
|
#include <renderdoc/app.h>
|
|
RENDERDOC_API_1_1_2 *rdoc_api = NULL;
|
|
#endif
|
|
|
|
mp_state_main state;
|
|
|
|
static int movie_start = 0;
|
|
|
|
static MPProcessPipelineBuffer *current_preview_buffer = NULL;
|
|
|
|
static char last_path[260] = "";
|
|
|
|
static MPZBarScanResult *zbar_result = NULL;
|
|
|
|
// Widgets
|
|
GtkWidget *window;
|
|
GtkWidget *preview;
|
|
GtkWidget *main_stack;
|
|
GtkWidget *open_last_stack;
|
|
GtkWidget *thumb_last;
|
|
GtkWidget *process_spinner;
|
|
GtkWidget *scanned_codes;
|
|
GtkWidget *preview_top_box;
|
|
GtkWidget *preview_bottom_box;
|
|
|
|
GtkWidget *message_box;
|
|
GtkWidget *message_label;
|
|
|
|
GtkWidget *iso_button;
|
|
GtkWidget *shutter_button;
|
|
GtkWidget *focus_button;
|
|
GtkWidget *flash_button;
|
|
GtkWidget *status_button;
|
|
|
|
LfbEvent *capture_event;
|
|
static GtkWidget *movie;
|
|
|
|
GSettings *settings;
|
|
GSettings *fb_settings;
|
|
|
|
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
|
|
display_error(const char *message)
|
|
{
|
|
gtk_label_set_label(GTK_LABEL(message_label), message);
|
|
gtk_widget_set_visible(message_box, true);
|
|
}
|
|
|
|
bool
|
|
check_window_active()
|
|
{
|
|
return gtk_window_is_active(GTK_WINDOW(window));
|
|
}
|
|
|
|
static void
|
|
update_io_pipeline()
|
|
{
|
|
mp_state_io new_state = {
|
|
.camera = state.camera,
|
|
.configuration = state.configuration,
|
|
.burst_length = state.burst_length,
|
|
.preview_width = state.preview_width,
|
|
.preview_height = state.preview_height,
|
|
.device_rotation = state.device_rotation,
|
|
.device_accel_rotation = state.device_accel_rotation,
|
|
.device_accel_rotation_good = state.device_accel_rotation_good,
|
|
|
|
.gain.control = state.gain.control,
|
|
.gain.auto_control = state.gain.auto_control,
|
|
.gain.value = state.gain.value,
|
|
.gain.value_req = state.gain.value_req,
|
|
.gain.max = state.gain.max,
|
|
.gain.min = state.gain.min,
|
|
.gain.manual = state.gain.manual,
|
|
.gain.manual_req = state.gain.manual_req,
|
|
|
|
.dgain.control = state.dgain.control,
|
|
.dgain.auto_control = state.dgain.auto_control,
|
|
.dgain.value = state.dgain.value,
|
|
.dgain.value_req = state.dgain.value_req,
|
|
.dgain.max = state.dgain.max,
|
|
.dgain.min = state.dgain.min,
|
|
.dgain.manual = state.dgain.manual,
|
|
.dgain.manual_req = state.dgain.manual_req,
|
|
|
|
.exposure.control = state.exposure.control,
|
|
.exposure.auto_control = state.exposure.auto_control,
|
|
.exposure.value = state.exposure.value,
|
|
.exposure.value_req = state.exposure.value_req,
|
|
.exposure.max = state.exposure.max,
|
|
.exposure.manual = state.exposure.manual,
|
|
.exposure.manual_req = state.exposure.manual_req,
|
|
|
|
.focus.control = state.focus.control,
|
|
.focus.auto_control = state.focus.auto_control,
|
|
.focus.value = state.focus.value,
|
|
.focus.value_req = state.focus.value_req,
|
|
.focus.max = state.focus.max,
|
|
.focus.manual = state.focus.manual,
|
|
.focus.manual_req = state.focus.manual_req,
|
|
|
|
.stats.exposure = state.stats.exposure,
|
|
.stats.temp = state.stats.temp,
|
|
.stats.tint = state.stats.tint,
|
|
.stats.focus = state.stats.focus,
|
|
|
|
.flash_enabled = state.flash_enabled,
|
|
};
|
|
mp_io_pipeline_update_state(&new_state);
|
|
}
|
|
|
|
void
|
|
notify_auto_status(gchar *msg)
|
|
{
|
|
gtk_button_set_label(GTK_BUTTON(status_button), msg);
|
|
}
|
|
|
|
/*
|
|
* State transfer from Process -> Main
|
|
*/
|
|
static bool
|
|
update_state(const mp_state_main *new_state)
|
|
{
|
|
if (state.exposure.manual != new_state->exposure.manual) {
|
|
const char *icon_name = new_state->exposure.manual ?
|
|
"shutter-man-symbolic" :
|
|
"shutter-auto-symbolic";
|
|
gtk_button_set_icon_name(GTK_BUTTON(shutter_button), icon_name);
|
|
}
|
|
|
|
if (state.gain.manual != new_state->gain.manual) {
|
|
const char *icon_name = new_state->gain.manual ? "iso-man-symbolic" :
|
|
"iso-auto-symbolic";
|
|
gtk_button_set_icon_name(GTK_BUTTON(iso_button), icon_name);
|
|
}
|
|
|
|
if (state.focus.manual != new_state->focus.manual) {
|
|
const char *icon_name = new_state->focus.manual ?
|
|
"focus-man-symbolic" :
|
|
"focus-auto-symbolic";
|
|
gtk_button_set_icon_name(GTK_BUTTON(focus_button), icon_name);
|
|
}
|
|
|
|
state.gain.control = new_state->gain.control;
|
|
state.gain.auto_control = new_state->gain.auto_control;
|
|
state.gain.value = new_state->gain.value;
|
|
state.gain.max = new_state->gain.max;
|
|
state.gain.min = new_state->gain.min;
|
|
state.gain.manual = new_state->gain.manual;
|
|
|
|
state.dgain.control = new_state->dgain.control;
|
|
state.dgain.auto_control = new_state->dgain.auto_control;
|
|
state.dgain.value = new_state->dgain.value;
|
|
state.dgain.max = new_state->dgain.max;
|
|
state.dgain.min = new_state->dgain.min;
|
|
state.dgain.manual = new_state->dgain.manual;
|
|
|
|
state.exposure.control = new_state->exposure.control;
|
|
state.exposure.auto_control = new_state->exposure.auto_control;
|
|
state.exposure.value = new_state->exposure.value;
|
|
state.exposure.max = new_state->exposure.max;
|
|
state.exposure.manual = new_state->exposure.manual;
|
|
|
|
state.focus.control = new_state->focus.control;
|
|
state.focus.auto_control = new_state->focus.auto_control;
|
|
state.focus.value = new_state->focus.value;
|
|
state.focus.max = new_state->focus.max;
|
|
state.focus.manual = new_state->focus.manual;
|
|
|
|
state.has_auto_focus_continuous = new_state->has_auto_focus_continuous;
|
|
state.has_auto_focus_start = new_state->has_auto_focus_start;
|
|
|
|
state.preview_buffer_width = new_state->preview_buffer_width;
|
|
state.preview_buffer_height = new_state->preview_buffer_height;
|
|
|
|
state.stats.exposure = new_state->stats.exposure;
|
|
state.stats.temp = new_state->stats.temp;
|
|
state.stats.tint = new_state->stats.tint;
|
|
state.stats.focus = new_state->stats.focus;
|
|
|
|
// Make the right settings available for the camera
|
|
gtk_widget_set_visible(iso_button, state.gain.control.id != 0);
|
|
gtk_widget_set_visible(shutter_button, state.exposure.control.id != 0);
|
|
gtk_widget_set_visible(focus_button, state.focus.control.id != 0);
|
|
// Even if there's no flash led/v4l, it'll just default to using the screen
|
|
// as flash, so always enable this button
|
|
gtk_widget_set_visible(flash_button, true);
|
|
gtk_widget_set_visible(status_button, true);
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
mp_main_update_state(const mp_state_main *new_state)
|
|
{
|
|
mp_state_main *state_copy = malloc(sizeof(mp_state_main));
|
|
*state_copy = *new_state;
|
|
|
|
g_main_context_invoke_full(g_main_context_default(),
|
|
G_PRIORITY_DEFAULT_IDLE,
|
|
(GSourceFunc)update_state,
|
|
state_copy,
|
|
free);
|
|
}
|
|
|
|
static bool
|
|
set_zbar_result(MPZBarScanResult *result)
|
|
{
|
|
if (zbar_result) {
|
|
for (uint8_t i = 0; i < zbar_result->size; ++i) {
|
|
free(zbar_result->codes[i].data);
|
|
}
|
|
|
|
free(zbar_result);
|
|
}
|
|
|
|
zbar_result = result;
|
|
gtk_widget_queue_draw(preview);
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
mp_main_set_zbar_result(MPZBarScanResult *result)
|
|
{
|
|
g_main_context_invoke_full(g_main_context_default(),
|
|
G_PRIORITY_DEFAULT_IDLE,
|
|
(GSourceFunc)set_zbar_result,
|
|
result,
|
|
NULL);
|
|
}
|
|
|
|
static bool
|
|
set_preview(MPProcessPipelineBuffer *buffer)
|
|
{
|
|
if (current_preview_buffer) {
|
|
mp_process_pipeline_buffer_unref(current_preview_buffer);
|
|
}
|
|
current_preview_buffer = buffer;
|
|
gtk_widget_queue_draw(preview);
|
|
return false;
|
|
}
|
|
|
|
void
|
|
mp_main_set_preview(MPProcessPipelineBuffer *buffer)
|
|
{
|
|
g_main_context_invoke_full(g_main_context_default(),
|
|
G_PRIORITY_DEFAULT_IDLE,
|
|
(GSourceFunc)set_preview,
|
|
buffer,
|
|
NULL);
|
|
}
|
|
|
|
struct capture_completed_args {
|
|
GdkTexture *thumb;
|
|
char *fname;
|
|
};
|
|
|
|
static bool
|
|
capture_completed(struct capture_completed_args *args)
|
|
{
|
|
snprintf(last_path, sizeof(last_path), "%s", args->fname);
|
|
|
|
gtk_image_set_from_paintable(GTK_IMAGE(thumb_last),
|
|
GDK_PAINTABLE(args->thumb));
|
|
|
|
gtk_spinner_stop(GTK_SPINNER(process_spinner));
|
|
gtk_stack_set_visible_child(GTK_STACK(open_last_stack), thumb_last);
|
|
|
|
g_object_unref(args->thumb);
|
|
g_free(args->fname);
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
mp_main_capture_completed(GdkTexture *thumb, const char *fname)
|
|
{
|
|
struct capture_completed_args *args =
|
|
malloc(sizeof(struct capture_completed_args));
|
|
args->thumb = thumb;
|
|
args->fname = g_strdup(fname);
|
|
g_main_context_invoke_full(g_main_context_default(),
|
|
G_PRIORITY_DEFAULT_IDLE,
|
|
(GSourceFunc)capture_completed,
|
|
args,
|
|
free);
|
|
}
|
|
|
|
static GLuint blit_program;
|
|
static GLuint blit_uniform_transform;
|
|
static GLuint blit_uniform_texture;
|
|
static GLuint solid_program;
|
|
static GLuint solid_uniform_color;
|
|
static GLuint quad;
|
|
|
|
static void
|
|
preview_realize(GtkGLArea *area)
|
|
{
|
|
gtk_gl_area_make_current(area);
|
|
|
|
if (gtk_gl_area_get_error(area) != NULL) {
|
|
return;
|
|
}
|
|
|
|
// Make a VAO for OpenGL
|
|
if (!gtk_gl_area_get_use_es(area)) {
|
|
GLuint vao;
|
|
glGenVertexArrays(1, &vao);
|
|
glBindVertexArray(vao);
|
|
check_gl();
|
|
}
|
|
|
|
GLuint blit_shaders[] = {
|
|
gl_util_load_shader("/org/postmarketos/Megapixels/blit.vert",
|
|
GL_VERTEX_SHADER,
|
|
NULL,
|
|
0),
|
|
gl_util_load_shader("/org/postmarketos/Megapixels/blit.frag",
|
|
GL_FRAGMENT_SHADER,
|
|
NULL,
|
|
0),
|
|
};
|
|
|
|
blit_program = gl_util_link_program(blit_shaders, 2);
|
|
glBindAttribLocation(blit_program, GL_UTIL_VERTEX_ATTRIBUTE, "vert");
|
|
glBindAttribLocation(blit_program, GL_UTIL_TEX_COORD_ATTRIBUTE, "tex_coord");
|
|
check_gl();
|
|
|
|
blit_uniform_transform = glGetUniformLocation(blit_program, "transform");
|
|
blit_uniform_texture = glGetUniformLocation(blit_program, "texture");
|
|
|
|
GLuint solid_shaders[] = {
|
|
gl_util_load_shader("/org/postmarketos/Megapixels/solid.vert",
|
|
GL_VERTEX_SHADER,
|
|
NULL,
|
|
0),
|
|
gl_util_load_shader("/org/postmarketos/Megapixels/solid.frag",
|
|
GL_FRAGMENT_SHADER,
|
|
NULL,
|
|
0),
|
|
};
|
|
|
|
solid_program = gl_util_link_program(solid_shaders, 2);
|
|
glBindAttribLocation(solid_program, GL_UTIL_VERTEX_ATTRIBUTE, "vert");
|
|
check_gl();
|
|
|
|
solid_uniform_color = glGetUniformLocation(solid_program, "color");
|
|
|
|
quad = gl_util_new_quad();
|
|
}
|
|
|
|
static void
|
|
position_preview(float *offset_x, float *offset_y, float *size_x, float *size_y)
|
|
{
|
|
int buffer_width, buffer_height;
|
|
if (state.device_rotation == 0 || state.device_rotation == 180) {
|
|
buffer_width = state.preview_buffer_width;
|
|
buffer_height = state.preview_buffer_height;
|
|
} else {
|
|
buffer_width = state.preview_buffer_height;
|
|
buffer_height = state.preview_buffer_width;
|
|
}
|
|
|
|
int scale_factor = gtk_widget_get_scale_factor(preview);
|
|
int top_height = gtk_widget_get_height(preview_top_box) * scale_factor;
|
|
int bottom_height = gtk_widget_get_height(preview_bottom_box) * scale_factor;
|
|
int inner_height = state.preview_height - top_height - bottom_height;
|
|
|
|
float scale = (float)MIN(state.preview_width / (float)buffer_width,
|
|
state.preview_height / (float)buffer_height);
|
|
|
|
*size_x = scale * (float)buffer_width;
|
|
*size_y = scale * (float)buffer_height;
|
|
|
|
*offset_x = ((float)state.preview_width - *size_x) / 2.0f;
|
|
|
|
if (*size_y > (float)inner_height) {
|
|
*offset_y = ((float)state.preview_height - *size_y) / 2.0f;
|
|
} else {
|
|
*offset_y =
|
|
(float)top_height + ((float)inner_height - *size_y) / 2.0f;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
preview_draw(GtkGLArea *area, GdkGLContext *ctx, gpointer data)
|
|
{
|
|
if (gtk_gl_area_get_error(area) != NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (current_preview_buffer == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
#ifdef RENDERDOC
|
|
if (rdoc_api) {
|
|
rdoc_api->StartFrameCapture(NULL, NULL);
|
|
}
|
|
#endif
|
|
|
|
glClearColor(0, 0, 0, 1);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
float offset_x, offset_y, size_x, size_y;
|
|
position_preview(&offset_x, &offset_y, &size_x, &size_y);
|
|
glViewport(
|
|
offset_x, state.preview_height - size_y - offset_y, size_x, size_y);
|
|
|
|
// This rotates the camera preview based on the phone rotation
|
|
if (current_preview_buffer) {
|
|
glUseProgram(blit_program);
|
|
|
|
GLfloat rotation_list[4] = { 0, -1, 0, 1 };
|
|
int rotation_index = state.device_rotation / 90;
|
|
|
|
GLfloat sin_rot = rotation_list[rotation_index];
|
|
GLfloat cos_rot = rotation_list[(4 + rotation_index - 1) % 4];
|
|
GLfloat matrix[9] = {
|
|
// clang-format off
|
|
cos_rot, sin_rot, 0,
|
|
-sin_rot, cos_rot, 0,
|
|
0, 0, 1,
|
|
// clang-format on
|
|
};
|
|
glUniformMatrix3fv(blit_uniform_transform, 1, GL_FALSE, matrix);
|
|
check_gl();
|
|
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D,
|
|
mp_process_pipeline_buffer_get_texture_id(
|
|
current_preview_buffer));
|
|
glUniform1i(blit_uniform_texture, 0);
|
|
check_gl();
|
|
|
|
gl_util_bind_quad(quad);
|
|
gl_util_draw_quad(quad);
|
|
}
|
|
|
|
if (zbar_result) {
|
|
GLuint buffer;
|
|
if (!gtk_gl_area_get_use_es(area)) {
|
|
glGenBuffers(1, &buffer);
|
|
glBindBuffer(GL_ARRAY_BUFFER, buffer);
|
|
check_gl();
|
|
}
|
|
|
|
glUseProgram(solid_program);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
glUniform4f(solid_uniform_color, 1, 0, 0, 0.5);
|
|
|
|
for (uint8_t i = 0; i < zbar_result->size; ++i) {
|
|
MPZBarCode *code = &zbar_result->codes[i];
|
|
|
|
GLfloat vertices[] = {
|
|
code->bounds_x[0], code->bounds_y[0], // Bottom left
|
|
code->bounds_x[1], code->bounds_y[1], // Bottom right
|
|
code->bounds_x[3], code->bounds_y[3], // Top left
|
|
code->bounds_x[2], code->bounds_y[2], // Top right
|
|
};
|
|
|
|
// Convert the pixel X/Y coordinates to OpenGL coordinates
|
|
// (-1 to 1)
|
|
for (int i = 0; i < 4; ++i) {
|
|
// Width/height need to be swapped between portrait
|
|
// mode and landscape mode for symbols to render
|
|
// correctly
|
|
if (state.device_rotation == 0 ||
|
|
state.device_rotation == 180) {
|
|
vertices[i * 2] =
|
|
2 * vertices[i * 2] /
|
|
state.preview_buffer_width -
|
|
1.0;
|
|
vertices[i * 2 + 1] =
|
|
1.0 -
|
|
2 * vertices[i * 2 + 1] /
|
|
state.preview_buffer_height;
|
|
} else {
|
|
vertices[i * 2] =
|
|
2 * vertices[i * 2] /
|
|
state.preview_buffer_height -
|
|
1.0;
|
|
vertices[i * 2 + 1] =
|
|
1.0 -
|
|
2 * vertices[i * 2 + 1] /
|
|
state.preview_buffer_width;
|
|
}
|
|
}
|
|
|
|
if (gtk_gl_area_get_use_es(area)) {
|
|
glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE,
|
|
2,
|
|
GL_FLOAT,
|
|
0,
|
|
0,
|
|
vertices);
|
|
check_gl();
|
|
glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE);
|
|
check_gl();
|
|
} else {
|
|
glBufferData(GL_ARRAY_BUFFER,
|
|
sizeof(vertices),
|
|
vertices,
|
|
GL_STREAM_DRAW);
|
|
check_gl();
|
|
|
|
glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE,
|
|
2,
|
|
GL_FLOAT,
|
|
GL_FALSE,
|
|
0,
|
|
0);
|
|
glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE);
|
|
check_gl();
|
|
}
|
|
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
check_gl();
|
|
}
|
|
|
|
glDisable(GL_BLEND);
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
}
|
|
|
|
glFlush();
|
|
|
|
#ifdef RENDERDOC
|
|
if (rdoc_api) {
|
|
rdoc_api->EndFrameCapture(NULL, NULL);
|
|
}
|
|
#endif
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
preview_resize(GtkWidget *widget, int width, int height, gpointer data)
|
|
{
|
|
if (state.preview_width != width || state.preview_height != height) {
|
|
state.preview_width = width;
|
|
state.preview_height = height;
|
|
if (state.configuration->count == 0)
|
|
return TRUE;
|
|
update_io_pipeline();
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
run_open_last_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
char uri[275];
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
if (strlen(last_path) == 0) {
|
|
return;
|
|
}
|
|
snprintf(uri, sizeof(uri), "file://%s", last_path);
|
|
if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) {
|
|
g_printerr("Could not launch image viewer for '%s': %s\n",
|
|
uri,
|
|
error->message);
|
|
}
|
|
}
|
|
|
|
void
|
|
run_open_photos_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
char uri[270];
|
|
g_autoptr(GError) error = NULL;
|
|
snprintf(uri, sizeof(uri), "file://%s", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES));
|
|
if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) {
|
|
g_printerr("Could not launch image viewer: %s\n", error->message);
|
|
}
|
|
}
|
|
|
|
void
|
|
run_capture_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
gtk_spinner_start(GTK_SPINNER(process_spinner));
|
|
gtk_stack_set_visible_child(GTK_STACK(open_last_stack), process_spinner);
|
|
if (capture_event)
|
|
lfb_event_trigger_feedback_async(capture_event, NULL, NULL, NULL);
|
|
|
|
mp_io_pipeline_capture();
|
|
}
|
|
|
|
void
|
|
run_about_action(GSimpleAction *action, GVariant *param, GApplication *app)
|
|
{
|
|
GtkWindow *parent = gtk_application_get_active_window(GTK_APPLICATION(app));
|
|
gtk_show_about_dialog(parent,
|
|
"program-name",
|
|
"Megapixels",
|
|
"title",
|
|
"Megapixels",
|
|
"logo-icon-name",
|
|
"me.gapixels.Megapixels",
|
|
"comments",
|
|
"The postmarketOS camera application",
|
|
"website",
|
|
"https://gitlab.com/megapixels-org/Megapixels",
|
|
"version",
|
|
VERSION,
|
|
"license-type",
|
|
GTK_LICENSE_GPL_3_0_ONLY,
|
|
NULL);
|
|
}
|
|
|
|
void
|
|
run_quit_action(GSimpleAction *action, GVariant *param, GApplication *app)
|
|
{
|
|
g_application_quit(app);
|
|
}
|
|
|
|
static bool
|
|
check_point_inside_bounds(int x, int y, const int *bounds_x, const int *bounds_y)
|
|
{
|
|
bool right = false, left = false, top = false, bottom = false;
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
if (x <= bounds_x[i])
|
|
left = true;
|
|
if (x >= bounds_x[i])
|
|
right = true;
|
|
if (y <= bounds_y[i])
|
|
top = true;
|
|
if (y >= bounds_y[i])
|
|
bottom = true;
|
|
}
|
|
|
|
return right && left && top && bottom;
|
|
}
|
|
|
|
static void
|
|
on_zbar_dialog_response(GtkDialog *dialog, int response, char *data)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
switch (response) {
|
|
case GTK_RESPONSE_YES:
|
|
if (!g_app_info_launch_default_for_uri(data, NULL, &error)) {
|
|
g_printerr("Could not launch application: %s\n",
|
|
error->message);
|
|
}
|
|
case GTK_RESPONSE_ACCEPT: {
|
|
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(dialog));
|
|
gdk_clipboard_set_text(gdk_display_get_clipboard(display), data);
|
|
}
|
|
case GTK_RESPONSE_CANCEL:
|
|
break;
|
|
default:
|
|
g_printerr("Wrong dialog response: %d\n", response);
|
|
}
|
|
|
|
g_free(data);
|
|
gtk_window_destroy(GTK_WINDOW(dialog));
|
|
}
|
|
|
|
static void
|
|
on_zbar_code_tapped(GtkWidget *widget, const MPZBarCode *code)
|
|
{
|
|
GtkWidget *dialog;
|
|
GtkDialogFlags flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
|
|
bool data_is_url =
|
|
g_uri_is_valid(code->data, G_URI_FLAGS_PARSE_RELAXED, NULL);
|
|
|
|
char *data = strdup(code->data);
|
|
|
|
if (data_is_url) {
|
|
dialog = gtk_message_dialog_new(
|
|
GTK_WINDOW(gtk_widget_get_root(widget)),
|
|
flags,
|
|
GTK_MESSAGE_QUESTION,
|
|
GTK_BUTTONS_NONE,
|
|
"Found a URL '%s' encoded in a %s.",
|
|
code->data,
|
|
code->type);
|
|
gtk_dialog_add_buttons(
|
|
GTK_DIALOG(dialog), "_Open URL", GTK_RESPONSE_YES, NULL);
|
|
} else {
|
|
dialog = gtk_message_dialog_new(
|
|
GTK_WINDOW(gtk_widget_get_root(widget)),
|
|
flags,
|
|
GTK_MESSAGE_QUESTION,
|
|
GTK_BUTTONS_NONE,
|
|
"Found data encoded in a %s.",
|
|
code->type);
|
|
gtk_message_dialog_format_secondary_markup(
|
|
GTK_MESSAGE_DIALOG(dialog), "<small>%s</small>", code->data);
|
|
}
|
|
gtk_dialog_add_buttons(GTK_DIALOG(dialog),
|
|
"_Copy",
|
|
GTK_RESPONSE_ACCEPT,
|
|
"_Cancel",
|
|
GTK_RESPONSE_CANCEL,
|
|
NULL);
|
|
|
|
g_signal_connect(
|
|
dialog, "response", G_CALLBACK(on_zbar_dialog_response), data);
|
|
|
|
gtk_widget_set_visible(GTK_WIDGET(dialog), true);
|
|
}
|
|
|
|
static void
|
|
preview_pressed(GtkGestureClick *gesture, int n_press, double x, double y)
|
|
{
|
|
GtkWidget *widget =
|
|
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
|
|
int scale_factor = gtk_widget_get_scale_factor(widget);
|
|
|
|
// Tapped zbar result
|
|
if (zbar_result) {
|
|
// Transform the event coordinates to the image
|
|
float offset_x, offset_y, size_x, size_y;
|
|
position_preview(&offset_x, &offset_y, &size_x, &size_y);
|
|
|
|
int zbar_x = (x - offset_x) * scale_factor / size_x *
|
|
state.preview_buffer_width;
|
|
int zbar_y = (y - offset_y) * scale_factor / size_y *
|
|
state.preview_buffer_height;
|
|
|
|
for (uint8_t i = 0; i < zbar_result->size; ++i) {
|
|
MPZBarCode *code = &zbar_result->codes[i];
|
|
|
|
if (check_point_inside_bounds(zbar_x,
|
|
zbar_y,
|
|
code->bounds_x,
|
|
code->bounds_y)) {
|
|
on_zbar_code_tapped(widget, code);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tapped preview image itself, try focussing
|
|
if (state.has_auto_focus_start) {
|
|
mp_io_pipeline_focus();
|
|
}
|
|
}
|
|
|
|
static void
|
|
run_camera_switch_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
int new_index = state.camera->index + 1;
|
|
if (new_index > state.configuration->count - 1 || new_index < 0) {
|
|
new_index = 0;
|
|
}
|
|
|
|
state.camera = state.configuration->cameras[new_index];
|
|
// TODO: allow setting burst length in the config
|
|
state.burst_length = 5;
|
|
update_io_pipeline();
|
|
}
|
|
|
|
static void
|
|
run_open_settings_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "settings");
|
|
}
|
|
|
|
static void
|
|
run_close_settings_action(GSimpleAction *action, GVariant *param, gpointer user_data)
|
|
{
|
|
gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "main");
|
|
}
|
|
|
|
static void
|
|
on_controls_scale_changed(GtkAdjustment *adjustment, void (*set_fn)(double))
|
|
{
|
|
set_fn(gtk_adjustment_get_value(adjustment));
|
|
}
|
|
|
|
static void
|
|
update_value(GtkAdjustment *adjustment, GtkLabel *label)
|
|
{
|
|
char buf[12];
|
|
snprintf(buf, 12, "%.0f", gtk_adjustment_get_value(adjustment));
|
|
gtk_label_set_label(label, buf);
|
|
}
|
|
|
|
static void
|
|
on_auto_controls_toggled(GtkToggleButton *button, void (*set_auto_fn)(bool))
|
|
{
|
|
set_auto_fn(gtk_toggle_button_get_active(button));
|
|
}
|
|
|
|
static void
|
|
update_scale(GtkToggleButton *button, GtkScale *scale)
|
|
{
|
|
gtk_widget_set_sensitive(GTK_WIDGET(scale),
|
|
!gtk_toggle_button_get_active(button));
|
|
}
|
|
|
|
static void
|
|
open_controls(GtkWidget *parent,
|
|
const char *title_name,
|
|
double min_value,
|
|
double max_value,
|
|
double current,
|
|
bool auto_enabled,
|
|
void (*set_fn)(double),
|
|
void (*set_auto_fn)(bool))
|
|
{
|
|
GtkBuilder *builder = gtk_builder_new_from_resource(
|
|
"/org/postmarketos/Megapixels/controls-popover.ui");
|
|
GtkPopover *popover =
|
|
GTK_POPOVER(gtk_builder_get_object(builder, "controls"));
|
|
GtkScale *scale = GTK_SCALE(gtk_builder_get_object(builder, "scale"));
|
|
GtkLabel *title = GTK_LABEL(gtk_builder_get_object(builder, "title"));
|
|
GtkLabel *value_label =
|
|
GTK_LABEL(gtk_builder_get_object(builder, "value-label"));
|
|
GtkToggleButton *auto_button =
|
|
GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "auto-button"));
|
|
|
|
gtk_label_set_label(title, title_name);
|
|
|
|
GtkAdjustment *adjustment = gtk_range_get_adjustment(GTK_RANGE(scale));
|
|
gtk_adjustment_set_lower(adjustment, min_value);
|
|
gtk_adjustment_set_upper(adjustment, max_value);
|
|
gtk_adjustment_set_value(adjustment, current);
|
|
update_value(adjustment, value_label);
|
|
|
|
gtk_toggle_button_set_active(auto_button, auto_enabled);
|
|
update_scale(auto_button, scale);
|
|
|
|
g_signal_connect(adjustment,
|
|
"value-changed",
|
|
G_CALLBACK(on_controls_scale_changed),
|
|
set_fn);
|
|
g_signal_connect(
|
|
adjustment, "value-changed", G_CALLBACK(update_value), value_label);
|
|
g_signal_connect(auto_button,
|
|
"toggled",
|
|
G_CALLBACK(on_auto_controls_toggled),
|
|
set_auto_fn);
|
|
g_signal_connect(auto_button, "toggled", G_CALLBACK(update_scale), scale);
|
|
|
|
gtk_widget_set_parent(GTK_WIDGET(popover), parent);
|
|
gtk_popover_popup(popover);
|
|
// g_object_unref(popover);
|
|
}
|
|
|
|
static void
|
|
set_gain(double value)
|
|
{
|
|
if (state.gain.value != (int)value) {
|
|
state.gain.value_req = (int)value;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_gain_auto(bool is_auto)
|
|
{
|
|
if (state.gain.manual != !is_auto) {
|
|
state.gain.manual_req = !is_auto;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
open_iso_controls(GtkWidget *button, gpointer user_data)
|
|
{
|
|
open_controls(button,
|
|
"ISO",
|
|
0,
|
|
state.gain.max,
|
|
state.gain.value,
|
|
!state.gain.manual,
|
|
set_gain,
|
|
set_gain_auto);
|
|
}
|
|
|
|
static void
|
|
set_shutter(double value)
|
|
{
|
|
int new_exposure = (int)(value / 360.0 * state.camera->current_mode->height);
|
|
if (new_exposure != state.exposure.value) {
|
|
state.exposure.value_req = new_exposure;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_shutter_auto(bool is_auto)
|
|
{
|
|
if (state.exposure.manual != !is_auto) {
|
|
state.exposure.manual_req = !is_auto;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_focus(double value)
|
|
{
|
|
if (state.focus.value != (int)value) {
|
|
state.focus.value_req = (int)value;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_focus_auto(bool is_auto)
|
|
{
|
|
if (state.focus.manual != !is_auto) {
|
|
state.focus.manual_req = !is_auto;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
open_shutter_controls(GtkWidget *button, gpointer user_data)
|
|
{
|
|
float value = (float)state.exposure.value * 360.0f /
|
|
(float)state.camera->current_mode->height;
|
|
open_controls(button,
|
|
"Shutter",
|
|
1.0,
|
|
360.0,
|
|
value,
|
|
!state.exposure.manual,
|
|
set_shutter,
|
|
set_shutter_auto);
|
|
}
|
|
|
|
static void
|
|
open_focus_controls(GtkWidget *button, gpointer user_data)
|
|
{
|
|
open_controls(button,
|
|
"Focus",
|
|
0,
|
|
state.focus.max,
|
|
state.focus.value,
|
|
!state.focus.manual,
|
|
set_focus,
|
|
set_focus_auto);
|
|
}
|
|
|
|
static void
|
|
flash_button_clicked(GtkWidget *button, gpointer user_data)
|
|
{
|
|
state.flash_enabled = !state.flash_enabled;
|
|
update_io_pipeline();
|
|
|
|
const char *icon_name = state.flash_enabled ? "flash-enabled-symbolic" :
|
|
"flash-disabled-symbolic";
|
|
gtk_button_set_icon_name(GTK_BUTTON(button), icon_name);
|
|
}
|
|
|
|
void
|
|
notify_movie_record_ready(void)
|
|
{
|
|
movie_start = 0;
|
|
gtk_button_set_icon_name(GTK_BUTTON(movie),
|
|
"video-inactive-symbolic");
|
|
}
|
|
|
|
void
|
|
notify_movie_message(gchar *msg)
|
|
{
|
|
gtk_button_set_label(GTK_BUTTON(movie), msg);
|
|
}
|
|
|
|
void
|
|
on_movie_clicked(GtkWidget *widget, gpointer user_data)
|
|
{
|
|
if (movie_start) {
|
|
movie_start = 0;
|
|
gtk_button_set_icon_name(GTK_BUTTON(movie),
|
|
"video-processing-symbolic");
|
|
on_movie_stop();
|
|
} else {
|
|
movie_start = 1;
|
|
gtk_button_set_icon_name(GTK_BUTTON(movie),
|
|
"video-recording-symbolic");
|
|
on_movie_start();
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_realize(GtkWidget *window, gpointer *data)
|
|
{
|
|
if (state.configuration->count == 0) {
|
|
return;
|
|
}
|
|
GtkNative *native = gtk_widget_get_native(window);
|
|
mp_process_pipeline_init_gl(gtk_native_get_surface(native));
|
|
state.camera = state.configuration->cameras[0];
|
|
update_io_pipeline();
|
|
}
|
|
|
|
static GSimpleAction *
|
|
create_simple_action(GtkApplication *app, const char *name, GCallback callback)
|
|
{
|
|
GSimpleAction *action = g_simple_action_new(name, NULL);
|
|
g_signal_connect(action, "activate", callback, app);
|
|
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
|
|
return action;
|
|
}
|
|
|
|
static void
|
|
update_ui_rotation()
|
|
{
|
|
if (state.device_rotation == 0 || state.device_rotation == 180) {
|
|
// Portrait
|
|
gtk_widget_set_halign(preview_top_box, GTK_ALIGN_FILL);
|
|
gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_top_box),
|
|
GTK_ORIENTATION_VERTICAL);
|
|
|
|
gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_FILL);
|
|
gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_bottom_box),
|
|
GTK_ORIENTATION_HORIZONTAL);
|
|
|
|
if (state.device_rotation == 0) {
|
|
gtk_widget_set_valign(preview_top_box, GTK_ALIGN_START);
|
|
gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_END);
|
|
} else {
|
|
gtk_widget_set_valign(preview_top_box, GTK_ALIGN_END);
|
|
gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_START);
|
|
}
|
|
} else {
|
|
// Landscape
|
|
gtk_widget_set_valign(preview_top_box, GTK_ALIGN_FILL);
|
|
gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_top_box),
|
|
GTK_ORIENTATION_HORIZONTAL);
|
|
|
|
gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_FILL);
|
|
gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_bottom_box),
|
|
GTK_ORIENTATION_VERTICAL);
|
|
|
|
if (state.device_rotation == 90) {
|
|
gtk_widget_set_halign(preview_top_box, GTK_ALIGN_END);
|
|
gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_START);
|
|
} else {
|
|
gtk_widget_set_halign(preview_top_box, GTK_ALIGN_START);
|
|
gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_END);
|
|
}
|
|
}
|
|
}
|
|
|
|
char *
|
|
munge_app_id(const char *app_id)
|
|
{
|
|
char *id = g_strdup(app_id);
|
|
int i;
|
|
|
|
if (g_str_has_suffix(id, ".desktop")) {
|
|
char *c = g_strrstr(id, ".desktop");
|
|
if (c)
|
|
*c = '\0';
|
|
}
|
|
|
|
g_strcanon(id,
|
|
"0123456789"
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
"-",
|
|
'-');
|
|
for (i = 0; id[i] != '\0'; i++)
|
|
id[i] = g_ascii_tolower(id[i]);
|
|
|
|
return id;
|
|
}
|
|
|
|
/* Verbatim from feedbackd */
|
|
#define FEEDBACKD_SCHEMA_ID "org.sigxcpu.feedbackd"
|
|
#define FEEDBACKD_KEY_PROFILE "profile"
|
|
#define FEEDBACKD_APP_SCHEMA FEEDBACKD_SCHEMA_ID ".application"
|
|
#define FEEDBACKD_APP_PREFIX "/org/sigxcpu/feedbackd/application/"
|
|
|
|
static gboolean
|
|
fb_profile_to_state(GValue *value, GVariant *variant, gpointer user_data)
|
|
{
|
|
const gchar *name;
|
|
gboolean fb_state = FALSE;
|
|
|
|
name = g_variant_get_string(variant, NULL);
|
|
|
|
if (g_strcmp0(name, "full") == 0)
|
|
fb_state = TRUE;
|
|
|
|
g_value_set_boolean(value, fb_state);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static GVariant *
|
|
state_to_fb_profile(const GValue *value,
|
|
const GVariantType *expected_type,
|
|
gpointer user_data)
|
|
{
|
|
gboolean fb_state = g_value_get_boolean(value);
|
|
|
|
return g_variant_new_string(fb_state ? "full" : "silent");
|
|
}
|
|
|
|
static void
|
|
setup_fb_switch(GtkBuilder *builder)
|
|
{
|
|
g_autofree char *path = NULL;
|
|
g_autofree char *munged_id = NULL;
|
|
g_autoptr(GSettingsSchema) schema = NULL;
|
|
GSettingsSchemaSource *schema_source =
|
|
g_settings_schema_source_get_default();
|
|
GtkWidget *shutter_sound_switch =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "shutter-sound-switch"));
|
|
GtkWidget *feedback_box =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "feedback-box"));
|
|
|
|
schema = g_settings_schema_source_lookup(
|
|
schema_source, FEEDBACKD_APP_SCHEMA, TRUE);
|
|
if (schema == NULL) {
|
|
gtk_widget_set_sensitive(feedback_box, FALSE);
|
|
return;
|
|
}
|
|
|
|
munged_id = munge_app_id(APP_ID);
|
|
path = g_strconcat(FEEDBACKD_APP_PREFIX, munged_id, "/", NULL);
|
|
fb_settings = g_settings_new_with_path(FEEDBACKD_APP_SCHEMA, path);
|
|
g_settings_bind_with_mapping(fb_settings,
|
|
FEEDBACKD_KEY_PROFILE,
|
|
shutter_sound_switch,
|
|
"active",
|
|
G_SETTINGS_BIND_DEFAULT,
|
|
fb_profile_to_state,
|
|
state_to_fb_profile,
|
|
NULL,
|
|
NULL);
|
|
}
|
|
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
static void
|
|
wl_handle_geometry(void *data,
|
|
struct wl_output *wl_output,
|
|
int32_t x,
|
|
int32_t y,
|
|
int32_t physical_width,
|
|
int32_t physical_height,
|
|
int32_t subpixel,
|
|
const char *make,
|
|
const char *model,
|
|
int32_t transform)
|
|
{
|
|
assert(transform < 4);
|
|
int new_rotation = transform * 90;
|
|
|
|
if (new_rotation != state.device_rotation) {
|
|
state.device_rotation = new_rotation;
|
|
update_io_pipeline();
|
|
update_ui_rotation();
|
|
}
|
|
}
|
|
|
|
static void
|
|
wl_handle_mode(void *data,
|
|
struct wl_output *wl_output,
|
|
uint32_t flags,
|
|
int32_t width,
|
|
int32_t height,
|
|
int32_t refresh)
|
|
{
|
|
// Do nothing
|
|
}
|
|
|
|
static const struct wl_output_listener output_listener = {
|
|
.geometry = wl_handle_geometry,
|
|
.mode = wl_handle_mode
|
|
};
|
|
|
|
static void
|
|
wl_handle_global(void *data,
|
|
struct wl_registry *wl_registry,
|
|
uint32_t name,
|
|
const char *interface,
|
|
uint32_t version)
|
|
{
|
|
if (strcmp(interface, wl_output_interface.name) == 0) {
|
|
struct wl_output *output =
|
|
wl_registry_bind(wl_registry, name, &wl_output_interface, 1);
|
|
wl_output_add_listener(output, &output_listener, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
wl_handle_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name)
|
|
{
|
|
// Do nothing
|
|
}
|
|
|
|
static const struct wl_registry_listener registry_listener = {
|
|
.global = wl_handle_global,
|
|
.global_remove = wl_handle_global_remove
|
|
};
|
|
#endif // GDK_WINDOWING_WAYLAND
|
|
|
|
#ifdef GDK_WINDOWING_X11
|
|
static gboolean
|
|
xevent_handler(GdkDisplay *display, XEvent *xevent, gpointer data)
|
|
{
|
|
Display *xdisplay = gdk_x11_display_get_xdisplay(display);
|
|
int event_base, error_base;
|
|
XRRQueryExtension(xdisplay, &event_base, &error_base);
|
|
if (xevent->type - event_base == RRScreenChangeNotify) {
|
|
Rotation xrotation =
|
|
((XRRScreenChangeNotifyEvent *)xevent)->rotation;
|
|
int new_rotation = 0;
|
|
switch (xrotation) {
|
|
case RR_Rotate_0:
|
|
new_rotation = 0;
|
|
break;
|
|
case RR_Rotate_90:
|
|
new_rotation = 90;
|
|
break;
|
|
case RR_Rotate_180:
|
|
new_rotation = 180;
|
|
break;
|
|
case RR_Rotate_270:
|
|
new_rotation = 270;
|
|
break;
|
|
}
|
|
if (new_rotation != state.device_rotation) {
|
|
state.device_rotation = new_rotation;
|
|
update_io_pipeline();
|
|
update_ui_rotation();
|
|
}
|
|
}
|
|
|
|
// The return value of this function should always be FALSE; if it's
|
|
// TRUE, we prevent GTK/GDK from handling the event.
|
|
return FALSE;
|
|
}
|
|
#endif // GDK_WINDOWING_X11
|
|
|
|
static void
|
|
rotation_sensor_handler(int rotation, bool good)
|
|
{
|
|
if (rotation != state.device_accel_rotation ||
|
|
good != state.device_accel_rotation_good) {
|
|
state.device_accel_rotation = rotation;
|
|
state.device_accel_rotation_good = good;
|
|
update_io_pipeline();
|
|
}
|
|
}
|
|
|
|
static void
|
|
activate(GtkApplication *app, gpointer data)
|
|
{
|
|
g_object_set(gtk_settings_get_default(),
|
|
"gtk-application-prefer-dark-theme",
|
|
TRUE,
|
|
NULL);
|
|
GdkDisplay *display = gdk_display_get_default();
|
|
GtkIconTheme *icon_theme = gtk_icon_theme_get_for_display(display);
|
|
gtk_icon_theme_add_resource_path(icon_theme, "/org/postmarketos/Megapixels");
|
|
|
|
GtkCssProvider *provider = gtk_css_provider_new();
|
|
gtk_css_provider_load_from_resource(
|
|
provider, "/org/postmarketos/Megapixels/camera.css");
|
|
gtk_style_context_add_provider_for_display(
|
|
display,
|
|
GTK_STYLE_PROVIDER(provider),
|
|
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
|
|
GtkBuilder *builder = gtk_builder_new_from_resource(
|
|
"/org/postmarketos/Megapixels/camera.ui");
|
|
|
|
window = GTK_WIDGET(gtk_builder_get_object(builder, "window"));
|
|
iso_button =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "iso-controls-button"));
|
|
shutter_button = GTK_WIDGET(
|
|
gtk_builder_get_object(builder, "shutter-controls-button"));
|
|
focus_button =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "focus-controls-button"));
|
|
flash_button =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "flash-controls-button"));
|
|
status_button =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "status-button"));
|
|
GtkWidget *setting_dng_button =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "setting-raw"));
|
|
GtkWidget *setting_postprocessor_combo =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "setting-processor"));
|
|
GtkListStore *setting_postprocessor_list = GTK_LIST_STORE(
|
|
gtk_builder_get_object(builder, "list-postprocessors"));
|
|
preview = GTK_WIDGET(gtk_builder_get_object(builder, "preview"));
|
|
main_stack = GTK_WIDGET(gtk_builder_get_object(builder, "main_stack"));
|
|
open_last_stack =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "open_last_stack"));
|
|
thumb_last = GTK_WIDGET(gtk_builder_get_object(builder, "thumb_last"));
|
|
process_spinner =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "process_spinner"));
|
|
scanned_codes = GTK_WIDGET(gtk_builder_get_object(builder, "scanned-codes"));
|
|
preview_top_box = GTK_WIDGET(gtk_builder_get_object(builder, "top-box"));
|
|
preview_bottom_box =
|
|
GTK_WIDGET(gtk_builder_get_object(builder, "bottom-box"));
|
|
movie = GTK_WIDGET(gtk_builder_get_object(builder, "movie"));
|
|
|
|
message_box = GTK_WIDGET(gtk_builder_get_object(builder, "message-box"));
|
|
message_label = GTK_WIDGET(gtk_builder_get_object(builder, "message-label"));
|
|
|
|
g_signal_connect(window, "realize", G_CALLBACK(on_realize), NULL);
|
|
|
|
g_signal_connect(preview, "realize", G_CALLBACK(preview_realize), NULL);
|
|
g_signal_connect(preview, "render", G_CALLBACK(preview_draw), NULL);
|
|
g_signal_connect(preview, "resize", G_CALLBACK(preview_resize), NULL);
|
|
GtkGesture *click = gtk_gesture_click_new();
|
|
g_signal_connect(click, "pressed", G_CALLBACK(preview_pressed), NULL);
|
|
gtk_widget_add_controller(preview, GTK_EVENT_CONTROLLER(click));
|
|
|
|
g_signal_connect(iso_button, "clicked", G_CALLBACK(open_iso_controls), NULL);
|
|
g_signal_connect(
|
|
shutter_button, "clicked", G_CALLBACK(open_shutter_controls), NULL);
|
|
g_signal_connect(
|
|
focus_button, "clicked", G_CALLBACK(open_focus_controls), NULL);
|
|
g_signal_connect(
|
|
flash_button, "clicked", G_CALLBACK(flash_button_clicked), NULL);
|
|
|
|
g_signal_connect(movie, "clicked", G_CALLBACK(on_movie_clicked), NULL);
|
|
|
|
setup_fb_switch(builder);
|
|
|
|
// Setup actions
|
|
create_simple_action(app, "capture", G_CALLBACK(run_capture_action));
|
|
create_simple_action(
|
|
app, "switch-camera", G_CALLBACK(run_camera_switch_action));
|
|
create_simple_action(
|
|
app, "open-settings", G_CALLBACK(run_open_settings_action));
|
|
create_simple_action(
|
|
app, "close-settings", G_CALLBACK(run_close_settings_action));
|
|
create_simple_action(app, "open-last", G_CALLBACK(run_open_last_action));
|
|
create_simple_action(app, "open-photos", G_CALLBACK(run_open_photos_action));
|
|
create_simple_action(app, "about", G_CALLBACK(run_about_action));
|
|
create_simple_action(app, "quit", G_CALLBACK(run_quit_action));
|
|
|
|
// Setup shortcuts
|
|
const char *capture_accels[] = {
|
|
"space",
|
|
"AudioLowerVolume",
|
|
"AudioRaiseVolume",
|
|
NULL
|
|
};
|
|
gtk_application_set_accels_for_action(app, "app.capture", capture_accels);
|
|
|
|
const char *quit_accels[] = { "<Ctrl>q", "<Ctrl>w", NULL };
|
|
gtk_application_set_accels_for_action(app, "app.quit", quit_accels);
|
|
|
|
// Setup settings
|
|
settings = g_settings_new(APP_ID);
|
|
char *setting_postproc = g_settings_get_string(settings, "postprocessor");
|
|
|
|
// Initialize the postprocessing gsetting to the old processor if
|
|
// it was not set yet
|
|
if (setting_postproc == NULL || setting_postproc[0] == '\0') {
|
|
const int size = 512;
|
|
printf("Initializing postprocessor gsetting\n");
|
|
setting_postproc = malloc(size);
|
|
if (!mp_process_find_processor(setting_postproc, size, "postprocess.sh")) {
|
|
printf("No processor found\n");
|
|
exit(1);
|
|
}
|
|
g_settings_set_string(settings, "postprocessor", setting_postproc);
|
|
printf("Initialized postprocessor to %s\n", setting_postproc);
|
|
}
|
|
free(setting_postproc);
|
|
|
|
// Find all postprocessors for the settings list
|
|
mp_process_find_all_processors(setting_postprocessor_list);
|
|
|
|
// Bind settings widgets to the actual settings
|
|
g_settings_bind(settings,
|
|
"save-raw",
|
|
setting_dng_button,
|
|
"active",
|
|
G_SETTINGS_BIND_DEFAULT);
|
|
g_settings_bind(settings,
|
|
"postprocessor",
|
|
setting_postprocessor_combo,
|
|
"active-id",
|
|
G_SETTINGS_BIND_DEFAULT);
|
|
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
// Listen for Wayland rotation
|
|
if (GDK_IS_WAYLAND_DISPLAY(display)) {
|
|
struct wl_display *wl_display =
|
|
gdk_wayland_display_get_wl_display(display);
|
|
struct wl_registry *wl_registry =
|
|
wl_display_get_registry(wl_display);
|
|
// The registry listener will bind to our wl_output and add our
|
|
// listeners
|
|
wl_registry_add_listener(wl_registry, ®istry_listener, NULL);
|
|
// GTK will take care of dispatching wayland events for us.
|
|
// Wayland sends us a geometry event as soon as we bind to the
|
|
// wl_output, so we don't need to manually check the initial
|
|
// rotation here.
|
|
}
|
|
#endif
|
|
#ifdef GDK_WINDOWING_X11
|
|
// Listen for X rotation
|
|
if (GDK_IS_X11_DISPLAY(display)) {
|
|
g_signal_connect(
|
|
display, "xevent", G_CALLBACK(xevent_handler), NULL);
|
|
// Set initial rotation
|
|
Display *xdisplay = gdk_x11_display_get_xdisplay(display);
|
|
int screen =
|
|
XScreenNumberOfScreen(gdk_x11_display_get_xscreen(display));
|
|
Rotation xrotation;
|
|
XRRRotations(xdisplay, screen, &xrotation);
|
|
int new_rotation = 0;
|
|
switch (xrotation) {
|
|
case RR_Rotate_0:
|
|
new_rotation = 0;
|
|
break;
|
|
case RR_Rotate_90:
|
|
new_rotation = 90;
|
|
break;
|
|
case RR_Rotate_180:
|
|
new_rotation = 180;
|
|
break;
|
|
case RR_Rotate_270:
|
|
new_rotation = 270;
|
|
break;
|
|
}
|
|
if (new_rotation != state.device_rotation) {
|
|
state.device_rotation = new_rotation;
|
|
update_ui_rotation();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Initialize display flash
|
|
GDBusConnection *conn =
|
|
g_application_get_dbus_connection(G_APPLICATION(app));
|
|
mp_flash_gtk_init(conn);
|
|
mp_device_rotation_init(rotation_sensor_handler);
|
|
|
|
if (state.configuration->count > 0) {
|
|
mp_io_pipeline_start();
|
|
} else {
|
|
display_error("No camera found");
|
|
}
|
|
|
|
gtk_application_add_window(app, GTK_WINDOW(window));
|
|
gtk_widget_set_visible(window, true);
|
|
|
|
// Inhibit system shortcuts to be able to use
|
|
// VolumeUp and VolumeDown hardware buttons to take a picture
|
|
GtkNative *native = gtk_widget_get_native (GTK_WIDGET (window));
|
|
GdkSurface *surface = gtk_native_get_surface (native);
|
|
gdk_toplevel_inhibit_system_shortcuts (GDK_TOPLEVEL (surface), NULL);
|
|
}
|
|
|
|
static void
|
|
startup(GApplication *app, gpointer data)
|
|
{
|
|
g_autoptr(GError) err = NULL;
|
|
|
|
if (lfb_init(APP_ID, &err))
|
|
capture_event = lfb_event_new("camera-shutter");
|
|
else
|
|
g_warning("Failed to init libfeedback: %s", err->message);
|
|
}
|
|
|
|
static void
|
|
shutdown(GApplication *app, gpointer data)
|
|
{
|
|
// Only do cleanup in development, let the OS clean up otherwise
|
|
#ifdef DEBUG
|
|
mp_io_pipeline_stop();
|
|
mp_flash_gtk_clean();
|
|
mp_device_rotation_clean();
|
|
|
|
g_clear_object(&fb_settings);
|
|
g_clear_object(&capture_event);
|
|
lfb_uninit();
|
|
#endif
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
#ifdef RENDERDOC
|
|
{
|
|
void *mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
|
|
if (mod) {
|
|
pRENDERDOC_GetAPI RENDERDOC_GetAPI =
|
|
(pRENDERDOC_GetAPI)dlsym(mod, "RENDERDOC_GetAPI");
|
|
int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2,
|
|
(void **)&rdoc_api);
|
|
assert(ret == 1);
|
|
} else {
|
|
printf("Renderdoc not found\n");
|
|
}
|
|
}
|
|
#endif
|
|
char configfile[PATH_MAX];
|
|
libmegapixels_init(&state.configuration);
|
|
if (libmegapixels_find_config(PATH_MAX, configfile)) {
|
|
if (!libmegapixels_load_file(state.configuration, configfile)) {
|
|
fprintf(stderr, "Could not load config\n");
|
|
return 1;
|
|
}
|
|
} else {
|
|
printf("megapixels: No suitable config, defaulting to uvc\n");
|
|
if (!libmegapixels_load_uvc(state.configuration)) {
|
|
fprintf(stderr, "No config found\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
setenv("LC_NUMERIC", "C", 1);
|
|
|
|
GtkApplication *app = gtk_application_new(APP_ID, 0);
|
|
|
|
g_signal_connect(app, "startup", G_CALLBACK(startup), NULL);
|
|
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
|
g_signal_connect(app, "shutdown", G_CALLBACK(shutdown), NULL);
|
|
|
|
g_application_run(G_APPLICATION(app), argc, argv);
|
|
|
|
return 0;
|
|
}
|