diff options
author | Toshiaki Asai <toshi.alternative@gmail.com> | 2016-10-10 00:49:06 +0900 |
---|---|---|
committer | Toshiaki Asai <toshi.alternative@gmail.com> | 2016-10-10 00:49:06 +0900 |
commit | 01a1d0d5b04e26a30c5aede548649d49adfe4614 (patch) | |
tree | 9430390f9da140687f11e692038c5c88eabec720 | |
parent | 9d354353f3d42151d3d12a0817ad474ab8c6e53e (diff) | |
parent | 442a7e8c0ad852531e88c4f67bab6dfd10974fcc (diff) | |
download | mikutter-01a1d0d5b04e26a30c5aede548649d49adfe4614.tar.gz |
Merge branch 'topic/900-photo-model' into develop
-rw-r--r-- | core/plugin/openimg/.mikutter.yml | 10 | ||||
-rw-r--r-- | core/plugin/openimg/model/photo.rb | 146 | ||||
-rw-r--r-- | core/plugin/openimg/openimg.rb | 162 | ||||
-rw-r--r-- | core/plugin/openimg/window.rb | 187 |
4 files changed, 350 insertions, 155 deletions
diff --git a/core/plugin/openimg/.mikutter.yml b/core/plugin/openimg/.mikutter.yml index 599ffdc1..b4835f46 100644 --- a/core/plugin/openimg/.mikutter.yml +++ b/core/plugin/openimg/.mikutter.yml @@ -1,11 +1,13 @@ --- slug: :openimg depends: - mikutter: '3.2' + mikutter: '3.5' plugin: - gtk - uitranslator -version: '2.0' + - intent +version: '2.1' author: toshi_a -name: 画像プレビュー -description: メジャーな画像投稿サービスのURLがクリックされた時に、画像だけを専用のウィンドウに表示する +name: 画像ビューア +description: >- + 画像をmikutter上で表示する diff --git a/core/plugin/openimg/model/photo.rb b/core/plugin/openimg/model/photo.rb new file mode 100644 index 00000000..69c1ad43 --- /dev/null +++ b/core/plugin/openimg/model/photo.rb @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +module Plugin::Openimg + class Photo < Retriever::Model + register :openimg_photo, name: Plugin[:openimg]._('画像ビューア') + + field.uri :perma_link + field.string :blob + + handle ->uri{ + uri_str = uri.to_s + openers = Plugin.filtering(:openimg_image_openers, Set.new).first + openers.any?{ |opener| opener.condition === uri_str } if !openers.empty? + } do |uri| + new(perma_link: uri) + end + + # 画像をダウンロードする。 + # partialを指定すると、ダウンロードの進捗があれば、前回呼び出されたときから + # ダウンロードできた内容を引数に呼び出される。 + # 既にダウンロードが終わっていれば、 _blob_ の戻り値がそのまま渡される。 + # このメソッドは、複数回呼び出されても画像のダウンロードを一度しか行わない。 + # ==== Args + # [&partial_callback] 現在ダウンロードできたデータの一部(String) + # ==== Return + # [Delayer::Deferred::Deferredable] ダウンロードが完了したらselfを引数に呼び出される + def download(&partial_callback) # :yield: part + case @state + when :complete + partial_callback.(blob) if block_given? + Delayer::Deferred.new.next{ self } + when :download + append_download_queue(&partial_callback) + else + download!(&partial_callback) + end + end + + # 画像のダウンロードが終わっていれば真を返す。 + # 真を返す時、 _blob_ には完全な画像の情報が存在している + def completed? + @state == :complete + end + + # 画像をダウロード中なら真 + def downloading? + @state == :download + end + + # ダウンロードが始まっていなければ真 + def ready? + !@state + end + + private + + def download!(&partial_callback) + atomic do + return download(&partial_callback) unless ready? + promise = initialize_download(&partial_callback) + Thread.new(&gen_download_routine).next{|success| + if success + finalize_download_as_success + else + Delayer::Deferred.fail false + end + }.trap{|exception| + finalize_download_as_fail(exception) + }.terminate('error') + promise + end + end + + def append_download_queue(&partial_callback) + atomic do + return download(&partial_callback) unless downloading? + register_partial_callback(partial_callback) + register_promise + end + end + + def register_promise + promise = Delayer::Deferred.new(true) + (@promises ||= Set.new) << promise + promise + end + + def register_partial_callback(cb) + @partials ||= Set.new + if cb + @partials << cb + cb.(@buffer) if !@buffer.empty? + end + end + + def gen_download_routine + -> do + begin + _, raw = Plugin.filtering(:openimg_raw_image_from_display_url, perma_link.to_s, nil) + if raw + download_mainloop(raw) + else + raise "couldn't resolve actual image url of #{perma_link}." + end + rescue EOFError + true + ensure + raw.close rescue nil + end + end + end + + def download_mainloop(raw) + loop do + Thread.pass + partial = raw.readpartial(1024**2).freeze + @buffer << partial + atomic{ @partials.each{|c|c.(partial)} } + end + end + + def initialize_download(&partial_callback) + @state = :download + @buffer = String.new + register_partial_callback(partial_callback) + register_promise + end + + def finalize_download_as_success + atomic do + self.blob = @buffer.freeze + @state = :complete + @promises.each{|p| p.call(self) } + @buffer = @promises = @partials = nil + end + end + + def finalize_download_as_fail(exception) + atomic do + @state = nil + @promises.each{|p| p.fail(exception) } + @buffer = @promises = @partials = nil + end + end + end +end diff --git a/core/plugin/openimg/openimg.rb b/core/plugin/openimg/openimg.rb index 0e81c8d8..d29d6286 100644 --- a/core/plugin/openimg/openimg.rb +++ b/core/plugin/openimg/openimg.rb @@ -2,6 +2,8 @@ require 'gtk2' require 'cairo' +require_relative 'window' +require_relative 'model/photo' module Plugin::Openimg ImageOpener = Struct.new(:name, :condition, :open) @@ -30,7 +32,6 @@ Plugin.create :openimg do prototype: [String, Message] defdsl :defimageopener do |name, condition, &proc| - type_strict condition => :===, name => String opener = Plugin::Openimg::ImageOpener.new(name.freeze, condition, proc).freeze filter_openimg_image_openers do |openers| openers << opener @@ -43,30 +44,10 @@ Plugin.create :openimg do error _ nil end end - filter_openimg_pixbuf_from_display_url do |display_url, loader, thread| - raw = Plugin.filtering(:openimg_raw_image_from_display_url, display_url, nil).last - if raw - begin - loader = Gdk::PixbufLoader.new - thread = Thread.new do - begin - loop do - Thread.pass - partial = raw.readpartial(1024*HYDE) - atomic{ loader.write partial } - end - nil - rescue EOFError - true - ensure - raw.close rescue nil - loader.close rescue nil end end - [display_url, loader, thread] - rescue => _ - error _ - [display_url, loader, thread] end - else - [display_url, loader, thread] end end + filter_openimg_pixbuf_from_display_url do |photo, loader, thread| + loader = Gdk::PixbufLoader.new + [photo, loader, photo.download{|partial| Delayer.new{ loader.write partial } }] + end filter_openimg_raw_image_from_display_url do |display_url, content| unless content @@ -79,134 +60,13 @@ Plugin.create :openimg do [display_url, content] end on_openimg_open do |display_url| - image_surface = loading_surface + Plugin.call(:open, display_url) + end - window = ::Gtk::Window.new(). - set_title(display_url). - set_role('mikutter_image_preview'.freeze). - set_type_hint(Gdk::Window::TYPE_HINT_DIALOG). - set_default_size(*default_size) - w_wrap = ::Gtk::DrawingArea.new - w_toolbar = ::Gtk::Toolbar.new - w_browser = ::Gtk::ToolButton.new(Gtk::Image.new(GdkPixbuf::Pixbuf.new(file: Skin.get('forward.png'), width: 24, height: 24))) - - window.ssc(:destroy, &:destroy) - last_size = nil - w_wrap.ssc(:size_allocate) do - if w_wrap.window && last_size != w_wrap.window.geometry[2,2] - last_size = w_wrap.window.geometry[2,2] - redraw(w_wrap, image_surface) end - false end - w_wrap.ssc(:expose_event) do - redraw(w_wrap, image_surface) - true end - w_browser.ssc(:clicked) do - Gtk.openurl(display_url) - false end - - w_toolbar.insert(0, w_browser) - window.add(Gtk::VBox.new.closeup(w_toolbar).add(w_wrap)) - Thread.new { - Plugin.filtering(:openimg_pixbuf_from_display_url, display_url, nil, nil) - }.next { |result| - if result[1].is_a? Gdk::PixbufLoader - _, pixbufloader, thread = result - pixbufloader.ssc(:area_updated, window) do |_, x, y, width, height| - Delayer.new do - if thread.alive? - image_surface = progress(w_wrap, pixbufloader.pixbuf, image_surface, x: x, y: y, width: width, height: height) end end - true end - - pixbufloader.ssc(:closed, window) do - image_surface = progress(w_wrap, pixbufloader.pixbuf, image_surface, paint: true) - true end - - thread.next { |flag| - Deferred.fail flag unless flag - }.trap { |exception| - error exception - image_surface = error_surface - } - else - warn "cant open: #{display_url}" - image_surface = error_surface - redraw(w_wrap, image_surface) end - }.trap{ |exception| - error exception - image_surface = error_surface - redraw(w_wrap, image_surface) - } - window.show_all end - - def progress(w_wrap, pixbuf, image_surface, x: 0, y: 0, width: 0, height: 0, paint: false) - return unless pixbuf - context = nil - size_changed = false - unless image_surface.width == pixbuf.width and image_surface.height == pixbuf.height - size_changed = true - image_surface = Cairo::ImageSurface.new(pixbuf.width, pixbuf.height) - context = Cairo::Context.new(image_surface) - context.save do - context.set_source_color(Cairo::Color::BLACK) - context.paint end end - context ||= Cairo::Context.new(image_surface) - context.save do - context.set_source_pixbuf(pixbuf) - if paint - context.paint - else - context.rectangle(x, y, width, height) - context.fill end end - redraw(w_wrap, image_surface, repaint: paint || size_changed) - image_surface end - - def default_size - @size || [640, 480] end - - def changesize(w_wrap, window, url) - w_wrap.remove(w_wrap.children.first) - @size = window.window.geometry[2,2].freeze - w_wrap.add(::Gtk::WebIcon.new(url, *@size).show_all) - @size end - - def redraw(w_wrap, image_surface, repaint: true) - gdk_window = w_wrap.window - return unless gdk_window - ew, eh = gdk_window.geometry[2,2] - return if(ew == 0 or eh == 0) - context = gdk_window.create_cairo_context - context.save do - if repaint - context.set_source_color(Cairo::Color::BLACK) - context.paint end - if (ew * image_surface.height) > (eh * image_surface.width) - rate = eh.to_f / image_surface.height - context.translate((ew - image_surface.width*rate)/2, 0) - else - rate = ew.to_f / image_surface.width - context.translate(0, (eh - image_surface.height*rate)/2) end - context.scale(rate, rate) - context.set_source(Cairo::SurfacePattern.new(image_surface)) - context.paint end - rescue => _ - error _ end - - ::Gtk::TimeLine.addopenway(->_{ - openers = Plugin.filtering(:openimg_image_openers, Set.new).first - openers.any?{ |opener| opener.condition === _ } - }) do |shrinked_url, cancel| - Thread.new do - url = (Plugin.filtering(:expand_url, [shrinked_url]).first.first rescue shrinked_url) - Plugin.call(:openimg_open, url) end end + intent Plugin::Openimg::Photo do |intent_token| + Plugin::Openimg::Window.new(intent_token.model, intent_token).start_loading.show_all + end def addsupport(cond, element_rule = {}, &block); end - def loading_surface - surface = Cairo::ImageSurface.from_png(Skin.get('loading.png')) - surface end - - def error_surface - surface = Cairo::ImageSurface.from_png(Skin.get('notfound.png')) - surface end - end diff --git a/core/plugin/openimg/window.rb b/core/plugin/openimg/window.rb new file mode 100644 index 00000000..4b3c95e6 --- /dev/null +++ b/core/plugin/openimg/window.rb @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +module Plugin::Openimg + class Window < Gtk::Window + attr_reader :photo + + def initialize(photo, next_opener) + super() + @photo = photo + @image_surface = loading_surface + @next_opener = next_opener + window_settings + ssc(:destroy, &:destroy) + end + + def start_loading + Thread.new { + Plugin.filtering(:openimg_pixbuf_from_display_url, photo, nil, nil) + }.next { |_, pixbufloader, complete_promise| + if pixbufloader.is_a? Gdk::PixbufLoader + rect = nil + pixbufloader.ssc(:area_updated, self) do |_, x, y, width, height| + if rect + rect[:left] = [rect[:left], x].min + rect[:top] = [rect[:top], y].min + rect[:right] = [rect[:right], x+width].max + rect[:bottom] = [rect[:bottom], y+height].max + else + rect = {left: x, top: y, right: x+width, bottom: y+height} + Delayer.new do + progress(pixbufloader.pixbuf, + x: rect[:left], + y: rect[:top], + width: rect[:right] - rect[:left], + height: rect[:bottom] - rect[:top]) + rect = nil + end + end + true end + + pixbufloader.ssc(:closed, self) do + progress(pixbufloader.pixbuf, paint: true) + true end + + complete_promise.trap { |exception| + @image_surface = error_surface + redraw(repaint: true) + } + else + warn "cant open: #{photo}" + @image_surface = error_surface + redraw(repaint: true) end + }.trap{ |exception| + error exception + @image_surface = error_surface + redraw(repaint: true) + } + self + end + + private + + def window_settings + set_title(photo.perma_link.to_s) + set_role('mikutter_image_preview'.freeze) + set_type_hint(Gdk::Window::TYPE_HINT_DIALOG) + set_default_size(*default_size) + add(Gtk::VBox.new.closeup(w_toolbar).add(w_wrap)) + end + + def redraw(repaint: true) + return if w_wrap.destroyed? + gdk_window = w_wrap.window + return unless gdk_window + ew, eh = gdk_window.geometry[2,2] + return if(ew == 0 or eh == 0) + context = gdk_window.create_cairo_context + context.save do + if repaint + context.set_source_color(Cairo::Color::BLACK) + context.paint end + if (ew * @image_surface.height) > (eh * @image_surface.width) + rate = eh.to_f / @image_surface.height + context.translate((ew - @image_surface.width*rate)/2, 0) + else + rate = ew.to_f / @image_surface.width + context.translate(0, (eh - @image_surface.height*rate)/2) end + context.scale(rate, rate) + context.set_source(Cairo::SurfacePattern.new(@image_surface)) + context.paint end + rescue => _ + error _ end + + def progress(pixbuf, x: 0, y: 0, width: 0, height: 0, paint: false) + return unless pixbuf + context = nil + size_changed = false + unless @image_surface.width == pixbuf.width and @image_surface.height == pixbuf.height + size_changed = true + @image_surface = Cairo::ImageSurface.new(pixbuf.width, pixbuf.height) + context = Cairo::Context.new(@image_surface) + context.save do + context.set_source_color(Cairo::Color::BLACK) + context.paint end end + context ||= Cairo::Context.new(@image_surface) + context.save do + context.set_source_pixbuf(pixbuf) + if paint + context.paint + else + context.rectangle(x, y, width, height) + context.fill end end + redraw(repaint: paint || size_changed) + end + + # + # === Widgetたち + # + + def w_wrap + @w_wrap ||= ::Gtk::DrawingArea.new.tap{|w| + w.ssc(:size_allocate, &gen_wrap_size_allocate) + w.ssc(:expose_event, &gen_wrap_expose_event) + } + end + + def w_toolbar + @w_toolbar ||= ::Gtk::Toolbar.new.tap{|w| w.insert(0, w_browser) } + end + + def w_browser + @w_browser ||= ::Gtk::ToolButton.new( + Gtk::Image.new(GdkPixbuf::Pixbuf.new(file: Skin.get('forward.png'), width: 24, height: 24)) + ).tap{|w| + w.ssc(:clicked, &gen_browser_clicked) + } + end + + # + # === イベントハンドラ + # + + def gen_browser_clicked + proc do + Plugin.call(:open, @next_opener) + false + end + end + + def gen_wrap_expose_event + proc do |widget| + redraw(repaint: true) + true + end + end + + def gen_wrap_size_allocate + last_size = nil + proc do |widget| + if widget.window && last_size != widget.window.geometry[2,2] + last_size = widget.window.geometry[2,2] + redraw(repaint: true) + end + false + end + end + + # + # === その他 + # + + def default_size + @size || [640, 480] + end + + def loading_surface + surface = Cairo::ImageSurface.from_png(Skin.get('loading.png')) + surface + end + + def error_surface + surface = Cairo::ImageSurface.from_png(Skin.get('notfound.png')) + surface + end + + end +end |