metaruby 1.0.0.rc2 → 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +4 -0
- data/README.md +4 -4
- data/lib/metaruby.rb +8 -1
- data/lib/metaruby/class.rb +13 -9
- data/lib/metaruby/dsls.rb +6 -0
- data/lib/metaruby/gui.rb +16 -0
- data/lib/metaruby/gui/exception_rendering.rb +281 -0
- data/lib/metaruby/gui/exception_view.rb +32 -111
- data/lib/metaruby/gui/html.rb +7 -0
- data/lib/metaruby/gui/html/button.rb +54 -12
- data/lib/metaruby/gui/html/collection.rb +33 -9
- data/lib/metaruby/gui/html/exception_view.css +4 -6
- data/lib/metaruby/gui/html/list.rhtml +3 -3
- data/lib/metaruby/gui/html/page.rb +233 -53
- data/lib/metaruby/gui/html/page.rhtml +6 -5
- data/lib/metaruby/gui/model_browser.rb +15 -6
- data/lib/metaruby/gui/model_selector.rb +57 -11
- data/lib/metaruby/gui/rendering_manager.rb +36 -7
- data/lib/metaruby/gui/ruby_constants_item_model.rb +155 -42
- data/lib/metaruby/inherited_attribute.rb +59 -8
- data/lib/metaruby/module.rb +29 -12
- data/lib/metaruby/registration.rb +44 -12
- data/lib/metaruby/test.rb +6 -10
- data/lib/metaruby/version.rb +2 -1
- data/lib/yard-metaruby.rb +10 -3
- data/manifest.xml +1 -0
- data/metaruby.gemspec +1 -1
- metadata +5 -4
@@ -1,20 +1,43 @@
|
|
1
1
|
require 'metaruby/gui/html'
|
2
|
+
require 'metaruby/gui/exception_rendering'
|
2
3
|
|
3
4
|
module MetaRuby
|
4
5
|
module GUI
|
5
6
|
# Widget that allows to display a list of exceptions
|
7
|
+
#
|
8
|
+
# @deprecated use {HTML::Page} and {HTML::Page#push_exception} directly
|
9
|
+
# instead
|
6
10
|
class ExceptionView < Qt::WebView
|
7
|
-
RESSOURCES_DIR = File.expand_path('html', File.dirname(__FILE__))
|
8
|
-
|
9
11
|
attr_reader :displayed_exceptions
|
12
|
+
|
13
|
+
# @return [HTML::Page] the page object that allows to infer
|
10
14
|
attr_reader :metaruby_page
|
11
15
|
|
16
|
+
# @return [#head,#scripts,#render] an object that allows to render
|
17
|
+
# exceptions in HTML
|
18
|
+
attr_reader :exception_rendering
|
19
|
+
|
12
20
|
def initialize(parent = nil)
|
13
21
|
super
|
14
|
-
|
15
|
-
connect(@metaruby_page, SIGNAL('fileOpenClicked(const QUrl&)'), self, SLOT('fileOpenClicked(const QUrl&)'))
|
22
|
+
|
16
23
|
@displayed_exceptions = []
|
17
24
|
self.focus_policy = Qt::NoFocus
|
25
|
+
|
26
|
+
@metaruby_page = HTML::Page.new(self.page)
|
27
|
+
connect(@metaruby_page, SIGNAL('fileOpenClicked(const QUrl&)'),
|
28
|
+
self, SLOT('fileOpenClicked(const QUrl&)'))
|
29
|
+
@exception_rendering = ExceptionRendering.new(metaruby_page)
|
30
|
+
|
31
|
+
if ENV['METARUBY_GUI_DEBUG_HTML']
|
32
|
+
page.settings.setAttribute(Qt::WebSettings::DeveloperExtrasEnabled, true)
|
33
|
+
@inspector = Qt::WebInspector.new
|
34
|
+
@inspector.page = page
|
35
|
+
@inspector.show
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_file_filter=(filter)
|
40
|
+
exception_rendering.user_file_filter = filter
|
18
41
|
end
|
19
42
|
|
20
43
|
def push(exception, reason = nil)
|
@@ -33,29 +56,14 @@ module MetaRuby
|
|
33
56
|
|
34
57
|
TEMPLATE = <<-EOD
|
35
58
|
<head>
|
36
|
-
|
37
|
-
<script type="text/javascript" src="file://#{File.join(RESSOURCES_DIR, 'jquery.min.js')}"></script>
|
59
|
+
<%= exception_rendering.head %>
|
38
60
|
</head>
|
39
|
-
|
40
|
-
$(document).ready(function () {
|
41
|
-
$("tr.backtrace").hide()
|
42
|
-
$("a.backtrace_toggle_filtered").click(function (event) {
|
43
|
-
var eventId = $(this).attr("id");
|
44
|
-
$("#backtrace_full_" + eventId).hide();
|
45
|
-
$("#backtrace_filtered_" + eventId).toggle();
|
46
|
-
event.preventDefault();
|
47
|
-
});
|
48
|
-
$("a.backtrace_toggle_full").click(function (event) {
|
49
|
-
var eventId = $(this).attr("id");
|
50
|
-
$("#backtrace_full_" + eventId).toggle();
|
51
|
-
$("#backtrace_filtered_" + eventId).hide();
|
52
|
-
event.preventDefault();
|
53
|
-
});
|
54
|
-
});
|
55
|
-
</script>
|
61
|
+
<%= exception_rendering.scripts %>
|
56
62
|
<body>
|
57
63
|
<table class="exception_list">
|
58
|
-
<%= each_exception.each_with_index.map
|
64
|
+
<%= each_exception.each_with_index.map do |(e, reason), idx|
|
65
|
+
exception_rendering.render(e, reason, idx)
|
66
|
+
end.join("\\n") %>
|
59
67
|
</table>
|
60
68
|
</body>
|
61
69
|
EOD
|
@@ -64,93 +72,6 @@ module MetaRuby
|
|
64
72
|
self.html = ERB.new(TEMPLATE).result(binding)
|
65
73
|
end
|
66
74
|
|
67
|
-
class BacktraceParser
|
68
|
-
def initialize(backtrace)
|
69
|
-
@backtrace = backtrace || []
|
70
|
-
end
|
71
|
-
|
72
|
-
def parse
|
73
|
-
call_stack(0)
|
74
|
-
end
|
75
|
-
|
76
|
-
def pp_callstack(level)
|
77
|
-
@backtrace[level..-1]
|
78
|
-
end
|
79
|
-
|
80
|
-
def pp_call_stack(level)
|
81
|
-
@backtrace[level..-1]
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
EXCEPTION_TEMPLATE_WITHOUT_BACKTRACE = <<-EOF
|
86
|
-
<tr class="message">
|
87
|
-
<td id="<%= idx %>"><%= escape_html(reason) if reason %><pre><%= message.join("\n") %></pre></td>
|
88
|
-
</tr>
|
89
|
-
EOF
|
90
|
-
|
91
|
-
EXCEPTION_TEMPLATE_WITH_BACKTRACE = <<-EOF
|
92
|
-
<tr class="message">
|
93
|
-
<td id="<%= idx %>"><%= escape_html(reason) if reason %><pre><%= message.join("\n") %></pre>
|
94
|
-
<span class="backtrace_links">
|
95
|
-
(show: <a class="backtrace_toggle_filtered" id="<%= idx %>">filtered backtrace</a>,
|
96
|
-
<a class=\"backtrace_toggle_full\" id="<%= idx %>">full backtrace</a>)
|
97
|
-
</span>
|
98
|
-
</td>
|
99
|
-
</tr>
|
100
|
-
<tr class="backtrace_summary">
|
101
|
-
<td>from <%= origin_file %>:<%= origin_line %>:in <%= escape_html(origin_method.to_s) %></td>
|
102
|
-
</tr>
|
103
|
-
<tr class="backtrace" id="backtrace_filtered_<%= idx %>">
|
104
|
-
<td><%= render_backtrace(filtered_backtrace) %></td>
|
105
|
-
</tr>
|
106
|
-
<tr class="backtrace" id="backtrace_full_<%= idx %>">
|
107
|
-
<td><%= render_backtrace(full_backtrace) %></td>
|
108
|
-
</tr>
|
109
|
-
EOF
|
110
|
-
|
111
|
-
def filter_backtrace(backtrace)
|
112
|
-
backtrace
|
113
|
-
end
|
114
|
-
|
115
|
-
def user_file?(file)
|
116
|
-
true
|
117
|
-
end
|
118
|
-
|
119
|
-
def render_exception(e, reason, idx)
|
120
|
-
message = PP.pp(e, "").split("\n").map { |line| escape_html(line) }
|
121
|
-
|
122
|
-
if e.backtrace && !e.backtrace.empty?
|
123
|
-
filtered_backtrace = BacktraceParser.new(filter_backtrace(e.backtrace)).parse
|
124
|
-
full_backtrace = BacktraceParser.new(e.backtrace).parse
|
125
|
-
origin_file, origin_line, origin_method =
|
126
|
-
filtered_backtrace.find { |file, _| user_file?(file) } ||
|
127
|
-
filtered_backtrace.first ||
|
128
|
-
full_backtrace.first
|
129
|
-
|
130
|
-
origin_file = metaruby_page.link_to(Pathname.new(origin_file), origin_file, lineno: origin_line)
|
131
|
-
ERB.new(EXCEPTION_TEMPLATE_WITH_BACKTRACE).result(binding)
|
132
|
-
else
|
133
|
-
ERB.new(EXCEPTION_TEMPLATE_WITHOUT_BACKTRACE).result(binding)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def render_backtrace(backtrace)
|
138
|
-
result = []
|
139
|
-
backtrace.each do |file, line, method|
|
140
|
-
file_link = metaruby_page.link_to(Pathname.new(file), file, lineno: line)
|
141
|
-
if user_file?(file)
|
142
|
-
result << " <span class=\"app_file\">#{file_link}:#{line}:in #{escape_html(method.to_s)}</span><br/>"
|
143
|
-
else
|
144
|
-
result << " #{file_link}:#{line}:in #{escape_html(method.to_s)}<br/>"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
result.join("\n")
|
148
|
-
end
|
149
|
-
|
150
|
-
def escape_html(l)
|
151
|
-
l.gsub('<', '<').gsub('>', '>')
|
152
|
-
end
|
153
|
-
|
154
75
|
def exceptions=(list)
|
155
76
|
@displayed_exceptions = list.dup
|
156
77
|
update_html
|
data/lib/metaruby/gui/html.rb
CHANGED
@@ -4,7 +4,14 @@ require 'metaruby/gui/html/collection'
|
|
4
4
|
|
5
5
|
module MetaRuby
|
6
6
|
module GUI
|
7
|
+
# Basic functionality to generate HTML pages
|
8
|
+
#
|
9
|
+
# The core functionality is in {Page}
|
7
10
|
module HTML
|
11
|
+
# Escape the string to include in HTML
|
12
|
+
#
|
13
|
+
# @param [String] string
|
14
|
+
# @return [String]
|
8
15
|
def self.escape_html(string)
|
9
16
|
string.
|
10
17
|
gsub('<', '<').
|
@@ -1,59 +1,101 @@
|
|
1
1
|
module MetaRuby
|
2
2
|
module GUI
|
3
3
|
module HTML
|
4
|
+
# Representation of a button in {Page}
|
4
5
|
class Button
|
6
|
+
# The button ID
|
7
|
+
#
|
8
|
+
# It is used to generate the button's {#base_url}
|
9
|
+
#
|
10
|
+
# @return [String]
|
5
11
|
attr_reader :id
|
12
|
+
|
13
|
+
# The text when the button is ON
|
14
|
+
#
|
15
|
+
# @return [String]
|
6
16
|
attr_reader :on_text
|
17
|
+
|
18
|
+
# The text when the button is OFF
|
19
|
+
#
|
20
|
+
# @return [String]
|
7
21
|
attr_reader :off_text
|
8
|
-
attr_accessor :state
|
9
22
|
|
10
|
-
|
11
|
-
|
12
|
-
:text => nil,
|
13
|
-
:on_text => "#{id} (on)", :off_text => "#{id} (off)",
|
14
|
-
:state => false
|
23
|
+
# The current button state
|
24
|
+
attr_accessor :state
|
15
25
|
|
26
|
+
# Create a button
|
27
|
+
#
|
28
|
+
# @param [String] id the button {#id}
|
29
|
+
# @param [String] text the button text for a non-toggling button
|
30
|
+
# @param [String] on_text the button text for a toggling button
|
31
|
+
# when it is ON
|
32
|
+
# @param [String] off_text the button text for a toggling button
|
33
|
+
# when it is OFF
|
34
|
+
# @param [Boolean] state the initial button state
|
35
|
+
def initialize(id, text: nil, on_text: "#{id} (on)", off_text: "#{id} (off)", state: false)
|
16
36
|
if id[0, 1] != '/'
|
17
37
|
id = "/#{id}"
|
18
38
|
elsif id[-1, 1] == '/'
|
19
39
|
id = id[0..-2]
|
20
40
|
end
|
21
41
|
@id = id
|
22
|
-
if
|
23
|
-
@on_text =
|
24
|
-
@off_text =
|
42
|
+
if text
|
43
|
+
@on_text = text
|
44
|
+
@off_text = text
|
25
45
|
@state = true
|
26
46
|
else
|
27
|
-
@on_text =
|
28
|
-
@off_text =
|
29
|
-
@state =
|
47
|
+
@on_text = on_text
|
48
|
+
@off_text = off_text
|
49
|
+
@state = state
|
30
50
|
end
|
31
51
|
end
|
32
52
|
|
53
|
+
# @api private
|
54
|
+
#
|
55
|
+
# The ID, quoted for HTML
|
56
|
+
#
|
57
|
+
# @return [String]
|
33
58
|
def html_id; id.gsub(/[^\w]/, '_') end
|
34
59
|
|
60
|
+
# The button base URL
|
61
|
+
#
|
62
|
+
# @return [String]
|
35
63
|
def base_url; "btn://metaruby#{id}" end
|
64
|
+
|
65
|
+
# The URL that would toggle the button (i.e. turn it off if it
|
66
|
+
# is ON)
|
36
67
|
def toggle_url
|
37
68
|
if state then "#{base_url}#off"
|
38
69
|
else "#{base_url}#on"
|
39
70
|
end
|
40
71
|
end
|
72
|
+
|
73
|
+
# The URL that represents this button and its current state
|
41
74
|
def url
|
42
75
|
if state then "#{base_url}#on"
|
43
76
|
else "#{base_url}#off"
|
44
77
|
end
|
45
78
|
end
|
79
|
+
|
80
|
+
# The button text
|
46
81
|
def text
|
47
82
|
if state then off_text
|
48
83
|
else on_text
|
49
84
|
end
|
50
85
|
end
|
51
86
|
|
87
|
+
# Render the button as HTML
|
88
|
+
#
|
89
|
+
# @return [String]
|
52
90
|
def render
|
53
91
|
"<a id=\"#{html_id}\" href=\"#{toggle_url}\">#{text}</a>"
|
54
92
|
end
|
55
93
|
end
|
56
94
|
|
95
|
+
# Render a button bar into HTML
|
96
|
+
#
|
97
|
+
# @param [Array<Button>] buttons
|
98
|
+
# @return [String]
|
57
99
|
def self.render_button_bar(buttons)
|
58
100
|
if !buttons.empty?
|
59
101
|
"<div class=\"button_bar\"><span>#{buttons.map(&:render).join(" / ")}</span></div>"
|
@@ -1,7 +1,11 @@
|
|
1
1
|
module MetaRuby::GUI
|
2
2
|
module HTML
|
3
|
-
# Base class providing functionality to
|
4
|
-
#
|
3
|
+
# Base class providing functionality to first rendering a collection of
|
4
|
+
# object, and then render one item in the collection when the user
|
5
|
+
# clicks on the collection element
|
6
|
+
#
|
7
|
+
# Rendering is delegated to other objects through a {RenderingManager}.
|
8
|
+
# The main interface to the manager is {#register_type}
|
5
9
|
class Collection < Qt::Object
|
6
10
|
# @return [#push] the page on which we publish the HTML
|
7
11
|
attr_reader :page
|
@@ -15,8 +19,11 @@ module MetaRuby::GUI
|
|
15
19
|
# @return [<Exception>] exceptions caught during element rendering
|
16
20
|
attr_reader :registered_exceptions
|
17
21
|
|
18
|
-
|
22
|
+
# Representation of an element in the collection
|
23
|
+
class Element < Struct.new(:object, :format, :url, :text, :rendering_options, :attributes)
|
24
|
+
end
|
19
25
|
|
26
|
+
# Create a collection that acts on a page
|
20
27
|
def initialize(page)
|
21
28
|
super()
|
22
29
|
@page = page
|
@@ -26,26 +33,31 @@ module MetaRuby::GUI
|
|
26
33
|
@registered_exceptions = Array.new
|
27
34
|
end
|
28
35
|
|
29
|
-
|
30
|
-
|
36
|
+
# (see RenderingManager#register_type)
|
37
|
+
def register_type(type, rendering_class, render_options = Hash.new)
|
38
|
+
manager.register_type(type, rendering_class, render_options)
|
31
39
|
end
|
32
40
|
|
41
|
+
# (see RenderingManager#enable)
|
33
42
|
def enable
|
34
|
-
connect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('
|
43
|
+
connect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('linkClickedHandler(const QUrl&)'))
|
35
44
|
manager.enable
|
36
45
|
end
|
37
46
|
|
47
|
+
# (see RenderingManager#disable)
|
38
48
|
def disable
|
39
|
-
disconnect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('
|
49
|
+
disconnect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('linkClickedHandler(const QUrl&)'))
|
40
50
|
manager.disable
|
41
51
|
end
|
42
52
|
|
53
|
+
# Clear the current view
|
43
54
|
def clear
|
44
55
|
@object_id_to_object.clear
|
45
56
|
registered_exceptions.clear
|
46
57
|
manager.clear
|
47
58
|
end
|
48
59
|
|
60
|
+
# The namespace used on URIs generated by the collection
|
49
61
|
def namespace
|
50
62
|
object_id.to_s + "/"
|
51
63
|
end
|
@@ -58,6 +70,12 @@ module MetaRuby::GUI
|
|
58
70
|
end
|
59
71
|
end
|
60
72
|
|
73
|
+
# Render a list of {Element}
|
74
|
+
#
|
75
|
+
# @param [String] title the section title
|
76
|
+
# @param [Array<Element>] links the links that should be rendered
|
77
|
+
# @param [Hash] push_options additional options that should be
|
78
|
+
# passed to page#render_list
|
61
79
|
def render_links(title, links, push_options = Hash.new)
|
62
80
|
links.each do |el|
|
63
81
|
object_id_to_object[el.object.object_id] = el.object
|
@@ -79,7 +97,12 @@ module MetaRuby::GUI
|
|
79
97
|
end
|
80
98
|
end
|
81
99
|
|
82
|
-
|
100
|
+
# @api private
|
101
|
+
#
|
102
|
+
# Handler called when a link is clicked on the page. It renders an
|
103
|
+
# object when the link is a link to an object of the collection, or
|
104
|
+
# passes the URL to the linkClicked signal otherwise.
|
105
|
+
def linkClickedHandler(url)
|
83
106
|
if url.host == "metaruby" && url.path =~ /^\/#{Regexp.quote(namespace)}(\d+)/
|
84
107
|
object = object_id_to_object[Integer($1)]
|
85
108
|
render_element(object)
|
@@ -87,7 +110,8 @@ module MetaRuby::GUI
|
|
87
110
|
super
|
88
111
|
end
|
89
112
|
end
|
90
|
-
slots '
|
113
|
+
slots 'linkClickedHandler(const QUrl&)'
|
114
|
+
signals 'linkClicked(const QUrl&)'
|
91
115
|
|
92
116
|
def render_element(object, options = Hash.new)
|
93
117
|
page.restore
|
@@ -1,8 +1,6 @@
|
|
1
|
-
|
2
|
-
table { width: 100%; font-size: 12px; }
|
3
|
-
.message td { font-size: 12px; font-weight: normal; background-color: rgb(176,206,234); text-align: left; }
|
1
|
+
.message { font-size: 80%; font-weight: normal; background-color: rgb(176,206,234); text-align: left; }
|
4
2
|
.backtrace_links { font-size: 80%; margin-left: 2em;}
|
5
|
-
.backtrace
|
6
|
-
.backtrace_summary
|
7
|
-
.
|
3
|
+
.backtrace { font-size: 80%; background-color: rgb(219,230,241); padding-left: 1em; }
|
4
|
+
.backtrace_summary { font-size: 80%; background-color: rgb(219,230,241); padding-left: 1em; }
|
5
|
+
.user_file { font-weight: bold; }
|
8
6
|
|
@@ -1,12 +1,12 @@
|
|
1
|
-
<% if
|
1
|
+
<% if filter %>
|
2
2
|
<script type="text/javascript">
|
3
3
|
jQuery(document).ready(function(){
|
4
|
-
jQuery("div#<%=
|
4
|
+
jQuery("div#<%= id %>-list").selectFilter();
|
5
5
|
});
|
6
6
|
</script>
|
7
7
|
<% end %>
|
8
8
|
|
9
|
-
<div name="index_filter" id="<%=
|
9
|
+
<div name="index_filter" id="<%= id %>-list">
|
10
10
|
<%
|
11
11
|
items.each_with_index do |(data, attributes), index|
|
12
12
|
if attributes
|
@@ -1,43 +1,157 @@
|
|
1
1
|
module MetaRuby::GUI
|
2
2
|
module HTML
|
3
|
+
# The directory relative to which ressources (such as css or javascript
|
4
|
+
# files) are resolved by default
|
3
5
|
RESSOURCES_DIR = File.expand_path(File.dirname(__FILE__))
|
4
6
|
|
5
7
|
# A class that can be used as the webpage container for the Page class
|
6
8
|
class HTMLPage
|
9
|
+
# The HTML content
|
7
10
|
attr_accessor :html
|
8
11
|
|
12
|
+
# Method expected by {Page}
|
9
13
|
def main_frame; self end
|
10
14
|
end
|
11
15
|
|
12
16
|
# A helper class that gives us easy-to-use page elements on a
|
13
17
|
# Qt::WebView
|
18
|
+
#
|
19
|
+
# Such a page is managed as a list of sections (called {Fragment}). A
|
20
|
+
# new fragment is added or updated with {#push}
|
14
21
|
class Page < Qt::Object
|
22
|
+
# The content of the <title> tag
|
23
|
+
# @return [String,nil]
|
24
|
+
attr_accessor :page_name
|
25
|
+
|
26
|
+
# The content of a toplevel <h1> tag
|
27
|
+
# @return [String,nil]
|
28
|
+
attr_accessor :title
|
29
|
+
|
30
|
+
# The underlying page rendering object
|
31
|
+
#
|
32
|
+
# @return [Qt::WebPage,HTMLPage]
|
33
|
+
attr_reader :page
|
34
|
+
|
35
|
+
# List of fragments
|
36
|
+
#
|
37
|
+
# @return [Array<Fragment>]
|
15
38
|
attr_reader :fragments
|
16
|
-
|
39
|
+
|
40
|
+
# Static mapping of objects to URIs
|
41
|
+
#
|
42
|
+
# @see #uri_fo
|
17
43
|
attr_accessor :object_uris
|
18
|
-
attr_reader :javascript
|
19
44
|
|
45
|
+
# Content to be rendered in the page head
|
46
|
+
#
|
47
|
+
# @return [Array<String>]
|
48
|
+
attr_reader :head
|
49
|
+
|
50
|
+
# Scripts to be loaded in the page
|
51
|
+
#
|
52
|
+
# @return [Array<String>]
|
53
|
+
attr_reader :scripts
|
54
|
+
|
55
|
+
# Object used to render exceptions in {#push_exception}
|
56
|
+
#
|
57
|
+
# It is set by {#enable_exception_rendering}
|
58
|
+
#
|
59
|
+
# @return [#render]
|
60
|
+
attr_reader :exception_rendering
|
61
|
+
|
62
|
+
# Creates a new Page object
|
63
|
+
#
|
64
|
+
# @param [Qt::WebPage,HTMLPage] page
|
65
|
+
def initialize(page)
|
66
|
+
super()
|
67
|
+
@page = page
|
68
|
+
@head = Array.new
|
69
|
+
@scripts = Array.new
|
70
|
+
@fragments = []
|
71
|
+
@templates = Hash.new
|
72
|
+
@auto_id = 0
|
73
|
+
|
74
|
+
if page.kind_of?(Qt::WebPage)
|
75
|
+
page.link_delegation_policy = Qt::WebPage::DelegateAllLinks
|
76
|
+
Qt::Object.connect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('pageLinkClicked(const QUrl&)'))
|
77
|
+
end
|
78
|
+
@object_uris = Hash.new
|
79
|
+
end
|
80
|
+
|
81
|
+
# A page fragment (or section)
|
20
82
|
class Fragment
|
83
|
+
# The fragmen title (rendered with <h2>)
|
21
84
|
attr_accessor :title
|
85
|
+
# The fragment's HTML content
|
22
86
|
attr_accessor :html
|
87
|
+
# The fragment ID (as, in, HTML id)
|
23
88
|
attr_accessor :id
|
89
|
+
# A list of buttons to be rendered before the fragment title and
|
90
|
+
# its content
|
91
|
+
#
|
92
|
+
# @return [Array<Button>]
|
24
93
|
attr_reader :buttons
|
25
94
|
|
26
|
-
|
27
|
-
|
28
|
-
:id => nil, :buttons => []
|
95
|
+
# Create a new fragment
|
96
|
+
def initialize(title, html, id: nil, buttons: Array.new)
|
29
97
|
@title = title
|
30
98
|
@html = html
|
31
|
-
@id =
|
32
|
-
@buttons =
|
99
|
+
@id = id
|
100
|
+
@buttons = buttons
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Add content to the page setup (head and scripts)
|
105
|
+
#
|
106
|
+
# @param [#head,#scripts] obj the object defining the content to be
|
107
|
+
# added
|
108
|
+
# @see add_to_head add_scripts
|
109
|
+
def add_to_setup(obj)
|
110
|
+
add_to_head(obj.head)
|
111
|
+
add_script(obj.scripts)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add content to {#head}
|
115
|
+
#
|
116
|
+
# @param [String] html
|
117
|
+
def add_to_head(html)
|
118
|
+
head << html
|
119
|
+
end
|
120
|
+
|
121
|
+
# Add content to {#scripts}
|
122
|
+
#
|
123
|
+
# @param [String] html
|
124
|
+
def add_script(html)
|
125
|
+
scripts << html
|
126
|
+
end
|
127
|
+
|
128
|
+
# Resolves a relative path to a path in the underlying application's
|
129
|
+
# resource folder
|
130
|
+
#
|
131
|
+
# @return [String]
|
132
|
+
def path_in_resource(path)
|
133
|
+
if Pathname.new(path).absolute?
|
134
|
+
path
|
135
|
+
else
|
136
|
+
File.join('${RESOURCE_DIR}', path)
|
33
137
|
end
|
34
138
|
end
|
35
139
|
|
140
|
+
# Load a javascript file in the head
|
36
141
|
def load_javascript(file)
|
37
|
-
|
142
|
+
add_to_head(
|
143
|
+
"<script type=\"text/javascript\" src=\"#{path_in_resource(file)}\"></script>")
|
38
144
|
end
|
39
145
|
|
40
|
-
|
146
|
+
# Helper that generates a HTML link to a given object
|
147
|
+
#
|
148
|
+
# The object URI is resolved using {#uri_for}. If there is no known
|
149
|
+
# link to the object, it is returned as text
|
150
|
+
#
|
151
|
+
# @param [Object] object the object to create a link to
|
152
|
+
# @param [String] text the link text. Defaults to object#name
|
153
|
+
# @return [String]
|
154
|
+
def link_to(object, text = nil, **args)
|
41
155
|
text = HTML.escape_html(text || object.name || "<anonymous>")
|
42
156
|
if uri = uri_for(object)
|
43
157
|
if uri !~ /^\w+:\/\//
|
@@ -67,12 +181,30 @@ module MetaRuby::GUI
|
|
67
181
|
self.class.main_doc(text)
|
68
182
|
end
|
69
183
|
|
184
|
+
# The ERB template for a page
|
185
|
+
#
|
186
|
+
# @see html
|
70
187
|
PAGE_TEMPLATE = File.join(RESSOURCES_DIR, "page.rhtml")
|
188
|
+
# The ERB template for a page body
|
189
|
+
#
|
190
|
+
# @see html_body
|
71
191
|
PAGE_BODY_TEMPLATE = File.join(RESSOURCES_DIR, "page_body.rhtml")
|
192
|
+
# The ERB template for a page fragment
|
193
|
+
#
|
194
|
+
# @see push
|
72
195
|
FRAGMENT_TEMPLATE = File.join(RESSOURCES_DIR, "fragment.rhtml")
|
196
|
+
# The ERB template for a list
|
197
|
+
#
|
198
|
+
# @see render_list
|
73
199
|
LIST_TEMPLATE = File.join(RESSOURCES_DIR, "list.rhtml")
|
200
|
+
# Assets (CSS, javascript) that are included in every page
|
74
201
|
ASSETS = %w{page.css jquery.min.js jquery.selectfilter.js}
|
75
202
|
|
203
|
+
# Copy the assets to a target directory
|
204
|
+
#
|
205
|
+
# This can be used to create self-contained HTML pages using the
|
206
|
+
# Page class, by providing a different ressource dir to e.g. {#html}
|
207
|
+
# or {#html_body} and copying the assets to it.
|
76
208
|
def self.copy_assets_to(target_dir, assets = ASSETS)
|
77
209
|
FileUtils.mkdir_p target_dir
|
78
210
|
assets.each do |file|
|
@@ -80,6 +212,9 @@ module MetaRuby::GUI
|
|
80
212
|
end
|
81
213
|
end
|
82
214
|
|
215
|
+
# Lazy loads a template
|
216
|
+
#
|
217
|
+
# @return [ERB]
|
83
218
|
def load_template(*path)
|
84
219
|
path = File.join(*path)
|
85
220
|
@templates[path] ||= ERB.new(File.read(path))
|
@@ -87,27 +222,16 @@ module MetaRuby::GUI
|
|
87
222
|
@templates[path]
|
88
223
|
end
|
89
224
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
@auto_id = 0
|
101
|
-
@javascript = Array.new
|
102
|
-
|
103
|
-
if page.kind_of?(Qt::WebPage)
|
104
|
-
page.link_delegation_policy = Qt::WebPage::DelegateAllLinks
|
105
|
-
Qt::Object.connect(page, SIGNAL('linkClicked(const QUrl&)'), self, SLOT('pageLinkClicked(const QUrl&)'))
|
106
|
-
end
|
107
|
-
@object_uris = Hash.new
|
108
|
-
end
|
109
|
-
|
110
|
-
|
225
|
+
# Generate a URI for an object
|
226
|
+
#
|
227
|
+
# The method must either return a string that is a URI representing
|
228
|
+
# the object, or nil if there is none. The choice of the URI is
|
229
|
+
# application-specific, used by the application to recognize links
|
230
|
+
#
|
231
|
+
# The default application returns a file:/// URI for a Pathname
|
232
|
+
# object, and then uses {#object_uris}
|
233
|
+
#
|
234
|
+
# @see link_to
|
111
235
|
def uri_for(object)
|
112
236
|
if object.kind_of?(Pathname)
|
113
237
|
"file://#{object.expand_path}"
|
@@ -128,22 +252,38 @@ module MetaRuby::GUI
|
|
128
252
|
end
|
129
253
|
end
|
130
254
|
|
255
|
+
# Generate the HTML and update the underlying {#page}
|
131
256
|
def update_html
|
132
257
|
page.main_frame.html = html
|
133
258
|
end
|
134
259
|
|
260
|
+
# Generate the HTML
|
261
|
+
#
|
262
|
+
# @param [String] ressource_dir the path to the ressource directory
|
263
|
+
# that {#path_in_resource} should use
|
135
264
|
def html(ressource_dir: RESSOURCES_DIR)
|
136
265
|
load_template(PAGE_TEMPLATE).result(binding)
|
137
266
|
end
|
138
267
|
|
268
|
+
# Generate the body of the HTML document
|
269
|
+
#
|
270
|
+
# @param [String] ressource_dir the path to the ressource directory
|
271
|
+
# that {#path_in_resource} should use
|
139
272
|
def html_body(ressource_dir: RESSOURCES_DIR)
|
140
273
|
load_template(PAGE_BODY_TEMPLATE).result(binding)
|
141
274
|
end
|
142
275
|
|
276
|
+
# Generate the HTML of a fragment
|
277
|
+
#
|
278
|
+
# @param [String] ressource_dir the path to the ressource directory
|
279
|
+
# that {#path_in_resource} should use
|
143
280
|
def html_fragment(fragment, ressource_dir: RESSOURCES_DIR)
|
144
281
|
load_template(FRAGMENT_TEMPLATE).result(binding)
|
145
282
|
end
|
146
283
|
|
284
|
+
# Find a button from its URI
|
285
|
+
#
|
286
|
+
# @return [Button,nil]
|
147
287
|
def find_button_by_url(url)
|
148
288
|
id = url.path
|
149
289
|
fragments.each do |fragment|
|
@@ -158,6 +298,11 @@ module MetaRuby::GUI
|
|
158
298
|
page.main_frame.find_first_element(selector)
|
159
299
|
end
|
160
300
|
|
301
|
+
# @api private
|
302
|
+
#
|
303
|
+
# Slot that catches the page's link-clicked signal and dispatches
|
304
|
+
# into the buttonClicked signal (for buttons), fileClicked for files
|
305
|
+
# and linkClicked for links
|
161
306
|
def pageLinkClicked(url)
|
162
307
|
if url.scheme == 'btn' && url.host == 'metaruby'
|
163
308
|
if btn = find_button_by_url(url)
|
@@ -186,12 +331,12 @@ module MetaRuby::GUI
|
|
186
331
|
signals 'linkClicked(const QUrl&)', 'buttonClicked(const QString&,bool)', 'fileOpenClicked(const QUrl&)'
|
187
332
|
|
188
333
|
# Save the current state of the page, so that it can be restored by
|
189
|
-
# calling {restore}
|
334
|
+
# calling {#restore}
|
190
335
|
def save
|
191
336
|
@saved_state = fragments.map(&:dup)
|
192
337
|
end
|
193
338
|
|
194
|
-
# Restore the page at the state it was at the last call to {save}
|
339
|
+
# Restore the page at the state it was at the last call to {#save}
|
195
340
|
def restore
|
196
341
|
return if !@saved_state
|
197
342
|
|
@@ -225,8 +370,8 @@ module MetaRuby::GUI
|
|
225
370
|
# fragment replaces the existing one, and the view is updated
|
226
371
|
# accordingly.
|
227
372
|
#
|
228
|
-
def push(title, html,
|
229
|
-
if id
|
373
|
+
def push(title, html, id: auto_id, **view_options)
|
374
|
+
if id
|
230
375
|
# Check whether we should replace the existing content or
|
231
376
|
# push it new
|
232
377
|
fragment = fragments.find do |fragment|
|
@@ -240,14 +385,36 @@ module MetaRuby::GUI
|
|
240
385
|
end
|
241
386
|
end
|
242
387
|
|
243
|
-
fragments << Fragment.new(title, html,
|
388
|
+
fragments << Fragment.new(title, html, id: id, **view_options)
|
244
389
|
update_html
|
245
390
|
end
|
246
391
|
|
392
|
+
# Automatic generation of a fragment ID
|
247
393
|
def auto_id
|
248
394
|
"metaruby-html-page-fragment-#{@auto_id += 1}"
|
249
395
|
end
|
250
396
|
|
397
|
+
# Enable rendering of exceptions using the given renderer
|
398
|
+
#
|
399
|
+
# @param [ExceptionRendering] renderer
|
400
|
+
def enable_exception_rendering(renderer = ExceptionRendering.new(self))
|
401
|
+
add_to_setup(renderer)
|
402
|
+
@exception_rendering = renderer
|
403
|
+
end
|
404
|
+
|
405
|
+
# Push a fragment that represents the given exception
|
406
|
+
#
|
407
|
+
# {#enable_exception_rendering} must have been called first
|
408
|
+
#
|
409
|
+
# @param [String] title the fragment title
|
410
|
+
# @param [Exception] e the exception to render
|
411
|
+
# @param [String] id the fragment ID
|
412
|
+
# @param [Hash] options additional options passed to {#push}
|
413
|
+
def push_exception(title, e, id: auto_id, **options)
|
414
|
+
html = exception_rendering.render(e, nil, id)
|
415
|
+
push(title, html, id: id, **options)
|
416
|
+
end
|
417
|
+
|
251
418
|
# Create an item for the rendering in tables
|
252
419
|
def render_item(name, value = nil)
|
253
420
|
if value
|
@@ -264,38 +431,51 @@ module MetaRuby::GUI
|
|
264
431
|
# @param [Array<Object>,Array<(Object,Hash)>] items the list
|
265
432
|
# items, one item per line. If a hash is provided, it is used as
|
266
433
|
# HTML attributes for the lines
|
267
|
-
# @param [
|
268
|
-
# @
|
269
|
-
#
|
270
|
-
#
|
271
|
-
|
272
|
-
|
273
|
-
options, push_options = Kernel.filter_options options, :filter => false, :id => nil
|
274
|
-
if options[:filter] && !options[:id]
|
434
|
+
# @param [Boolean] filter only render the items with the given 'id'
|
435
|
+
# @param [String] id the id to filter if filter: is true
|
436
|
+
# @param [Hash] push_options options that are passed to
|
437
|
+
# {#push}. The id: option is added to it.
|
438
|
+
def render_list(title, items, filter: false, id: nil, **push_options)
|
439
|
+
if filter && !id
|
275
440
|
raise ArgumentError, ":filter is true, but no :id has been given"
|
276
441
|
end
|
277
442
|
html = load_template(LIST_TEMPLATE).result(binding)
|
278
|
-
push(title, html, push_options.merge(:
|
443
|
+
push(title, html, push_options.merge(id: id))
|
279
444
|
end
|
280
445
|
|
281
446
|
signals 'updated()'
|
282
447
|
|
283
|
-
|
448
|
+
# Renders an object into a HTML page
|
449
|
+
#
|
450
|
+
# @param [Object] object
|
451
|
+
# @param [#render] renderer the object that renders into the page.
|
452
|
+
# The object must accept a {Page} at initialization and its
|
453
|
+
# #render method gets called passing the object and rendering
|
454
|
+
# options
|
455
|
+
# @return [Page]
|
456
|
+
def self.to_html_page(object, renderer, **options)
|
284
457
|
webpage = HTMLPage.new
|
285
458
|
page = new(webpage)
|
286
|
-
renderer.new(page).render(object, options)
|
459
|
+
renderer.new(page).render(object, **options)
|
287
460
|
page
|
288
461
|
end
|
289
462
|
|
290
|
-
# Renders an object
|
291
|
-
|
292
|
-
|
293
|
-
|
463
|
+
# Renders an object into a HTML page
|
464
|
+
#
|
465
|
+
# @param (see to_html_page)
|
466
|
+
# @return [String]
|
467
|
+
def self.to_html(object, renderer, ressource_dir: RESSOURCES_DIR, **options)
|
468
|
+
to_html_page(object, renderer, **options).
|
469
|
+
html(ressource_dir: ressource_dir)
|
294
470
|
end
|
295
471
|
|
296
|
-
|
297
|
-
|
298
|
-
|
472
|
+
# Renders an object into a HTML body
|
473
|
+
#
|
474
|
+
# @param (see to_html_page)
|
475
|
+
# @return [String]
|
476
|
+
def self.to_html_body(object, renderer, ressource_dir: RESSOURCES_DIR, **options)
|
477
|
+
to_html_page(object, renderer, **options).
|
478
|
+
html_body(ressource_dir: ressource_dir)
|
299
479
|
end
|
300
480
|
end
|
301
481
|
end
|