aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToshiaki Asai <toshi.alternative@gmail.com>2016-10-10 00:49:06 +0900
committerToshiaki Asai <toshi.alternative@gmail.com>2016-10-10 00:49:06 +0900
commit01a1d0d5b04e26a30c5aede548649d49adfe4614 (patch)
tree9430390f9da140687f11e692038c5c88eabec720
parent9d354353f3d42151d3d12a0817ad474ab8c6e53e (diff)
parent442a7e8c0ad852531e88c4f67bab6dfd10974fcc (diff)
downloadmikutter-01a1d0d5b04e26a30c5aede548649d49adfe4614.tar.gz
Merge branch 'topic/900-photo-model' into develop
-rw-r--r--core/plugin/openimg/.mikutter.yml10
-rw-r--r--core/plugin/openimg/model/photo.rb146
-rw-r--r--core/plugin/openimg/openimg.rb162
-rw-r--r--core/plugin/openimg/window.rb187
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