Serenity Operating System
at hosted 338 lines 11 kB view raw
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}