Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "DirectoryView.h"
28#include <AK/FileSystemPath.h>
29#include <AK/StringBuilder.h>
30#include <LibGUI/SortingProxyModel.h>
31#include <stdio.h>
32#include <unistd.h>
33
34// FIXME: Remove this hackery once printf() supports floats.
35static String number_string_with_one_decimal(float number, const char* suffix)
36{
37 float decimals = number - (int)number;
38 return String::format("%d.%d %s", (int)number, (int)(decimals * 10), suffix);
39}
40
41static String human_readable_size(size_t size)
42{
43 if (size < 1 * KB)
44 return String::format("%zu bytes", size);
45 if (size < 1 * MB)
46 return number_string_with_one_decimal((float)size / (float)KB, "KB");
47 if (size < 1 * GB)
48 return number_string_with_one_decimal((float)size / (float)MB, "MB");
49 return number_string_with_one_decimal((float)size / (float)GB, "GB");
50}
51
52void DirectoryView::handle_activation(const GUI::ModelIndex& index)
53{
54 if (!index.is_valid())
55 return;
56 dbgprintf("on activation: %d,%d, this=%p, m_model=%p\n", index.row(), index.column(), this, m_model.ptr());
57 auto& node = model().node(index);
58 auto path = node.full_path(model());
59
60 struct stat st;
61 if (stat(path.characters(), &st) < 0) {
62 perror("stat");
63 return;
64 }
65
66 if (S_ISDIR(st.st_mode)) {
67 open(path);
68 return;
69 }
70
71 ASSERT(!S_ISLNK(st.st_mode));
72
73 if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) {
74 if (fork() == 0) {
75 int rc = execl(path.characters(), path.characters(), nullptr);
76 if (rc < 0)
77 perror("exec");
78 ASSERT_NOT_REACHED();
79 }
80 return;
81 }
82
83 if (path.to_lowercase().ends_with(".png")) {
84 if (fork() == 0) {
85 int rc = execl("/bin/qs", "/bin/qs", path.characters(), nullptr);
86 if (rc < 0)
87 perror("exec");
88 ASSERT_NOT_REACHED();
89 }
90 return;
91 }
92
93 if (path.to_lowercase().ends_with(".html")) {
94 if (fork() == 0) {
95 int rc = execl("/bin/Browser", "/bin/Browser", path.characters(), nullptr);
96 if (rc < 0)
97 perror("exec");
98 ASSERT_NOT_REACHED();
99 }
100 return;
101 }
102
103 if (path.to_lowercase().ends_with(".wav")) {
104 if (fork() == 0) {
105 int rc = execl("/bin/SoundPlayer", "/bin/SoundPlayer", path.characters(), nullptr);
106 if (rc < 0)
107 perror("exec");
108 ASSERT_NOT_REACHED();
109 }
110 return;
111 }
112
113 if (fork() == 0) {
114 int rc = execl("/bin/TextEditor", "/bin/TextEditor", path.characters(), nullptr);
115 if (rc < 0)
116 perror("exec");
117 ASSERT_NOT_REACHED();
118 }
119};
120
121DirectoryView::DirectoryView()
122 : m_model(GUI::FileSystemModel::create())
123{
124 set_active_widget(nullptr);
125 m_item_view = add<GUI::ItemView>();
126 m_item_view->set_model(model());
127
128 m_columns_view = add<GUI::ColumnsView>();
129 m_columns_view->set_model(model());
130
131 m_table_view = add<GUI::TableView>();
132 m_table_view->set_model(GUI::SortingProxyModel::create(m_model));
133
134 m_table_view->model()->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
135
136 m_item_view->set_model_column(GUI::FileSystemModel::Column::Name);
137 m_columns_view->set_model_column(GUI::FileSystemModel::Column::Name);
138
139 m_model->on_root_path_change = [this] {
140 m_table_view->selection().clear();
141 m_item_view->selection().clear();
142 if (on_path_change)
143 on_path_change(model().root_path());
144 };
145
146 // NOTE: We're using the on_update hook on the GUI::SortingProxyModel here instead of
147 // the GUI::FileSystemModel's hook. This is because GUI::SortingProxyModel has already
148 // installed an on_update hook on the GUI::FileSystemModel internally.
149 // FIXME: This is an unfortunate design. We should come up with something better.
150 m_table_view->model()->on_update = [this] {
151 for_each_view_implementation([](auto& view) {
152 view.selection().clear();
153 });
154 update_statusbar();
155 };
156
157 m_model->on_thumbnail_progress = [this](int done, int total) {
158 if (on_thumbnail_progress)
159 on_thumbnail_progress(done, total);
160 };
161
162 m_item_view->on_activation = [&](const GUI::ModelIndex& index) {
163 handle_activation(index);
164 };
165 m_columns_view->on_activation = [&](const GUI::ModelIndex& index) {
166 handle_activation(index);
167 };
168 m_table_view->on_activation = [&](auto& index) {
169 auto& filter_model = (GUI::SortingProxyModel&)*m_table_view->model();
170 handle_activation(filter_model.map_to_target(index));
171 };
172
173 m_table_view->on_selection_change = [this] {
174 update_statusbar();
175 if (on_selection_change)
176 on_selection_change(*m_table_view);
177 };
178 m_item_view->on_selection_change = [this] {
179 update_statusbar();
180 if (on_selection_change)
181 on_selection_change(*m_item_view);
182 };
183 m_columns_view->on_selection_change = [this] {
184 update_statusbar();
185 if (on_selection_change)
186 on_selection_change(*m_columns_view);
187 };
188
189 m_table_view->on_context_menu_request = [this](auto& index, auto& event) {
190 if (on_context_menu_request)
191 on_context_menu_request(*m_table_view, index, event);
192 };
193 m_item_view->on_context_menu_request = [this](auto& index, auto& event) {
194 if (on_context_menu_request)
195 on_context_menu_request(*m_item_view, index, event);
196 };
197 m_columns_view->on_context_menu_request = [this](auto& index, auto& event) {
198 if (on_context_menu_request)
199 on_context_menu_request(*m_columns_view, index, event);
200 };
201
202 m_table_view->on_drop = [this](auto& index, auto& event) {
203 if (on_drop)
204 on_drop(*m_table_view, index, event);
205 };
206 m_item_view->on_drop = [this](auto& index, auto& event) {
207 if (on_drop)
208 on_drop(*m_item_view, index, event);
209 };
210 m_columns_view->on_drop = [this](auto& index, auto& event) {
211 if (on_drop)
212 on_drop(*m_columns_view, index, event);
213 };
214
215 set_view_mode(ViewMode::Icon);
216}
217
218DirectoryView::~DirectoryView()
219{
220}
221
222void DirectoryView::set_view_mode(ViewMode mode)
223{
224 if (m_view_mode == mode)
225 return;
226 m_view_mode = mode;
227 update();
228 if (mode == ViewMode::List) {
229 set_active_widget(m_table_view);
230 return;
231 }
232 if (mode == ViewMode::Columns) {
233 set_active_widget(m_columns_view);
234 return;
235 }
236 if (mode == ViewMode::Icon) {
237 set_active_widget(m_item_view);
238 return;
239 }
240 ASSERT_NOT_REACHED();
241}
242
243void DirectoryView::add_path_to_history(const StringView& path)
244{
245 if (m_path_history_position < m_path_history.size())
246 m_path_history.resize(m_path_history_position + 1);
247
248 m_path_history.append(path);
249 m_path_history_position = m_path_history.size() - 1;
250}
251
252void DirectoryView::open(const StringView& path)
253{
254 add_path_to_history(path);
255 model().set_root_path(path);
256}
257
258void DirectoryView::set_status_message(const StringView& message)
259{
260 if (on_status_message)
261 on_status_message(message);
262}
263
264void DirectoryView::open_parent_directory()
265{
266 auto path = String::format("%s/..", model().root_path().characters());
267 add_path_to_history(path);
268 model().set_root_path(path);
269}
270
271void DirectoryView::refresh()
272{
273 model().update();
274}
275
276void DirectoryView::open_previous_directory()
277{
278 if (m_path_history_position > 0) {
279 m_path_history_position--;
280 model().set_root_path(m_path_history[m_path_history_position]);
281 }
282}
283void DirectoryView::open_next_directory()
284{
285 if (m_path_history_position < m_path_history.size() - 1) {
286 m_path_history_position++;
287 model().set_root_path(m_path_history[m_path_history_position]);
288 }
289}
290
291void DirectoryView::update_statusbar()
292{
293 size_t total_size = model().node({}).total_size;
294 if (current_view().selection().is_empty()) {
295 set_status_message(String::format("%d item%s (%s)",
296 model().row_count(),
297 model().row_count() != 1 ? "s" : "",
298 human_readable_size(total_size).characters()));
299 return;
300 }
301
302 int selected_item_count = current_view().selection().size();
303 size_t selected_byte_count = 0;
304
305 current_view().selection().for_each_index([&](auto& index) {
306 auto& model = *current_view().model();
307 auto size_index = model.sibling(index.row(), GUI::FileSystemModel::Column::Size, model.parent_index(index));
308 auto file_size = model.data(size_index).to_i32();
309 selected_byte_count += file_size;
310 });
311
312 StringBuilder builder;
313 builder.append(String::number(selected_item_count));
314 builder.append(" item");
315 if (selected_item_count != 1)
316 builder.append('s');
317 builder.append(" selected (");
318 builder.append(human_readable_size(selected_byte_count).characters());
319 builder.append(')');
320
321 if (selected_item_count == 1) {
322 auto index = current_view().selection().first();
323
324 // FIXME: This is disgusting. This code should not even be aware that there is a GUI::SortingProxyModel in the table view.
325 if (m_view_mode == ViewMode::List) {
326 auto& filter_model = (GUI::SortingProxyModel&)*m_table_view->model();
327 index = filter_model.map_to_target(index);
328 }
329
330 auto& node = model().node(index);
331 if (!node.symlink_target.is_empty()) {
332 builder.append(" -> ");
333 builder.append(node.symlink_target);
334 }
335 }
336
337 set_status_message(builder.to_string());
338}