2019-12-28 12:16:32 +01:00
|
|
|
local core = require "core"
|
|
|
|
local common = require "core.common"
|
|
|
|
local style = require "core.style"
|
2021-12-06 22:28:29 +01:00
|
|
|
local Node = require "core.node"
|
2019-12-28 12:16:32 +01:00
|
|
|
local View = require "core.view"
|
|
|
|
local DocView = require "core.docview"
|
|
|
|
|
|
|
|
|
|
|
|
local RootView = View:extend()
|
|
|
|
|
|
|
|
function RootView:new()
|
|
|
|
RootView.super.new(self)
|
|
|
|
self.root_node = Node()
|
|
|
|
self.deferred_draws = {}
|
|
|
|
self.mouse = { x = 0, y = 0 }
|
2021-09-17 02:47:34 +02:00
|
|
|
self.drag_overlay = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
|
|
|
|
base_color = style.drag_overlay,
|
|
|
|
color = { table.unpack(style.drag_overlay) } }
|
|
|
|
self.drag_overlay.to = { x = 0, y = 0, w = 0, h = 0 }
|
|
|
|
self.drag_overlay_tab = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
|
|
|
|
base_color = style.drag_overlay_tab,
|
|
|
|
color = { table.unpack(style.drag_overlay_tab) } }
|
|
|
|
self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 }
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:defer_draw(fn, ...)
|
|
|
|
table.insert(self.deferred_draws, 1, { fn = fn, ... })
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:get_active_node()
|
2022-02-09 14:58:20 +01:00
|
|
|
local node = self.root_node:get_node_for_view(core.active_view)
|
|
|
|
if not node then node = self:get_primary_node() end
|
|
|
|
return node
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2020-12-28 23:35:52 +01:00
|
|
|
|
2020-12-14 14:19:28 +01:00
|
|
|
local function get_primary_node(node)
|
|
|
|
if node.is_primary_node then
|
2020-11-16 18:12:31 +01:00
|
|
|
return node
|
|
|
|
end
|
|
|
|
if node.type ~= "leaf" then
|
2020-12-14 14:19:28 +01:00
|
|
|
return get_primary_node(node.a) or get_primary_node(node.b)
|
2020-11-16 18:12:31 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
|
2020-12-28 23:35:52 +01:00
|
|
|
function RootView:get_active_node_default()
|
|
|
|
local node = self.root_node:get_node_for_view(core.active_view)
|
2022-02-09 14:58:20 +01:00
|
|
|
if not node then node = self:get_primary_node() end
|
2020-11-16 18:12:31 +01:00
|
|
|
if node.locked then
|
2020-12-14 14:19:28 +01:00
|
|
|
local default_view = self:get_primary_node().views[1]
|
2020-12-06 01:03:54 +01:00
|
|
|
assert(default_view, "internal error: cannot find original document node.")
|
2020-11-16 18:12:31 +01:00
|
|
|
core.set_active_view(default_view)
|
2020-05-22 16:43:47 +02:00
|
|
|
node = self:get_active_node()
|
|
|
|
end
|
2020-12-28 23:35:52 +01:00
|
|
|
return node
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:get_primary_node()
|
|
|
|
return get_primary_node(self.root_node)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-10-23 03:34:24 +02:00
|
|
|
local function select_next_primary_node(node)
|
|
|
|
if node.is_primary_node then return end
|
|
|
|
if node.type ~= "leaf" then
|
|
|
|
return select_next_primary_node(node.a) or select_next_primary_node(node.b)
|
|
|
|
else
|
|
|
|
local lx, ly = node:get_locked_size()
|
|
|
|
if not lx and not ly then
|
|
|
|
return node
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:select_next_primary_node()
|
|
|
|
return select_next_primary_node(self.root_node)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2020-12-28 23:35:52 +01:00
|
|
|
function RootView:open_doc(doc)
|
|
|
|
local node = self:get_active_node_default()
|
2019-12-28 12:16:32 +01:00
|
|
|
for i, view in ipairs(node.views) do
|
|
|
|
if view.doc == doc then
|
|
|
|
node:set_active_view(node.views[i])
|
|
|
|
return view
|
|
|
|
end
|
|
|
|
end
|
|
|
|
local view = DocView(doc)
|
|
|
|
node:add_view(view)
|
|
|
|
self.root_node:update_layout()
|
|
|
|
view:scroll_to_line(view.doc:get_selection(), true, true)
|
|
|
|
return view
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-08-11 05:29:39 +02:00
|
|
|
function RootView:close_all_docviews(keep_active)
|
|
|
|
self.root_node:close_all_docviews(keep_active)
|
2020-12-06 00:57:27 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-06-17 10:13:04 +02:00
|
|
|
-- Function to intercept mouse pressed events on the active view.
|
|
|
|
-- Do nothing by default.
|
|
|
|
function RootView.on_view_mouse_pressed(button, x, y, clicks)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
function RootView:on_mouse_pressed(button, x, y, clicks)
|
|
|
|
local div = self.root_node:get_divider_overlapping_point(x, y)
|
2021-11-24 02:34:01 +01:00
|
|
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
|
|
|
if div and (node and not node.active_view:scrollbar_overlaps_point(x, y)) then
|
2019-12-28 12:16:32 +01:00
|
|
|
self.dragged_divider = div
|
2021-10-06 02:02:55 +02:00
|
|
|
return true
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2021-05-17 13:37:05 +02:00
|
|
|
if node.hovered_scroll_button > 0 then
|
|
|
|
node:scroll_tabs(node.hovered_scroll_button)
|
2021-10-06 02:02:55 +02:00
|
|
|
return true
|
2021-05-16 15:50:27 +02:00
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
local idx = node:get_tab_overlapping_point(x, y)
|
|
|
|
if idx then
|
2021-02-27 15:29:18 +01:00
|
|
|
if button == "middle" or node.hovered_close == idx then
|
2021-02-27 23:55:36 +01:00
|
|
|
node:close_view(self.root_node, node.views[idx])
|
2021-10-06 02:02:55 +02:00
|
|
|
return true
|
2021-02-27 23:55:36 +01:00
|
|
|
else
|
2021-09-17 02:47:34 +02:00
|
|
|
if button == "left" then
|
|
|
|
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
|
|
|
|
end
|
2021-02-27 23:55:36 +01:00
|
|
|
node:set_active_view(node.views[idx])
|
2021-10-06 04:07:23 +02:00
|
|
|
return true
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2021-09-17 02:47:34 +02:00
|
|
|
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
|
2020-05-19 13:58:41 +02:00
|
|
|
core.set_active_view(node.active_view)
|
2021-11-24 03:56:07 +01:00
|
|
|
return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-09-17 02:47:34 +02:00
|
|
|
function RootView:get_overlay_base_color(overlay)
|
|
|
|
if overlay == self.drag_overlay then
|
|
|
|
return style.drag_overlay
|
|
|
|
else
|
|
|
|
return style.drag_overlay_tab
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:set_show_overlay(overlay, status)
|
|
|
|
overlay.visible = status
|
|
|
|
if status then -- reset colors
|
|
|
|
-- reload base_color
|
|
|
|
overlay.base_color = self:get_overlay_base_color(overlay)
|
|
|
|
overlay.color[1] = overlay.base_color[1]
|
|
|
|
overlay.color[2] = overlay.base_color[2]
|
|
|
|
overlay.color[3] = overlay.base_color[3]
|
|
|
|
overlay.color[4] = overlay.base_color[4]
|
|
|
|
overlay.opacity = 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:on_mouse_released(button, x, y, ...)
|
2019-12-28 12:16:32 +01:00
|
|
|
if self.dragged_divider then
|
|
|
|
self.dragged_divider = nil
|
|
|
|
end
|
2021-05-02 09:13:21 +02:00
|
|
|
if self.dragged_node then
|
2021-09-17 02:47:34 +02:00
|
|
|
if button == "left" then
|
|
|
|
if self.dragged_node.dragging then
|
|
|
|
local node = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
|
|
|
|
local dragged_node = self.dragged_node.node
|
|
|
|
|
|
|
|
if node and not node.locked
|
|
|
|
-- don't do anything if dragging onto own node, with only one view
|
|
|
|
and (node ~= dragged_node or #node.views > 1) then
|
|
|
|
local split_type = node:get_split_type(self.mouse.x, self.mouse.y)
|
|
|
|
local view = dragged_node.views[self.dragged_node.idx]
|
|
|
|
|
|
|
|
if split_type ~= "middle" and split_type ~= "tab" then -- needs splitting
|
|
|
|
local new_node = node:split(split_type)
|
|
|
|
self.root_node:get_node_for_view(view):remove_view(self.root_node, view)
|
|
|
|
new_node:add_view(view)
|
|
|
|
elseif split_type == "middle" and node ~= dragged_node then -- move to other node
|
|
|
|
dragged_node:remove_view(self.root_node, view)
|
|
|
|
node:add_view(view)
|
|
|
|
self.root_node:get_node_for_view(view):set_active_view(view)
|
|
|
|
elseif split_type == "tab" then -- move besides other tabs
|
|
|
|
local tab_index = node:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y, dragged_node, self.dragged_node.idx)
|
|
|
|
dragged_node:remove_view(self.root_node, view)
|
|
|
|
node:add_view(view, tab_index)
|
|
|
|
self.root_node:get_node_for_view(view):set_active_view(view)
|
|
|
|
end
|
|
|
|
self.root_node:update_layout()
|
|
|
|
core.redraw = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
|
|
if self.dragged_node and self.dragged_node.dragging then
|
|
|
|
core.request_cursor("arrow")
|
|
|
|
end
|
|
|
|
self.dragged_node = nil
|
|
|
|
end
|
|
|
|
else -- avoid sending on_mouse_released events when dragging tabs
|
|
|
|
self.root_node:on_mouse_released(button, x, y, ...)
|
2021-05-02 09:13:21 +02:00
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-02-11 11:53:53 +01:00
|
|
|
local function resize_child_node(node, axis, value, delta)
|
2021-05-06 15:35:40 +02:00
|
|
|
local accept_resize = node.a:resize(axis, value)
|
|
|
|
if not accept_resize then
|
|
|
|
accept_resize = node.b:resize(axis, node.size[axis] - value)
|
|
|
|
end
|
2021-02-15 00:36:39 +01:00
|
|
|
if not accept_resize then
|
2021-02-11 11:53:53 +01:00
|
|
|
node.divider = node.divider + delta / node.size[axis]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
function RootView:on_mouse_moved(x, y, dx, dy)
|
2021-03-13 12:57:52 +01:00
|
|
|
if core.active_view == core.nag_view then
|
2021-06-03 22:49:37 +02:00
|
|
|
core.request_cursor("arrow")
|
2021-03-13 12:57:52 +01:00
|
|
|
core.active_view:on_mouse_moved(x, y, dx, dy)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
if self.dragged_divider then
|
2020-05-23 10:40:42 +02:00
|
|
|
local node = self.dragged_divider
|
|
|
|
if node.type == "hsplit" then
|
2021-02-20 20:13:25 +01:00
|
|
|
x = common.clamp(x, 0, self.root_node.size.x * 0.95)
|
2021-02-11 11:53:53 +01:00
|
|
|
resize_child_node(node, "x", x, dx)
|
|
|
|
elseif node.type == "vsplit" then
|
2021-03-06 23:05:04 +01:00
|
|
|
y = common.clamp(y, 0, self.root_node.size.y * 0.95)
|
2021-02-11 11:53:53 +01:00
|
|
|
resize_child_node(node, "y", y, dy)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2020-05-23 10:40:42 +02:00
|
|
|
node.divider = common.clamp(node.divider, 0.01, 0.99)
|
2019-12-28 12:16:32 +01:00
|
|
|
return
|
|
|
|
end
|
2021-05-06 15:35:40 +02:00
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
self.mouse.x, self.mouse.y = x, y
|
2021-09-17 02:47:34 +02:00
|
|
|
|
|
|
|
local dn = self.dragged_node
|
|
|
|
if dn and not dn.dragging then
|
|
|
|
-- start dragging only after enough movement
|
|
|
|
dn.dragging = common.distance(x, y, dn.drag_start_x, dn.drag_start_y) > style.tab_width * .05
|
|
|
|
if dn.dragging then
|
|
|
|
core.request_cursor("hand")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- avoid sending on_mouse_moved events when dragging tabs
|
|
|
|
if dn then return end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
self.root_node:on_mouse_moved(x, y, dx, dy)
|
|
|
|
|
2021-11-17 01:12:39 +01:00
|
|
|
self.overlapping_node = self.root_node:get_child_overlapping_point(x, y)
|
2022-02-09 14:58:20 +01:00
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
local div = self.root_node:get_divider_overlapping_point(x, y)
|
2021-11-17 01:12:39 +01:00
|
|
|
local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y)
|
|
|
|
if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then
|
2021-06-03 22:49:37 +02:00
|
|
|
core.request_cursor("arrow")
|
2021-11-24 02:34:01 +01:00
|
|
|
elseif div and (self.overlapping_node and not self.overlapping_node.active_view:scrollbar_overlaps_point(x, y)) then
|
2021-08-04 22:13:56 +02:00
|
|
|
core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
|
2021-05-02 09:13:21 +02:00
|
|
|
elseif tab_index then
|
2021-06-03 22:49:37 +02:00
|
|
|
core.request_cursor("arrow")
|
2021-11-17 01:12:39 +01:00
|
|
|
elseif self.overlapping_node then
|
|
|
|
core.request_cursor(self.overlapping_node.active_view.cursor)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:on_mouse_wheel(...)
|
|
|
|
local x, y = self.mouse.x, self.mouse.y
|
|
|
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
2021-11-17 01:12:39 +01:00
|
|
|
return node.active_view:on_mouse_wheel(...)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:on_text_input(...)
|
|
|
|
core.active_view:on_text_input(...)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2020-11-21 16:36:13 +01:00
|
|
|
function RootView:on_focus_lost(...)
|
|
|
|
-- We force a redraw so documents can redraw without the cursor.
|
|
|
|
core.redraw = true
|
|
|
|
end
|
|
|
|
|
2021-09-17 02:47:34 +02:00
|
|
|
|
|
|
|
function RootView:interpolate_drag_overlay(overlay)
|
|
|
|
self:move_towards(overlay, "x", overlay.to.x)
|
|
|
|
self:move_towards(overlay, "y", overlay.to.y)
|
|
|
|
self:move_towards(overlay, "w", overlay.to.w)
|
|
|
|
self:move_towards(overlay, "h", overlay.to.h)
|
|
|
|
|
|
|
|
self:move_towards(overlay, "opacity", overlay.visible and 100 or 0)
|
|
|
|
overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
function RootView:update()
|
2021-12-06 22:28:29 +01:00
|
|
|
Node.copy_position_and_size(self.root_node, self)
|
2019-12-28 12:16:32 +01:00
|
|
|
self.root_node:update()
|
|
|
|
self.root_node:update_layout()
|
2021-09-17 02:47:34 +02:00
|
|
|
|
|
|
|
self:update_drag_overlay()
|
|
|
|
self:interpolate_drag_overlay(self.drag_overlay)
|
|
|
|
self:interpolate_drag_overlay(self.drag_overlay_tab)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:set_drag_overlay(overlay, x, y, w, h, immediate)
|
|
|
|
overlay.to.x = x
|
|
|
|
overlay.to.y = y
|
|
|
|
overlay.to.w = w
|
|
|
|
overlay.to.h = h
|
|
|
|
if immediate then
|
|
|
|
overlay.x = x
|
|
|
|
overlay.y = y
|
|
|
|
overlay.w = w
|
|
|
|
overlay.h = h
|
|
|
|
end
|
|
|
|
if not overlay.visible then
|
|
|
|
self:set_show_overlay(overlay, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function get_split_sizes(split_type, x, y, w, h)
|
|
|
|
if split_type == "left" then
|
|
|
|
w = w * .5
|
|
|
|
elseif split_type == "right" then
|
|
|
|
x = x + w * .5
|
|
|
|
w = w * .5
|
|
|
|
elseif split_type == "up" then
|
|
|
|
h = h * .5
|
|
|
|
elseif split_type == "down" then
|
|
|
|
y = y + h * .5
|
|
|
|
h = h * .5
|
|
|
|
end
|
|
|
|
return x, y, w, h
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:update_drag_overlay()
|
|
|
|
if not (self.dragged_node and self.dragged_node.dragging) then return end
|
|
|
|
local over = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
|
|
|
|
if over and not over.locked then
|
|
|
|
local _, _, _, tab_h = over:get_scroll_button_rect(1)
|
|
|
|
local x, y = over.position.x, over.position.y
|
|
|
|
local w, h = over.size.x, over.size.y
|
|
|
|
local split_type = over:get_split_type(self.mouse.x, self.mouse.y)
|
|
|
|
|
|
|
|
if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then
|
|
|
|
local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y)
|
|
|
|
self:set_drag_overlay(self.drag_overlay_tab,
|
2021-11-24 02:34:01 +01:00
|
|
|
tab_x + (tab_index and 0 or tab_w), tab_y,
|
|
|
|
style.caret_width, tab_h,
|
|
|
|
-- avoid showing tab overlay moving between nodes
|
|
|
|
over ~= self.drag_overlay_tab.last_over)
|
2021-09-17 02:47:34 +02:00
|
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
|
|
self.drag_overlay_tab.last_over = over
|
|
|
|
else
|
|
|
|
if (over ~= self.dragged_node.node or #over.views > 1) then
|
|
|
|
y = y + tab_h
|
|
|
|
h = h - tab_h
|
|
|
|
x, y, w, h = get_split_sizes(split_type, x, y, w, h)
|
|
|
|
end
|
|
|
|
self:set_drag_overlay(self.drag_overlay, x, y, w, h)
|
|
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:draw_grabbed_tab()
|
|
|
|
local dn = self.dragged_node
|
|
|
|
local _,_, w, h = dn.node:get_tab_rect(dn.idx)
|
|
|
|
local x = self.mouse.x - w / 2
|
|
|
|
local y = self.mouse.y - h / 2
|
|
|
|
local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or ""
|
|
|
|
self.root_node:draw_tab(text, true, true, false, x, y, w, h, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:draw_drag_overlay(ov)
|
|
|
|
if ov.opacity > 0 then
|
|
|
|
renderer.draw_rect(ov.x, ov.y, ov.w, ov.h, ov.color)
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function RootView:draw()
|
|
|
|
self.root_node:draw()
|
|
|
|
while #self.deferred_draws > 0 do
|
|
|
|
local t = table.remove(self.deferred_draws)
|
|
|
|
t.fn(table.unpack(t))
|
|
|
|
end
|
2021-09-17 02:47:34 +02:00
|
|
|
|
|
|
|
self:draw_drag_overlay(self.drag_overlay)
|
|
|
|
self:draw_drag_overlay(self.drag_overlay_tab)
|
|
|
|
if self.dragged_node and self.dragged_node.dragging then
|
|
|
|
self:draw_grabbed_tab()
|
|
|
|
end
|
2021-06-03 22:49:37 +02:00
|
|
|
if core.cursor_change_req then
|
|
|
|
system.set_cursor(core.cursor_change_req)
|
|
|
|
core.cursor_change_req = nil
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return RootView
|