diff --git a/CMakeLists.txt b/CMakeLists.txt index fa6a5aa2..619cce50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,28 +11,35 @@ include(UseVala) include(GResource) # Check dependencies -find_package(Vala REQUIRED) +find_package(Vala "0.22" REQUIRED) find_package(PkgConfig) -pkg_check_modules(GTK REQUIRED gtk+-3.0) +pkg_check_modules(GTK REQUIRED gtk+-3.0>=3.10) add_definitions(${GTK_CFLAGS} ${GTK_CFLAGS_OTHER}) link_libraries(${GTK_LIBRARIES}) link_directories(${GTK_LIBRARY_DIRS}) -pkg_check_modules(REQUIRED cairo gmodule-2.0) +pkg_check_modules(GLIB2 REQUIRED glib-2.0>=2.38) +pkg_check_modules(CAIRO REQUIRED cairo) +pkg_check_modules(GMODULE REQUIRED gmodule-2.0) + +# compile glib resource files to c code +GLIB_COMPILE_RESOURCES(GLIB_RESOURCES + SOURCE + ui/${PROJECT_NAME}.gresource.xml +) # Compile Vala to C vala_precompile(VALA_C - src/main.vala src/screen-recorder.vala + src/main.vala + src/peek-application-window.vala + src/screen-recorder.vala PACKAGES gtk+-3.0 gmodule-2.0 -) - -# compile glib resource files to c code -GLIB_COMPILE_RESOURCES( GLIB_RESOURCES - SOURCE - ui/${PROJECT_NAME}.gresource.xml + OPTIONS + --target-glib=2.38 + --gresources=../ui/${PROJECT_NAME}.gresource.xml ) # Compile C code diff --git a/README.md b/README.md index 9d1e6aae..9d23de8b 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ A simple tool that allows you to record short animated Gifs from your screen. ## Requirements ### Runtime - * Gtk 3.? - * ffmpeg or byzanz? + * Gtk 3.10 or higher + * ffmpeg + * ImageMagick ### Development diff --git a/src/main.vala b/src/main.vala index 5f436f7a..df5cc91e 100644 --- a/src/main.vala +++ b/src/main.vala @@ -7,273 +7,13 @@ This software is licensed under the GNU General Public License (version 3 or later). See the LICENSE file in this distribution. */ -using Gtk; -using Cairo; - -Window window; -Widget recording_view; -Button record_button; -Button stop_button; -Label size_indicator; -uint size_indicator_timeout = 0; -bool supports_alpha = true; -ScreenRecorder recorder = null; - -public void on_application_window_screen_changed (Widget widget, Gdk.Screen oldScreen) { - var screen = widget.get_screen (); - var visual = screen.get_rgba_visual (); - - if (visual == null) { - stderr.printf ("Screen does not support alpha channels!"); - visual = screen.get_system_visual (); - supports_alpha = false; - } - else { - supports_alpha = true; - } - - widget.set_visual (visual); -} - -public bool on_application_window_configure_event (Gdk.EventConfigure event) { - if (recorder.is_recording) { - recorder.cancel (); - } - - return false; -} - -public bool on_recording_view_draw (Widget widget, Context ctx) { - if (supports_alpha) { - ctx.set_source_rgba (0.0, 0.0, 0.0, 0.0); - } - else { - ctx.set_source_rgb (0.0, 0.0, 0.0); - } - - // Stance out the transparent inner part - ctx.set_operator (Operator.CLEAR); - ctx.paint (); - ctx.fill (); - - // Set an input shape so that the recording view is not clickable - var window_region = create_region_from_widget (widget.get_toplevel()); - var recording_viewRegion = create_region_from_widget (widget); - window_region.subtract (recording_viewRegion); - window.input_shape_combine_region (window_region); - - return false; -} - -public void on_recording_view_size_allocate (Widget widget, Rectangle rectangle) { - // Show the size - var size_label = new StringBuilder (); - var area = get_recording_area (); - size_label.printf ("%i x %i", area.width, area.height); - size_indicator.set_text (size_label.str); - size_indicator.show (); - - if (size_indicator_timeout != 0) { - Source.remove (size_indicator_timeout); - } - - if (!recorder.is_recording) { - size_indicator.opacity = 1.0; - size_indicator_timeout = Timeout.add (800, () => { - size_indicator.opacity = 0.0; - return false; - }); - } -} - -public void on_application_window_delete_event (string[] args) { - recorder.cancel (); - Gtk.main_quit (); -} - -public void on_cancel_button_clicked (Button source) { - recorder.cancel (); - Gtk.main_quit (); -} - -public void on_record_button_clicked (Button source) { - var area = get_recording_area (); - stdout.printf ("Recording area: %i, %i, %i, %i\n", - area.left, area.top, area.width, area.height); - recorder.record (area); -} - -public void on_stop_button_clicked (Button source) { - recorder.stop (); -} - -private void enter_recording_state () { - size_indicator.opacity = 0.0; - record_button.hide (); - stop_button.show (); - freeze_window_size (); -} - -private void leave_recording_state () { - stop_button.hide (); - record_button.show (); - unfreeze_window_size (); -} - -private void freeze_window_size () { - var width = window.get_allocated_width (); - var height = window.get_allocated_height (); - window.set_size_request (width, height); - window.resizable = false; -} - -private void unfreeze_window_size () { - var width = window.get_allocated_width (); - var height = window.get_allocated_height (); - window.set_size_request (0, 0); - window.set_default_size (width, height); - window.resizable = true; -} - -private Region create_region_from_widget (Widget widget) { - var rectangle = Cairo.RectangleInt () { - width = widget.get_allocated_width (), - height = widget.get_allocated_height () - }; - - widget.translate_coordinates (widget.get_toplevel(), 0, 0, out rectangle.x, out rectangle.y); - var region = new Region.rectangle (rectangle); - - return region; -} - -private RecordingArea get_recording_area () { - var area = RecordingArea() { - width = recording_view.get_allocated_width (), - height = recording_view.get_allocated_height () - }; - - // Get absoulte window coordinates - var recording_view_window = recording_view.get_window (); - recording_view_window.get_origin (out area.left, out area.top); - - // FIXME: This is necessary for an exact position, not sure why. - area.top -= 1; - - // Add relative widget coordinates - int relative_left, relative_top; - recording_view.translate_coordinates (recording_view.get_toplevel(), 0, 0, - out relative_left, out relative_top); - area.left += relative_left; - area.top += relative_top; - - return area; -} - -private void save_output (File in_file) { - var chooser = new FileChooserDialog ( - "Select your favorite file", null, FileChooserAction.SAVE, - "_Cancel", - ResponseType.CANCEL, - "_Save", - ResponseType.ACCEPT); - - var filter = new FileFilter (); - chooser.do_overwrite_confirmation = true; - chooser.filter = filter; - filter.add_mime_type ("image/gif"); - - var folder = get_video_folder (); - chooser.set_current_folder (folder); - - var now = new DateTime.now_local (); - var default_name = now.format ("Peek %Y-%m-%d %H-%M.gif"); - chooser.set_current_name (default_name); - - if (chooser.run () == ResponseType.ACCEPT) { - var out_file = chooser.get_file (); - - in_file.copy_async.begin (out_file, FileCopyFlags.OVERWRITE, - Priority.DEFAULT, null, null, (obj, res) => { - try { - bool copy_success = in_file.copy_async.end (res); - stdout.printf ("File saved %s: %s\n", - copy_success.to_string (), - out_file.get_uri ()); - - in_file.delete_async.begin (Priority.DEFAULT, null, (obj, res) => { - try { - bool delete_success = in_file.delete_async.end (res); - stdout.printf ("Temp file deleted: %s\n", - delete_success.to_string ()); - } catch (Error e) { - stderr.printf ("Temp file delete error: %s\n", e.message); - } - }); - } - catch (GLib.Error e) { - stderr.printf ("File save error: %s\n", e.message); - } - }); - } - - // Close the FileChooserDialog: - chooser.close (); -} - -private string get_video_folder () { - string folder; - folder = GLib.Environment.get_user_special_dir (GLib.UserDirectory.VIDEOS); - - if (folder == null) { - folder = GLib.Environment.get_user_special_dir (GLib.UserDirectory.PICTURES); - } - - if (folder == null) { - folder = GLib.Environment.get_home_dir (); - } - - return folder; -} - int main (string[] args) { - Gtk.init (ref args); - - try { - recorder = new ScreenRecorder(); - recorder.recording_started.connect (() => { - enter_recording_state (); - }); - recorder.recording_finished.connect ((file) => { - leave_recording_state (); - if (file != null) { - save_output (file); - } - }); - recorder.recording_aborted.connect ((status) => { - stderr.printf ("Recording stopped unexpectedly with return code %i\n", status); - leave_recording_state (); - }); - - var builder = new Builder (); - builder.add_from_resource ("/de/uploadedlobster/peek/peek.ui"); - builder.connect_signals (null); - - window = builder.get_object ("application_window") as Gtk.Window; - window.set_keep_above (true); - - recording_view = builder.get_object ("recording_view") as Widget; - record_button = builder.get_object ("record_button") as Button; - stop_button = builder.get_object ("stop_button") as Button; - size_indicator = builder.get_object ("size_indicator") as Label; + Gtk.init (ref args); + var window = new PeekApplicationWindow (); + window.show_all (); - window.show_all (); - Gtk.main (); - } catch (Error e) { - stderr.printf ("Could not load UI: %s\n", e.message); - return 1; - } + Gtk.main (); return 0; } diff --git a/src/peek-application-window.vala b/src/peek-application-window.vala new file mode 100644 index 00000000..9a7499fd --- /dev/null +++ b/src/peek-application-window.vala @@ -0,0 +1,281 @@ +/* +Peek Copyright (c) 2015 by Philipp Wolfer + +This file is part of Peek. + +This software is licensed under the GNU General Public License +(version 3 or later). See the LICENSE file in this distribution. +*/ + +using Gtk; +using Cairo; + +[GtkTemplate (ui = "/de/uploadedlobster/peek/peek.ui")] +class PeekApplicationWindow : ApplicationWindow { + + [GtkChild] + private Widget recording_view; + + [GtkChild] + private Button record_button; + + [GtkChild] + private Button stop_button; + + [GtkChild] + private Label size_indicator; + + private uint size_indicator_timeout = 0; + private bool screen_supports_alpha = true; + private ScreenRecorder recorder = null; + + public PeekApplicationWindow () { + Object (); + + recorder = new ScreenRecorder (); + recorder.recording_started.connect (() => { + enter_recording_state (); + }); + + recorder.recording_finished.connect ((file) => { + leave_recording_state (); + + if (file != null) { + save_output (file); + } + }); + + recorder.recording_aborted.connect ((status) => { + stderr.printf ("Recording stopped unexpectedly with return code %i\n", status); + leave_recording_state (); + }); + + this.set_keep_above (true); + } + + [GtkCallback] + private void on_application_window_screen_changed (Widget widget, Gdk.Screen oldScreen) { + var screen = widget.get_screen (); + var visual = screen.get_rgba_visual (); + + if (visual == null) { + stderr.printf ("Screen does not support alpha channels!"); + visual = screen.get_system_visual (); + screen_supports_alpha = false; + } + else { + screen_supports_alpha = true; + } + + widget.set_visual (visual); + } + + [GtkCallback] + private bool on_application_window_configure_event (Gdk.EventConfigure event) { + if (recorder.is_recording) { + recorder.cancel (); + } + + return false; + } + + [GtkCallback] + private bool on_application_window_delete_event (Gdk.EventAny event) { + recorder.cancel (); + Gtk.main_quit (); + return false; + } + + [GtkCallback] + private bool on_recording_view_draw (Widget widget, Context ctx) { + if (screen_supports_alpha) { + ctx.set_source_rgba (0.0, 0.0, 0.0, 0.0); + } + else { + ctx.set_source_rgb (0.0, 0.0, 0.0); + } + + // Stance out the transparent inner part + ctx.set_operator (Operator.CLEAR); + ctx.paint (); + ctx.fill (); + + // Set an input shape so that the recording view is not clickable + var window_region = create_region_from_widget (widget.get_toplevel()); + var recording_viewRegion = create_region_from_widget (widget); + window_region.subtract (recording_viewRegion); + this.input_shape_combine_region (window_region); + + return false; + } + + [GtkCallback] + private void on_recording_view_size_allocate (Allocation allocation) { + // Show the size + var size_label = new StringBuilder (); + var area = get_recording_area (); + size_label.printf ("%i x %i", area.width, area.height); + size_indicator.set_text (size_label.str); + size_indicator.show (); + + if (size_indicator_timeout != 0) { + Source.remove (size_indicator_timeout); + } + + if (!recorder.is_recording) { + size_indicator.opacity = 1.0; + size_indicator_timeout = Timeout.add (800, () => { + size_indicator.opacity = 0.0; + return false; + }); + } + } + + [GtkCallback] + private void on_cancel_button_clicked (Button source) { + recorder.cancel (); + Gtk.main_quit (); + } + + [GtkCallback] + private void on_record_button_clicked (Button source) { + var area = get_recording_area (); + stdout.printf ("Recording area: %i, %i, %i, %i\n", + area.left, area.top, area.width, area.height); + recorder.record (area); + } + + [GtkCallback] + private void on_stop_button_clicked (Button source) { + recorder.stop (); + } + + private void enter_recording_state () { + size_indicator.opacity = 0.0; + record_button.hide (); + stop_button.show (); + freeze_window_size (); + } + + private void leave_recording_state () { + stop_button.hide (); + record_button.show (); + unfreeze_window_size (); + } + + private void freeze_window_size () { + var width = this.get_allocated_width (); + var height = this.get_allocated_height (); + this.set_size_request (width, height); + this.resizable = false; + } + + private void unfreeze_window_size () { + var width = this.get_allocated_width (); + var height = this.get_allocated_height (); + this.set_size_request (0, 0); + this.set_default_size (width, height); + this.resizable = true; + } + + private Region create_region_from_widget (Widget widget) { + var rectangle = Cairo.RectangleInt () { + width = widget.get_allocated_width (), + height = widget.get_allocated_height () + }; + + widget.translate_coordinates (widget.get_toplevel(), 0, 0, out rectangle.x, out rectangle.y); + var region = new Region.rectangle (rectangle); + + return region; + } + + private RecordingArea get_recording_area () { + var area = RecordingArea() { + width = recording_view.get_allocated_width (), + height = recording_view.get_allocated_height () + }; + + // Get absoulte window coordinates + var recording_view_window = recording_view.get_window (); + recording_view_window.get_origin (out area.left, out area.top); + + // FIXME: This is necessary for an exact position, not sure why. + area.top -= 1; + + // Add relative widget coordinates + int relative_left, relative_top; + recording_view.translate_coordinates (recording_view.get_toplevel(), 0, 0, + out relative_left, out relative_top); + area.left += relative_left; + area.top += relative_top; + + return area; + } + + private void save_output (File in_file) { + var chooser = new FileChooserDialog ( + "Select your favorite file", null, FileChooserAction.SAVE, + "_Cancel", + ResponseType.CANCEL, + "_Save", + ResponseType.ACCEPT); + + var filter = new FileFilter (); + chooser.do_overwrite_confirmation = true; + chooser.filter = filter; + filter.add_mime_type ("image/gif"); + + var folder = get_video_folder (); + chooser.set_current_folder (folder); + + var now = new DateTime.now_local (); + var default_name = now.format ("Peek %Y-%m-%d %H-%M.gif"); + chooser.set_current_name (default_name); + + if (chooser.run () == ResponseType.ACCEPT) { + var out_file = chooser.get_file (); + + in_file.copy_async.begin (out_file, FileCopyFlags.OVERWRITE, + Priority.DEFAULT, null, null, (obj, res) => { + try { + bool copy_success = in_file.copy_async.end (res); + stdout.printf ("File saved %s: %s\n", + copy_success.to_string (), + out_file.get_uri ()); + + in_file.delete_async.begin (Priority.DEFAULT, null, (obj, res) => { + try { + bool delete_success = in_file.delete_async.end (res); + stdout.printf ("Temp file deleted: %s\n", + delete_success.to_string ()); + } catch (Error e) { + stderr.printf ("Temp file delete error: %s\n", e.message); + } + }); + } + catch (GLib.Error e) { + stderr.printf ("File save error: %s\n", e.message); + } + }); + } + + // Close the FileChooserDialog: + chooser.close (); + } + + private string get_video_folder () { + string folder; + folder = GLib.Environment.get_user_special_dir (GLib.UserDirectory.VIDEOS); + + if (folder == null) { + folder = GLib.Environment.get_user_special_dir (GLib.UserDirectory.PICTURES); + } + + if (folder == null) { + folder = GLib.Environment.get_home_dir (); + } + + return folder; + } +} diff --git a/ui/peek.ui b/ui/peek.ui index 5da1e475..d3877765 100644 --- a/ui/peek.ui +++ b/ui/peek.ui @@ -1,8 +1,33 @@ - + - - + + + + + +