summaryrefslogtreecommitdiff
path: root/Userland/Applications
diff options
context:
space:
mode:
Diffstat (limited to 'Userland/Applications')
-rw-r--r--Userland/Applications/About/CMakeLists.txt12
-rw-r--r--Userland/Applications/About/main.cpp58
-rw-r--r--Userland/Applications/Browser/BookmarksBarWidget.cpp241
-rw-r--r--Userland/Applications/Browser/BookmarksBarWidget.h81
-rw-r--r--Userland/Applications/Browser/Browser.h35
-rw-r--r--Userland/Applications/Browser/BrowserConsoleClient.cpp130
-rw-r--r--Userland/Applications/Browser/BrowserConsoleClient.h60
-rw-r--r--Userland/Applications/Browser/BrowserWindow.gml15
-rw-r--r--Userland/Applications/Browser/CMakeLists.txt19
-rw-r--r--Userland/Applications/Browser/ConsoleWidget.cpp169
-rw-r--r--Userland/Applications/Browser/ConsoleWidget.h57
-rw-r--r--Userland/Applications/Browser/DownloadWidget.cpp182
-rw-r--r--Userland/Applications/Browser/DownloadWidget.h61
-rw-r--r--Userland/Applications/Browser/History.cpp73
-rw-r--r--Userland/Applications/Browser/History.h54
-rw-r--r--Userland/Applications/Browser/InspectorWidget.cpp94
-rw-r--r--Userland/Applications/Browser/InspectorWidget.h53
-rw-r--r--Userland/Applications/Browser/Tab.cpp550
-rw-r--r--Userland/Applications/Browser/Tab.gml22
-rw-r--r--Userland/Applications/Browser/Tab.h123
-rw-r--r--Userland/Applications/Browser/WindowActions.cpp82
-rw-r--r--Userland/Applications/Browser/WindowActions.h59
-rw-r--r--Userland/Applications/Browser/main.cpp254
-rw-r--r--Userland/Applications/CMakeLists.txt26
-rw-r--r--Userland/Applications/Calculator/CMakeLists.txt11
-rw-r--r--Userland/Applications/Calculator/Calculator.cpp143
-rw-r--r--Userland/Applications/Calculator/Calculator.h72
-rw-r--r--Userland/Applications/Calculator/CalculatorWidget.cpp208
-rw-r--r--Userland/Applications/Calculator/CalculatorWidget.h73
-rw-r--r--Userland/Applications/Calculator/CalculatorWindow.gml272
-rw-r--r--Userland/Applications/Calculator/Keypad.cpp171
-rw-r--r--Userland/Applications/Calculator/Keypad.h69
-rw-r--r--Userland/Applications/Calculator/main.cpp84
-rw-r--r--Userland/Applications/Calendar/AddEventDialog.cpp154
-rw-r--r--Userland/Applications/Calendar/AddEventDialog.h69
-rw-r--r--Userland/Applications/Calendar/CMakeLists.txt7
-rw-r--r--Userland/Applications/Calendar/main.cpp182
-rw-r--r--Userland/Applications/CrashReporter/CMakeLists.txt10
-rw-r--r--Userland/Applications/CrashReporter/CrashReporterWindow.gml95
-rw-r--r--Userland/Applications/CrashReporter/main.cpp179
-rw-r--r--Userland/Applications/Debugger/CMakeLists.txt6
-rw-r--r--Userland/Applications/Debugger/main.cpp313
-rw-r--r--Userland/Applications/DisplaySettings/CMakeLists.txt11
-rw-r--r--Userland/Applications/DisplaySettings/DisplaySettings.cpp264
-rw-r--r--Userland/Applications/DisplaySettings/DisplaySettings.h55
-rw-r--r--Userland/Applications/DisplaySettings/DisplaySettingsWindow.gml117
-rw-r--r--Userland/Applications/DisplaySettings/MonitorWidget.cpp118
-rw-r--r--Userland/Applications/DisplaySettings/MonitorWidget.h64
-rw-r--r--Userland/Applications/DisplaySettings/main.cpp82
-rw-r--r--Userland/Applications/FileManager/CMakeLists.txt13
-rw-r--r--Userland/Applications/FileManager/DesktopWidget.cpp47
-rw-r--r--Userland/Applications/FileManager/DesktopWidget.h45
-rw-r--r--Userland/Applications/FileManager/DirectoryView.cpp591
-rw-r--r--Userland/Applications/FileManager/DirectoryView.h190
-rw-r--r--Userland/Applications/FileManager/FileManagerWindow.gml53
-rw-r--r--Userland/Applications/FileManager/FileUtils.cpp280
-rw-r--r--Userland/Applications/FileManager/FileUtils.h50
-rw-r--r--Userland/Applications/FileManager/PropertiesWindow.cpp304
-rw-r--r--Userland/Applications/FileManager/PropertiesWindow.h98
-rw-r--r--Userland/Applications/FileManager/main.cpp974
-rw-r--r--Userland/Applications/FontEditor/.gitignore1
-rw-r--r--Userland/Applications/FontEditor/CMakeLists.txt11
-rw-r--r--Userland/Applications/FontEditor/FontEditor.cpp372
-rw-r--r--Userland/Applications/FontEditor/FontEditor.h58
-rw-r--r--Userland/Applications/FontEditor/GlyphEditorWidget.cpp125
-rw-r--r--Userland/Applications/FontEditor/GlyphEditorWidget.h60
-rw-r--r--Userland/Applications/FontEditor/GlyphMapWidget.cpp168
-rw-r--r--Userland/Applications/FontEditor/GlyphMapWidget.h69
-rw-r--r--Userland/Applications/FontEditor/main.cpp155
-rw-r--r--Userland/Applications/Help/CMakeLists.txt10
-rw-r--r--Userland/Applications/Help/History.cpp59
-rw-r--r--Userland/Applications/Help/History.h48
-rw-r--r--Userland/Applications/Help/ManualModel.cpp199
-rw-r--r--Userland/Applications/Help/ManualModel.h66
-rw-r--r--Userland/Applications/Help/ManualNode.h41
-rw-r--r--Userland/Applications/Help/ManualPageNode.cpp44
-rw-r--r--Userland/Applications/Help/ManualPageNode.h53
-rw-r--r--Userland/Applications/Help/ManualSectionNode.cpp66
-rw-r--r--Userland/Applications/Help/ManualSectionNode.h63
-rw-r--r--Userland/Applications/Help/main.cpp312
-rw-r--r--Userland/Applications/HexEditor/CMakeLists.txt8
-rw-r--r--Userland/Applications/HexEditor/HexEditor.cpp583
-rw-r--r--Userland/Applications/HexEditor/HexEditor.h105
-rw-r--r--Userland/Applications/HexEditor/HexEditorWidget.cpp241
-rw-r--r--Userland/Applications/HexEditor/HexEditorWidget.h65
-rw-r--r--Userland/Applications/HexEditor/main.cpp67
-rw-r--r--Userland/Applications/IRCClient/CMakeLists.txt14
-rw-r--r--Userland/Applications/IRCClient/IRCAppWindow.cpp375
-rw-r--r--Userland/Applications/IRCClient/IRCAppWindow.h76
-rw-r--r--Userland/Applications/IRCClient/IRCChannel.cpp144
-rw-r--r--Userland/Applications/IRCClient/IRCChannel.h94
-rw-r--r--Userland/Applications/IRCClient/IRCChannelMemberListModel.cpp81
-rw-r--r--Userland/Applications/IRCClient/IRCChannelMemberListModel.h53
-rw-r--r--Userland/Applications/IRCClient/IRCClient.cpp1188
-rw-r--r--Userland/Applications/IRCClient/IRCClient.h236
-rw-r--r--Userland/Applications/IRCClient/IRCLogBuffer.cpp98
-rw-r--r--Userland/Applications/IRCClient/IRCLogBuffer.h58
-rw-r--r--Userland/Applications/IRCClient/IRCQuery.cpp65
-rw-r--r--Userland/Applications/IRCClient/IRCQuery.h64
-rw-r--r--Userland/Applications/IRCClient/IRCWindow.cpp278
-rw-r--r--Userland/Applications/IRCClient/IRCWindow.h82
-rw-r--r--Userland/Applications/IRCClient/IRCWindowListModel.cpp91
-rw-r--r--Userland/Applications/IRCClient/IRCWindowListModel.h54
-rw-r--r--Userland/Applications/IRCClient/main.cpp109
-rw-r--r--Userland/Applications/KeyboardMapper/CMakeLists.txt8
-rw-r--r--Userland/Applications/KeyboardMapper/KeyButton.cpp96
-rw-r--r--Userland/Applications/KeyboardMapper/KeyButton.h49
-rw-r--r--Userland/Applications/KeyboardMapper/KeyPositions.h115
-rw-r--r--Userland/Applications/KeyboardMapper/KeyboardMapperWidget.cpp293
-rw-r--r--Userland/Applications/KeyboardMapper/KeyboardMapperWidget.h60
-rw-r--r--Userland/Applications/KeyboardMapper/main.cpp116
-rw-r--r--Userland/Applications/KeyboardSettings/CMakeLists.txt6
-rw-r--r--Userland/Applications/KeyboardSettings/CharacterMapFileListModel.h74
-rw-r--r--Userland/Applications/KeyboardSettings/main.cpp202
-rw-r--r--Userland/Applications/MouseSettings/CMakeLists.txt6
-rw-r--r--Userland/Applications/MouseSettings/main.cpp133
-rw-r--r--Userland/Applications/Piano/CMakeLists.txt14
-rw-r--r--Userland/Applications/Piano/KeysWidget.cpp328
-rw-r--r--Userland/Applications/Piano/KeysWidget.h62
-rw-r--r--Userland/Applications/Piano/KnobsWidget.cpp183
-rw-r--r--Userland/Applications/Piano/KnobsWidget.h76
-rw-r--r--Userland/Applications/Piano/MainWidget.cpp179
-rw-r--r--Userland/Applications/Piano/MainWidget.h74
-rw-r--r--Userland/Applications/Piano/Music.h329
-rw-r--r--Userland/Applications/Piano/RollWidget.cpp248
-rw-r--r--Userland/Applications/Piano/RollWidget.h64
-rw-r--r--Userland/Applications/Piano/SamplerWidget.cpp130
-rw-r--r--Userland/Applications/Piano/SamplerWidget.h62
-rw-r--r--Userland/Applications/Piano/Track.cpp369
-rw-r--r--Userland/Applications/Piano/Track.h104
-rw-r--r--Userland/Applications/Piano/TrackManager.cpp104
-rw-r--r--Userland/Applications/Piano/TrackManager.h74
-rw-r--r--Userland/Applications/Piano/WaveWidget.cpp88
-rw-r--r--Userland/Applications/Piano/WaveWidget.h47
-rw-r--r--Userland/Applications/Piano/main.cpp137
-rw-r--r--Userland/Applications/PixelPaint/BrushTool.cpp169
-rw-r--r--Userland/Applications/PixelPaint/BrushTool.h54
-rw-r--r--Userland/Applications/PixelPaint/BucketTool.cpp133
-rw-r--r--Userland/Applications/PixelPaint/BucketTool.h46
-rw-r--r--Userland/Applications/PixelPaint/CMakeLists.txt27
-rw-r--r--Userland/Applications/PixelPaint/CreateNewImageDialog.cpp91
-rw-r--r--Userland/Applications/PixelPaint/CreateNewImageDialog.h49
-rw-r--r--Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp95
-rw-r--r--Userland/Applications/PixelPaint/CreateNewLayerDialog.h49
-rw-r--r--Userland/Applications/PixelPaint/EllipseTool.cpp137
-rw-r--r--Userland/Applications/PixelPaint/EllipseTool.h64
-rw-r--r--Userland/Applications/PixelPaint/EraseTool.cpp120
-rw-r--r--Userland/Applications/PixelPaint/EraseTool.h56
-rw-r--r--Userland/Applications/PixelPaint/FilterParams.h191
-rw-r--r--Userland/Applications/PixelPaint/Image.cpp331
-rw-r--r--Userland/Applications/PixelPaint/Image.h114
-rw-r--r--Userland/Applications/PixelPaint/ImageEditor.cpp418
-rw-r--r--Userland/Applications/PixelPaint/ImageEditor.h125
-rw-r--r--Userland/Applications/PixelPaint/Layer.cpp108
-rw-r--r--Userland/Applications/PixelPaint/Layer.h95
-rw-r--r--Userland/Applications/PixelPaint/LayerListWidget.cpp285
-rw-r--r--Userland/Applications/PixelPaint/LayerListWidget.h89
-rw-r--r--Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp107
-rw-r--r--Userland/Applications/PixelPaint/LayerPropertiesWidget.h53
-rw-r--r--Userland/Applications/PixelPaint/LineTool.cpp156
-rw-r--r--Userland/Applications/PixelPaint/LineTool.h59
-rw-r--r--Userland/Applications/PixelPaint/MoveTool.cpp139
-rw-r--r--Userland/Applications/PixelPaint/MoveTool.h52
-rw-r--r--Userland/Applications/PixelPaint/PaletteWidget.cpp178
-rw-r--r--Userland/Applications/PixelPaint/PaletteWidget.h52
-rw-r--r--Userland/Applications/PixelPaint/PenTool.cpp128
-rw-r--r--Userland/Applications/PixelPaint/PenTool.h54
-rw-r--r--Userland/Applications/PixelPaint/PickerTool.cpp53
-rw-r--r--Userland/Applications/PixelPaint/PickerTool.h41
-rw-r--r--Userland/Applications/PixelPaint/RectangleTool.cpp137
-rw-r--r--Userland/Applications/PixelPaint/RectangleTool.h63
-rw-r--r--Userland/Applications/PixelPaint/SprayTool.cpp176
-rw-r--r--Userland/Applications/PixelPaint/SprayTool.h60
-rw-r--r--Userland/Applications/PixelPaint/Tool.cpp51
-rw-r--r--Userland/Applications/PixelPaint/Tool.h63
-rw-r--r--Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp61
-rw-r--r--Userland/Applications/PixelPaint/ToolPropertiesWidget.h54
-rw-r--r--Userland/Applications/PixelPaint/ToolboxWidget.cpp140
-rw-r--r--Userland/Applications/PixelPaint/ToolboxWidget.h60
-rw-r--r--Userland/Applications/PixelPaint/main.cpp387
-rw-r--r--Userland/Applications/QuickShow/CMakeLists.txt7
-rw-r--r--Userland/Applications/QuickShow/QSWidget.cpp283
-rw-r--r--Userland/Applications/QuickShow/QSWidget.h89
-rw-r--r--Userland/Applications/QuickShow/main.cpp309
-rw-r--r--Userland/Applications/SoundPlayer/CMakeLists.txt9
-rw-r--r--Userland/Applications/SoundPlayer/PlaybackManager.cpp188
-rw-r--r--Userland/Applications/SoundPlayer/PlaybackManager.h77
-rw-r--r--Userland/Applications/SoundPlayer/SampleWidget.cpp82
-rw-r--r--Userland/Applications/SoundPlayer/SampleWidget.h47
-rw-r--r--Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp194
-rw-r--r--Userland/Applications/SoundPlayer/SoundPlayerWidget.h94
-rw-r--r--Userland/Applications/SoundPlayer/main.cpp109
-rw-r--r--Userland/Applications/SpaceAnalyzer/CMakeLists.txt10
-rw-r--r--Userland/Applications/SpaceAnalyzer/SpaceAnalyzer.gml20
-rw-r--r--Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp376
-rw-r--r--Userland/Applications/SpaceAnalyzer/TreeMapWidget.h90
-rw-r--r--Userland/Applications/SpaceAnalyzer/main.cpp300
-rw-r--r--Userland/Applications/Spreadsheet/CMakeLists.txt28
-rw-r--r--Userland/Applications/Spreadsheet/Cell.cpp211
-rw-r--r--Userland/Applications/Spreadsheet/Cell.h138
-rw-r--r--Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp81
-rw-r--r--Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h47
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Date.cpp68
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Date.h42
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Format.cpp69
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Format.h35
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Identity.cpp52
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Identity.h42
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Numeric.cpp76
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Numeric.h42
-rw-r--r--Userland/Applications/Spreadsheet/CellType/String.cpp57
-rw-r--r--Userland/Applications/Spreadsheet/CellType/String.h42
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Type.cpp63
-rw-r--r--Userland/Applications/Spreadsheet/CellType/Type.h64
-rw-r--r--Userland/Applications/Spreadsheet/CellTypeDialog.cpp483
-rw-r--r--Userland/Applications/Spreadsheet/CellTypeDialog.h69
-rw-r--r--Userland/Applications/Spreadsheet/CondFormatting.gml40
-rw-r--r--Userland/Applications/Spreadsheet/CondView.gml54
-rw-r--r--Userland/Applications/Spreadsheet/ConditionalFormatting.h73
-rw-r--r--Userland/Applications/Spreadsheet/Forward.h41
-rw-r--r--Userland/Applications/Spreadsheet/HelpWindow.cpp229
-rw-r--r--Userland/Applications/Spreadsheet/HelpWindow.h63
-rw-r--r--Userland/Applications/Spreadsheet/JSIntegration.cpp444
-rw-r--r--Userland/Applications/Spreadsheet/JSIntegration.h82
-rw-r--r--Userland/Applications/Spreadsheet/Position.h60
-rw-r--r--Userland/Applications/Spreadsheet/Readers/CSV.h43
-rw-r--r--Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp110
-rw-r--r--Userland/Applications/Spreadsheet/Readers/XSV.cpp272
-rw-r--r--Userland/Applications/Spreadsheet/Readers/XSV.h208
-rw-r--r--Userland/Applications/Spreadsheet/Spreadsheet.cpp734
-rw-r--r--Userland/Applications/Spreadsheet/Spreadsheet.h188
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetModel.cpp178
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetModel.h60
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetView.cpp334
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetView.h168
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp347
-rw-r--r--Userland/Applications/Spreadsheet/SpreadsheetWidget.h86
-rw-r--r--Userland/Applications/Spreadsheet/Workbook.cpp190
-rw-r--r--Userland/Applications/Spreadsheet/Workbook.h78
-rw-r--r--Userland/Applications/Spreadsheet/Writers/CSV.h44
-rw-r--r--Userland/Applications/Spreadsheet/Writers/Test/TestXSVWriter.cpp96
-rw-r--r--Userland/Applications/Spreadsheet/Writers/XSV.h215
-rw-r--r--Userland/Applications/Spreadsheet/main.cpp253
-rw-r--r--Userland/Applications/SystemMonitor/CMakeLists.txt16
-rw-r--r--Userland/Applications/SystemMonitor/DevicesModel.cpp198
-rw-r--r--Userland/Applications/SystemMonitor/DevicesModel.h69
-rw-r--r--Userland/Applications/SystemMonitor/GraphWidget.cpp163
-rw-r--r--Userland/Applications/SystemMonitor/GraphWidget.h70
-rw-r--r--Userland/Applications/SystemMonitor/InterruptsWidget.cpp68
-rw-r--r--Userland/Applications/SystemMonitor/InterruptsWidget.h44
-rw-r--r--Userland/Applications/SystemMonitor/MemoryStatsWidget.cpp138
-rw-r--r--Userland/Applications/SystemMonitor/MemoryStatsWidget.h53
-rw-r--r--Userland/Applications/SystemMonitor/NetworkStatisticsWidget.cpp98
-rw-r--r--Userland/Applications/SystemMonitor/NetworkStatisticsWidget.h46
-rw-r--r--Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp74
-rw-r--r--Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h45
-rw-r--r--Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp141
-rw-r--r--Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.h46
-rw-r--r--Userland/Applications/SystemMonitor/ProcessModel.cpp454
-rw-r--r--Userland/Applications/SystemMonitor/ProcessModel.h172
-rw-r--r--Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.cpp57
-rw-r--r--Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.h45
-rw-r--r--Userland/Applications/SystemMonitor/ThreadStackWidget.cpp68
-rw-r--r--Userland/Applications/SystemMonitor/ThreadStackWidget.h47
-rw-r--r--Userland/Applications/SystemMonitor/main.cpp670
-rw-r--r--Userland/Applications/Terminal/CMakeLists.txt9
-rw-r--r--Userland/Applications/Terminal/TerminalSettingsWindow.gml63
-rw-r--r--Userland/Applications/Terminal/main.cpp525
-rw-r--r--Userland/Applications/TextEditor/CMakeLists.txt10
-rw-r--r--Userland/Applications/TextEditor/TextEditorWidget.cpp667
-rw-r--r--Userland/Applications/TextEditor/TextEditorWidget.h123
-rw-r--r--Userland/Applications/TextEditor/TextEditorWindow.gml88
-rw-r--r--Userland/Applications/TextEditor/main.cpp93
-rw-r--r--Userland/Applications/ThemeEditor/CMakeLists.txt7
-rw-r--r--Userland/Applications/ThemeEditor/PreviewWidget.cpp176
-rw-r--r--Userland/Applications/ThemeEditor/PreviewWidget.h63
-rw-r--r--Userland/Applications/ThemeEditor/main.cpp136
-rw-r--r--Userland/Applications/Welcome/BackgroundWidget.cpp52
-rw-r--r--Userland/Applications/Welcome/BackgroundWidget.h41
-rw-r--r--Userland/Applications/Welcome/CMakeLists.txt9
-rw-r--r--Userland/Applications/Welcome/TextWidget.cpp140
-rw-r--r--Userland/Applications/Welcome/TextWidget.h65
-rw-r--r--Userland/Applications/Welcome/UnuncheckableButton.cpp35
-rw-r--r--Userland/Applications/Welcome/UnuncheckableButton.h40
-rw-r--r--Userland/Applications/Welcome/main.cpp234
285 files changed, 37234 insertions, 0 deletions
diff --git a/Userland/Applications/About/CMakeLists.txt b/Userland/Applications/About/CMakeLists.txt
new file mode 100644
index 0000000000..0b3023c819
--- /dev/null
+++ b/Userland/Applications/About/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES
+ main.cpp
+)
+
+execute_process(COMMAND "git rev-parse --short HEAD" OUTPUT_VARIABLE GIT_COMMIT)
+execute_process(COMMAND "git rev-parse --abbrev-ref HEAD" OUTPUT_VARIABLE GIT_BRANCH)
+execute_process(COMMAND "git diff-index --quiet HEAD -- && echo tracked || echo untracked" OUTPUT_VARIABLE GIT_CHANGES)
+
+add_definitions(-DGIT_COMMIT="${GIT_COMMIT}" -DGIT_BRANCH="${GIT_BRANCH}" -DGIT_CHANGES="${GIT_CHANGES}")
+
+serenity_bin(About)
+target_link_libraries(About LibGUI)
diff --git a/Userland/Applications/About/main.cpp b/Userland/Applications/About/main.cpp
new file mode 100644
index 0000000000..b7e18caee3
--- /dev/null
+++ b/Userland/Applications/About/main.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGUI/AboutDialog.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGfx/Bitmap.h>
+#include <stdio.h>
+#include <sys/utsname.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept rpath unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept rpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ auto app_icon = GUI::Icon::default_icon("ladybug");
+ GUI::AboutDialog::show("SerenityOS", nullptr, nullptr, app_icon.bitmap_for_size(32));
+ return app->exec();
+}
diff --git a/Userland/Applications/Browser/BookmarksBarWidget.cpp b/Userland/Applications/Browser/BookmarksBarWidget.cpp
new file mode 100644
index 0000000000..ef9af48a60
--- /dev/null
+++ b/Userland/Applications/Browser/BookmarksBarWidget.cpp
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2020, Emanuel Sprung <emanuel.sprung@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BookmarksBarWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Event.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Palette.h>
+
+namespace Browser {
+
+static BookmarksBarWidget* s_the;
+
+BookmarksBarWidget& BookmarksBarWidget::the()
+{
+ return *s_the;
+}
+
+BookmarksBarWidget::BookmarksBarWidget(const String& bookmarks_file, bool enabled)
+{
+ s_the = this;
+ set_layout<GUI::HorizontalBoxLayout>();
+ layout()->set_spacing(0);
+
+ set_fixed_height(20);
+
+ if (!enabled)
+ set_visible(false);
+
+ m_additional = GUI::Button::construct();
+ m_additional->set_button_style(Gfx::ButtonStyle::CoolBar);
+ m_additional->set_text(">");
+ m_additional->set_fixed_size(14, 20);
+ m_additional->set_focus_policy(GUI::FocusPolicy::TabFocus);
+ m_additional->on_click = [this](auto) {
+ if (m_additional_menu) {
+ m_additional_menu->popup(m_additional->relative_position().translated(relative_position().translated(m_additional->window()->position())));
+ }
+ };
+
+ m_separator = GUI::Widget::construct();
+
+ m_context_menu = GUI::Menu::construct();
+ auto default_action = GUI::Action::create("Open", [this](auto&) {
+ if (on_bookmark_click)
+ on_bookmark_click(m_context_menu_url, Mod_None);
+ });
+ m_context_menu_default_action = default_action;
+ m_context_menu->add_action(default_action);
+ m_context_menu->add_action(GUI::Action::create("Open in new tab", [this](auto&) {
+ if (on_bookmark_click)
+ on_bookmark_click(m_context_menu_url, Mod_Ctrl);
+ }));
+ m_context_menu->add_action(GUI::Action::create("Delete", [this](auto&) {
+ remove_bookmark(m_context_menu_url);
+ }));
+
+ Vector<GUI::JsonArrayModel::FieldSpec> fields;
+ fields.empend("title", "Title", Gfx::TextAlignment::CenterLeft);
+ fields.empend("url", "Url", Gfx::TextAlignment::CenterRight);
+ set_model(GUI::JsonArrayModel::create(bookmarks_file, move(fields)));
+ model()->update();
+}
+
+BookmarksBarWidget::~BookmarksBarWidget()
+{
+ if (m_model)
+ m_model->unregister_client(*this);
+}
+
+void BookmarksBarWidget::set_model(RefPtr<GUI::Model> model)
+{
+ if (model == m_model)
+ return;
+ if (m_model)
+ m_model->unregister_client(*this);
+ m_model = move(model);
+ m_model->register_client(*this);
+}
+
+void BookmarksBarWidget::resize_event(GUI::ResizeEvent& event)
+{
+ Widget::resize_event(event);
+ update_content_size();
+}
+
+void BookmarksBarWidget::model_did_update(unsigned)
+{
+ remove_all_children();
+
+ m_bookmarks.clear();
+
+ int width = 0;
+ for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
+
+ auto title = model()->index(item_index, 0).data().to_string();
+ auto url = model()->index(item_index, 1).data().to_string();
+
+ Gfx::IntRect rect { width, 0, font().width(title) + 32, height() };
+
+ auto& button = add<GUI::Button>();
+ m_bookmarks.append(button);
+
+ button.set_button_style(Gfx::ButtonStyle::CoolBar);
+ button.set_text(title);
+ button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png"));
+ button.set_fixed_size(font().width(title) + 32, 20);
+ button.set_relative_rect(rect);
+ button.set_focus_policy(GUI::FocusPolicy::TabFocus);
+ button.set_tooltip(url);
+
+ button.on_click = [title, url, this](auto modifiers) {
+ if (on_bookmark_click)
+ on_bookmark_click(url, modifiers);
+ };
+
+ button.on_context_menu_request = [this, url](auto& context_menu_event) {
+ m_context_menu_url = url;
+ m_context_menu->popup(context_menu_event.screen_position(), m_context_menu_default_action);
+ };
+
+ width += rect.width();
+ }
+
+ add_child(*m_separator);
+ add_child(*m_additional);
+
+ update_content_size();
+ update();
+}
+
+void BookmarksBarWidget::update_content_size()
+{
+ int x_position = 0;
+ m_last_visible_index = -1;
+
+ for (size_t i = 0; i < m_bookmarks.size(); ++i) {
+ auto& bookmark = m_bookmarks.at(i);
+ if (x_position + bookmark.width() > width()) {
+ m_last_visible_index = i;
+ break;
+ }
+ bookmark.set_x(x_position);
+ bookmark.set_visible(true);
+ x_position += bookmark.width();
+ }
+
+ if (m_last_visible_index < 0) {
+ m_additional->set_visible(false);
+ } else {
+ // hide all items > m_last_visible_index and create new bookmarks menu for them
+ m_additional->set_visible(true);
+ m_additional_menu = GUI::Menu::construct("Additional Bookmarks");
+ for (size_t i = m_last_visible_index; i < m_bookmarks.size(); ++i) {
+ auto& bookmark = m_bookmarks.at(i);
+ bookmark.set_visible(false);
+ m_additional_menu->add_action(GUI::Action::create(bookmark.text(),
+ Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png"),
+ [&](auto&) {
+ bookmark.on_click(0);
+ }));
+ }
+ }
+}
+
+bool BookmarksBarWidget::contains_bookmark(const String& url)
+{
+ for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
+
+ auto item_title = model()->index(item_index, 0).data().to_string();
+ auto item_url = model()->index(item_index, 1).data().to_string();
+ if (item_url == url) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool BookmarksBarWidget::remove_bookmark(const String& url)
+{
+ for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
+
+ auto item_title = model()->index(item_index, 0).data().to_string();
+ auto item_url = model()->index(item_index, 1).data().to_string();
+ if (item_url == url) {
+ auto& json_model = *static_cast<GUI::JsonArrayModel*>(model());
+
+ const auto item_removed = json_model.remove(item_index);
+ if (item_removed)
+ json_model.store();
+
+ return item_removed;
+ }
+ }
+
+ return false;
+}
+bool BookmarksBarWidget::add_bookmark(const String& url, const String& title)
+{
+ Vector<JsonValue> values;
+ values.append(title);
+ values.append(url);
+
+ auto& json_model = *static_cast<GUI::JsonArrayModel*>(model());
+ if (json_model.add(move(values))) {
+ json_model.store();
+ return true;
+ }
+ return false;
+}
+
+}
diff --git a/Userland/Applications/Browser/BookmarksBarWidget.h b/Userland/Applications/Browser/BookmarksBarWidget.h
new file mode 100644
index 0000000000..2eeed8fec5
--- /dev/null
+++ b/Userland/Applications/Browser/BookmarksBarWidget.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020, Emanuel Sprung <emanuel.sprung@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Forward.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Widget.h>
+
+namespace Browser {
+
+class BookmarksBarWidget final
+ : public GUI::Widget
+ , private GUI::ModelClient {
+ C_OBJECT(BookmarksBarWidget);
+
+public:
+ static BookmarksBarWidget& the();
+
+ virtual ~BookmarksBarWidget() override;
+
+ void set_model(RefPtr<GUI::Model>);
+ GUI::Model* model() { return m_model.ptr(); }
+ const GUI::Model* model() const { return m_model.ptr(); }
+
+ Function<void(const String& url, unsigned modifiers)> on_bookmark_click;
+ Function<void(const String&, const String&)> on_bookmark_hover;
+
+ bool contains_bookmark(const String& url);
+ bool remove_bookmark(const String& url);
+ bool add_bookmark(const String& url, const String& title);
+
+private:
+ BookmarksBarWidget(const String&, bool enabled);
+
+ // ^GUI::ModelClient
+ virtual void model_did_update(unsigned) override;
+
+ // ^GUI::Widget
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ void update_content_size();
+
+ RefPtr<GUI::Model> m_model;
+ RefPtr<GUI::Button> m_additional;
+ RefPtr<GUI::Widget> m_separator;
+ RefPtr<GUI::Menu> m_additional_menu;
+
+ RefPtr<GUI::Menu> m_context_menu;
+ RefPtr<GUI::Action> m_context_menu_default_action;
+ String m_context_menu_url;
+
+ NonnullRefPtrVector<GUI::Button> m_bookmarks;
+
+ int m_last_visible_index { -1 };
+};
+
+}
diff --git a/Userland/Applications/Browser/Browser.h b/Userland/Applications/Browser/Browser.h
new file mode 100644
index 0000000000..a4a4eefe1f
--- /dev/null
+++ b/Userland/Applications/Browser/Browser.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+
+namespace Browser {
+
+extern String g_home_url;
+
+}
diff --git a/Userland/Applications/Browser/BrowserConsoleClient.cpp b/Userland/Applications/Browser/BrowserConsoleClient.cpp
new file mode 100644
index 0000000000..ce1dd7e5cb
--- /dev/null
+++ b/Userland/Applications/Browser/BrowserConsoleClient.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BrowserConsoleClient.h"
+#include "ConsoleWidget.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/TextBox.h>
+#include <LibWeb/DOM/DocumentType.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/DOMTreeModel.h>
+#include <LibWeb/HTML/HTMLBodyElement.h>
+
+namespace Browser {
+
+JS::Value BrowserConsoleClient::log()
+{
+ m_console_widget.print_html(vm().join_arguments());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::info()
+{
+ StringBuilder html;
+ html.append("<span class=\"info\">");
+ html.append("(i) ");
+ html.append(vm().join_arguments());
+ html.append("</span>");
+ m_console_widget.print_html(html.string_view());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::debug()
+{
+ StringBuilder html;
+ html.append("<span class=\"debug\">");
+ html.append("(d) ");
+ html.append(vm().join_arguments());
+ html.append("</span>");
+ m_console_widget.print_html(html.string_view());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::warn()
+{
+ StringBuilder html;
+ html.append("<span class=\"warn\">");
+ html.append("(w) ");
+ html.append(vm().join_arguments());
+ html.append("</span>");
+ m_console_widget.print_html(html.string_view());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::error()
+{
+ StringBuilder html;
+ html.append("<span class=\"error\">");
+ html.append("(e) ");
+ html.append(vm().join_arguments());
+ html.append("</span>");
+ m_console_widget.print_html(html.string_view());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::clear()
+{
+ m_console_widget.clear_output();
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::trace()
+{
+ StringBuilder html;
+ html.append(vm().join_arguments());
+ auto trace = get_trace();
+ for (auto& function_name : trace) {
+ if (function_name.is_empty())
+ function_name = "&lt;anonymous&gt;";
+ html.appendff(" -> {}<br>", function_name);
+ }
+ m_console_widget.print_html(html.string_view());
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::count()
+{
+ auto label = vm().argument_count() ? vm().argument(0).to_string_without_side_effects() : "default";
+ auto counter_value = m_console.counter_increment(label);
+ m_console_widget.print_html(String::formatted("{}: {}", label, counter_value));
+ return JS::js_undefined();
+}
+
+JS::Value BrowserConsoleClient::count_reset()
+{
+ auto label = vm().argument_count() ? vm().argument(0).to_string_without_side_effects() : "default";
+ if (m_console.counter_reset(label)) {
+ m_console_widget.print_html(String::formatted("{}: 0", label));
+ } else {
+ m_console_widget.print_html(String::formatted("\"{}\" doesn't have a count", label));
+ }
+ return JS::js_undefined();
+}
+
+}
diff --git a/Userland/Applications/Browser/BrowserConsoleClient.h b/Userland/Applications/Browser/BrowserConsoleClient.h
new file mode 100644
index 0000000000..e87f6e32b0
--- /dev/null
+++ b/Userland/Applications/Browser/BrowserConsoleClient.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+#include <LibJS/Console.h>
+#include <LibJS/Forward.h>
+#include <LibWeb/InProcessWebView.h>
+
+namespace Browser {
+
+class ConsoleWidget;
+
+class BrowserConsoleClient final : public JS::ConsoleClient {
+public:
+ BrowserConsoleClient(JS::Console& console, ConsoleWidget& console_widget)
+ : ConsoleClient(console)
+ , m_console_widget(console_widget)
+ {
+ }
+
+private:
+ virtual JS::Value log() override;
+ virtual JS::Value info() override;
+ virtual JS::Value debug() override;
+ virtual JS::Value warn() override;
+ virtual JS::Value error() override;
+ virtual JS::Value clear() override;
+ virtual JS::Value trace() override;
+ virtual JS::Value count() override;
+ virtual JS::Value count_reset() override;
+
+ ConsoleWidget& m_console_widget;
+};
+
+}
diff --git a/Userland/Applications/Browser/BrowserWindow.gml b/Userland/Applications/Browser/BrowserWindow.gml
new file mode 100644
index 0000000000..f6ec7ef1af
--- /dev/null
+++ b/Userland/Applications/Browser/BrowserWindow.gml
@@ -0,0 +1,15 @@
+@GUI::Widget {
+ name: "browser"
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ spacing: 2
+ }
+
+ @GUI::TabWidget {
+ name: "tab_widget"
+ container_padding: 0
+ uniform_tabs: true
+ text_alignment: "CenterLeft"
+ }
+}
diff --git a/Userland/Applications/Browser/CMakeLists.txt b/Userland/Applications/Browser/CMakeLists.txt
new file mode 100644
index 0000000000..4652ec8b11
--- /dev/null
+++ b/Userland/Applications/Browser/CMakeLists.txt
@@ -0,0 +1,19 @@
+compile_gml(BrowserWindow.gml BrowserWindowGML.h browser_window_gml)
+compile_gml(Tab.gml TabGML.h tab_gml)
+
+set(SOURCES
+ BookmarksBarWidget.cpp
+ BrowserConsoleClient.cpp
+ ConsoleWidget.cpp
+ DownloadWidget.cpp
+ History.cpp
+ InspectorWidget.cpp
+ main.cpp
+ Tab.cpp
+ WindowActions.cpp
+ BrowserWindowGML.h
+ TabGML.h
+)
+
+serenity_app(Browser ICON app-browser)
+target_link_libraries(Browser LibWeb LibProtocol LibGUI LibDesktop)
diff --git a/Userland/Applications/Browser/ConsoleWidget.cpp b/Userland/Applications/Browser/ConsoleWidget.cpp
new file mode 100644
index 0000000000..55c545dc55
--- /dev/null
+++ b/Userland/Applications/Browser/ConsoleWidget.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ConsoleWidget.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibJS/Interpreter.h>
+#include <LibJS/MarkupGenerator.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/Error.h>
+#include <LibWeb/DOM/DocumentType.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/DOMTreeModel.h>
+#include <LibWeb/HTML/HTMLBodyElement.h>
+
+namespace Browser {
+
+ConsoleWidget::ConsoleWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ set_fill_with_background_color(true);
+
+ auto base_document = Web::DOM::Document::create();
+ base_document->append_child(adopt(*new Web::DOM::DocumentType(base_document)));
+ auto html_element = base_document->create_element("html");
+ base_document->append_child(html_element);
+ auto head_element = base_document->create_element("head");
+ html_element->append_child(head_element);
+ auto body_element = base_document->create_element("body");
+ html_element->append_child(body_element);
+ m_output_container = body_element;
+
+ m_output_view = add<Web::InProcessWebView>();
+ m_output_view->set_document(base_document);
+
+ auto& bottom_container = add<GUI::Widget>();
+ bottom_container.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_container.set_fixed_height(22);
+
+ m_input = bottom_container.add<GUI::TextBox>();
+ m_input->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
+ // FIXME: Syntax Highlighting breaks the cursor's position on non fixed-width fonts.
+ m_input->set_font(Gfx::FontDatabase::default_fixed_width_font());
+ m_input->set_history_enabled(true);
+
+ m_input->on_return_pressed = [this] {
+ auto js_source = m_input->text();
+
+ // FIXME: An is_blank check to check if there is only whitespace would probably be preferable.
+ if (js_source.is_empty())
+ return;
+
+ m_input->add_current_text_to_history();
+ m_input->clear();
+
+ print_source_line(js_source);
+
+ auto parser = JS::Parser(JS::Lexer(js_source));
+ auto program = parser.parse_program();
+
+ StringBuilder output_html;
+ if (parser.has_errors()) {
+ auto error = parser.errors()[0];
+ auto hint = error.source_location_hint(js_source);
+ if (!hint.is_empty())
+ output_html.append(String::formatted("<pre>{}</pre>", escape_html_entities(hint)));
+ m_interpreter->vm().throw_exception<JS::SyntaxError>(m_interpreter->global_object(), error.to_string());
+ } else {
+ m_interpreter->run(m_interpreter->global_object(), *program);
+ }
+
+ if (m_interpreter->exception()) {
+ output_html.append("Uncaught exception: ");
+ output_html.append(JS::MarkupGenerator::html_from_value(m_interpreter->exception()->value()));
+ print_html(output_html.string_view());
+
+ m_interpreter->vm().clear_exception();
+ return;
+ }
+
+ print_html(JS::MarkupGenerator::html_from_value(m_interpreter->vm().last_value()));
+ };
+
+ set_focus_proxy(m_input);
+
+ auto& clear_button = bottom_container.add<GUI::Button>();
+ clear_button.set_fixed_size(22, 22);
+ clear_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"));
+ clear_button.set_tooltip("Clear the console output");
+ clear_button.on_click = [this](auto) {
+ clear_output();
+ };
+}
+
+ConsoleWidget::~ConsoleWidget()
+{
+}
+
+void ConsoleWidget::set_interpreter(WeakPtr<JS::Interpreter> interpreter)
+{
+ if (m_interpreter.ptr() == interpreter.ptr())
+ return;
+
+ m_interpreter = interpreter;
+ m_console_client = make<BrowserConsoleClient>(interpreter->global_object().console(), *this);
+ interpreter->global_object().console().set_client(*m_console_client.ptr());
+
+ clear_output();
+}
+
+void ConsoleWidget::print_source_line(const StringView& source)
+{
+ StringBuilder html;
+ html.append("<span class=\"repl-indicator\">");
+ html.append("&gt; ");
+ html.append("</span>");
+
+ html.append(JS::MarkupGenerator::html_from_source(source));
+
+ print_html(html.string_view());
+}
+
+void ConsoleWidget::print_html(const StringView& line)
+{
+ auto paragraph = m_output_container->document().create_element("p");
+ paragraph->set_inner_html(line);
+
+ m_output_container->append_child(paragraph);
+ m_output_container->document().invalidate_layout();
+ m_output_container->document().update_layout();
+
+ m_output_view->scroll_to_bottom();
+}
+
+void ConsoleWidget::clear_output()
+{
+ m_output_container->remove_all_children();
+ m_output_view->update();
+}
+
+}
diff --git a/Userland/Applications/Browser/ConsoleWidget.h b/Userland/Applications/Browser/ConsoleWidget.h
new file mode 100644
index 0000000000..813ceed2ed
--- /dev/null
+++ b/Userland/Applications/Browser/ConsoleWidget.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "BrowserConsoleClient.h"
+#include "History.h"
+#include <LibGUI/Widget.h>
+#include <LibJS/Forward.h>
+#include <LibWeb/InProcessWebView.h>
+
+namespace Browser {
+
+class ConsoleWidget final : public GUI::Widget {
+ C_OBJECT(ConsoleWidget)
+public:
+ virtual ~ConsoleWidget();
+
+ void set_interpreter(WeakPtr<JS::Interpreter>);
+ void print_source_line(const StringView&);
+ void print_html(const StringView&);
+ void clear_output();
+
+private:
+ ConsoleWidget();
+
+ RefPtr<GUI::TextBox> m_input;
+ RefPtr<Web::InProcessWebView> m_output_view;
+ RefPtr<Web::DOM::Element> m_output_container;
+ WeakPtr<JS::Interpreter> m_interpreter;
+ OwnPtr<BrowserConsoleClient> m_console_client;
+};
+
+}
diff --git a/Userland/Applications/Browser/DownloadWidget.cpp b/Userland/Applications/Browser/DownloadWidget.cpp
new file mode 100644
index 0000000000..39d931e216
--- /dev/null
+++ b/Userland/Applications/Browser/DownloadWidget.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DownloadWidget.h"
+#include <AK/NumberFormat.h>
+#include <AK/SharedBuffer.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/File.h>
+#include <LibCore/FileStream.h>
+#include <LibCore/StandardPaths.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/ImageWidget.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/ProgressBar.h>
+#include <LibGUI/Window.h>
+#include <LibProtocol/Client.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+#include <math.h>
+
+namespace Browser {
+
+DownloadWidget::DownloadWidget(const URL& url)
+ : m_url(url)
+{
+ {
+ StringBuilder builder;
+ builder.append(Core::StandardPaths::downloads_directory());
+ builder.append('/');
+ builder.append(m_url.basename());
+ m_destination_path = builder.to_string();
+ }
+
+ m_elapsed_timer.start();
+ m_download = Web::ResourceLoader::the().protocol_client().start_download("GET", url.to_string());
+ ASSERT(m_download);
+ m_download->on_progress = [this](Optional<u32> total_size, u32 downloaded_size) {
+ did_progress(total_size.value(), downloaded_size);
+ };
+
+ {
+ auto file_or_error = Core::File::open(m_destination_path, Core::IODevice::WriteOnly);
+ if (file_or_error.is_error()) {
+ GUI::MessageBox::show(window(), String::formatted("Cannot open {} for writing", m_destination_path), "Download failed", GUI::MessageBox::Type::Error);
+ window()->close();
+ return;
+ }
+ m_output_file_stream = make<Core::OutputFileStream>(*file_or_error.value());
+ }
+
+ m_download->on_finish = [this](bool success, auto) { did_finish(success); };
+ m_download->stream_into(*m_output_file_stream);
+
+ set_fill_with_background_color(true);
+ auto& layout = set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& animation_container = add<GUI::Widget>();
+ animation_container.set_fixed_height(32);
+ auto& animation_layout = animation_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& browser_image = animation_container.add<GUI::ImageWidget>();
+ browser_image.load_from_file("/res/graphics/download-animation.gif");
+ animation_layout.add_spacer();
+
+ auto& source_label = add<GUI::Label>(String::formatted("From: {}", url));
+ source_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ source_label.set_fixed_height(16);
+
+ m_progress_bar = add<GUI::ProgressBar>();
+ m_progress_bar->set_fixed_height(20);
+
+ m_progress_label = add<GUI::Label>();
+ m_progress_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ m_progress_label->set_fixed_height(16);
+
+ auto& destination_label = add<GUI::Label>(String::formatted("To: {}", m_destination_path));
+ destination_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ destination_label.set_fixed_height(16);
+
+ auto& button_container = add<GUI::Widget>();
+ auto& button_container_layout = button_container.set_layout<GUI::HorizontalBoxLayout>();
+ button_container_layout.add_spacer();
+ m_cancel_button = button_container.add<GUI::Button>("Cancel");
+ m_cancel_button->set_fixed_size(100, 22);
+ m_cancel_button->on_click = [this](auto) {
+ bool success = m_download->stop();
+ ASSERT(success);
+ window()->close();
+ };
+
+ m_close_button = button_container.add<GUI::Button>("OK");
+ m_close_button->set_enabled(false);
+ m_close_button->set_fixed_size(100, 22);
+ m_close_button->on_click = [this](auto) {
+ window()->close();
+ };
+}
+
+DownloadWidget::~DownloadWidget()
+{
+}
+
+void DownloadWidget::did_progress(Optional<u32> total_size, u32 downloaded_size)
+{
+ m_progress_bar->set_min(0);
+ if (total_size.has_value()) {
+ int percent = roundf(((float)downloaded_size / (float)total_size.value()) * 100.0f);
+ window()->set_progress(percent);
+ m_progress_bar->set_max(total_size.value());
+ } else {
+ m_progress_bar->set_max(0);
+ }
+ m_progress_bar->set_value(downloaded_size);
+
+ {
+ StringBuilder builder;
+ builder.append("Downloaded ");
+ builder.append(human_readable_size(downloaded_size));
+ builder.appendff(" in {} sec", m_elapsed_timer.elapsed() / 1000);
+ m_progress_label->set_text(builder.to_string());
+ }
+
+ {
+ StringBuilder builder;
+ if (total_size.has_value()) {
+ int percent = roundf(((float)downloaded_size / (float)total_size.value()) * 100);
+ builder.appendff("{}%", percent);
+ } else {
+ builder.append(human_readable_size(downloaded_size));
+ }
+ builder.append(" of ");
+ builder.append(m_url.basename());
+ window()->set_title(builder.to_string());
+ }
+}
+
+void DownloadWidget::did_finish(bool success)
+{
+ dbgln("did_finish, success={}", success);
+
+ m_close_button->set_enabled(true);
+ m_cancel_button->set_text("Open in Folder");
+ m_cancel_button->on_click = [this](auto) {
+ Desktop::Launcher::open(URL::create_with_file_protocol(Core::StandardPaths::downloads_directory()));
+ window()->close();
+ };
+ m_cancel_button->update();
+
+ if (!success) {
+ GUI::MessageBox::show(window(), String::formatted("Download failed for some reason"), "Download failed", GUI::MessageBox::Type::Error);
+ window()->close();
+ return;
+ }
+}
+
+}
diff --git a/Userland/Applications/Browser/DownloadWidget.h b/Userland/Applications/Browser/DownloadWidget.h
new file mode 100644
index 0000000000..44fc21c60d
--- /dev/null
+++ b/Userland/Applications/Browser/DownloadWidget.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/URL.h>
+#include <LibCore/ElapsedTimer.h>
+#include <LibCore/FileStream.h>
+#include <LibGUI/ProgressBar.h>
+#include <LibGUI/Widget.h>
+#include <LibProtocol/Download.h>
+
+namespace Browser {
+
+class DownloadWidget final : public GUI::Widget {
+ C_OBJECT(DownloadWidget);
+
+public:
+ virtual ~DownloadWidget() override;
+
+private:
+ explicit DownloadWidget(const URL&);
+
+ void did_progress(Optional<u32> total_size, u32 downloaded_size);
+ void did_finish(bool success);
+
+ URL m_url;
+ String m_destination_path;
+ RefPtr<Protocol::Download> m_download;
+ RefPtr<GUI::ProgressBar> m_progress_bar;
+ RefPtr<GUI::Label> m_progress_label;
+ RefPtr<GUI::Button> m_cancel_button;
+ RefPtr<GUI::Button> m_close_button;
+ OwnPtr<Core::OutputFileStream> m_output_file_stream;
+ Core::ElapsedTimer m_elapsed_timer;
+};
+
+}
diff --git a/Userland/Applications/Browser/History.cpp b/Userland/Applications/Browser/History.cpp
new file mode 100644
index 0000000000..cecf28477c
--- /dev/null
+++ b/Userland/Applications/Browser/History.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "History.h"
+
+namespace Browser {
+
+void History::dump() const
+{
+ dbgln("Dump {} items(s)", m_items.size());
+ int i = 0;
+ for (auto& item : m_items) {
+ dbgln("[{}] {} {}", i, item, m_current == i ? '*' : ' ');
+ ++i;
+ }
+}
+
+void History::push(const URL& url)
+{
+ m_items.shrink(m_current + 1);
+ m_items.append(url);
+ m_current++;
+}
+
+URL History::current() const
+{
+ if (m_current == -1)
+ return {};
+ return m_items[m_current];
+}
+
+void History::go_back()
+{
+ ASSERT(can_go_back());
+ m_current--;
+}
+
+void History::go_forward()
+{
+ ASSERT(can_go_forward());
+ m_current++;
+}
+
+void History::clear()
+{
+ m_items = {};
+ m_current = -1;
+}
+
+}
diff --git a/Userland/Applications/Browser/History.h b/Userland/Applications/Browser/History.h
new file mode 100644
index 0000000000..84fc4151d7
--- /dev/null
+++ b/Userland/Applications/Browser/History.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/URL.h>
+#include <AK/Vector.h>
+
+namespace Browser {
+
+class History {
+public:
+ void dump() const;
+
+ void push(const URL&);
+ URL current() const;
+
+ void go_back();
+ void go_forward();
+
+ bool can_go_back() { return m_current > 0; }
+ bool can_go_forward() { return m_current + 1 < static_cast<int>(m_items.size()); }
+
+ void clear();
+
+private:
+ Vector<URL> m_items;
+ int m_current { -1 };
+};
+
+}
diff --git a/Userland/Applications/Browser/InspectorWidget.cpp b/Userland/Applications/Browser/InspectorWidget.cpp
new file mode 100644
index 0000000000..7e9111e513
--- /dev/null
+++ b/Userland/Applications/Browser/InspectorWidget.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "InspectorWidget.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/TreeView.h>
+#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/Element.h>
+#include <LibWeb/DOMTreeModel.h>
+#include <LibWeb/LayoutTreeModel.h>
+#include <LibWeb/StylePropertiesModel.h>
+
+namespace Browser {
+
+void InspectorWidget::set_inspected_node(Web::DOM::Node* node)
+{
+ m_document->set_inspected_node(node);
+ if (node && node->is_element()) {
+ auto& element = downcast<Web::DOM::Element>(*node);
+ if (element.specified_css_values()) {
+ m_style_table_view->set_model(Web::StylePropertiesModel::create(*element.specified_css_values()));
+ m_computed_style_table_view->set_model(Web::StylePropertiesModel::create(*element.computed_style()));
+ }
+ } else {
+ m_style_table_view->set_model(nullptr);
+ m_computed_style_table_view->set_model(nullptr);
+ }
+}
+
+InspectorWidget::InspectorWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ auto& splitter = add<GUI::VerticalSplitter>();
+
+ auto& top_tab_widget = splitter.add<GUI::TabWidget>();
+
+ m_dom_tree_view = top_tab_widget.add_tab<GUI::TreeView>("DOM");
+ m_dom_tree_view->on_selection = [this](auto& index) {
+ auto* node = static_cast<Web::DOM::Node*>(index.internal_data());
+ set_inspected_node(node);
+ };
+
+ m_layout_tree_view = top_tab_widget.add_tab<GUI::TreeView>("Layout");
+ m_layout_tree_view->on_selection = [this](auto& index) {
+ auto* node = static_cast<Web::Layout::Node*>(index.internal_data());
+ set_inspected_node(node->dom_node());
+ };
+
+ auto& bottom_tab_widget = splitter.add<GUI::TabWidget>();
+
+ m_style_table_view = bottom_tab_widget.add_tab<GUI::TableView>("Styles");
+ m_computed_style_table_view = bottom_tab_widget.add_tab<GUI::TableView>("Computed");
+}
+
+InspectorWidget::~InspectorWidget()
+{
+}
+
+void InspectorWidget::set_document(Web::DOM::Document* document)
+{
+ if (m_document == document)
+ return;
+ m_document = document;
+ m_dom_tree_view->set_model(Web::DOMTreeModel::create(*document));
+ m_layout_tree_view->set_model(Web::LayoutTreeModel::create(*document));
+}
+
+}
diff --git a/Userland/Applications/Browser/InspectorWidget.h b/Userland/Applications/Browser/InspectorWidget.h
new file mode 100644
index 0000000000..37f0f95754
--- /dev/null
+++ b/Userland/Applications/Browser/InspectorWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+#include <LibWeb/Forward.h>
+
+namespace Browser {
+
+class InspectorWidget final : public GUI::Widget {
+ C_OBJECT(InspectorWidget)
+public:
+ virtual ~InspectorWidget();
+
+ void set_document(Web::DOM::Document*);
+
+private:
+ InspectorWidget();
+
+ void set_inspected_node(Web::DOM::Node*);
+
+ RefPtr<GUI::TreeView> m_dom_tree_view;
+ RefPtr<GUI::TreeView> m_layout_tree_view;
+ RefPtr<GUI::TableView> m_style_table_view;
+ RefPtr<GUI::TableView> m_computed_style_table_view;
+ RefPtr<Web::DOM::Document> m_document;
+};
+
+}
diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp
new file mode 100644
index 0000000000..ea23d61d73
--- /dev/null
+++ b/Userland/Applications/Browser/Tab.cpp
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Tab.h"
+#include "BookmarksBarWidget.h"
+#include "Browser.h"
+#include "ConsoleWidget.h"
+#include "DownloadWidget.h"
+#include "InspectorWidget.h"
+#include "WindowActions.h"
+#include <AK/StringBuilder.h>
+#include <Applications/Browser/TabGML.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/Window.h>
+#include <LibJS/Interpreter.h>
+#include <LibWeb/CSS/Parser/CSSParser.h>
+#include <LibWeb/DOM/Element.h>
+#include <LibWeb/DOMTreeModel.h>
+#include <LibWeb/Dump.h>
+#include <LibWeb/InProcessWebView.h>
+#include <LibWeb/Layout/BlockBox.h>
+#include <LibWeb/Layout/InitialContainingBlockBox.h>
+#include <LibWeb/Layout/InlineNode.h>
+#include <LibWeb/Layout/Node.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+#include <LibWeb/OutOfProcessWebView.h>
+#include <LibWeb/Page/Frame.h>
+
+namespace Browser {
+
+URL url_from_user_input(const String& input)
+{
+ auto url = URL(input);
+ if (url.is_valid())
+ return url;
+
+ StringBuilder builder;
+ builder.append("http://");
+ builder.append(input);
+ return URL(builder.build());
+}
+
+static void start_download(const URL& url)
+{
+ auto window = GUI::Window::construct();
+ window->resize(300, 150);
+ window->set_title(String::formatted("0% of {}", url.basename()));
+ window->set_resizable(false);
+ window->set_main_widget<DownloadWidget>(url);
+ window->show();
+ [[maybe_unused]] auto& unused = window.leak_ref();
+}
+
+Tab::Tab(Type type)
+ : m_type(type)
+{
+ load_from_gml(tab_gml);
+
+ m_toolbar_container = *find_descendant_of_type_named<GUI::ToolBarContainer>("toolbar_container");
+ auto& toolbar = *find_descendant_of_type_named<GUI::ToolBar>("toolbar");
+
+ auto& webview_container = *find_descendant_of_type_named<GUI::Widget>("webview_container");
+
+ if (m_type == Type::InProcessWebView)
+ m_page_view = webview_container.add<Web::InProcessWebView>();
+ else
+ m_web_content_view = webview_container.add<Web::OutOfProcessWebView>();
+
+ m_go_back_action = GUI::CommonActions::make_go_back_action([this](auto&) { go_back(); }, this);
+ m_go_forward_action = GUI::CommonActions::make_go_forward_action([this](auto&) { go_forward(); }, this);
+
+ toolbar.add_action(*m_go_back_action);
+ toolbar.add_action(*m_go_forward_action);
+
+ toolbar.add_action(GUI::CommonActions::make_go_home_action([this](auto&) { load(g_home_url); }, this));
+ m_reload_action = GUI::CommonActions::make_reload_action([this](auto&) { reload(); }, this);
+
+ toolbar.add_action(*m_reload_action);
+
+ m_location_box = toolbar.add<GUI::TextBox>();
+ m_location_box->set_placeholder("Address");
+
+ m_location_box->on_return_pressed = [this] {
+ auto url = url_from_user_input(m_location_box->text());
+ load(url);
+ view().set_focus(true);
+ };
+
+ m_location_box->add_custom_context_menu_action(GUI::Action::create("Paste & Go", [this](auto&) {
+ m_location_box->set_text(GUI::Clipboard::the().data());
+ m_location_box->on_return_pressed();
+ }));
+
+ m_bookmark_button = toolbar.add<GUI::Button>();
+ m_bookmark_button->set_button_style(Gfx::ButtonStyle::CoolBar);
+ m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-contour.png"));
+ m_bookmark_button->set_fixed_size(22, 22);
+
+ m_bookmark_button->on_click = [this](auto) {
+ auto url = this->url().to_string();
+ if (BookmarksBarWidget::the().contains_bookmark(url)) {
+ BookmarksBarWidget::the().remove_bookmark(url);
+ } else {
+ BookmarksBarWidget::the().add_bookmark(url, m_title);
+ }
+ update_bookmark_button(url);
+ };
+
+ hooks().on_load_start = [this](auto& url) {
+ m_location_box->set_icon(nullptr);
+ m_location_box->set_text(url.to_string());
+
+ // don't add to history if back or forward is pressed
+ if (!m_is_history_navigation)
+ m_history.push(url);
+ m_is_history_navigation = false;
+
+ update_actions();
+ update_bookmark_button(url.to_string());
+ };
+
+ hooks().on_link_click = [this](auto& url, auto& target, unsigned modifiers) {
+ if (target == "_blank" || modifiers == Mod_Ctrl) {
+ on_tab_open_request(url);
+ } else {
+ load(url);
+ }
+ };
+
+ m_link_context_menu = GUI::Menu::construct();
+ auto link_default_action = GUI::Action::create("Open", [this](auto&) {
+ hooks().on_link_click(m_link_context_menu_url, "", 0);
+ });
+ m_link_context_menu->add_action(link_default_action);
+ m_link_context_menu_default_action = link_default_action;
+ m_link_context_menu->add_action(GUI::Action::create("Open in new tab", [this](auto&) {
+ hooks().on_link_click(m_link_context_menu_url, "_blank", 0);
+ }));
+ m_link_context_menu->add_separator();
+ m_link_context_menu->add_action(GUI::Action::create("Copy link", [this](auto&) {
+ GUI::Clipboard::the().set_plain_text(m_link_context_menu_url.to_string());
+ }));
+ m_link_context_menu->add_separator();
+ m_link_context_menu->add_action(GUI::Action::create("Download", [this](auto&) {
+ start_download(m_link_context_menu_url);
+ }));
+
+ hooks().on_link_context_menu_request = [this](auto& url, auto& screen_position) {
+ m_link_context_menu_url = url;
+ m_link_context_menu->popup(screen_position, m_link_context_menu_default_action);
+ };
+
+ m_image_context_menu = GUI::Menu::construct();
+ m_image_context_menu->add_action(GUI::Action::create("Open image", [this](auto&) {
+ hooks().on_link_click(m_image_context_menu_url, "", 0);
+ }));
+ m_image_context_menu->add_action(GUI::Action::create("Open image in new tab", [this](auto&) {
+ hooks().on_link_click(m_image_context_menu_url, "_blank", 0);
+ }));
+ m_image_context_menu->add_separator();
+ m_image_context_menu->add_action(GUI::Action::create("Copy image", [this](auto&) {
+ if (m_image_context_menu_bitmap.is_valid())
+ GUI::Clipboard::the().set_bitmap(*m_image_context_menu_bitmap.bitmap());
+ }));
+ m_image_context_menu->add_action(GUI::Action::create("Copy image URL", [this](auto&) {
+ GUI::Clipboard::the().set_plain_text(m_image_context_menu_url.to_string());
+ }));
+ m_image_context_menu->add_separator();
+ m_image_context_menu->add_action(GUI::Action::create("Download", [this](auto&) {
+ start_download(m_image_context_menu_url);
+ }));
+
+ hooks().on_image_context_menu_request = [this](auto& image_url, auto& screen_position, const Gfx::ShareableBitmap& shareable_bitmap) {
+ m_image_context_menu_url = image_url;
+ m_image_context_menu_bitmap = shareable_bitmap;
+ m_image_context_menu->popup(screen_position);
+ };
+
+ hooks().on_link_middle_click = [this](auto& href, auto&, auto) {
+ hooks().on_link_click(href, "_blank", 0);
+ };
+
+ hooks().on_title_change = [this](auto& title) {
+ if (title.is_null()) {
+ m_title = url().to_string();
+ } else {
+ m_title = title;
+ }
+ if (on_title_change)
+ on_title_change(m_title);
+ };
+
+ hooks().on_favicon_change = [this](auto& icon) {
+ m_icon = icon;
+ m_location_box->set_icon(&icon);
+ if (on_favicon_change)
+ on_favicon_change(icon);
+ };
+
+ // FIXME: Support JS console in multi-process mode.
+ if (m_type == Type::InProcessWebView) {
+ hooks().on_set_document = [this](auto* document) {
+ if (document && m_console_window) {
+ auto* console_widget = static_cast<ConsoleWidget*>(m_console_window->main_widget());
+ console_widget->set_interpreter(document->interpreter().make_weak_ptr());
+ }
+ };
+ }
+
+ auto focus_location_box_action = GUI::Action::create(
+ "Focus location box", { Mod_Ctrl, Key_L }, [this](auto&) {
+ m_location_box->select_all();
+ m_location_box->set_focus(true);
+ },
+ this);
+
+ m_statusbar = *find_descendant_of_type_named<GUI::StatusBar>("statusbar");
+
+ hooks().on_link_hover = [this](auto& url) {
+ if (url.is_valid())
+ m_statusbar->set_text(url.to_string());
+ else
+ m_statusbar->set_text("");
+ };
+
+ hooks().on_url_drop = [this](auto& url) {
+ load(url);
+ };
+
+ m_menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = m_menubar->add_menu("Browser");
+ app_menu.add_action(WindowActions::the().create_new_tab_action());
+ app_menu.add_action(GUI::Action::create(
+ "Close tab", { Mod_Ctrl, Key_W }, Gfx::Bitmap::load_from_file("/res/icons/16x16/close-tab.png"), [this](auto&) {
+ on_tab_close_request(*this);
+ },
+ this));
+
+ app_menu.add_action(*m_reload_action);
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& view_menu = m_menubar->add_menu("View");
+ view_menu.add_action(GUI::CommonActions::make_fullscreen_action(
+ [this](auto&) {
+ window()->set_fullscreen(!window()->is_fullscreen());
+
+ auto is_fullscreen = window()->is_fullscreen();
+ auto* tab_widget = static_cast<GUI::TabWidget*>(parent_widget());
+ tab_widget->set_bar_visible(!is_fullscreen && tab_widget->children().size() > 1);
+ m_toolbar_container->set_visible(!is_fullscreen);
+ m_statusbar->set_visible(!is_fullscreen);
+ },
+ this));
+
+ auto view_source_action = GUI::Action::create(
+ "View source", { Mod_Ctrl, Key_U }, [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ ASSERT(m_page_view->document());
+ auto url = m_page_view->document()->url().to_string();
+ auto source = m_page_view->document()->source();
+ auto window = GUI::Window::construct();
+ auto& editor = window->set_main_widget<GUI::TextEditor>();
+ editor.set_text(source);
+ editor.set_mode(GUI::TextEditor::ReadOnly);
+ editor.set_ruler_visible(true);
+ window->resize(640, 480);
+ window->set_title(url);
+ window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-text.png"));
+ window->show();
+ [[maybe_unused]] auto& unused = window.leak_ref();
+ } else {
+ TODO();
+ }
+ },
+ this);
+
+ auto inspect_dom_tree_action = GUI::Action::create(
+ "Inspect DOM tree", { Mod_None, Key_F12 }, [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ if (!m_dom_inspector_window) {
+ m_dom_inspector_window = GUI::Window::construct();
+ m_dom_inspector_window->resize(300, 500);
+ m_dom_inspector_window->set_title("DOM inspector");
+ m_dom_inspector_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
+ m_dom_inspector_window->set_main_widget<InspectorWidget>();
+ }
+ auto* inspector_widget = static_cast<InspectorWidget*>(m_dom_inspector_window->main_widget());
+ inspector_widget->set_document(m_page_view->document());
+ m_dom_inspector_window->show();
+ m_dom_inspector_window->move_to_front();
+ } else {
+ TODO();
+ }
+ },
+ this);
+
+ auto& inspect_menu = m_menubar->add_menu("Inspect");
+ inspect_menu.add_action(*view_source_action);
+ inspect_menu.add_action(*inspect_dom_tree_action);
+
+ inspect_menu.add_action(GUI::Action::create(
+ "Open JS Console", { Mod_Ctrl, Key_I }, [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ if (!m_console_window) {
+ m_console_window = GUI::Window::construct();
+ m_console_window->resize(500, 300);
+ m_console_window->set_title("JS Console");
+ m_console_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-javascript.png"));
+ m_console_window->set_main_widget<ConsoleWidget>();
+ }
+ auto* console_widget = static_cast<ConsoleWidget*>(m_console_window->main_widget());
+ console_widget->set_interpreter(m_page_view->document()->interpreter().make_weak_ptr());
+ m_console_window->show();
+ m_console_window->move_to_front();
+ } else {
+ TODO();
+ }
+ },
+ this));
+
+ auto& debug_menu = m_menubar->add_menu("Debug");
+ debug_menu.add_action(GUI::Action::create(
+ "Dump DOM tree", [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ Web::dump_tree(*m_page_view->document());
+ } else {
+ TODO();
+ }
+ },
+ this));
+ debug_menu.add_action(GUI::Action::create(
+ "Dump Layout tree", [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ Web::dump_tree(*m_page_view->document()->layout_node());
+ } else {
+ TODO();
+ }
+ },
+ this));
+ debug_menu.add_action(GUI::Action::create(
+ "Dump Style sheets", [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ for (auto& sheet : m_page_view->document()->style_sheets().sheets()) {
+ Web::dump_sheet(sheet);
+ }
+ } else {
+ TODO();
+ }
+ },
+ this));
+ debug_menu.add_action(GUI::Action::create("Dump history", { Mod_Ctrl, Key_H }, [&](auto&) {
+ m_history.dump();
+ }));
+ debug_menu.add_separator();
+ auto line_box_borders_action = GUI::Action::create_checkable(
+ "Line box borders", [this](auto& action) {
+ if (m_type == Type::InProcessWebView) {
+ m_page_view->set_should_show_line_box_borders(action.is_checked());
+ m_page_view->update();
+ } else {
+ TODO();
+ }
+ },
+ this);
+ line_box_borders_action->set_checked(false);
+ debug_menu.add_action(line_box_borders_action);
+
+ debug_menu.add_separator();
+ debug_menu.add_action(GUI::Action::create("Collect garbage", { Mod_Ctrl | Mod_Shift, Key_G }, [this](auto&) {
+ if (m_type == Type::InProcessWebView) {
+ if (auto* document = m_page_view->document()) {
+ document->interpreter().heap().collect_garbage(JS::Heap::CollectionType::CollectGarbage, true);
+ }
+ } else {
+ TODO();
+ }
+ }));
+
+ auto& bookmarks_menu = m_menubar->add_menu("Bookmarks");
+ bookmarks_menu.add_action(WindowActions::the().show_bookmarks_bar_action());
+
+ auto& help_menu = m_menubar->add_menu("Help");
+ help_menu.add_action(WindowActions::the().about_action());
+
+ m_tab_context_menu = GUI::Menu::construct();
+ m_tab_context_menu->add_action(GUI::Action::create("Reload Tab", [this](auto&) {
+ m_reload_action->activate();
+ }));
+ m_tab_context_menu->add_action(GUI::Action::create("Close Tab", [this](auto&) {
+ on_tab_close_request(*this);
+ }));
+
+ m_page_context_menu = GUI::Menu::construct();
+ m_page_context_menu->add_action(*m_go_back_action);
+ m_page_context_menu->add_action(*m_go_forward_action);
+ m_page_context_menu->add_action(*m_reload_action);
+ m_page_context_menu->add_separator();
+ m_page_context_menu->add_action(*view_source_action);
+ m_page_context_menu->add_action(*inspect_dom_tree_action);
+ hooks().on_context_menu_request = [&](auto& screen_position) {
+ m_page_context_menu->popup(screen_position);
+ };
+}
+
+Tab::~Tab()
+{
+}
+
+void Tab::load(const URL& url, LoadType load_type)
+{
+ m_is_history_navigation = (load_type == LoadType::HistoryNavigation);
+
+ if (m_type == Type::InProcessWebView)
+ m_page_view->load(url);
+ else
+ m_web_content_view->load(url);
+}
+
+URL Tab::url() const
+{
+ if (m_type == Type::InProcessWebView)
+ return m_page_view->url();
+ return m_web_content_view->url();
+}
+
+void Tab::reload()
+{
+ load(url());
+}
+
+void Tab::go_back()
+{
+ m_history.go_back();
+ update_actions();
+ load(m_history.current(), LoadType::HistoryNavigation);
+}
+
+void Tab::go_forward()
+{
+ m_history.go_forward();
+ update_actions();
+ load(m_history.current(), LoadType::HistoryNavigation);
+}
+
+void Tab::update_actions()
+{
+ m_go_back_action->set_enabled(m_history.can_go_back());
+ m_go_forward_action->set_enabled(m_history.can_go_forward());
+}
+
+void Tab::update_bookmark_button(const String& url)
+{
+ if (BookmarksBarWidget::the().contains_bookmark(url)) {
+ m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-filled.png"));
+ m_bookmark_button->set_tooltip("Remove Bookmark");
+ } else {
+ m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-contour.png"));
+ m_bookmark_button->set_tooltip("Add Bookmark");
+ }
+}
+
+void Tab::did_become_active()
+{
+ Web::ResourceLoader::the().on_load_counter_change = [this] {
+ if (Web::ResourceLoader::the().pending_loads() == 0) {
+ m_statusbar->set_text("");
+ return;
+ }
+ m_statusbar->set_text(String::formatted("Loading ({} pending resources...)", Web::ResourceLoader::the().pending_loads()));
+ };
+
+ BookmarksBarWidget::the().on_bookmark_click = [this](auto& url, unsigned modifiers) {
+ if (modifiers & Mod_Ctrl)
+ on_tab_open_request(url);
+ else
+ load(url);
+ };
+
+ BookmarksBarWidget::the().on_bookmark_hover = [this](auto&, auto& url) {
+ m_statusbar->set_text(url);
+ };
+
+ BookmarksBarWidget::the().remove_from_parent();
+ m_toolbar_container->add_child(BookmarksBarWidget::the());
+
+ auto is_fullscreen = window()->is_fullscreen();
+ m_toolbar_container->set_visible(!is_fullscreen);
+ m_statusbar->set_visible(!is_fullscreen);
+
+ GUI::Application::the()->set_menubar(m_menubar);
+}
+
+void Tab::context_menu_requested(const Gfx::IntPoint& screen_position)
+{
+ m_tab_context_menu->popup(screen_position);
+}
+
+GUI::Widget& Tab::view()
+{
+ if (m_type == Type::InProcessWebView)
+ return *m_page_view;
+ return *m_web_content_view;
+}
+
+Web::WebViewHooks& Tab::hooks()
+{
+ if (m_type == Type::InProcessWebView)
+ return *m_page_view;
+ return *m_web_content_view;
+}
+
+}
diff --git a/Userland/Applications/Browser/Tab.gml b/Userland/Applications/Browser/Tab.gml
new file mode 100644
index 0000000000..24c7456d77
--- /dev/null
+++ b/Userland/Applications/Browser/Tab.gml
@@ -0,0 +1,22 @@
+@GUI::Widget {
+ layout: @GUI::VerticalBoxLayout {
+ }
+
+ @GUI::ToolBarContainer {
+ name: "toolbar_container"
+
+ @GUI::ToolBar {
+ name: "toolbar"
+ }
+ }
+
+ @GUI::Widget {
+ name: "webview_container"
+ layout: @GUI::VerticalBoxLayout {
+ }
+ }
+
+ @GUI::StatusBar {
+ name: "statusbar"
+ }
+}
diff --git a/Userland/Applications/Browser/Tab.h b/Userland/Applications/Browser/Tab.h
new file mode 100644
index 0000000000..79f86e4931
--- /dev/null
+++ b/Userland/Applications/Browser/Tab.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "History.h"
+#include <AK/URL.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/ShareableBitmap.h>
+#include <LibHTTP/HttpJob.h>
+#include <LibWeb/Forward.h>
+
+namespace Web {
+class OutOfProcessWebView;
+class WebViewHooks;
+}
+
+namespace Browser {
+
+class Tab final : public GUI::Widget {
+ C_OBJECT(Tab);
+
+public:
+ enum class Type {
+ InProcessWebView,
+ OutOfProcessWebView,
+ };
+
+ virtual ~Tab() override;
+
+ URL url() const;
+
+ enum class LoadType {
+ Normal,
+ HistoryNavigation,
+ };
+
+ void load(const URL&, LoadType = LoadType::Normal);
+ void reload();
+ void go_back();
+ void go_forward();
+
+ void did_become_active();
+ void context_menu_requested(const Gfx::IntPoint& screen_position);
+
+ Function<void(String)> on_title_change;
+ Function<void(const URL&)> on_tab_open_request;
+ Function<void(Tab&)> on_tab_close_request;
+ Function<void(const Gfx::Bitmap&)> on_favicon_change;
+
+ const String& title() const { return m_title; }
+ const Gfx::Bitmap* icon() const { return m_icon; }
+
+ GUI::Widget& view();
+
+private:
+ explicit Tab(Type);
+
+ Web::WebViewHooks& hooks();
+ void update_actions();
+ void update_bookmark_button(const String& url);
+
+ Type m_type;
+
+ History m_history;
+
+ RefPtr<Web::InProcessWebView> m_page_view;
+ RefPtr<Web::OutOfProcessWebView> m_web_content_view;
+
+ RefPtr<GUI::Action> m_go_back_action;
+ RefPtr<GUI::Action> m_go_forward_action;
+ RefPtr<GUI::Action> m_reload_action;
+ RefPtr<GUI::TextBox> m_location_box;
+ RefPtr<GUI::Button> m_bookmark_button;
+ RefPtr<GUI::Window> m_dom_inspector_window;
+ RefPtr<GUI::Window> m_console_window;
+ RefPtr<GUI::StatusBar> m_statusbar;
+ RefPtr<GUI::MenuBar> m_menubar;
+ RefPtr<GUI::ToolBarContainer> m_toolbar_container;
+
+ RefPtr<GUI::Menu> m_link_context_menu;
+ RefPtr<GUI::Action> m_link_context_menu_default_action;
+ URL m_link_context_menu_url;
+
+ RefPtr<GUI::Menu> m_image_context_menu;
+ Gfx::ShareableBitmap m_image_context_menu_bitmap;
+ URL m_image_context_menu_url;
+
+ RefPtr<GUI::Menu> m_tab_context_menu;
+ RefPtr<GUI::Menu> m_page_context_menu;
+
+ String m_title;
+ RefPtr<const Gfx::Bitmap> m_icon;
+
+ bool m_is_history_navigation { false };
+};
+
+URL url_from_user_input(const String& input);
+
+}
diff --git a/Userland/Applications/Browser/WindowActions.cpp b/Userland/Applications/Browser/WindowActions.cpp
new file mode 100644
index 0000000000..ca0b12bd84
--- /dev/null
+++ b/Userland/Applications/Browser/WindowActions.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "WindowActions.h"
+#include <LibGUI/Icon.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+
+namespace Browser {
+
+static WindowActions* s_the;
+
+WindowActions& WindowActions::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+WindowActions::WindowActions(GUI::Window& window)
+{
+ ASSERT(!s_the);
+ s_the = this;
+ m_create_new_tab_action = GUI::Action::create(
+ "New tab", { Mod_Ctrl, Key_T }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"), [this](auto&) {
+ if (on_create_new_tab)
+ on_create_new_tab();
+ },
+ &window);
+
+ m_next_tab_action = GUI::Action::create(
+ "Next tab", { Mod_Ctrl, Key_PageDown }, [this](auto&) {
+ if (on_next_tab)
+ on_next_tab();
+ },
+ &window);
+
+ m_previous_tab_action = GUI::Action::create(
+ "Previous tab", { Mod_Ctrl, Key_PageUp }, [this](auto&) {
+ if (on_previous_tab)
+ on_previous_tab();
+ },
+ &window);
+
+ m_about_action = GUI::Action::create(
+ "About Browser", GUI::Icon::default_icon("app-browser").bitmap_for_size(16), [this](const GUI::Action&) {
+ if (on_about)
+ on_about();
+ },
+ &window);
+ m_show_bookmarks_bar_action = GUI::Action::create_checkable(
+ "Show bookmarks bar",
+ [this](auto& action) {
+ if (on_show_bookmarks_bar)
+ on_show_bookmarks_bar(action);
+ },
+ &window);
+}
+
+}
diff --git a/Userland/Applications/Browser/WindowActions.h b/Userland/Applications/Browser/WindowActions.h
new file mode 100644
index 0000000000..3212321837
--- /dev/null
+++ b/Userland/Applications/Browser/WindowActions.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Action.h>
+
+namespace Browser {
+
+class WindowActions {
+public:
+ static WindowActions& the();
+
+ WindowActions(GUI::Window&);
+
+ Function<void()> on_create_new_tab;
+ Function<void()> on_next_tab;
+ Function<void()> on_previous_tab;
+ Function<void()> on_about;
+ Function<void(GUI::Action&)> on_show_bookmarks_bar;
+
+ GUI::Action& create_new_tab_action() { return *m_create_new_tab_action; }
+ GUI::Action& next_tab_action() { return *m_next_tab_action; }
+ GUI::Action& previous_tab_action() { return *m_previous_tab_action; }
+ GUI::Action& about_action() { return *m_about_action; }
+ GUI::Action& show_bookmarks_bar_action() { return *m_show_bookmarks_bar_action; }
+
+private:
+ RefPtr<GUI::Action> m_create_new_tab_action;
+ RefPtr<GUI::Action> m_next_tab_action;
+ RefPtr<GUI::Action> m_previous_tab_action;
+ RefPtr<GUI::Action> m_about_action;
+ RefPtr<GUI::Action> m_show_bookmarks_bar_action;
+};
+
+}
diff --git a/Userland/Applications/Browser/main.cpp b/Userland/Applications/Browser/main.cpp
new file mode 100644
index 0000000000..d83b68f5f4
--- /dev/null
+++ b/Userland/Applications/Browser/main.cpp
@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BookmarksBarWidget.h"
+#include "Browser.h"
+#include "InspectorWidget.h"
+#include "Tab.h"
+#include "WindowActions.h"
+#include <AK/StringBuilder.h>
+#include <Applications/Browser/BrowserWindowGML.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/File.h>
+#include <LibCore/StandardPaths.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/AboutDialog.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibWeb/Loader/ContentFilter.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+namespace Browser {
+
+String g_home_url;
+bool g_multi_process = false;
+
+static String bookmarks_file_path()
+{
+ StringBuilder builder;
+ builder.append(Core::StandardPaths::config_directory());
+ builder.append("/bookmarks.json");
+ return builder.to_string();
+}
+
+}
+
+int main(int argc, char** argv)
+{
+ if (getuid() == 0) {
+ warnln("Refusing to run as root");
+ return 1;
+ }
+
+ if (pledge("stdio shared_buffer accept unix cpath rpath wpath fattr sendfd recvfd", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* specified_url = nullptr;
+
+ Core::ArgsParser args_parser;
+ args_parser.add_option(Browser::g_multi_process, "Multi-process mode", "multi-process", 'm');
+ args_parser.add_positional_argument(specified_url, "URL to open", "url", Core::ArgsParser::Required::No);
+ args_parser.parse(argc, argv);
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ // Connect to the ProtocolServer immediately so we can drop the "unix" pledge.
+ Web::ResourceLoader::the();
+
+ // Connect to LaunchServer immediately and let it know that we won't ask for anything other than opening
+ // the user's downloads directory.
+ // FIXME: This should go away with a standalone download manager at some point.
+ if (!Desktop::Launcher::add_allowed_url(URL::create_with_file_protocol(Core::StandardPaths::downloads_directory()))
+ || !Desktop::Launcher::seal_allowlist()) {
+ warnln("Failed to set up allowed launch URLs");
+ return 1;
+ }
+
+ if (pledge("stdio shared_buffer accept unix cpath rpath wpath sendfd recvfd", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/home", "rwc") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc/passwd", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/image", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/webcontent", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ auto app_icon = GUI::Icon::default_icon("app-browser");
+
+ auto m_config = Core::ConfigFile::get_for_app("Browser");
+ Browser::g_home_url = m_config->read_entry("Preferences", "Home", "about:blank");
+
+ auto ad_filter_list_or_error = Core::File::open(String::formatted("{}/BrowserContentFilters.txt", Core::StandardPaths::config_directory()), Core::IODevice::ReadOnly);
+ if (!ad_filter_list_or_error.is_error()) {
+ auto& ad_filter_list = *ad_filter_list_or_error.value();
+ while (!ad_filter_list.eof()) {
+ auto line = ad_filter_list.read_line();
+ if (line.is_empty())
+ continue;
+ Web::ContentFilter::the().add_pattern(line);
+ }
+ }
+
+ bool bookmarksbar_enabled = true;
+ auto bookmarks_bar = Browser::BookmarksBarWidget::construct(Browser::bookmarks_file_path(), bookmarksbar_enabled);
+
+ auto window = GUI::Window::construct();
+ window->resize(640, 480);
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->set_title("Browser");
+
+ auto& widget = window->set_main_widget<GUI::Widget>();
+ widget.load_from_gml(browser_window_gml);
+
+ auto& tab_widget = *widget.find_descendant_of_type_named<GUI::TabWidget>("tab_widget");
+
+ auto default_favicon = Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png");
+ ASSERT(default_favicon);
+
+ tab_widget.on_change = [&](auto& active_widget) {
+ auto& tab = static_cast<Browser::Tab&>(active_widget);
+ window->set_title(String::formatted("{} - Browser", tab.title()));
+ tab.did_become_active();
+ };
+
+ tab_widget.on_middle_click = [&](auto& clicked_widget) {
+ auto& tab = static_cast<Browser::Tab&>(clicked_widget);
+ tab.on_tab_close_request(tab);
+ };
+
+ tab_widget.on_context_menu_request = [&](auto& clicked_widget, const GUI::ContextMenuEvent& context_menu_event) {
+ auto& tab = static_cast<Browser::Tab&>(clicked_widget);
+ tab.context_menu_requested(context_menu_event.screen_position());
+ };
+
+ Browser::WindowActions window_actions(*window);
+
+ Function<void(URL url, bool activate)> create_new_tab;
+ create_new_tab = [&](auto url, auto activate) {
+ auto type = Browser::g_multi_process ? Browser::Tab::Type::OutOfProcessWebView : Browser::Tab::Type::InProcessWebView;
+ auto& new_tab = tab_widget.add_tab<Browser::Tab>("New tab", type);
+
+ tab_widget.set_bar_visible(!window->is_fullscreen() && tab_widget.children().size() > 1);
+ tab_widget.set_tab_icon(new_tab, default_favicon);
+
+ new_tab.on_title_change = [&](auto title) {
+ tab_widget.set_tab_title(new_tab, title);
+ if (tab_widget.active_widget() == &new_tab)
+ window->set_title(String::formatted("{} - Browser", title));
+ };
+
+ new_tab.on_favicon_change = [&](auto& bitmap) {
+ tab_widget.set_tab_icon(new_tab, &bitmap);
+ };
+
+ new_tab.on_tab_open_request = [&](auto& url) {
+ create_new_tab(url, true);
+ };
+
+ new_tab.on_tab_close_request = [&](auto& tab) {
+ tab_widget.deferred_invoke([&](auto&) {
+ tab_widget.remove_tab(tab);
+ tab_widget.set_bar_visible(!window->is_fullscreen() && tab_widget.children().size() > 1);
+ if (tab_widget.children().is_empty())
+ app->quit();
+ });
+ };
+
+ new_tab.load(url);
+
+ dbgln("Added new tab {:p}, loading {}", &new_tab, url);
+
+ if (activate)
+ tab_widget.set_active_widget(&new_tab);
+ };
+
+ URL first_url = Browser::g_home_url;
+ if (specified_url) {
+ if (Core::File::exists(specified_url)) {
+ first_url = URL::create_with_file_protocol(Core::File::real_path_for(specified_url));
+ } else {
+ first_url = Browser::url_from_user_input(specified_url);
+ }
+ }
+
+ window_actions.on_create_new_tab = [&] {
+ create_new_tab(Browser::g_home_url, true);
+ };
+
+ window_actions.on_next_tab = [&] {
+ tab_widget.activate_next_tab();
+ };
+
+ window_actions.on_previous_tab = [&] {
+ tab_widget.activate_previous_tab();
+ };
+
+ window_actions.on_about = [&] {
+ GUI::AboutDialog::show("Browser", app_icon.bitmap_for_size(32), window);
+ };
+
+ window_actions.on_show_bookmarks_bar = [&](auto& action) {
+ Browser::BookmarksBarWidget::the().set_visible(action.is_checked());
+ };
+ window_actions.show_bookmarks_bar_action().set_checked(bookmarksbar_enabled);
+
+ create_new_tab(first_url, true);
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/CMakeLists.txt b/Userland/Applications/CMakeLists.txt
new file mode 100644
index 0000000000..41803202e6
--- /dev/null
+++ b/Userland/Applications/CMakeLists.txt
@@ -0,0 +1,26 @@
+add_subdirectory(About)
+add_subdirectory(Browser)
+add_subdirectory(Calculator)
+add_subdirectory(Calendar)
+add_subdirectory(CrashReporter)
+add_subdirectory(Debugger)
+add_subdirectory(DisplaySettings)
+add_subdirectory(FileManager)
+add_subdirectory(FontEditor)
+add_subdirectory(Help)
+add_subdirectory(HexEditor)
+add_subdirectory(IRCClient)
+add_subdirectory(KeyboardMapper)
+add_subdirectory(KeyboardSettings)
+add_subdirectory(MouseSettings)
+add_subdirectory(Piano)
+add_subdirectory(PixelPaint)
+add_subdirectory(QuickShow)
+add_subdirectory(SoundPlayer)
+add_subdirectory(SpaceAnalyzer)
+add_subdirectory(Spreadsheet)
+add_subdirectory(SystemMonitor)
+add_subdirectory(ThemeEditor)
+add_subdirectory(Terminal)
+add_subdirectory(TextEditor)
+add_subdirectory(Welcome)
diff --git a/Userland/Applications/Calculator/CMakeLists.txt b/Userland/Applications/Calculator/CMakeLists.txt
new file mode 100644
index 0000000000..d72624c308
--- /dev/null
+++ b/Userland/Applications/Calculator/CMakeLists.txt
@@ -0,0 +1,11 @@
+compile_gml(CalculatorWindow.gml CalculatorGML.h calculator_gml)
+set(SOURCES
+ main.cpp
+ Calculator.cpp
+ CalculatorWidget.cpp
+ Keypad.cpp
+ CalculatorGML.h
+)
+
+serenity_app(Calculator ICON app-calculator)
+target_link_libraries(Calculator LibGUI)
diff --git a/Userland/Applications/Calculator/Calculator.cpp b/Userland/Applications/Calculator/Calculator.cpp
new file mode 100644
index 0000000000..c324fb84b9
--- /dev/null
+++ b/Userland/Applications/Calculator/Calculator.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Calculator.h"
+#include <AK/Assertions.h>
+#include <math.h>
+
+Calculator::Calculator()
+{
+}
+
+Calculator::~Calculator()
+{
+}
+
+double Calculator::begin_operation(Operation operation, double argument)
+{
+ double res = 0.0;
+
+ switch (operation) {
+ case Operation::None:
+ ASSERT_NOT_REACHED();
+
+ case Operation::Add:
+ case Operation::Subtract:
+ case Operation::Multiply:
+ case Operation::Divide:
+ m_saved_argument = argument;
+ m_operation_in_progress = operation;
+ return argument;
+
+ case Operation::Sqrt:
+ if (argument < 0.0) {
+ m_has_error = true;
+ return argument;
+ }
+ res = sqrt(argument);
+ clear_operation();
+ break;
+ case Operation::Inverse:
+ if (argument == 0.0) {
+ m_has_error = true;
+ return argument;
+ }
+ res = 1 / argument;
+ clear_operation();
+ break;
+ case Operation::Percent:
+ res = argument * 0.01;
+ break;
+ case Operation::ToggleSign:
+ res = -argument;
+ break;
+
+ case Operation::MemClear:
+ m_mem = 0.0;
+ res = argument;
+ break;
+ case Operation::MemRecall:
+ res = m_mem;
+ break;
+ case Operation::MemSave:
+ m_mem = argument;
+ res = argument;
+ break;
+ case Operation::MemAdd:
+ m_mem += argument;
+ res = m_mem;
+ break;
+ }
+
+ return res;
+}
+
+double Calculator::finish_operation(double argument)
+{
+ double res = 0.0;
+
+ switch (m_operation_in_progress) {
+ case Operation::None:
+ return argument;
+
+ case Operation::Add:
+ res = m_saved_argument + argument;
+ break;
+ case Operation::Subtract:
+ res = m_saved_argument - argument;
+ break;
+ case Operation::Multiply:
+ res = m_saved_argument * argument;
+ break;
+ case Operation::Divide:
+ if (argument == 0.0) {
+ m_has_error = true;
+ return argument;
+ }
+ res = m_saved_argument / argument;
+ break;
+
+ case Operation::Sqrt:
+ case Operation::Inverse:
+ case Operation::Percent:
+ case Operation::ToggleSign:
+ case Operation::MemClear:
+ case Operation::MemRecall:
+ case Operation::MemSave:
+ case Operation::MemAdd:
+ ASSERT_NOT_REACHED();
+ }
+
+ clear_operation();
+ return res;
+}
+
+void Calculator::clear_operation()
+{
+ m_operation_in_progress = Operation::None;
+ m_saved_argument = 0.0;
+ clear_error();
+}
diff --git a/Userland/Applications/Calculator/Calculator.h b/Userland/Applications/Calculator/Calculator.h
new file mode 100644
index 0000000000..c53e31c397
--- /dev/null
+++ b/Userland/Applications/Calculator/Calculator.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+// This type implements the regular calculator
+// behavior, such as performing arithmetic
+// operations and providing a memory cell.
+// It does not deal with number input; you
+// have to pass in already parsed double
+// values.
+
+class Calculator final {
+public:
+ Calculator();
+ ~Calculator();
+
+ enum class Operation {
+ None,
+ Add,
+ Subtract,
+ Multiply,
+ Divide,
+
+ Sqrt,
+ Inverse,
+ Percent,
+ ToggleSign,
+
+ MemClear,
+ MemRecall,
+ MemSave,
+ MemAdd
+ };
+
+ double begin_operation(Operation, double);
+ double finish_operation(double);
+
+ bool has_error() const { return m_has_error; }
+
+ void clear_operation();
+ void clear_error() { m_has_error = false; }
+
+private:
+ Operation m_operation_in_progress { Operation::None };
+ double m_saved_argument { 0.0 };
+ double m_mem { 0.0 };
+ bool m_has_error { false };
+};
diff --git a/Userland/Applications/Calculator/CalculatorWidget.cpp b/Userland/Applications/Calculator/CalculatorWidget.cpp
new file mode 100644
index 0000000000..083edae93b
--- /dev/null
+++ b/Userland/Applications/Calculator/CalculatorWidget.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * Copyright (c) 2021 Glenford Williams <gw_dev@outlook.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CalculatorWidget.h"
+#include "Applications/Calculator/CalculatorGML.h"
+#include <AK/Assertions.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/Palette.h>
+
+CalculatorWidget::CalculatorWidget()
+{
+ load_from_gml(calculator_gml);
+
+ m_entry = *find_descendant_of_type_named<GUI::TextBox>("entry_textbox");
+ m_entry->set_relative_rect(5, 5, 244, 26);
+ m_entry->set_text_alignment(Gfx::TextAlignment::CenterRight);
+ m_entry->set_font(Gfx::FontDatabase::default_fixed_width_font());
+
+ m_label = *find_descendant_of_type_named<GUI::Label>("label");
+
+ m_label->set_frame_shadow(Gfx::FrameShadow::Sunken);
+ m_label->set_frame_shape(Gfx::FrameShape::Container);
+ m_label->set_frame_thickness(2);
+
+ for (int i = 0; i < 10; i++) {
+ m_digit_button[i] = *find_descendant_of_type_named<GUI::Button>(String::formatted("{}_button", i));
+ add_digit_button(*m_digit_button[i], i);
+ }
+
+ m_mem_add_button = *find_descendant_of_type_named<GUI::Button>("mem_add_button");
+ add_operation_button(*m_mem_add_button, Calculator::Operation::MemAdd);
+
+ m_mem_save_button = *find_descendant_of_type_named<GUI::Button>("mem_save_button");
+ add_operation_button(*m_mem_save_button, Calculator::Operation::MemSave);
+
+ m_mem_recall_button = *find_descendant_of_type_named<GUI::Button>("mem_recall_button");
+ add_operation_button(*m_mem_recall_button, Calculator::Operation::MemRecall);
+
+ m_mem_clear_button = *find_descendant_of_type_named<GUI::Button>("mem_clear_button");
+ add_operation_button(*m_mem_clear_button, Calculator::Operation::MemClear);
+
+ m_clear_button = *find_descendant_of_type_named<GUI::Button>("clear_button");
+ m_clear_button->on_click = [this](auto) {
+ m_keypad.set_value(0.0);
+ m_calculator.clear_operation();
+ update_display();
+ };
+
+ m_clear_error_button = *find_descendant_of_type_named<GUI::Button>("clear_error_button");
+ m_clear_error_button->on_click = [this](auto) {
+ m_keypad.set_value(0.0);
+ update_display();
+ };
+
+ m_backspace_button = *find_descendant_of_type_named<GUI::Button>("backspace_button");
+ m_backspace_button->on_click = [this](auto) {
+ m_keypad.type_backspace();
+ update_display();
+ };
+
+ m_decimal_point_button = *find_descendant_of_type_named<GUI::Button>("decimal_button");
+ m_decimal_point_button->on_click = [this](auto) {
+ m_keypad.type_decimal_point();
+ update_display();
+ };
+
+ m_sign_button = *find_descendant_of_type_named<GUI::Button>("sign_button");
+ add_operation_button(*m_sign_button, Calculator::Operation::ToggleSign);
+
+ m_add_button = *find_descendant_of_type_named<GUI::Button>("add_button");
+ add_operation_button(*m_add_button, Calculator::Operation::Add);
+
+ m_subtract_button = *find_descendant_of_type_named<GUI::Button>("subtract_button");
+ add_operation_button(*m_subtract_button, Calculator::Operation::Subtract);
+
+ m_multiply_button = *find_descendant_of_type_named<GUI::Button>("multiply_button");
+ add_operation_button(*m_multiply_button, Calculator::Operation::Multiply);
+
+ m_divide_button = *find_descendant_of_type_named<GUI::Button>("divide_button");
+ add_operation_button(*m_divide_button, Calculator::Operation::Divide);
+
+ m_sqrt_button = *find_descendant_of_type_named<GUI::Button>("sqrt_button");
+ add_operation_button(*m_sqrt_button, Calculator::Operation::Sqrt);
+
+ m_inverse_button = *find_descendant_of_type_named<GUI::Button>("inverse_button");
+ add_operation_button(*m_inverse_button, Calculator::Operation::Inverse);
+
+ m_percent_button = *find_descendant_of_type_named<GUI::Button>("mod_button");
+ add_operation_button(*m_percent_button, Calculator::Operation::Percent);
+
+ m_equals_button = *find_descendant_of_type_named<GUI::Button>("equal_button");
+ m_equals_button->on_click = [this](auto) {
+ double argument = m_keypad.value();
+ double res = m_calculator.finish_operation(argument);
+ m_keypad.set_value(res);
+ update_display();
+ };
+}
+
+CalculatorWidget::~CalculatorWidget()
+{
+}
+
+void CalculatorWidget::add_operation_button(GUI::Button& button, Calculator::Operation operation)
+{
+ button.on_click = [this, operation](auto) {
+ double argument = m_keypad.value();
+ double res = m_calculator.begin_operation(operation, argument);
+ m_keypad.set_value(res);
+ update_display();
+ };
+}
+
+void CalculatorWidget::add_digit_button(GUI::Button& button, int digit)
+{
+ button.on_click = [this, digit](auto) {
+ m_keypad.type_digit(digit);
+ update_display();
+ };
+}
+
+void CalculatorWidget::update_display()
+{
+ m_entry->set_text(m_keypad.to_string());
+ if (m_calculator.has_error())
+ m_label->set_text("E");
+ else
+ m_label->set_text("");
+}
+
+void CalculatorWidget::keydown_event(GUI::KeyEvent& event)
+{
+ //Clear button selection when we are typing
+ m_equals_button->set_focus(true);
+ m_equals_button->set_focus(false);
+
+ if (event.key() == KeyCode::Key_Return) {
+ m_keypad.set_value(m_calculator.finish_operation(m_keypad.value()));
+
+ } else if (event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9) {
+ m_keypad.type_digit(atoi(event.text().characters()));
+
+ } else if (event.key() == KeyCode::Key_Period) {
+ m_keypad.type_decimal_point();
+
+ } else if (event.key() == KeyCode::Key_Escape) {
+ m_keypad.set_value(0.0);
+ m_calculator.clear_operation();
+
+ } else if (event.key() == KeyCode::Key_Backspace) {
+ m_keypad.type_backspace();
+
+ } else {
+ Calculator::Operation operation;
+
+ switch (event.key()) {
+ case KeyCode::Key_Plus:
+ operation = Calculator::Operation::Add;
+ break;
+ case KeyCode::Key_Minus:
+ operation = Calculator::Operation::Subtract;
+ break;
+ case KeyCode::Key_Asterisk:
+ operation = Calculator::Operation::Multiply;
+ break;
+ case KeyCode::Key_Slash:
+ operation = Calculator::Operation::Divide;
+ break;
+ case KeyCode::Key_Percent:
+ operation = Calculator::Operation::Percent;
+ break;
+ default:
+ return;
+ }
+
+ m_keypad.set_value(m_calculator.begin_operation(operation, m_keypad.value()));
+ }
+
+ update_display();
+}
diff --git a/Userland/Applications/Calculator/CalculatorWidget.h b/Userland/Applications/Calculator/CalculatorWidget.h
new file mode 100644
index 0000000000..31d9548b3c
--- /dev/null
+++ b/Userland/Applications/Calculator/CalculatorWidget.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * Copyright (c) 2021 Glenford Williams <gw_dev@outlook.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Calculator.h"
+#include "Keypad.h"
+#include <AK/Vector.h>
+#include <LibGUI/Widget.h>
+
+class CalculatorWidget final : public GUI::Widget {
+ C_OBJECT(CalculatorWidget)
+public:
+ virtual ~CalculatorWidget() override;
+
+private:
+ CalculatorWidget();
+ void add_operation_button(GUI::Button&, Calculator::Operation);
+ void add_digit_button(GUI::Button&, int digit);
+
+ void update_display();
+
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+ Calculator m_calculator;
+ Keypad m_keypad;
+
+ RefPtr<GUI::TextBox> m_entry;
+ RefPtr<GUI::Label> m_label;
+
+ RefPtr<GUI::Button> m_digit_button[10];
+ RefPtr<GUI::Button> m_mem_add_button;
+ RefPtr<GUI::Button> m_mem_save_button;
+ RefPtr<GUI::Button> m_mem_recall_button;
+ RefPtr<GUI::Button> m_mem_clear_button;
+ RefPtr<GUI::Button> m_clear_button;
+ RefPtr<GUI::Button> m_clear_error_button;
+ RefPtr<GUI::Button> m_backspace_button;
+ RefPtr<GUI::Button> m_decimal_point_button;
+ RefPtr<GUI::Button> m_sign_button;
+ RefPtr<GUI::Button> m_add_button;
+ RefPtr<GUI::Button> m_subtract_button;
+ RefPtr<GUI::Button> m_multiply_button;
+ RefPtr<GUI::Button> m_divide_button;
+ RefPtr<GUI::Button> m_sqrt_button;
+ RefPtr<GUI::Button> m_inverse_button;
+ RefPtr<GUI::Button> m_percent_button;
+ RefPtr<GUI::Button> m_equals_button;
+};
diff --git a/Userland/Applications/Calculator/CalculatorWindow.gml b/Userland/Applications/Calculator/CalculatorWindow.gml
new file mode 100644
index 0000000000..4763a4d8c2
--- /dev/null
+++ b/Userland/Applications/Calculator/CalculatorWindow.gml
@@ -0,0 +1,272 @@
+@GUI::Widget {
+ fixed_width: 254
+ fixed_height: 213
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [10, 0, 10, 0]
+ }
+
+ @GUI::TextBox {
+ name: "entry_textbox"
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ name: "label"
+ fixed_width: 35
+ fixed_height: 27
+ }
+
+ @GUI::Widget {
+ fixed_width: 5
+ }
+
+ @GUI::Button {
+ name: "backspace_button"
+ text: "Backspace"
+ fixed_width: 65
+ fixed_height: 28
+ foreground_color: "brown"
+ }
+
+ @GUI::Button {
+ name: "clear_error_button"
+ text: "CE"
+ fixed_width: 55
+ fixed_height: 28
+ foreground_color: "brown"
+ }
+
+ @GUI::Button {
+ name: "clear_button"
+ text: "C"
+ fixed_width: 60
+ fixed_height: 28
+ foreground_color: "brown"
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Button {
+ name: "mem_clear_button"
+ text: "MC"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "red"
+ }
+
+ @GUI::Widget {
+ fixed_width: 5
+ }
+
+ @GUI::Button {
+ name: "7_button"
+ text: "7"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "8_button"
+ text: "8"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "9_button"
+ text: "9"
+ fixed_width: 35
+ fixed_height: 28
+ }
+
+ @GUI::Button {
+ name: "divide_button"
+ text: "/"
+ fixed_width: 35
+ fixed_height: 28
+ }
+
+ @GUI::Button {
+ name: "sqrt_button"
+ text: "sqrt"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {}
+
+ @GUI::Button {
+ name: "mem_recall_button"
+ text: "MR"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "red"
+ }
+
+ @GUI::Widget {
+ fixed_width: 5
+ }
+
+ @GUI::Button {
+ name: "4_button"
+ text: "4"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "5_button"
+ text: "5"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "6_button"
+ text: "6"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "multiply_button"
+ text: "*"
+ fixed_width: 35
+ fixed_height: 28
+ }
+
+ @GUI::Button {
+ name: "mod_button"
+ text: "%"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {}
+
+ @GUI::Button {
+ name: "mem_save_button"
+ text: "MS"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "red"
+ }
+
+ @GUI::Widget {
+ fixed_width: 5
+ }
+
+ @GUI::Button {
+ name: "1_button"
+ text: "1"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "2_button"
+ text: "2"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "3_button"
+ text: "3"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "subtract_button"
+ text: "-"
+ fixed_width: 35
+ fixed_height: 28
+ }
+
+ @GUI::Button {
+ name: "inverse_button"
+ text: "1/x"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {}
+
+ @GUI::Button {
+ name: "mem_add_button"
+ text: "M+"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "red"
+ }
+
+ @GUI::Widget {
+ fixed_width: 5
+ }
+
+ @GUI::Button {
+ name: "0_button"
+ text: "0"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "sign_button"
+ text: "+/-"
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "decimal_button"
+ text: "."
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "blue"
+ }
+
+ @GUI::Button {
+ name: "add_button"
+ text: "+"
+ fixed_width: 35
+ fixed_height: 28
+ }
+
+ @GUI::Button {
+ name: "equal_button"
+ text: "="
+ fixed_width: 35
+ fixed_height: 28
+ foreground_color: "red"
+ }
+ }
+}
diff --git a/Userland/Applications/Calculator/Keypad.cpp b/Userland/Applications/Calculator/Keypad.cpp
new file mode 100644
index 0000000000..9b2d672af5
--- /dev/null
+++ b/Userland/Applications/Calculator/Keypad.cpp
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Keypad.h"
+#include <AK/StringBuilder.h>
+#include <math.h>
+
+Keypad::Keypad()
+{
+}
+
+Keypad::~Keypad()
+{
+}
+
+void Keypad::type_digit(int digit)
+{
+ switch (m_state) {
+ case State::External:
+ m_state = State::TypingInteger;
+ m_negative = false;
+ m_int_value = digit;
+ m_frac_value = 0;
+ m_frac_length = 0;
+ break;
+ case State::TypingInteger:
+ ASSERT(m_frac_value == 0);
+ ASSERT(m_frac_length == 0);
+ m_int_value *= 10;
+ m_int_value += digit;
+ break;
+ case State::TypingDecimal:
+ if (m_frac_length > 6)
+ break;
+ m_frac_value *= 10;
+ m_frac_value += digit;
+ m_frac_length++;
+ break;
+ }
+}
+
+void Keypad::type_decimal_point()
+{
+ switch (m_state) {
+ case State::External:
+ m_negative = false;
+ m_int_value = 0;
+ m_frac_value = 0;
+ m_frac_length = 0;
+ break;
+ case State::TypingInteger:
+ ASSERT(m_frac_value == 0);
+ ASSERT(m_frac_length == 0);
+ m_state = State::TypingDecimal;
+ break;
+ case State::TypingDecimal:
+ // Ignore it.
+ break;
+ }
+}
+
+void Keypad::type_backspace()
+{
+ switch (m_state) {
+ case State::External:
+ m_negative = false;
+ m_int_value = 0;
+ m_frac_value = 0;
+ m_frac_length = 0;
+ break;
+ case State::TypingDecimal:
+ if (m_frac_length > 0) {
+ m_frac_value /= 10;
+ m_frac_length--;
+ break;
+ }
+ ASSERT(m_frac_value == 0);
+ m_state = State::TypingInteger;
+ [[fallthrough]];
+ case State::TypingInteger:
+ ASSERT(m_frac_value == 0);
+ ASSERT(m_frac_length == 0);
+ m_int_value /= 10;
+ if (m_int_value == 0)
+ m_negative = false;
+ break;
+ }
+}
+
+double Keypad::value() const
+{
+ double res = 0.0;
+
+ long frac = m_frac_value;
+ for (int i = 0; i < m_frac_length; i++) {
+ int digit = frac % 10;
+ res += digit;
+ res /= 10.0;
+ frac /= 10;
+ }
+
+ res += m_int_value;
+ if (m_negative)
+ res = -res;
+
+ return res;
+}
+
+void Keypad::set_value(double value)
+{
+ m_state = State::External;
+
+ if (value < 0.0) {
+ m_negative = true;
+ value = -value;
+ } else
+ m_negative = false;
+
+ m_int_value = value;
+ value -= m_int_value;
+
+ m_frac_value = 0;
+ m_frac_length = 0;
+ while (value != 0) {
+ value *= 10.0;
+ int digit = value;
+ m_frac_value *= 10;
+ m_frac_value += digit;
+ m_frac_length++;
+ value -= digit;
+
+ if (m_frac_length > 6)
+ break;
+ }
+}
+
+String Keypad::to_string() const
+{
+ StringBuilder builder;
+ if (m_negative)
+ builder.append("-");
+ builder.appendff("{}", m_int_value);
+
+ if (m_frac_length > 0)
+ builder.appendff(".{:0{}}", m_frac_value, m_frac_length);
+
+ return builder.to_string();
+}
diff --git a/Userland/Applications/Calculator/Keypad.h b/Userland/Applications/Calculator/Keypad.h
new file mode 100644
index 0000000000..dae427a046
--- /dev/null
+++ b/Userland/Applications/Calculator/Keypad.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+
+// This type implements number typing and
+// displaying mechanics. It does not perform
+// any arithmetic operations or anything on
+// the values it deals with.
+
+class Keypad final {
+public:
+ Keypad();
+ ~Keypad();
+
+ void type_digit(int digit);
+ void type_decimal_point();
+ void type_backspace();
+
+ double value() const;
+ void set_value(double);
+
+ String to_string() const;
+
+private:
+ // Internal representation of the current decimal value.
+ bool m_negative { false };
+ long m_int_value { 0 };
+ long m_frac_value { 0 };
+ int m_frac_length { 0 };
+ // E.g. for -35.004200,
+ // m_negative = true
+ // m_int_value = 35
+ // m_frac_value = 4200
+ // m_frac_length = 6
+
+ enum class State {
+ External,
+ TypingInteger,
+ TypingDecimal
+ };
+
+ State m_state { State::External };
+};
diff --git a/Userland/Applications/Calculator/main.cpp b/Userland/Applications/Calculator/main.cpp
new file mode 100644
index 0000000000..7ff18b94bf
--- /dev/null
+++ b/Userland/Applications/Calculator/main.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CalculatorWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer rpath accept unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer rpath accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ auto app_icon = GUI::Icon::default_icon("app-calculator");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Calculator");
+ window->set_resizable(false);
+ window->resize(254, 213);
+
+ window->set_main_widget<CalculatorWidget>();
+
+ window->show();
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Calculator");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Calculator", app_icon));
+
+ app->set_menubar(move(menubar));
+
+ return app->exec();
+}
diff --git a/Userland/Applications/Calendar/AddEventDialog.cpp b/Userland/Applications/Calendar/AddEventDialog.cpp
new file mode 100644
index 0000000000..dbb70d33c1
--- /dev/null
+++ b/Userland/Applications/Calendar/AddEventDialog.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AddEventDialog.h"
+#include <LibCore/DateTime.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/ComboBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Layout.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+
+static const char* short_month_names[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
+ : Dialog(parent_window)
+ , m_date_time(date_time)
+{
+ resize(158, 100);
+ set_title("Add Event");
+ set_resizable(false);
+ set_icon(parent_window->icon());
+
+ auto& widget = set_main_widget<GUI::Widget>();
+ widget.set_fill_with_background_color(true);
+ widget.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& top_container = widget.add<GUI::Widget>();
+ top_container.set_layout<GUI::VerticalBoxLayout>();
+ top_container.set_fixed_height(45);
+ top_container.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& add_label = top_container.add<GUI::Label>("Add title & date:");
+ add_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ add_label.set_fixed_height(14);
+ add_label.set_font(Gfx::FontDatabase::default_bold_font());
+
+ auto& event_title_textbox = top_container.add<GUI::TextBox>();
+ event_title_textbox.set_fixed_height(20);
+
+ auto& middle_container = widget.add<GUI::Widget>();
+ middle_container.set_layout<GUI::HorizontalBoxLayout>();
+ middle_container.set_fixed_height(25);
+ middle_container.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& starting_month_combo = middle_container.add<GUI::ComboBox>();
+ starting_month_combo.set_only_allow_values_from_model(true);
+ starting_month_combo.set_fixed_size(50, 20);
+ starting_month_combo.set_model(MonthListModel::create());
+ starting_month_combo.set_selected_index(m_date_time.month() - 1);
+
+ auto& starting_day_combo = middle_container.add<GUI::SpinBox>();
+ starting_day_combo.set_fixed_size(40, 20);
+ starting_day_combo.set_value(m_date_time.day());
+ starting_day_combo.set_min(1);
+
+ auto& starting_year_combo = middle_container.add<GUI::SpinBox>();
+ starting_year_combo.set_fixed_size(55, 20);
+ starting_year_combo.set_range(0, 9999);
+ starting_year_combo.set_value(m_date_time.year());
+
+ widget.layout()->add_spacer();
+
+ auto& button_container = widget.add<GUI::Widget>();
+ button_container.set_fixed_height(20);
+ button_container.set_layout<GUI::HorizontalBoxLayout>();
+ button_container.layout()->add_spacer();
+ auto& ok_button = button_container.add<GUI::Button>("OK");
+ ok_button.set_fixed_size(80, 20);
+ ok_button.on_click = [this](auto) {
+ dbgln("TODO: Add event icon on specific tile");
+ done(Dialog::ExecOK);
+ };
+
+ event_title_textbox.set_focus(true);
+}
+
+AddEventDialog::~AddEventDialog()
+{
+}
+
+AddEventDialog::MonthListModel::MonthListModel()
+{
+}
+
+AddEventDialog::MonthListModel::~MonthListModel()
+{
+}
+
+void AddEventDialog::MonthListModel::update()
+{
+}
+
+int AddEventDialog::MonthListModel::row_count(const GUI::ModelIndex&) const
+{
+ return 12;
+}
+
+String AddEventDialog::MonthListModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Month:
+ return "Month";
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+GUI::Variant AddEventDialog::MonthListModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ auto& month = short_month_names[index.row()];
+ if (role == GUI::ModelRole::Display) {
+ switch (index.column()) {
+ case Column::Month:
+ return month;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ }
+ return {};
+}
diff --git a/Userland/Applications/Calendar/AddEventDialog.h b/Userland/Applications/Calendar/AddEventDialog.h
new file mode 100644
index 0000000000..c84e494527
--- /dev/null
+++ b/Userland/Applications/Calendar/AddEventDialog.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Calendar.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Window.h>
+
+class AddEventDialog final : public GUI::Dialog {
+ C_OBJECT(AddEventDialog)
+public:
+ virtual ~AddEventDialog() override;
+
+ static void show(Core::DateTime date_time, Window* parent_window = nullptr)
+ {
+ auto dialog = AddEventDialog::construct(date_time, parent_window);
+ dialog->exec();
+ }
+
+private:
+ AddEventDialog(Core::DateTime date_time, Window* parent_window = nullptr);
+
+ class MonthListModel final : public GUI::Model {
+ public:
+ enum Column {
+ Month,
+ __Count,
+ };
+
+ static NonnullRefPtr<MonthListModel> create() { return adopt(*new MonthListModel); }
+ virtual ~MonthListModel() override;
+
+ virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+ virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
+ virtual String column_name(int) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+
+ private:
+ MonthListModel();
+ };
+
+ Core::DateTime m_date_time;
+};
diff --git a/Userland/Applications/Calendar/CMakeLists.txt b/Userland/Applications/Calendar/CMakeLists.txt
new file mode 100644
index 0000000000..c23a6743cc
--- /dev/null
+++ b/Userland/Applications/Calendar/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ AddEventDialog.cpp
+ main.cpp
+)
+
+serenity_app(Calendar ICON app-calendar)
+target_link_libraries(Calendar LibGUI)
diff --git a/Userland/Applications/Calendar/main.cpp b/Userland/Applications/Calendar/main.cpp
new file mode 100644
index 0000000000..aebc5c4fed
--- /dev/null
+++ b/Userland/Applications/Calendar/main.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AddEventDialog.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Calendar.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+
+ if (pledge("stdio shared_buffer rpath accept unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer rpath accept", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ auto app_icon = GUI::Icon::default_icon("app-calendar");
+ auto window = GUI::Window::construct();
+ window->set_title("Calendar");
+ window->resize(600, 480);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& root_container = window->set_main_widget<GUI::Widget>();
+ root_container.set_fill_with_background_color(true);
+ root_container.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& toolbar_container = root_container.add<GUI::ToolBarContainer>();
+ auto& toolbar = toolbar_container.add<GUI::ToolBar>();
+
+ auto& calendar_container = root_container.add<GUI::Frame>();
+ calendar_container.set_layout<GUI::VerticalBoxLayout>();
+ calendar_container.layout()->set_margins({ 2, 2, 2, 2 });
+ auto& calendar_widget = calendar_container.add<GUI::Calendar>(Core::DateTime::now());
+
+ RefPtr<GUI::Button> selected_calendar_button;
+
+ auto prev_date_action = GUI::Action::create("Previous date", { Mod_Alt, Key_Left }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"), [&](const GUI::Action&) {
+ unsigned int target_month = calendar_widget.selected_month();
+ unsigned int target_year = calendar_widget.selected_year();
+
+ if (calendar_widget.mode() == GUI::Calendar::Month) {
+ target_month--;
+ if (calendar_widget.selected_month() <= 1) {
+ target_month = 12;
+ target_year--;
+ }
+ } else {
+ target_year--;
+ }
+
+ calendar_widget.update_tiles(target_year, target_month);
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ });
+
+ auto next_date_action = GUI::Action::create("Next date", { Mod_Alt, Key_Right }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [&](const GUI::Action&) {
+ unsigned int target_month = calendar_widget.selected_month();
+ unsigned int target_year = calendar_widget.selected_year();
+
+ if (calendar_widget.mode() == GUI::Calendar::Month) {
+ target_month++;
+ if (calendar_widget.selected_month() >= 12) {
+ target_month = 1;
+ target_year++;
+ }
+ } else {
+ target_year++;
+ }
+
+ calendar_widget.update_tiles(target_year, target_month);
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ });
+
+ auto add_event_action = GUI::Action::create("Add event", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"), [&](const GUI::Action&) {
+ AddEventDialog::show(calendar_widget.selected_date(), window);
+ });
+
+ auto jump_to_action = GUI::Action::create("Jump to today", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-date.png"), [&](const GUI::Action&) {
+ if (calendar_widget.mode() == GUI::Calendar::Year)
+ calendar_widget.toggle_mode();
+ calendar_widget.set_selected_date(Core::DateTime::now());
+ calendar_widget.update_tiles(Core::DateTime::now().year(), Core::DateTime::now().month());
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ });
+
+ toolbar.add_action(prev_date_action);
+ selected_calendar_button = toolbar.add<GUI::Button>(calendar_widget.selected_calendar_text());
+ selected_calendar_button->set_fixed_width(70);
+ selected_calendar_button->set_button_style(Gfx::ButtonStyle::CoolBar);
+ selected_calendar_button->set_font(Gfx::FontDatabase::default_bold_fixed_width_font());
+ selected_calendar_button->on_click = [&](auto) {
+ calendar_widget.toggle_mode();
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ };
+ toolbar.add_action(next_date_action);
+ toolbar.add_separator();
+ toolbar.add_action(jump_to_action);
+ toolbar.add_action(add_event_action);
+
+ calendar_widget.on_calendar_tile_click = [&] {
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ };
+
+ calendar_widget.on_calendar_tile_doubleclick = [&] {
+ AddEventDialog::show(calendar_widget.selected_date(), window);
+ };
+
+ calendar_widget.on_month_tile_click = [&] {
+ selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
+ };
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Calendar");
+ app_menu.add_action(GUI::Action::create("Add Event", { Mod_Ctrl | Mod_Shift, Key_E }, Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"),
+ [&](const GUI::Action&) {
+ AddEventDialog::show(calendar_widget.selected_date(), window);
+ return;
+ }));
+
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Calendar", app_icon));
+
+ app->set_menubar(move(menubar));
+ window->show();
+ app->exec();
+}
diff --git a/Userland/Applications/CrashReporter/CMakeLists.txt b/Userland/Applications/CrashReporter/CMakeLists.txt
new file mode 100644
index 0000000000..d391aa6b84
--- /dev/null
+++ b/Userland/Applications/CrashReporter/CMakeLists.txt
@@ -0,0 +1,10 @@
+compile_gml(CrashReporterWindow.gml CrashReporterWindowGML.h crash_reporter_window_gml)
+
+
+set(SOURCES
+ main.cpp
+ CrashReporterWindowGML.h
+)
+
+serenity_app(CrashReporter ICON app-crash-reporter)
+target_link_libraries(CrashReporter LibCore LibCoreDump LibDesktop LibGUI)
diff --git a/Userland/Applications/CrashReporter/CrashReporterWindow.gml b/Userland/Applications/CrashReporter/CrashReporterWindow.gml
new file mode 100644
index 0000000000..acca32bd13
--- /dev/null
+++ b/Userland/Applications/CrashReporter/CrashReporterWindow.gml
@@ -0,0 +1,95 @@
+@GUI::Widget {
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [5, 5, 5, 5]
+ }
+
+ @GUI::Widget {
+ fixed_height: 44
+
+ layout: @GUI::HorizontalBoxLayout {
+ spacing: 10
+ }
+
+ @GUI::ImageWidget {
+ name: "icon"
+ }
+
+ @GUI::Label {
+ name: "description"
+ text_alignment: "CenterLeft"
+ }
+ }
+
+ @GUI::Widget {
+ fixed_height: 18
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Executable path:"
+ text_alignment: "CenterLeft"
+ fixed_width: 90
+ }
+
+ @GUI::LinkLabel {
+ name: "executable_link"
+ text_alignment: "CenterLeft"
+ }
+ }
+
+ @GUI::Widget {
+ fixed_height: 18
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Coredump path:"
+ text_alignment: "CenterLeft"
+ fixed_width: 90
+ }
+
+ @GUI::LinkLabel {
+ name: "coredump_link"
+ text_alignment: "CenterLeft"
+ }
+ }
+
+ @GUI::Widget {
+ fixed_height: 18
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Backtrace:"
+ text_alignment: "CenterLeft"
+ }
+ }
+
+ @GUI::TextEditor {
+ name: "backtrace_text_editor"
+ mode: "ReadOnly"
+ }
+
+ @GUI::Widget {
+ fixed_height: 32
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ // HACK: We need something like Layout::add_spacer() in GML! :^)
+ @GUI::Widget {
+ }
+
+ @GUI::Button {
+ name: "close_button"
+ text: "Close"
+ fixed_width: 70
+ fixed_height: 22
+ }
+ }
+}
diff --git a/Userland/Applications/CrashReporter/main.cpp b/Userland/Applications/CrashReporter/main.cpp
new file mode 100644
index 0000000000..047a696ea3
--- /dev/null
+++ b/Userland/Applications/CrashReporter/main.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2020, Linus Groh <mail@linusgroh.de>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/LexicalPath.h>
+#include <AK/StringBuilder.h>
+#include <AK/Types.h>
+#include <AK/URL.h>
+#include <Applications/CrashReporter/CrashReporterWindowGML.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCoreDump/Backtrace.h>
+#include <LibCoreDump/Reader.h>
+#include <LibDesktop/AppFile.h>
+#include <LibDesktop/Launcher.h>
+#include <LibELF/CoreDump.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FileIconProvider.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/ImageWidget.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Layout.h>
+#include <LibGUI/LinkLabel.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Window.h>
+#include <string.h>
+
+static String build_backtrace(const CoreDump::Reader& coredump)
+{
+ StringBuilder builder;
+
+ auto assertion = coredump.metadata().get("assertion");
+ if (assertion.has_value() && !assertion.value().is_empty()) {
+ builder.append("ASSERTION FAILED: ");
+ builder.append(assertion.value().characters());
+ builder.append('\n');
+ builder.append('\n');
+ }
+
+ auto first = true;
+ for (auto& entry : coredump.backtrace().entries()) {
+ if (first)
+ first = false;
+ else
+ builder.append('\n');
+ builder.append(entry.to_string());
+ }
+
+ return builder.build();
+}
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept cpath rpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* coredump_path = nullptr;
+
+ Core::ArgsParser args_parser;
+ args_parser.set_general_help("Show information from an application crash coredump.");
+ args_parser.add_positional_argument(coredump_path, "Coredump path", "coredump-path");
+ args_parser.parse(argc, argv);
+
+ String backtrace;
+ String executable_path;
+ int pid { 0 };
+ u8 termination_signal { 0 };
+
+ {
+ auto coredump = CoreDump::Reader::create(coredump_path);
+ if (!coredump) {
+ warnln("Could not open coredump '{}'", coredump_path);
+ return 1;
+ }
+ auto& process_info = coredump->process_info();
+ backtrace = build_backtrace(*coredump);
+ executable_path = String(process_info.executable_path);
+ pid = process_info.pid;
+ termination_signal = process_info.termination_signal;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept rpath unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil(executable_path.characters(), "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/launch", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-crash-reporter");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Crash Reporter");
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->resize(460, 340);
+ window->center_on_screen();
+
+ auto& widget = window->set_main_widget<GUI::Widget>();
+ widget.load_from_gml(crash_reporter_window_gml);
+
+ auto& icon_image_widget = *widget.find_descendant_of_type_named<GUI::ImageWidget>("icon");
+ icon_image_widget.set_bitmap(GUI::FileIconProvider::icon_for_executable(executable_path).bitmap_for_size(32));
+
+ auto app_name = LexicalPath(executable_path).basename();
+ auto af = Desktop::AppFile::get_for_app(app_name);
+ if (af->is_valid())
+ app_name = af->name();
+
+ auto& description_label = *widget.find_descendant_of_type_named<GUI::Label>("description");
+ description_label.set_text(String::formatted("\"{}\" (PID {}) has crashed - {} (signal {})", app_name, pid, strsignal(termination_signal), termination_signal));
+
+ auto& executable_link_label = *widget.find_descendant_of_type_named<GUI::LinkLabel>("executable_link");
+ executable_link_label.set_text(LexicalPath::canonicalized_path(executable_path));
+ executable_link_label.on_click = [&] {
+ Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(executable_path).dirname()));
+ };
+
+ auto& coredump_link_label = *widget.find_descendant_of_type_named<GUI::LinkLabel>("coredump_link");
+ coredump_link_label.set_text(LexicalPath::canonicalized_path(coredump_path));
+ coredump_link_label.on_click = [&] {
+ Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(coredump_path).dirname()));
+ };
+
+ auto& backtrace_text_editor = *widget.find_descendant_of_type_named<GUI::TextEditor>("backtrace_text_editor");
+ backtrace_text_editor.set_text(backtrace);
+ backtrace_text_editor.set_should_hide_unnecessary_scrollbars(true);
+
+ auto& close_button = *widget.find_descendant_of_type_named<GUI::Button>("close_button");
+ close_button.on_click = [&](auto) {
+ app->quit();
+ };
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/Debugger/CMakeLists.txt b/Userland/Applications/Debugger/CMakeLists.txt
new file mode 100644
index 0000000000..7e6f200ea4
--- /dev/null
+++ b/Userland/Applications/Debugger/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES
+ main.cpp
+)
+
+serenity_bin(Debugger)
+target_link_libraries(Debugger LibCore LibDebug LibX86 LibLine)
diff --git a/Userland/Applications/Debugger/main.cpp b/Userland/Applications/Debugger/main.cpp
new file mode 100644
index 0000000000..ef5bbe9671
--- /dev/null
+++ b/Userland/Applications/Debugger/main.cpp
@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Assertions.h>
+#include <AK/ByteBuffer.h>
+#include <AK/Demangle.h>
+#include <AK/LogStream.h>
+#include <AK/StringBuilder.h>
+#include <AK/kmalloc.h>
+#include <LibC/sys/arch/i386/regs.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/File.h>
+#include <LibDebug/DebugInfo.h>
+#include <LibDebug/DebugSession.h>
+#include <LibLine/Editor.h>
+#include <LibX86/Disassembler.h>
+#include <LibX86/Instruction.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+RefPtr<Line::Editor> editor;
+
+OwnPtr<Debug::DebugSession> g_debug_session;
+
+static void handle_sigint(int)
+{
+ outln("Debugger: SIGINT");
+
+ // The destructor of DebugSession takes care of detaching
+ g_debug_session = nullptr;
+}
+
+static void handle_print_registers(const PtraceRegisters& regs)
+{
+ outln("eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}", regs.eax, regs.ebx, regs.ecx, regs.edx);
+ outln("esp={:08x} ebp={:08x} esi={:08x} edi={:08x}", regs.esp, regs.ebp, regs.esi, regs.edi);
+ outln("eip={:08x} eflags={:08x}", regs.eip, regs.eflags);
+}
+
+static bool handle_disassemble_command(const String& command, void* first_instruction)
+{
+ auto parts = command.split(' ');
+ size_t number_of_instructions_to_disassemble = 5;
+ if (parts.size() == 2) {
+ auto number = parts[1].to_uint();
+ if (!number.has_value())
+ return false;
+ number_of_instructions_to_disassemble = number.value();
+ }
+
+ // FIXME: Instead of using a fixed "dump_size",
+ // we can feed instructions to the disassembler one by one
+ constexpr size_t dump_size = 0x100;
+ ByteBuffer code;
+ for (size_t i = 0; i < dump_size / sizeof(u32); ++i) {
+ auto value = g_debug_session->peek(reinterpret_cast<u32*>(first_instruction) + i);
+ if (!value.has_value())
+ break;
+ code.append(&value, sizeof(u32));
+ }
+
+ X86::SimpleInstructionStream stream(code.data(), code.size());
+ X86::Disassembler disassembler(stream);
+
+ for (size_t i = 0; i < number_of_instructions_to_disassemble; ++i) {
+ auto offset = stream.offset();
+ auto insn = disassembler.next();
+ if (!insn.has_value())
+ break;
+
+ outln(" {:p} <+{}>:\t{}", offset + reinterpret_cast<size_t>(first_instruction), offset, insn.value().to_string(offset));
+ }
+
+ return true;
+}
+
+static bool insert_breakpoint_at_address(FlatPtr address)
+{
+ return g_debug_session->insert_breakpoint((void*)address);
+}
+
+static bool insert_breakpoint_at_source_position(const String& file, size_t line)
+{
+ auto result = g_debug_session->insert_breakpoint(file, line);
+ if (!result.has_value()) {
+ warnln("Could not insert breakpoint at {}:{}", file, line);
+ return false;
+ }
+ outln("Breakpoint inserted [{}:{} ({}:{:p})]", result.value().file_name, result.value().line_number, result.value().library_name, result.value().address);
+ return true;
+}
+
+static bool insert_breakpoint_at_symbol(const String& symbol)
+{
+ auto result = g_debug_session->insert_breakpoint(symbol);
+ if (!result.has_value()) {
+ warnln("Could not insert breakpoint at symbol: {}", symbol);
+ return false;
+ }
+ outln("Breakpoint inserted [{}:{:p}]", result.value().library_name, result.value().address);
+ return true;
+}
+
+static bool handle_breakpoint_command(const String& command)
+{
+ auto parts = command.split(' ');
+ if (parts.size() != 2)
+ return false;
+
+ auto argument = parts[1];
+ if (argument.is_empty())
+ return false;
+
+ if (argument.contains(":")) {
+ auto source_arguments = argument.split(':');
+ if (source_arguments.size() != 2)
+ return false;
+ auto line = source_arguments[1].to_uint();
+ if (!line.has_value())
+ return false;
+ auto file = source_arguments[0];
+ return insert_breakpoint_at_source_position(file, line.value());
+ }
+ if ((argument.starts_with("0x"))) {
+ return insert_breakpoint_at_address(strtoul(argument.characters() + 2, nullptr, 16));
+ }
+
+ return insert_breakpoint_at_symbol(argument);
+}
+
+static bool handle_examine_command(const String& command)
+{
+ auto parts = command.split(' ');
+ if (parts.size() != 2)
+ return false;
+
+ auto argument = parts[1];
+ if (argument.is_empty())
+ return false;
+
+ if (!(argument.starts_with("0x"))) {
+ return false;
+ }
+ u32 address = strtoul(argument.characters() + 2, nullptr, 16);
+ auto res = g_debug_session->peek((u32*)address);
+ if (!res.has_value()) {
+ printf("could not examine memory at address 0x%x\n", address);
+ return true;
+ }
+ printf("0x%x\n", res.value());
+ return true;
+}
+
+static void print_help()
+{
+ out("Options:\n"
+ "cont - Continue execution\n"
+ "si - step to the next instruction\n"
+ "sl - step to the next source line\n"
+ "line - show the position of the current instruction in the source code\n"
+ "regs - Print registers\n"
+ "dis [number of instructions] - Print disassembly\n"
+ "bp <address/symbol/file:line> - Insert a breakpoint\n"
+ "x <address> - examine dword in memory\n");
+}
+
+int main(int argc, char** argv)
+{
+ editor = Line::Editor::construct();
+
+ if (pledge("stdio proc ptrace exec rpath tty sigaction cpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* command = nullptr;
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(command,
+ "The program to be debugged, along with its arguments",
+ "program", Core::ArgsParser::Required::Yes);
+ args_parser.parse(argc, argv);
+
+ auto result = Debug::DebugSession::exec_and_attach(command);
+ if (!result) {
+ warnln("Failed to start debugging session for: \"{}\"", command);
+ exit(1);
+ }
+ g_debug_session = result.release_nonnull();
+
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(struct sigaction));
+ sa.sa_handler = handle_sigint;
+ sigaction(SIGINT, &sa, nullptr);
+
+ Debug::DebugInfo::SourcePosition previous_source_position;
+ bool in_step_line = false;
+
+ g_debug_session->run(Debug::DebugSession::DesiredInitialDebugeeState::Stopped, [&](Debug::DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) {
+ if (reason == Debug::DebugSession::DebugBreakReason::Exited) {
+ outln("Program exited.");
+ return Debug::DebugSession::DebugDecision::Detach;
+ }
+
+ ASSERT(optional_regs.has_value());
+ const PtraceRegisters& regs = optional_regs.value();
+
+ auto symbol_at_ip = g_debug_session->symbolicate(regs.eip);
+
+ auto source_position = g_debug_session->get_source_position(regs.eip);
+
+ if (in_step_line) {
+ bool no_source_info = !source_position.has_value();
+ if (no_source_info || source_position.value() != previous_source_position) {
+ if (no_source_info)
+ outln("No source information for current instruction! stoppoing.");
+ in_step_line = false;
+ } else {
+ return Debug::DebugSession::DebugDecision::SingleStep;
+ }
+ }
+
+ if (symbol_at_ip.has_value())
+ outln("Program is stopped at: {:p} ({}:{})", regs.eip, symbol_at_ip.value().library_name, symbol_at_ip.value().symbol);
+ else
+ outln("Program is stopped at: {:p}", regs.eip);
+
+ if (source_position.has_value()) {
+ previous_source_position = source_position.value();
+ outln("Source location: {}:{}", source_position.value().file_path, source_position.value().line_number);
+ } else {
+ outln("(No source location information for the current instruction)");
+ }
+
+ for (;;) {
+ auto command_result = editor->get_line("(sdb) ");
+
+ if (command_result.is_error())
+ return Debug::DebugSession::DebugDecision::Detach;
+
+ auto& command = command_result.value();
+
+ bool success = false;
+ Optional<Debug::DebugSession::DebugDecision> decision;
+
+ if (command.is_empty() && !editor->history().is_empty()) {
+ command = editor->history().last().entry;
+ }
+ if (command == "cont") {
+ decision = Debug::DebugSession::DebugDecision::Continue;
+ success = true;
+ } else if (command == "si") {
+ decision = Debug::DebugSession::DebugDecision::SingleStep;
+ success = true;
+ } else if (command == "sl") {
+ if (source_position.has_value()) {
+ decision = Debug::DebugSession::DebugDecision::SingleStep;
+ in_step_line = true;
+ success = true;
+ } else {
+ outln("No source location information for the current instruction");
+ }
+ } else if (command == "regs") {
+ handle_print_registers(regs);
+ success = true;
+
+ } else if (command.starts_with("dis")) {
+ success = handle_disassemble_command(command, reinterpret_cast<void*>(regs.eip));
+
+ } else if (command.starts_with("bp")) {
+ success = handle_breakpoint_command(command);
+ } else if (command.starts_with("x")) {
+ success = handle_examine_command(command);
+ }
+
+ if (success && !command.is_empty()) {
+ // Don't add repeated commands to history
+ if (editor->history().is_empty() || editor->history().last().entry != command)
+ editor->add_to_history(command);
+ }
+ if (!success) {
+ print_help();
+ }
+ if (decision.has_value())
+ return decision.value();
+ }
+ });
+}
diff --git a/Userland/Applications/DisplaySettings/CMakeLists.txt b/Userland/Applications/DisplaySettings/CMakeLists.txt
new file mode 100644
index 0000000000..b541894194
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/CMakeLists.txt
@@ -0,0 +1,11 @@
+compile_gml(DisplaySettingsWindow.gml DisplaySettingsWindowGML.h display_settings_window_gml)
+
+set(SOURCES
+ DisplaySettings.cpp
+ DisplaySettingsWindowGML.h
+ main.cpp
+ MonitorWidget.cpp
+)
+
+serenity_app(DisplaySettings ICON app-display-settings)
+target_link_libraries(DisplaySettings LibGUI)
diff --git a/Userland/Applications/DisplaySettings/DisplaySettings.cpp b/Userland/Applications/DisplaySettings/DisplaySettings.cpp
new file mode 100644
index 0000000000..ad68ebf827
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/DisplaySettings.cpp
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2019-2020, Jesse Buhagiar <jooster669@gmail.com>
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DisplaySettings.h"
+#include <AK/StringBuilder.h>
+#include <Applications/DisplaySettings/DisplaySettingsWindowGML.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/DirIterator.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/ComboBox.h>
+#include <LibGUI/Desktop.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/ItemListModel.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/WindowServerConnection.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/SystemTheme.h>
+
+REGISTER_WIDGET(DisplaySettings, MonitorWidget)
+
+DisplaySettingsWidget::DisplaySettingsWidget()
+{
+ create_resolution_list();
+ create_wallpaper_list();
+
+ create_frame();
+
+ load_current_settings();
+}
+
+void DisplaySettingsWidget::create_resolution_list()
+{
+ // TODO: Find a better way to get the default resolution
+ m_resolutions.append({ 640, 480 });
+ m_resolutions.append({ 800, 600 });
+ m_resolutions.append({ 1024, 768 });
+ m_resolutions.append({ 1280, 720 });
+ m_resolutions.append({ 1280, 768 });
+ m_resolutions.append({ 1280, 1024 });
+ m_resolutions.append({ 1360, 768 });
+ m_resolutions.append({ 1368, 768 });
+ m_resolutions.append({ 1440, 900 });
+ m_resolutions.append({ 1600, 900 });
+ m_resolutions.append({ 1920, 1080 });
+ m_resolutions.append({ 2560, 1080 });
+}
+
+void DisplaySettingsWidget::create_wallpaper_list()
+{
+ Core::DirIterator iterator("/res/wallpapers/", Core::DirIterator::Flags::SkipDots);
+
+ m_wallpapers.append("Use background color");
+
+ while (iterator.has_next()) {
+ m_wallpapers.append(iterator.next_path());
+ }
+
+ m_modes.append("simple");
+ m_modes.append("tile");
+ m_modes.append("center");
+ m_modes.append("scaled");
+}
+
+void DisplaySettingsWidget::create_frame()
+{
+ load_from_gml(display_settings_window_gml);
+
+ m_monitor_widget = *find_descendant_of_type_named<DisplaySettings::MonitorWidget>("monitor_widget");
+
+ m_wallpaper_combo = *find_descendant_of_type_named<GUI::ComboBox>("wallpaper_combo");
+ m_wallpaper_combo->set_only_allow_values_from_model(true);
+ m_wallpaper_combo->set_model(*GUI::ItemListModel<AK::String>::create(m_wallpapers));
+ m_wallpaper_combo->on_change = [this](auto& text, const GUI::ModelIndex& index) {
+ String path = text;
+ if (path.starts_with("/") && m_monitor_widget->set_wallpaper(path)) {
+ m_monitor_widget->update();
+ return;
+ }
+
+ if (index.row() == 0) {
+ path = "";
+ } else {
+ if (index.is_valid()) {
+ StringBuilder builder;
+ builder.append("/res/wallpapers/");
+ builder.append(path);
+ path = builder.to_string();
+ }
+ }
+
+ m_monitor_widget->set_wallpaper(path);
+ m_monitor_widget->update();
+ };
+
+ auto& button = *find_descendant_of_type_named<GUI::Button>("wallpaper_open_button");
+ button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"));
+ button.on_click = [this](auto) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(nullptr, "Select wallpaper from file system.");
+
+ if (!open_path.has_value())
+ return;
+
+ m_wallpaper_combo->set_only_allow_values_from_model(false);
+ m_wallpaper_combo->set_text(open_path.value());
+ m_wallpaper_combo->set_only_allow_values_from_model(true);
+ };
+
+ m_mode_combo = *find_descendant_of_type_named<GUI::ComboBox>("mode_combo");
+ m_mode_combo->set_only_allow_values_from_model(true);
+ m_mode_combo->set_model(*GUI::ItemListModel<AK::String>::create(m_modes));
+ m_mode_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
+ m_monitor_widget->set_wallpaper_mode(m_modes.at(index.row()));
+ m_monitor_widget->update();
+ };
+
+ m_resolution_combo = *find_descendant_of_type_named<GUI::ComboBox>("resolution_combo");
+ m_resolution_combo->set_only_allow_values_from_model(true);
+ m_resolution_combo->set_model(*GUI::ItemListModel<Gfx::IntSize>::create(m_resolutions));
+ m_resolution_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
+ m_monitor_widget->set_desktop_resolution(m_resolutions.at(index.row()));
+ m_monitor_widget->update();
+ };
+
+ m_color_input = *find_descendant_of_type_named<GUI::ColorInput>("color_input");
+ m_color_input->set_color_has_alpha_channel(false);
+ m_color_input->set_color_picker_title("Select color for desktop");
+ m_color_input->on_change = [this] {
+ m_monitor_widget->set_background_color(m_color_input->color());
+ m_monitor_widget->update();
+ };
+
+ auto& ok_button = *find_descendant_of_type_named<GUI::Button>("ok_button");
+ ok_button.on_click = [this](auto) {
+ send_settings_to_window_server();
+ GUI::Application::the()->quit();
+ };
+
+ auto& cancel_button = *find_descendant_of_type_named<GUI::Button>("cancel_button");
+ cancel_button.on_click = [](auto) {
+ GUI::Application::the()->quit();
+ };
+
+ auto& apply_button = *find_descendant_of_type_named<GUI::Button>("apply_button");
+ apply_button.on_click = [this](auto) {
+ send_settings_to_window_server();
+ };
+}
+
+void DisplaySettingsWidget::load_current_settings()
+{
+ auto ws_config(Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"));
+ auto wm_config = Core::ConfigFile::get_for_app("WindowManager");
+
+ /// Wallpaper path ////////////////////////////////////////////////////////////////////////////
+ /// Read wallpaper path from config file and set value to monitor widget and combo box.
+ auto selected_wallpaper = wm_config->read_entry("Background", "Wallpaper", "");
+ if (!selected_wallpaper.is_empty()) {
+ m_monitor_widget->set_wallpaper(selected_wallpaper);
+
+ Optional<size_t> optional_index;
+ if (selected_wallpaper.starts_with("/res/wallpapers/")) {
+ auto name_parts = selected_wallpaper.split('/', true);
+ optional_index = m_wallpapers.find_first_index(name_parts[2]);
+
+ if (optional_index.has_value()) {
+ m_wallpaper_combo->set_selected_index(optional_index.value());
+ }
+ }
+
+ if (!optional_index.has_value()) {
+ m_wallpaper_combo->set_only_allow_values_from_model(false);
+ m_wallpaper_combo->set_text(selected_wallpaper);
+ m_wallpaper_combo->set_only_allow_values_from_model(true);
+ }
+ } else {
+ m_wallpaper_combo->set_selected_index(0);
+ }
+
+ size_t index;
+
+ /// Mode //////////////////////////////////////////////////////////////////////////////////////
+ auto mode = ws_config->read_entry("Background", "Mode", "simple");
+ if (!m_modes.contains_slow(mode)) {
+ warnln("Invalid background mode '{}' in WindowServer config, falling back to 'simple'", mode);
+ mode = "simple";
+ }
+ m_monitor_widget->set_wallpaper_mode(mode);
+ index = m_modes.find_first_index(mode).value();
+ m_mode_combo->set_selected_index(index);
+
+ /// Resolution ////////////////////////////////////////////////////////////////////////////////
+ Gfx::IntSize find_size;
+
+ // Let's attempt to find the current resolution and select it!
+ find_size.set_width(ws_config->read_num_entry("Screen", "Width", 1024));
+ find_size.set_height(ws_config->read_num_entry("Screen", "Height", 768));
+
+ index = m_resolutions.find_first_index(find_size).value_or(0);
+ Gfx::IntSize m_current_resolution = m_resolutions.at(index);
+ m_monitor_widget->set_desktop_resolution(m_current_resolution);
+ m_resolution_combo->set_selected_index(index);
+
+ /// Color /////////////////////////////////////////////////////////////////////////////////////
+ /// If presend read from config file. If not paint with palet color.
+ Color palette_desktop_color = palette().desktop_background();
+
+ auto background_color = ws_config->read_entry("Background", "Color", "");
+ if (!background_color.is_empty()) {
+ auto opt_color = Color::from_string(background_color);
+ if (opt_color.has_value())
+ palette_desktop_color = opt_color.value();
+ }
+
+ m_color_input->set_color(palette_desktop_color);
+ m_monitor_widget->set_background_color(palette_desktop_color);
+
+ m_monitor_widget->update();
+}
+
+void DisplaySettingsWidget::send_settings_to_window_server()
+{
+ auto result = GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::SetResolution>(m_monitor_widget->desktop_resolution());
+ if (!result->success()) {
+ GUI::MessageBox::show(nullptr, String::formatted("Reverting to resolution {}x{}", result->resolution().width(), result->resolution().height()),
+ "Unable to set resolution", GUI::MessageBox::Type::Error);
+ }
+
+ if (!m_monitor_widget->wallpaper().is_empty()) {
+ GUI::Desktop::the().set_wallpaper(m_monitor_widget->wallpaper());
+ } else {
+ dbgln("Setting color input: __{}__", m_color_input->text());
+ GUI::Desktop::the().set_wallpaper("");
+ GUI::Desktop::the().set_background_color(m_color_input->text());
+ }
+
+ GUI::Desktop::the().set_wallpaper_mode(m_monitor_widget->wallpaper_mode());
+}
diff --git a/Userland/Applications/DisplaySettings/DisplaySettings.h b/Userland/Applications/DisplaySettings/DisplaySettings.h
new file mode 100644
index 0000000000..30fb26ebed
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/DisplaySettings.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2019-2020, Jesse Buhagiar <jooster669@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "MonitorWidget.h"
+#include <LibGUI/ColorInput.h>
+#include <LibGUI/ComboBox.h>
+
+class DisplaySettingsWidget : public GUI::Widget {
+ C_OBJECT(DisplaySettingsWidget);
+
+public:
+ DisplaySettingsWidget();
+
+private:
+ void create_frame();
+ void create_wallpaper_list();
+ void create_resolution_list();
+ void load_current_settings();
+ void send_settings_to_window_server(); // Apply the settings to the Window Server
+
+ Vector<String> m_wallpapers;
+ Vector<String> m_modes;
+ Vector<Gfx::IntSize> m_resolutions;
+
+ RefPtr<DisplaySettings::MonitorWidget> m_monitor_widget;
+ RefPtr<GUI::ComboBox> m_wallpaper_combo;
+ RefPtr<GUI::ComboBox> m_mode_combo;
+ RefPtr<GUI::ComboBox> m_resolution_combo;
+ RefPtr<GUI::ColorInput> m_color_input;
+};
diff --git a/Userland/Applications/DisplaySettings/DisplaySettingsWindow.gml b/Userland/Applications/DisplaySettings/DisplaySettingsWindow.gml
new file mode 100644
index 0000000000..af9f204f03
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/DisplaySettingsWindow.gml
@@ -0,0 +1,117 @@
+@GUI::Widget {
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [4, 4, 4, 4]
+ }
+
+ @DisplaySettings::MonitorWidget {
+ name: "monitor_widget"
+ fixed_width: 338
+ fixed_height: 248
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Wallpaper:"
+ text_alignment: "CenterLeft"
+ fixed_width: 70
+ }
+
+ @GUI::ComboBox {
+ name: "wallpaper_combo"
+ }
+
+ @GUI::Button {
+ name: "wallpaper_open_button"
+ tooltip: "Select wallpaper from file system."
+ button_style: "CoolBar"
+ fixed_width: 22
+ fixed_height: 22
+ }
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Modes:"
+ text_alignment: "CenterLeft"
+ fixed_width: 70
+ }
+
+ @GUI::ComboBox {
+ name: "mode_combo"
+ }
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Resolution:"
+ text_alignment: "CenterLeft"
+ fixed_width: 70
+ }
+
+ @GUI::ComboBox {
+ name: "resolution_combo"
+ }
+ }
+
+
+ @GUI::Widget {
+ shrink_to_fit: true
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Color:"
+ text_alignment: "CenterLeft"
+ fixed_width: 70
+ }
+
+ @GUI::ColorInput {
+ name: "color_input"
+ fixed_width: 90
+ }
+ }
+
+ @GUI::Widget {
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Widget {
+ }
+
+ @GUI::Button {
+ name: "ok_button"
+ text: "OK"
+ fixed_width: 60
+ }
+
+ @GUI::Button {
+ name: "cancel_button"
+ text: "Cancel"
+ fixed_width: 60
+ }
+
+ @GUI::Button {
+ name: "apply_button"
+ text: "Apply"
+ fixed_width: 60
+ }
+ }
+}
diff --git a/Userland/Applications/DisplaySettings/MonitorWidget.cpp b/Userland/Applications/DisplaySettings/MonitorWidget.cpp
new file mode 100644
index 0000000000..d6b809d449
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/MonitorWidget.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MonitorWidget.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+
+namespace DisplaySettings {
+
+MonitorWidget::MonitorWidget()
+{
+ m_monitor_bitmap = Gfx::Bitmap::load_from_file("/res/graphics/monitor.png");
+ m_monitor_rect = { 8, 9, 320, 180 };
+}
+
+bool MonitorWidget::set_wallpaper(String path)
+{
+ auto bitmap_ptr = Gfx::Bitmap::load_from_file(path);
+ if (!bitmap_ptr && !path.is_empty())
+ return false;
+ m_desktop_wallpaper_path = path;
+ m_desktop_wallpaper_bitmap = bitmap_ptr;
+ return true;
+}
+
+String MonitorWidget::wallpaper()
+{
+ return m_desktop_wallpaper_path;
+}
+
+void MonitorWidget::set_wallpaper_mode(String mode)
+{
+ m_desktop_wallpaper_mode = mode;
+}
+
+String MonitorWidget::wallpaper_mode()
+{
+ return m_desktop_wallpaper_mode;
+}
+
+void MonitorWidget::set_desktop_resolution(Gfx::IntSize resolution)
+{
+ m_desktop_resolution = resolution;
+}
+
+Gfx::IntSize MonitorWidget::desktop_resolution()
+{
+ return m_desktop_resolution;
+}
+
+void MonitorWidget::set_background_color(Gfx::Color color)
+{
+ m_desktop_color = color;
+}
+
+Gfx::Color MonitorWidget::background_color()
+{
+ return m_desktop_color;
+}
+
+void MonitorWidget::paint_event(GUI::PaintEvent& event)
+{
+ Gfx::IntRect screen_rect = { 0, 0, m_desktop_resolution.width(), m_desktop_resolution.height() };
+ auto screen_bitmap = Gfx::Bitmap::create(m_monitor_bitmap->format(), m_desktop_resolution);
+ GUI::Painter screen_painter(*screen_bitmap);
+ screen_painter.fill_rect(screen_rect, m_desktop_color);
+
+ if (!m_desktop_wallpaper_bitmap.is_null()) {
+ if (m_desktop_wallpaper_mode == "simple") {
+ screen_painter.blit({ 0, 0 }, *m_desktop_wallpaper_bitmap, m_desktop_wallpaper_bitmap->rect());
+ } else if (m_desktop_wallpaper_mode == "center") {
+ Gfx::IntPoint offset { screen_rect.width() / 2 - m_desktop_wallpaper_bitmap->size().width() / 2, screen_rect.height() / 2 - m_desktop_wallpaper_bitmap->size().height() / 2 };
+ screen_painter.blit_offset(screen_rect.location(), *m_desktop_wallpaper_bitmap, screen_rect, offset);
+ } else if (m_desktop_wallpaper_mode == "tile") {
+ screen_painter.draw_tiled_bitmap(screen_bitmap->rect(), *m_desktop_wallpaper_bitmap);
+ } else if (m_desktop_wallpaper_mode == "scaled") {
+ screen_painter.draw_scaled_bitmap(screen_bitmap->rect(), *m_desktop_wallpaper_bitmap, m_desktop_wallpaper_bitmap->rect());
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.blit({ 0, 0 }, *m_monitor_bitmap, m_monitor_bitmap->rect());
+ painter.draw_scaled_bitmap(m_monitor_rect, *screen_bitmap, screen_bitmap->rect());
+
+ if (!m_desktop_resolution.is_null()) {
+ painter.draw_text(m_monitor_rect.translated(1, 1), m_desktop_resolution.to_string(), Gfx::TextAlignment::Center, Color::Black);
+ painter.draw_text(m_monitor_rect, m_desktop_resolution.to_string(), Gfx::TextAlignment::Center, Color::White);
+ }
+}
+
+}
diff --git a/Userland/Applications/DisplaySettings/MonitorWidget.h b/Userland/Applications/DisplaySettings/MonitorWidget.h
new file mode 100644
index 0000000000..1961e91330
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/MonitorWidget.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+namespace DisplaySettings {
+
+class MonitorWidget final : public GUI::Widget {
+ C_OBJECT(MonitorWidget);
+
+public:
+ bool set_wallpaper(String path);
+ String wallpaper();
+
+ void set_wallpaper_mode(String mode);
+ String wallpaper_mode();
+
+ void set_desktop_resolution(Gfx::IntSize resolution);
+ Gfx::IntSize desktop_resolution();
+
+ void set_background_color(Gfx::Color background_color);
+ Gfx::Color background_color();
+
+private:
+ MonitorWidget();
+
+ virtual void paint_event(GUI::PaintEvent& event) override;
+
+ Gfx::IntRect m_monitor_rect;
+ RefPtr<Gfx::Bitmap> m_monitor_bitmap;
+
+ String m_desktop_wallpaper_path;
+ RefPtr<Gfx::Bitmap> m_desktop_wallpaper_bitmap;
+ String m_desktop_wallpaper_mode;
+ Gfx::IntSize m_desktop_resolution;
+ Gfx::Color m_desktop_color;
+};
+
+}
diff --git a/Userland/Applications/DisplaySettings/main.cpp b/Userland/Applications/DisplaySettings/main.cpp
new file mode 100644
index 0000000000..5018893470
--- /dev/null
+++ b/Userland/Applications/DisplaySettings/main.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2019-2020, Jesse Buhagiar <jooster669@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DisplaySettings.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread shared_buffer rpath accept cpath wpath unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread shared_buffer rpath accept cpath wpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-display-settings");
+
+ // Let's create the tab pane that we'll hook our widgets up to :^)
+ auto tab_widget = GUI::TabWidget::construct();
+ tab_widget->add_tab<DisplaySettingsWidget>("Display Settings");
+ tab_widget->set_fill_with_background_color(true); // No black backgrounds!
+
+ auto window = GUI::Window::construct();
+ dbgln("main window: {}", window);
+ window->set_title("Display Settings");
+ window->resize(360, 410);
+ window->set_resizable(false);
+ window->set_main_widget(tab_widget.ptr());
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Display Settings");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](const GUI::Action&) {
+ app->quit();
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Display Settings", app_icon));
+
+ app->set_menubar(move(menubar));
+ window->show();
+ return app->exec();
+}
diff --git a/Userland/Applications/FileManager/CMakeLists.txt b/Userland/Applications/FileManager/CMakeLists.txt
new file mode 100644
index 0000000000..d81fc89bef
--- /dev/null
+++ b/Userland/Applications/FileManager/CMakeLists.txt
@@ -0,0 +1,13 @@
+compile_gml(FileManagerWindow.gml FileManagerWindowGML.h file_manager_window_gml)
+
+set(SOURCES
+ DesktopWidget.cpp
+ DirectoryView.cpp
+ FileManagerWindowGML.h
+ FileUtils.cpp
+ main.cpp
+ PropertiesWindow.cpp
+)
+
+serenity_app(FileManager ICON filetype-folder)
+target_link_libraries(FileManager LibGUI LibDesktop)
diff --git a/Userland/Applications/FileManager/DesktopWidget.cpp b/Userland/Applications/FileManager/DesktopWidget.cpp
new file mode 100644
index 0000000000..7ea01de9e8
--- /dev/null
+++ b/Userland/Applications/FileManager/DesktopWidget.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DesktopWidget.h"
+#include <LibGUI/Painter.h>
+
+namespace FileManager {
+
+DesktopWidget::DesktopWidget()
+{
+}
+
+DesktopWidget::~DesktopWidget()
+{
+}
+
+void DesktopWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.clear_rect(event.rect(), Color(0, 0, 0, 0));
+}
+
+}
diff --git a/Userland/Applications/FileManager/DesktopWidget.h b/Userland/Applications/FileManager/DesktopWidget.h
new file mode 100644
index 0000000000..a6fd5b614b
--- /dev/null
+++ b/Userland/Applications/FileManager/DesktopWidget.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+namespace FileManager {
+
+class DesktopWidget final : public GUI::Widget {
+ C_OBJECT(DesktopWidget);
+
+public:
+ virtual ~DesktopWidget() override;
+
+private:
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ DesktopWidget();
+};
+
+}
diff --git a/Userland/Applications/FileManager/DirectoryView.cpp b/Userland/Applications/FileManager/DirectoryView.cpp
new file mode 100644
index 0000000000..6d47d89671
--- /dev/null
+++ b/Userland/Applications/FileManager/DirectoryView.cpp
@@ -0,0 +1,591 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DirectoryView.h"
+#include "FileUtils.h"
+#include <AK/LexicalPath.h>
+#include <AK/NumberFormat.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/MimeData.h>
+#include <LibCore/StandardPaths.h>
+#include <LibGUI/FileIconProvider.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/ModelEditingDelegate.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <serenity.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <unistd.h>
+
+namespace FileManager {
+
+NonnullRefPtr<GUI::Action> LauncherHandler::create_launch_action(Function<void(const LauncherHandler&)> launch_handler)
+{
+ auto icon = GUI::FileIconProvider::icon_for_executable(details().executable).bitmap_for_size(16);
+ return GUI::Action::create(details().name, move(icon), [this, launch_handler = move(launch_handler)](auto&) {
+ launch_handler(*this);
+ });
+}
+
+RefPtr<LauncherHandler> DirectoryView::get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers)
+{
+ // If this is an application, pick it first
+ for (size_t i = 0; i < handlers.size(); i++) {
+ if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::Application)
+ return handlers[i];
+ }
+ // If there's a handler preferred by the user, pick this first
+ for (size_t i = 0; i < handlers.size(); i++) {
+ if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserPreferred)
+ return handlers[i];
+ }
+ // Otherwise, use the user's default, if available
+ for (size_t i = 0; i < handlers.size(); i++) {
+ if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserDefault)
+ return handlers[i];
+ }
+ // If still no match, use the first one we find
+ if (!handlers.is_empty()) {
+ return handlers[0];
+ }
+
+ return {};
+}
+
+NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const URL& url)
+{
+ NonnullRefPtrVector<LauncherHandler> handlers;
+ for (auto& h : Desktop::Launcher::get_handlers_with_details_for_url(url)) {
+ handlers.append(adopt(*new LauncherHandler(h)));
+ }
+ return handlers;
+}
+
+NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const String& path)
+{
+ return get_launch_handlers(URL::create_with_file_protocol(path));
+}
+
+void DirectoryView::handle_activation(const GUI::ModelIndex& index)
+{
+ if (!index.is_valid())
+ return;
+ dbgln("on activation: {},{}, this={:p}, m_model={:p}", index.row(), index.column(), this, m_model.ptr());
+ auto& node = this->node(index);
+ auto path = node.full_path();
+
+ struct stat st;
+ if (stat(path.characters(), &st) < 0) {
+ perror("stat");
+ return;
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ if (is_desktop()) {
+ Desktop::Launcher::open(URL::create_with_file_protocol(path));
+ return;
+ }
+ open(path);
+ return;
+ }
+
+ auto url = URL::create_with_file_protocol(path);
+ auto launcher_handlers = get_launch_handlers(url);
+ auto default_launcher = get_default_launch_handler(launcher_handlers);
+ if (default_launcher) {
+ launch(url, *default_launcher);
+ } else {
+ auto error_message = String::format("Could not open %s", path.characters());
+ GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
+ }
+}
+
+DirectoryView::DirectoryView(Mode mode)
+ : m_mode(mode)
+ , m_model(GUI::FileSystemModel::create({}))
+ , m_sorting_model(GUI::SortingProxyModel::create(m_model))
+{
+ set_active_widget(nullptr);
+ set_content_margins({ 2, 2, 2, 2 });
+
+ setup_actions();
+
+ m_error_label = add<GUI::Label>();
+ m_error_label->set_font(m_error_label->font().bold_variant());
+
+ setup_model();
+
+ setup_icon_view();
+ if (mode != Mode::Desktop) {
+ setup_columns_view();
+ setup_table_view();
+ }
+
+ set_view_mode(ViewMode::Icon);
+}
+
+const GUI::FileSystemModel::Node& DirectoryView::node(const GUI::ModelIndex& index) const
+{
+ return model().node(m_sorting_model->map_to_source(index));
+}
+
+void DirectoryView::setup_model()
+{
+ m_model->on_error = [this](int, const char* error_string) {
+ auto failed_path = m_model->root_path();
+ auto error_message = String::formatted("Could not read {}:\n{}", failed_path, error_string);
+ m_error_label->set_text(error_message);
+ set_active_widget(m_error_label);
+
+ m_mkdir_action->set_enabled(false);
+ m_touch_action->set_enabled(false);
+
+ add_path_to_history(model().root_path());
+
+ if (on_path_change)
+ on_path_change(failed_path, false);
+ };
+
+ m_model->on_complete = [this] {
+ if (m_table_view)
+ m_table_view->selection().clear();
+ if (m_icon_view)
+ m_icon_view->selection().clear();
+
+ add_path_to_history(model().root_path());
+
+ bool can_write_in_path = access(model().root_path().characters(), W_OK) == 0;
+
+ m_mkdir_action->set_enabled(can_write_in_path);
+ m_touch_action->set_enabled(can_write_in_path);
+
+ if (on_path_change)
+ on_path_change(model().root_path(), can_write_in_path);
+ };
+
+ m_model->register_client(*this);
+
+ m_model->on_thumbnail_progress = [this](int done, int total) {
+ if (on_thumbnail_progress)
+ on_thumbnail_progress(done, total);
+ };
+
+ if (is_desktop())
+ m_model->set_root_path(Core::StandardPaths::desktop_directory());
+}
+
+void DirectoryView::setup_icon_view()
+{
+ m_icon_view = add<GUI::IconView>();
+ m_icon_view->set_should_hide_unnecessary_scrollbars(true);
+ m_icon_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
+ m_icon_view->set_editable(true);
+ m_icon_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
+ m_icon_view->aid_create_editing_delegate = [](auto&) {
+ return make<GUI::StringModelEditingDelegate>();
+ };
+
+ if (is_desktop()) {
+ m_icon_view->set_frame_shape(Gfx::FrameShape::NoFrame);
+ m_icon_view->set_scrollbars_enabled(false);
+ m_icon_view->set_fill_with_background_color(false);
+ m_icon_view->set_draw_item_text_with_shadow(true);
+ m_icon_view->set_flow_direction(GUI::IconView::FlowDirection::TopToBottom);
+ }
+
+ m_icon_view->set_model(m_sorting_model);
+ m_icon_view->set_model_column(GUI::FileSystemModel::Column::Name);
+ m_icon_view->on_activation = [&](auto& index) {
+ handle_activation(index);
+ };
+ m_icon_view->on_selection_change = [this] {
+ handle_selection_change();
+ };
+ m_icon_view->on_context_menu_request = [this](auto& index, auto& event) {
+ if (on_context_menu_request)
+ on_context_menu_request(index, event);
+ };
+ m_icon_view->on_drop = [this](auto& index, auto& event) {
+ handle_drop(index, event);
+ };
+}
+
+void DirectoryView::setup_columns_view()
+{
+ m_columns_view = add<GUI::ColumnsView>();
+ m_columns_view->set_should_hide_unnecessary_scrollbars(true);
+ m_columns_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
+ m_columns_view->set_editable(true);
+ m_columns_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
+ m_columns_view->aid_create_editing_delegate = [](auto&) {
+ return make<GUI::StringModelEditingDelegate>();
+ };
+
+ m_columns_view->set_model(m_sorting_model);
+ m_columns_view->set_model_column(GUI::FileSystemModel::Column::Name);
+
+ m_columns_view->on_activation = [&](auto& index) {
+ handle_activation(index);
+ };
+
+ m_columns_view->on_selection_change = [this] {
+ handle_selection_change();
+ };
+
+ m_columns_view->on_context_menu_request = [this](auto& index, auto& event) {
+ if (on_context_menu_request)
+ on_context_menu_request(index, event);
+ };
+
+ m_columns_view->on_drop = [this](auto& index, auto& event) {
+ handle_drop(index, event);
+ };
+}
+
+void DirectoryView::setup_table_view()
+{
+ m_table_view = add<GUI::TableView>();
+ m_table_view->set_should_hide_unnecessary_scrollbars(true);
+ m_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
+ m_table_view->set_editable(true);
+ m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
+ m_table_view->aid_create_editing_delegate = [](auto&) {
+ return make<GUI::StringModelEditingDelegate>();
+ };
+
+ m_table_view->set_model(m_sorting_model);
+ m_table_view->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
+
+ m_table_view->on_activation = [&](auto& index) {
+ handle_activation(index);
+ };
+
+ m_table_view->on_selection_change = [this] {
+ handle_selection_change();
+ };
+
+ m_table_view->on_context_menu_request = [this](auto& index, auto& event) {
+ if (on_context_menu_request)
+ on_context_menu_request(index, event);
+ };
+
+ m_table_view->on_drop = [this](auto& index, auto& event) {
+ handle_drop(index, event);
+ };
+}
+
+DirectoryView::~DirectoryView()
+{
+ m_model->unregister_client(*this);
+}
+
+void DirectoryView::model_did_update(unsigned flags)
+{
+ if (flags & GUI::Model::UpdateFlag::InvalidateAllIndexes) {
+ for_each_view_implementation([](auto& view) {
+ view.selection().clear();
+ });
+ }
+ update_statusbar();
+}
+
+void DirectoryView::set_view_mode(ViewMode mode)
+{
+ if (m_view_mode == mode)
+ return;
+ m_view_mode = mode;
+ update();
+ if (mode == ViewMode::Table) {
+ set_active_widget(m_table_view);
+ return;
+ }
+ if (mode == ViewMode::Columns) {
+ set_active_widget(m_columns_view);
+ return;
+ }
+ if (mode == ViewMode::Icon) {
+ set_active_widget(m_icon_view);
+ return;
+ }
+ ASSERT_NOT_REACHED();
+}
+
+void DirectoryView::add_path_to_history(const StringView& path)
+{
+ if (m_path_history.size() && m_path_history.at(m_path_history_position) == path)
+ return;
+
+ if (m_path_history_position < m_path_history.size())
+ m_path_history.resize(m_path_history_position + 1);
+
+ m_path_history.append(path);
+ m_path_history_position = m_path_history.size() - 1;
+}
+
+void DirectoryView::open(const StringView& path)
+{
+ if (model().root_path() == path) {
+ model().update();
+ return;
+ }
+
+ set_active_widget(&current_view());
+ model().set_root_path(path);
+}
+
+void DirectoryView::set_status_message(const StringView& message)
+{
+ if (on_status_message)
+ on_status_message(message);
+}
+
+void DirectoryView::open_parent_directory()
+{
+ auto path = String::formatted("{}/..", model().root_path());
+ model().set_root_path(path);
+}
+
+void DirectoryView::refresh()
+{
+ model().update();
+}
+
+void DirectoryView::open_previous_directory()
+{
+ if (m_path_history_position > 0) {
+ set_active_widget(&current_view());
+ m_path_history_position--;
+ model().set_root_path(m_path_history[m_path_history_position]);
+ }
+}
+void DirectoryView::open_next_directory()
+{
+ if (m_path_history_position < m_path_history.size() - 1) {
+ set_active_widget(&current_view());
+ m_path_history_position++;
+ model().set_root_path(m_path_history[m_path_history_position]);
+ }
+}
+
+void DirectoryView::update_statusbar()
+{
+ // If we're triggered during widget construction, just ignore it.
+ if (m_view_mode == ViewMode::Invalid)
+ return;
+
+ size_t total_size = model().node({}).total_size;
+ if (current_view().selection().is_empty()) {
+ set_status_message(String::formatted("{} item(s) ({})",
+ model().row_count(),
+ human_readable_size(total_size)));
+ return;
+ }
+
+ int selected_item_count = current_view().selection().size();
+ size_t selected_byte_count = 0;
+
+ current_view().selection().for_each_index([&](auto& index) {
+ auto& model = *current_view().model();
+ auto size_index = model.index(index.row(), GUI::FileSystemModel::Column::Size, model.parent_index(index));
+ auto file_size = size_index.data().to_i32();
+ selected_byte_count += file_size;
+ });
+
+ StringBuilder builder;
+ builder.append(String::number(selected_item_count));
+ builder.append(" item");
+ if (selected_item_count != 1)
+ builder.append('s');
+ builder.append(" selected (");
+ builder.append(human_readable_size(selected_byte_count).characters());
+ builder.append(')');
+
+ if (selected_item_count == 1) {
+ auto& node = this->node(current_view().selection().first());
+ if (!node.symlink_target.is_empty()) {
+ builder.append(" -> ");
+ builder.append(node.symlink_target);
+ }
+ }
+
+ set_status_message(builder.to_string());
+}
+
+void DirectoryView::set_should_show_dotfiles(bool show_dotfiles)
+{
+ m_model->set_should_show_dotfiles(show_dotfiles);
+}
+
+void DirectoryView::launch(const URL&, const LauncherHandler& launcher_handler)
+{
+ pid_t child;
+ if (launcher_handler.details().launcher_type == Desktop::Launcher::LauncherType::Application) {
+ const char* argv[] = { launcher_handler.details().name.characters(), nullptr };
+ posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
+ if (disown(child) < 0)
+ perror("disown");
+ } else {
+ for (auto& path : selected_file_paths()) {
+ const char* argv[] = { launcher_handler.details().name.characters(), path.characters(), nullptr };
+ posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
+ if (disown(child) < 0)
+ perror("disown");
+ }
+ }
+}
+
+Vector<String> DirectoryView::selected_file_paths() const
+{
+ Vector<String> paths;
+ auto& view = current_view();
+ auto& model = *view.model();
+ view.selection().for_each_index([&](const GUI::ModelIndex& index) {
+ auto parent_index = model.parent_index(index);
+ auto name_index = model.index(index.row(), GUI::FileSystemModel::Column::Name, parent_index);
+ auto path = name_index.data(GUI::ModelRole::Custom).to_string();
+ paths.append(path);
+ });
+ return paths;
+}
+
+void DirectoryView::do_delete(bool should_confirm)
+{
+ auto paths = selected_file_paths();
+ ASSERT(!paths.is_empty());
+ FileUtils::delete_paths(paths, should_confirm, window());
+}
+
+void DirectoryView::handle_selection_change()
+{
+ update_statusbar();
+
+ bool can_delete = !current_view().selection().is_empty() && access(path().characters(), W_OK) == 0;
+ m_delete_action->set_enabled(can_delete);
+ m_force_delete_action->set_enabled(can_delete);
+
+ if (on_selection_change)
+ on_selection_change(current_view());
+}
+
+void DirectoryView::setup_actions()
+{
+ m_mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter name:", "New directory") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto new_dir_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
+ int rc = mkdir(new_dir_path.characters(), 0777);
+ if (rc < 0) {
+ auto saved_errno = errno;
+ GUI::MessageBox::show(window(), String::formatted("mkdir(\"{}\") failed: {}", new_dir_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
+ }
+ }
+ });
+
+ m_touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter name:", "New file") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto new_file_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
+ struct stat st;
+ int rc = stat(new_file_path.characters(), &st);
+ if ((rc < 0 && errno != ENOENT)) {
+ auto saved_errno = errno;
+ GUI::MessageBox::show(window(), String::formatted("stat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ if (rc == 0) {
+ GUI::MessageBox::show(window(), String::formatted("{}: Already exists", new_file_path), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ int fd = creat(new_file_path.characters(), 0666);
+ if (fd < 0) {
+ auto saved_errno = errno;
+ GUI::MessageBox::show(window(), String::formatted("creat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ rc = close(fd);
+ ASSERT(rc >= 0);
+ }
+ });
+
+ m_open_terminal_action = GUI::Action::create("Open Terminal here", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
+ posix_spawn_file_actions_t spawn_actions;
+ posix_spawn_file_actions_init(&spawn_actions);
+ posix_spawn_file_actions_addchdir(&spawn_actions, path().characters());
+ pid_t pid;
+ const char* argv[] = { "Terminal", nullptr };
+ if ((errno = posix_spawn(&pid, "/bin/Terminal", &spawn_actions, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ } else {
+ if (disown(pid) < 0)
+ perror("disown");
+ }
+ posix_spawn_file_actions_destroy(&spawn_actions);
+ });
+
+ m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { do_delete(true); }, window());
+
+ m_force_delete_action = GUI::Action::create(
+ "Delete without confirmation", { Mod_Shift, Key_Delete },
+ [this](auto&) { do_delete(false); },
+ window());
+}
+
+void DirectoryView::handle_drop(const GUI::ModelIndex& index, const GUI::DropEvent& event)
+{
+ if (!event.mime_data().has_urls())
+ return;
+ auto urls = event.mime_data().urls();
+ if (urls.is_empty()) {
+ dbgln("No files to drop");
+ return;
+ }
+
+ auto& target_node = node(index);
+ if (!target_node.is_directory())
+ return;
+
+ bool had_accepted_drop = false;
+ for (auto& url_to_copy : urls) {
+ if (!url_to_copy.is_valid() || url_to_copy.path() == target_node.full_path())
+ continue;
+ auto new_path = String::formatted("{}/{}", target_node.full_path(), LexicalPath(url_to_copy.path()).basename());
+ if (url_to_copy.path() == new_path)
+ continue;
+
+ if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
+ auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
+ GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
+ } else {
+ had_accepted_drop = true;
+ }
+ }
+ if (had_accepted_drop && on_accepted_drop)
+ on_accepted_drop();
+}
+
+}
diff --git a/Userland/Applications/FileManager/DirectoryView.h b/Userland/Applications/FileManager/DirectoryView.h
new file mode 100644
index 0000000000..a72b63aaf5
--- /dev/null
+++ b/Userland/Applications/FileManager/DirectoryView.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/URL.h>
+#include <AK/Vector.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ColumnsView.h>
+#include <LibGUI/FileSystemModel.h>
+#include <LibGUI/IconView.h>
+#include <LibGUI/StackWidget.h>
+#include <LibGUI/TableView.h>
+#include <sys/stat.h>
+
+namespace FileManager {
+
+class LauncherHandler : public RefCounted<LauncherHandler> {
+public:
+ LauncherHandler(const NonnullRefPtr<Desktop::Launcher::Details>& details)
+ : m_details(details)
+ {
+ }
+
+ NonnullRefPtr<GUI::Action> create_launch_action(Function<void(const LauncherHandler&)>);
+ const Desktop::Launcher::Details& details() const { return *m_details; }
+
+private:
+ NonnullRefPtr<Desktop::Launcher::Details> m_details;
+};
+
+class DirectoryView final
+ : public GUI::StackWidget
+ , private GUI::ModelClient {
+ C_OBJECT(DirectoryView);
+
+public:
+ enum class Mode {
+ Desktop,
+ Normal,
+ };
+
+ virtual ~DirectoryView() override;
+
+ void open(const StringView& path);
+ String path() const { return model().root_path(); }
+ void open_parent_directory();
+ void open_previous_directory();
+ void open_next_directory();
+ int path_history_size() const { return m_path_history.size(); }
+ int path_history_position() const { return m_path_history_position; }
+ static RefPtr<LauncherHandler> get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers);
+ NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const URL& url);
+ NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const String& path);
+
+ void refresh();
+
+ void launch(const AK::URL&, const LauncherHandler&);
+
+ Function<void(const StringView& path, bool can_write_in_path)> on_path_change;
+ Function<void(GUI::AbstractView&)> on_selection_change;
+ Function<void(const GUI::ModelIndex&, const GUI::ContextMenuEvent&)> on_context_menu_request;
+ Function<void(const StringView&)> on_status_message;
+ Function<void(int done, int total)> on_thumbnail_progress;
+ Function<void()> on_accepted_drop;
+
+ enum ViewMode {
+ Invalid,
+ Table,
+ Columns,
+ Icon
+ };
+ void set_view_mode(ViewMode);
+ ViewMode view_mode() const { return m_view_mode; }
+
+ GUI::AbstractView& current_view()
+ {
+ switch (m_view_mode) {
+ case ViewMode::Table:
+ return *m_table_view;
+ case ViewMode::Columns:
+ return *m_columns_view;
+ case ViewMode::Icon:
+ return *m_icon_view;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ const GUI::AbstractView& current_view() const
+ {
+ return const_cast<DirectoryView*>(this)->current_view();
+ }
+
+ template<typename Callback>
+ void for_each_view_implementation(Callback callback)
+ {
+ if (m_icon_view)
+ callback(*m_icon_view);
+ if (m_table_view)
+ callback(*m_table_view);
+ if (m_columns_view)
+ callback(*m_columns_view);
+ }
+
+ void set_should_show_dotfiles(bool);
+
+ const GUI::FileSystemModel::Node& node(const GUI::ModelIndex&) const;
+
+ bool is_desktop() const { return m_mode == Mode::Desktop; }
+
+ Vector<String> selected_file_paths() const;
+
+ GUI::Action& mkdir_action() { return *m_mkdir_action; }
+ GUI::Action& touch_action() { return *m_touch_action; }
+ GUI::Action& open_terminal_action() { return *m_open_terminal_action; }
+ GUI::Action& delete_action() { return *m_delete_action; }
+ GUI::Action& force_delete_action() { return *m_force_delete_action; }
+
+private:
+ explicit DirectoryView(Mode);
+
+ const GUI::FileSystemModel& model() const { return *m_model; }
+ GUI::FileSystemModel& model() { return *m_model; }
+
+ void handle_selection_change();
+ void handle_drop(const GUI::ModelIndex&, const GUI::DropEvent&);
+ void do_delete(bool should_confirm);
+
+ // ^GUI::ModelClient
+ virtual void model_did_update(unsigned) override;
+
+ void setup_actions();
+ void setup_model();
+ void setup_icon_view();
+ void setup_columns_view();
+ void setup_table_view();
+
+ void handle_activation(const GUI::ModelIndex&);
+
+ void set_status_message(const StringView&);
+ void update_statusbar();
+
+ Mode m_mode { Mode::Normal };
+ ViewMode m_view_mode { Invalid };
+
+ NonnullRefPtr<GUI::FileSystemModel> m_model;
+ NonnullRefPtr<GUI::SortingProxyModel> m_sorting_model;
+ size_t m_path_history_position { 0 };
+ Vector<String> m_path_history;
+ void add_path_to_history(const StringView& path);
+
+ RefPtr<GUI::Label> m_error_label;
+
+ RefPtr<GUI::TableView> m_table_view;
+ RefPtr<GUI::IconView> m_icon_view;
+ RefPtr<GUI::ColumnsView> m_columns_view;
+
+ RefPtr<GUI::Action> m_mkdir_action;
+ RefPtr<GUI::Action> m_touch_action;
+ RefPtr<GUI::Action> m_open_terminal_action;
+ RefPtr<GUI::Action> m_delete_action;
+ RefPtr<GUI::Action> m_force_delete_action;
+};
+
+}
diff --git a/Userland/Applications/FileManager/FileManagerWindow.gml b/Userland/Applications/FileManager/FileManagerWindow.gml
new file mode 100644
index 0000000000..a13218dd16
--- /dev/null
+++ b/Userland/Applications/FileManager/FileManagerWindow.gml
@@ -0,0 +1,53 @@
+@GUI::Widget {
+ fill_with_background_color: true
+ layout: @GUI::VerticalBoxLayout {
+ spacing: 2
+ }
+
+ @GUI::ToolBarContainer {
+ @GUI::ToolBar {
+ name: "main_toolbar"
+ }
+ @GUI::ToolBar {
+ name: "location_toolbar"
+ visible: false
+
+ @GUI::Label {
+ text: "Location: "
+ autosize: true
+ }
+
+ @GUI::TextBox {
+ name: "location_textbox"
+ fixed_height: 22
+ }
+ }
+ @GUI::ToolBar {
+ name: "breadcrumb_toolbar"
+
+ @GUI::BreadcrumbBar {
+ name: "breadcrumb_bar"
+ }
+ }
+ }
+
+ @GUI::HorizontalSplitter {
+ name: "splitter"
+
+ @GUI::TreeView {
+ name: "tree_view"
+ fixed_width: 175
+ }
+
+ }
+
+ @GUI::StatusBar {
+ name: "statusbar"
+
+ @GUI::ProgressBar {
+ name: "progressbar"
+ text: "Generating thumbnails: "
+ visible: false
+ }
+ }
+}
diff --git a/Userland/Applications/FileManager/FileUtils.cpp b/Userland/Applications/FileManager/FileUtils.cpp
new file mode 100644
index 0000000000..76d2ee0079
--- /dev/null
+++ b/Userland/Applications/FileManager/FileUtils.cpp
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FileUtils.h"
+#include <AK/LexicalPath.h>
+#include <AK/ScopeGuard.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <LibGUI/MessageBox.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+namespace FileUtils {
+
+void delete_path(const String& path, GUI::Window* parent_window)
+{
+ struct stat st;
+ if (lstat(path.characters(), &st)) {
+ GUI::MessageBox::show(parent_window,
+ String::formatted("lstat({}) failed: {}", path, strerror(errno)),
+ "Delete failed",
+ GUI::MessageBox::Type::Error);
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ String error_path;
+ int error = FileUtils::delete_directory(path, error_path);
+
+ if (error) {
+ GUI::MessageBox::show(parent_window,
+ String::formatted("Failed to delete directory \"{}\": {}", error_path, strerror(error)),
+ "Delete failed",
+ GUI::MessageBox::Type::Error);
+ }
+ } else if (unlink(path.characters()) < 0) {
+ int saved_errno = errno;
+ GUI::MessageBox::show(parent_window,
+ String::formatted("unlink(\"{}\") failed: {}", path, strerror(saved_errno)),
+ "Delete failed",
+ GUI::MessageBox::Type::Error);
+ }
+}
+
+void delete_paths(const Vector<String>& paths, bool should_confirm, GUI::Window* parent_window)
+{
+ String message;
+ if (paths.size() == 1) {
+ message = String::formatted("Really delete {}?", LexicalPath(paths[0]).basename());
+ } else {
+ message = String::formatted("Really delete {} files?", paths.size());
+ }
+
+ if (should_confirm) {
+ auto result = GUI::MessageBox::show(parent_window,
+ message,
+ "Confirm deletion",
+ GUI::MessageBox::Type::Warning,
+ GUI::MessageBox::InputType::OKCancel);
+ if (result == GUI::MessageBox::ExecCancel)
+ return;
+ }
+
+ for (auto& path : paths) {
+ delete_path(path, parent_window);
+ }
+}
+
+int delete_directory(String directory, String& file_that_caused_error)
+{
+ Core::DirIterator iterator(directory, Core::DirIterator::SkipDots);
+ if (iterator.has_error()) {
+ file_that_caused_error = directory;
+ return -1;
+ }
+
+ while (iterator.has_next()) {
+ auto file_to_delete = String::formatted("{}/{}", directory, iterator.next_path());
+ struct stat st;
+
+ if (lstat(file_to_delete.characters(), &st)) {
+ file_that_caused_error = file_to_delete;
+ return errno;
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ if (delete_directory(file_to_delete, file_to_delete)) {
+ file_that_caused_error = file_to_delete;
+ return errno;
+ }
+ } else if (unlink(file_to_delete.characters())) {
+ file_that_caused_error = file_to_delete;
+ return errno;
+ }
+ }
+
+ if (rmdir(directory.characters())) {
+ file_that_caused_error = directory;
+ return errno;
+ }
+
+ return 0;
+}
+
+bool copy_file_or_directory(const String& src_path, const String& dst_path)
+{
+ int duplicate_count = 0;
+ while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
+ ++duplicate_count;
+ }
+ if (duplicate_count != 0) {
+ return copy_file_or_directory(src_path, get_duplicate_name(dst_path, duplicate_count));
+ }
+
+ auto source_or_error = Core::File::open(src_path, Core::IODevice::ReadOnly);
+ if (source_or_error.is_error())
+ return false;
+
+ auto& source = *source_or_error.value();
+
+ struct stat src_stat;
+ int rc = fstat(source.fd(), &src_stat);
+ if (rc < 0)
+ return false;
+
+ if (source.is_directory())
+ return copy_directory(src_path, dst_path, src_stat);
+
+ return copy_file(dst_path, src_stat, source);
+}
+
+bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat)
+{
+ int rc = mkdir(dst_path.characters(), 0755);
+ if (rc < 0) {
+ return false;
+ }
+ Core::DirIterator di(src_path, Core::DirIterator::SkipDots);
+ if (di.has_error()) {
+ return false;
+ }
+ while (di.has_next()) {
+ String filename = di.next_path();
+ bool is_copied = copy_file_or_directory(
+ String::formatted("{}/{}", src_path, filename),
+ String::formatted("{}/{}", dst_path, filename));
+ if (!is_copied) {
+ return false;
+ }
+ }
+
+ auto my_umask = umask(0);
+ umask(my_umask);
+ rc = chmod(dst_path.characters(), src_stat.st_mode & ~my_umask);
+ if (rc < 0) {
+ return false;
+ }
+ return true;
+}
+
+bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File& source)
+{
+ int dst_fd = creat(dst_path.characters(), 0666);
+ if (dst_fd < 0) {
+ if (errno != EISDIR) {
+ return false;
+ }
+ auto dst_dir_path = String::formatted("{}/{}", dst_path, LexicalPath(source.filename()).basename());
+ dst_fd = creat(dst_dir_path.characters(), 0666);
+ if (dst_fd < 0) {
+ return false;
+ }
+ }
+
+ ScopeGuard close_fd_guard([dst_fd]() { close(dst_fd); });
+
+ if (src_stat.st_size > 0) {
+ if (ftruncate(dst_fd, src_stat.st_size) < 0) {
+ perror("cp: ftruncate");
+ return false;
+ }
+ }
+
+ for (;;) {
+ char buffer[32768];
+ ssize_t nread = read(source.fd(), buffer, sizeof(buffer));
+ if (nread < 0) {
+ return false;
+ }
+ if (nread == 0)
+ break;
+ ssize_t remaining_to_write = nread;
+ char* bufptr = buffer;
+ while (remaining_to_write) {
+ ssize_t nwritten = write(dst_fd, bufptr, remaining_to_write);
+ if (nwritten < 0) {
+ return false;
+ }
+ assert(nwritten > 0);
+ remaining_to_write -= nwritten;
+ bufptr += nwritten;
+ }
+ }
+
+ auto my_umask = umask(0);
+ umask(my_umask);
+ int rc = fchmod(dst_fd, src_stat.st_mode & ~my_umask);
+ if (rc < 0) {
+ return false;
+ }
+
+ return true;
+}
+
+bool link_file(const String& src_path, const String& dst_path)
+{
+ int duplicate_count = 0;
+ while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
+ ++duplicate_count;
+ }
+ if (duplicate_count != 0) {
+ return link_file(src_path, get_duplicate_name(dst_path, duplicate_count));
+ }
+ int rc = symlink(src_path.characters(), dst_path.characters());
+ if (rc < 0) {
+ return false;
+ }
+
+ return true;
+}
+
+String get_duplicate_name(const String& path, int duplicate_count)
+{
+ if (duplicate_count == 0) {
+ return path;
+ }
+ LexicalPath lexical_path(path);
+ StringBuilder duplicated_name;
+ duplicated_name.append('/');
+ for (size_t i = 0; i < lexical_path.parts().size() - 1; ++i) {
+ duplicated_name.appendff("{}/", lexical_path.parts()[i]);
+ }
+ auto prev_duplicate_tag = String::formatted("({})", duplicate_count);
+ auto title = lexical_path.title();
+ if (title.ends_with(prev_duplicate_tag)) {
+ // remove the previous duplicate tag "(n)" so we can add a new tag.
+ title = title.substring(0, title.length() - prev_duplicate_tag.length());
+ }
+ duplicated_name.appendff("{} ({})", lexical_path.title(), duplicate_count);
+ if (!lexical_path.extension().is_empty()) {
+ duplicated_name.appendff(".{}", lexical_path.extension());
+ }
+ return duplicated_name.build();
+}
+}
diff --git a/Userland/Applications/FileManager/FileUtils.h b/Userland/Applications/FileManager/FileUtils.h
new file mode 100644
index 0000000000..fcf6b483ac
--- /dev/null
+++ b/Userland/Applications/FileManager/FileUtils.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <LibCore/Forward.h>
+#include <LibGUI/Forward.h>
+#include <sys/stat.h>
+
+namespace FileUtils {
+
+enum class FileOperation {
+ Copy = 0,
+ Cut
+};
+
+void delete_path(const String&, GUI::Window*);
+void delete_paths(const Vector<String>&, bool should_confirm, GUI::Window*);
+int delete_directory(String directory, String& file_that_caused_error);
+bool copy_file_or_directory(const String& src_path, const String& dst_path);
+String get_duplicate_name(const String& path, int duplicate_count);
+bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File&);
+bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat);
+bool link_file(const String& src_path, const String& dst_path);
+
+}
diff --git a/Userland/Applications/FileManager/PropertiesWindow.cpp b/Userland/Applications/FileManager/PropertiesWindow.cpp
new file mode 100644
index 0000000000..8c0ff9f74e
--- /dev/null
+++ b/Userland/Applications/FileManager/PropertiesWindow.cpp
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PropertiesWindow.h"
+#include <AK/LexicalPath.h>
+#include <AK/StringBuilder.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/FileIconProvider.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/LinkLabel.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/SeparatorWidget.h>
+#include <LibGUI/TabWidget.h>
+#include <grp.h>
+#include <limits.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+PropertiesWindow::PropertiesWindow(const String& path, bool disable_rename, Window* parent_window)
+ : Window(parent_window)
+{
+ auto lexical_path = LexicalPath(path);
+ ASSERT(lexical_path.is_valid());
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_layout<GUI::VerticalBoxLayout>();
+ main_widget.layout()->set_margins({ 4, 4, 4, 4 });
+ main_widget.set_fill_with_background_color(true);
+
+ set_rect({ 0, 0, 360, 420 });
+ set_resizable(false);
+
+ auto& tab_widget = main_widget.add<GUI::TabWidget>();
+
+ auto& general_tab = tab_widget.add_tab<GUI::Widget>("General");
+ general_tab.set_layout<GUI::VerticalBoxLayout>();
+ general_tab.layout()->set_margins({ 12, 8, 12, 8 });
+ general_tab.layout()->set_spacing(10);
+
+ auto& file_container = general_tab.add<GUI::Widget>();
+ file_container.set_layout<GUI::HorizontalBoxLayout>();
+ file_container.layout()->set_spacing(20);
+ file_container.set_fixed_height(34);
+
+ m_icon = file_container.add<GUI::ImageWidget>();
+ m_icon->set_fixed_size(32, 32);
+
+ m_name = lexical_path.basename();
+ m_path = lexical_path.string();
+ m_parent_path = lexical_path.dirname();
+
+ m_name_box = file_container.add<GUI::TextBox>();
+ m_name_box->set_text(m_name);
+ m_name_box->set_mode(disable_rename ? GUI::TextBox::Mode::DisplayOnly : GUI::TextBox::Mode::Editable);
+ m_name_box->on_change = [&]() {
+ m_name_dirty = m_name != m_name_box->text();
+ m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
+ };
+
+ set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"));
+ general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
+
+ struct stat st;
+ if (lstat(path.characters(), &st)) {
+ perror("stat");
+ return;
+ }
+
+ String owner_name;
+ String group_name;
+
+ if (auto* pw = getpwuid(st.st_uid)) {
+ owner_name = pw->pw_name;
+ } else {
+ owner_name = "n/a";
+ }
+
+ if (auto* gr = getgrgid(st.st_gid)) {
+ group_name = gr->gr_name;
+ } else {
+ group_name = "n/a";
+ }
+
+ m_mode = st.st_mode;
+ m_old_mode = st.st_mode;
+
+ auto properties = Vector<PropertyValuePair>();
+ properties.append({ "Type:", get_description(m_mode) });
+ auto parent_link = URL::create_with_file_protocol(m_parent_path);
+ properties.append(PropertyValuePair { "Location:", path, Optional(parent_link) });
+
+ if (S_ISLNK(m_mode)) {
+ auto link_destination = Core::File::read_link(path);
+ if (link_destination.is_null()) {
+ perror("readlink");
+ } else {
+ auto link_directory = LexicalPath(link_destination);
+ ASSERT(link_directory.is_valid());
+ auto link_parent = URL::create_with_file_protocol(link_directory.dirname());
+ properties.append({ "Link target:", link_destination, Optional(link_parent) });
+ }
+ }
+
+ properties.append({ "Size:", String::formatted("{} bytes", st.st_size) });
+ properties.append({ "Owner:", String::formatted("{} ({})", owner_name, st.st_uid) });
+ properties.append({ "Group:", String::formatted("{} ({})", group_name, st.st_gid) });
+ properties.append({ "Created at:", GUI::FileSystemModel::timestamp_string(st.st_ctime) });
+ properties.append({ "Last modified:", GUI::FileSystemModel::timestamp_string(st.st_mtime) });
+
+ make_property_value_pairs(properties, general_tab);
+
+ general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
+
+ make_permission_checkboxes(general_tab, { S_IRUSR, S_IWUSR, S_IXUSR }, "Owner:", m_mode);
+ make_permission_checkboxes(general_tab, { S_IRGRP, S_IWGRP, S_IXGRP }, "Group:", m_mode);
+ make_permission_checkboxes(general_tab, { S_IROTH, S_IWOTH, S_IXOTH }, "Others:", m_mode);
+
+ general_tab.layout()->add_spacer();
+
+ auto& button_widget = main_widget.add<GUI::Widget>();
+ button_widget.set_layout<GUI::HorizontalBoxLayout>();
+ button_widget.set_fixed_height(24);
+ button_widget.layout()->set_spacing(5);
+
+ button_widget.layout()->add_spacer();
+
+ make_button("OK", button_widget).on_click = [this](auto) {
+ if (apply_changes())
+ close();
+ };
+ make_button("Cancel", button_widget).on_click = [this](auto) {
+ close();
+ };
+
+ m_apply_button = make_button("Apply", button_widget);
+ m_apply_button->on_click = [this](auto) { apply_changes(); };
+ m_apply_button->set_enabled(false);
+
+ update();
+}
+
+PropertiesWindow::~PropertiesWindow()
+{
+}
+
+void PropertiesWindow::update()
+{
+ m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32));
+ set_title(String::formatted("{} - Properties", m_name));
+}
+
+void PropertiesWindow::permission_changed(mode_t mask, bool set)
+{
+ if (set) {
+ m_mode |= mask;
+ } else {
+ m_mode &= ~mask;
+ }
+
+ m_permissions_dirty = m_mode != m_old_mode;
+ m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
+}
+
+String PropertiesWindow::make_full_path(const String& name)
+{
+ return String::formatted("{}/{}", m_parent_path, name);
+}
+
+bool PropertiesWindow::apply_changes()
+{
+ if (m_name_dirty) {
+ String new_name = m_name_box->text();
+ String new_file = make_full_path(new_name).characters();
+
+ if (GUI::FilePicker::file_exists(new_file)) {
+ GUI::MessageBox::show(this, String::formatted("A file \"{}\" already exists!", new_name), "Error", GUI::MessageBox::Type::Error);
+ return false;
+ }
+
+ if (rename(make_full_path(m_name).characters(), new_file.characters())) {
+ GUI::MessageBox::show(this, String::formatted("Could not rename file: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
+ return false;
+ }
+
+ m_name = new_name;
+ m_name_dirty = false;
+ update();
+ }
+
+ if (m_permissions_dirty) {
+ if (chmod(make_full_path(m_name).characters(), m_mode)) {
+ GUI::MessageBox::show(this, String::formatted("Could not update permissions: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
+ return false;
+ }
+
+ m_old_mode = m_mode;
+ m_permissions_dirty = false;
+ }
+
+ update();
+ m_apply_button->set_enabled(false);
+ return true;
+}
+
+void PropertiesWindow::make_permission_checkboxes(GUI::Widget& parent, PermissionMasks masks, String label_string, mode_t mode)
+{
+ auto& widget = parent.add<GUI::Widget>();
+ widget.set_layout<GUI::HorizontalBoxLayout>();
+ widget.set_fixed_height(16);
+ widget.layout()->set_spacing(10);
+
+ auto& label = widget.add<GUI::Label>(label_string);
+ label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ struct stat st;
+ if (lstat(m_path.characters(), &st)) {
+ perror("stat");
+ return;
+ }
+
+ auto can_edit_checkboxes = st.st_uid == getuid();
+
+ auto& box_read = widget.add<GUI::CheckBox>("Read");
+ box_read.set_checked(mode & masks.read);
+ box_read.on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); };
+ box_read.set_enabled(can_edit_checkboxes);
+
+ auto& box_write = widget.add<GUI::CheckBox>("Write");
+ box_write.set_checked(mode & masks.write);
+ box_write.on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); };
+ box_write.set_enabled(can_edit_checkboxes);
+
+ auto& box_execute = widget.add<GUI::CheckBox>("Execute");
+ box_execute.set_checked(mode & masks.execute);
+ box_execute.on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); };
+ box_execute.set_enabled(can_edit_checkboxes);
+}
+
+void PropertiesWindow::make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent)
+{
+ int max_width = 0;
+ Vector<NonnullRefPtr<GUI::Label>> property_labels;
+
+ property_labels.ensure_capacity(pairs.size());
+ for (auto pair : pairs) {
+ auto& label_container = parent.add<GUI::Widget>();
+ label_container.set_layout<GUI::HorizontalBoxLayout>();
+ label_container.set_fixed_height(14);
+ label_container.layout()->set_spacing(12);
+
+ auto& label_property = label_container.add<GUI::Label>(pair.property);
+ label_property.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ if (!pair.link.has_value()) {
+ label_container.add<GUI::Label>(pair.value).set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ } else {
+ auto& link = label_container.add<GUI::LinkLabel>(pair.value);
+ link.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ link.on_click = [pair]() {
+ Desktop::Launcher::open(pair.link.value());
+ };
+ }
+
+ max_width = max(max_width, label_property.font().width(pair.property));
+ property_labels.append(label_property);
+ }
+
+ for (auto label : property_labels)
+ label->set_fixed_width(max_width);
+}
+
+GUI::Button& PropertiesWindow::make_button(String text, GUI::Widget& parent)
+{
+ auto& button = parent.add<GUI::Button>(text);
+ button.set_fixed_size(70, 22);
+ return button;
+}
diff --git a/Userland/Applications/FileManager/PropertiesWindow.h b/Userland/Applications/FileManager/PropertiesWindow.h
new file mode 100644
index 0000000000..d2801f8049
--- /dev/null
+++ b/Userland/Applications/FileManager/PropertiesWindow.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/File.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/FileSystemModel.h>
+#include <LibGUI/ImageWidget.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/TextBox.h>
+
+class PropertiesWindow final : public GUI::Window {
+ C_OBJECT(PropertiesWindow);
+
+public:
+ virtual ~PropertiesWindow() override;
+
+private:
+ PropertiesWindow(const String& path, bool disable_rename, Window* parent = nullptr);
+
+ struct PropertyValuePair {
+ String property;
+ String value;
+ Optional<URL> link = {};
+ };
+
+ struct PermissionMasks {
+ mode_t read;
+ mode_t write;
+ mode_t execute;
+ };
+
+ static const String get_description(const mode_t mode)
+ {
+ if (S_ISREG(mode))
+ return "File";
+ if (S_ISDIR(mode))
+ return "Directory";
+ if (S_ISLNK(mode))
+ return "Symbolic link";
+ if (S_ISCHR(mode))
+ return "Character device";
+ if (S_ISBLK(mode))
+ return "Block device";
+ if (S_ISFIFO(mode))
+ return "FIFO (named pipe)";
+ if (S_ISSOCK(mode))
+ return "Socket";
+ if (mode & S_IXUSR)
+ return "Executable";
+
+ return "Unknown";
+ }
+
+ GUI::Button& make_button(String, GUI::Widget& parent);
+ void make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent);
+ void make_permission_checkboxes(GUI::Widget& parent, PermissionMasks, String label_string, mode_t mode);
+ void permission_changed(mode_t mask, bool set);
+ bool apply_changes();
+ void update();
+ String make_full_path(const String& name);
+
+ RefPtr<GUI::Button> m_apply_button;
+ RefPtr<GUI::TextBox> m_name_box;
+ RefPtr<GUI::ImageWidget> m_icon;
+ String m_name;
+ String m_parent_path;
+ String m_path;
+ mode_t m_mode { 0 };
+ mode_t m_old_mode { 0 };
+ bool m_permissions_dirty { false };
+ bool m_name_dirty { false };
+};
diff --git a/Userland/Applications/FileManager/main.cpp b/Userland/Applications/FileManager/main.cpp
new file mode 100644
index 0000000000..9766876f21
--- /dev/null
+++ b/Userland/Applications/FileManager/main.cpp
@@ -0,0 +1,974 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DesktopWidget.h"
+#include "DirectoryView.h"
+#include "FileUtils.h"
+#include "PropertiesWindow.h"
+#include <AK/LexicalPath.h>
+#include <AK/StringBuilder.h>
+#include <AK/URL.h>
+#include <Applications/FileManager/FileManagerWindowGML.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/MimeData.h>
+#include <LibCore/StandardPaths.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/BreadcrumbBar.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/Desktop.h>
+#include <LibGUI/FileIconProvider.h>
+#include <LibGUI/FileSystemModel.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/ProgressBar.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/TreeView.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Palette.h>
+#include <pthread.h>
+#include <serenity.h>
+#include <signal.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+using namespace FileManager;
+
+static int run_in_desktop_mode(RefPtr<Core::ConfigFile>);
+static int run_in_windowed_mode(RefPtr<Core::ConfigFile>, String initial_location);
+static void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation);
+static void do_paste(const String& target_directory, GUI::Window* window);
+static void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window);
+static void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window);
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread shared_buffer accept unix cpath rpath wpath fattr proc exec sigaction", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ struct sigaction act;
+ memset(&act, 0, sizeof(act));
+ act.sa_flags = SA_NOCLDWAIT;
+ act.sa_handler = SIG_IGN;
+ int rc = sigaction(SIGCHLD, &act, nullptr);
+ if (rc < 0) {
+ perror("sigaction");
+ return 1;
+ }
+
+ RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("FileManager");
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread shared_buffer accept cpath rpath wpath fattr proc exec unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (app->args().contains_slow("--desktop") || app->args().contains_slow("-d"))
+ return run_in_desktop_mode(move(config));
+
+ // our initial location is defined as, in order of precedence:
+ // 1. the first command-line argument (e.g. FileManager /bin)
+ // 2. the user's home directory
+ // 3. the root directory
+ String initial_location;
+
+ if (argc >= 2) {
+ char* buffer = realpath(argv[1], nullptr);
+ initial_location = buffer;
+ free(buffer);
+ }
+
+ if (initial_location.is_empty())
+ initial_location = Core::StandardPaths::home_directory();
+
+ if (initial_location.is_empty())
+ initial_location = "/";
+
+ return run_in_windowed_mode(move(config), initial_location);
+}
+
+void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation)
+{
+ if (selected_file_paths.is_empty())
+ ASSERT_NOT_REACHED();
+
+ StringBuilder copy_text;
+ if (file_operation == FileUtils::FileOperation::Cut) {
+ copy_text.append("#cut\n"); // This exploits the comment lines in the text/uri-list specification, which might be a bit hackish
+ }
+ for (auto& path : selected_file_paths) {
+ auto url = URL::create_with_file_protocol(path);
+ copy_text.appendff("{}\n", url);
+ }
+ GUI::Clipboard::the().set_data(copy_text.build().bytes(), "text/uri-list");
+}
+
+void do_paste(const String& target_directory, GUI::Window* window)
+{
+ auto data_and_type = GUI::Clipboard::the().data_and_type();
+ if (data_and_type.mime_type != "text/uri-list") {
+ dbgln("Cannot paste clipboard type {}", data_and_type.mime_type);
+ return;
+ }
+ auto copied_lines = String::copy(data_and_type.data).split('\n');
+ if (copied_lines.is_empty()) {
+ dbgln("No files to paste");
+ return;
+ }
+
+ bool should_delete_src = false;
+ if (copied_lines[0] == "#cut") { // cut operation encoded as a text/uri-list commen
+ should_delete_src = true;
+ copied_lines.remove(0);
+ }
+
+ for (auto& uri_as_string : copied_lines) {
+ if (uri_as_string.is_empty())
+ continue;
+ URL url = uri_as_string;
+ if (!url.is_valid() || url.protocol() != "file") {
+ dbgln("Cannot paste URI {}", uri_as_string);
+ continue;
+ }
+
+ auto new_path = String::formatted("{}/{}", target_directory, url.basename());
+ if (!FileUtils::copy_file_or_directory(url.path(), new_path)) {
+ auto error_message = String::formatted("Could not paste {}.", url.path());
+ GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
+ } else if (should_delete_src) {
+ FileUtils::delete_path(url.path(), window);
+ }
+ }
+}
+
+void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window)
+{
+ auto path = selected_file_paths.first();
+ auto destination = String::formatted("{}/{}", Core::StandardPaths::desktop_directory(), LexicalPath { path }.basename());
+ if (!FileUtils::link_file(path, destination)) {
+ GUI::MessageBox::show(window, "Could not create desktop shortcut", "File Manager",
+ GUI::MessageBox::Type::Error);
+ }
+}
+
+void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window)
+{
+ RefPtr<PropertiesWindow> properties;
+ if (selected.is_empty()) {
+ properties = window->add<PropertiesWindow>(path, true);
+ } else {
+ properties = window->add<PropertiesWindow>(selected.first(), access(container_dir_path.characters(), W_OK) != 0);
+ }
+ properties->on_close = [properties = properties.ptr()] {
+ properties->remove_from_parent();
+ };
+ properties->center_on_screen();
+ properties->show();
+}
+
+int run_in_desktop_mode([[maybe_unused]] RefPtr<Core::ConfigFile> config)
+{
+ static constexpr const char* process_name = "FileManager (Desktop)";
+ set_process_name(process_name, strlen(process_name));
+ pthread_setname_np(pthread_self(), process_name);
+
+ auto window = GUI::Window::construct();
+ window->set_title("Desktop Manager");
+ window->set_window_type(GUI::WindowType::Desktop);
+ window->set_has_alpha_channel(true);
+
+ auto& desktop_widget = window->set_main_widget<FileManager::DesktopWidget>();
+ desktop_widget.set_layout<GUI::VerticalBoxLayout>();
+
+ [[maybe_unused]] auto& directory_view = desktop_widget.add<DirectoryView>(DirectoryView::Mode::Desktop);
+
+ auto copy_action = GUI::CommonActions::make_copy_action(
+ [&](auto&) {
+ auto paths = directory_view.selected_file_paths();
+
+ if (paths.is_empty())
+ ASSERT_NOT_REACHED();
+
+ do_copy(paths, FileUtils::FileOperation::Copy);
+ },
+ window);
+ copy_action->set_enabled(false);
+
+ auto cut_action = GUI::CommonActions::make_cut_action(
+ [&](auto&) {
+ auto paths = directory_view.selected_file_paths();
+
+ if (paths.is_empty())
+ ASSERT_NOT_REACHED();
+
+ do_copy(paths, FileUtils::FileOperation::Cut);
+ },
+ window);
+ cut_action->set_enabled(false);
+
+ directory_view.on_selection_change = [&](const GUI::AbstractView& view) {
+ copy_action->set_enabled(!view.selection().is_empty());
+ cut_action->set_enabled(!view.selection().is_empty());
+ };
+
+ auto properties_action
+ = GUI::Action::create(
+ "Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action&) {
+ String path = directory_view.path();
+ Vector<String> selected = directory_view.selected_file_paths();
+
+ show_properties(path, path, selected, directory_view.window());
+ },
+ window);
+
+ auto paste_action = GUI::CommonActions::make_paste_action(
+ [&](const GUI::Action&) {
+ do_paste(directory_view.path(), directory_view.window());
+ },
+ window);
+ paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
+
+ GUI::Clipboard::the().on_change = [&](const String& data_type) {
+ paste_action->set_enabled(data_type == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
+ };
+
+ auto desktop_view_context_menu = GUI::Menu::construct("Directory View");
+
+ auto file_manager_action = GUI::Action::create("Show in File Manager", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-folder.png"), [&](const GUI::Action&) {
+ Desktop::Launcher::open(URL::create_with_file_protocol(directory_view.path()));
+ });
+
+ auto display_properties_action = GUI::Action::create("Display Settings", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"), [&](const GUI::Action&) {
+ Desktop::Launcher::open(URL::create_with_file_protocol("/bin/DisplaySettings"));
+ });
+
+ desktop_view_context_menu->add_action(directory_view.mkdir_action());
+ desktop_view_context_menu->add_action(directory_view.touch_action());
+ desktop_view_context_menu->add_action(paste_action);
+ desktop_view_context_menu->add_separator();
+ desktop_view_context_menu->add_action(file_manager_action);
+ desktop_view_context_menu->add_action(directory_view.open_terminal_action());
+ desktop_view_context_menu->add_separator();
+ desktop_view_context_menu->add_action(display_properties_action);
+
+ auto desktop_context_menu = GUI::Menu::construct("Directory View Directory");
+ desktop_context_menu->add_action(copy_action);
+ desktop_context_menu->add_action(cut_action);
+ desktop_context_menu->add_action(paste_action);
+ desktop_context_menu->add_action(directory_view.delete_action());
+ desktop_context_menu->add_separator();
+ desktop_context_menu->add_action(properties_action);
+
+ directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
+ if (!index.is_valid())
+ desktop_view_context_menu->popup(event.screen_position());
+ else
+ desktop_context_menu->popup(event.screen_position());
+ };
+
+ auto wm_config = Core::ConfigFile::get_for_app("WindowManager");
+ auto selected_wallpaper = wm_config->read_entry("Background", "Wallpaper", "");
+ if (!selected_wallpaper.is_empty()) {
+ GUI::Desktop::the().set_wallpaper(selected_wallpaper, false);
+ }
+
+ window->show();
+ return GUI::Application::the()->exec();
+}
+
+int run_in_windowed_mode(RefPtr<Core::ConfigFile> config, String initial_location)
+{
+ auto window = GUI::Window::construct();
+ window->set_title("File Manager");
+
+ auto left = config->read_num_entry("Window", "Left", 150);
+ auto top = config->read_num_entry("Window", "Top", 75);
+ auto width = config->read_num_entry("Window", "Width", 640);
+ auto height = config->read_num_entry("Window", "Height", 480);
+ window->set_rect({ left, top, width, height });
+
+ auto& widget = window->set_main_widget<GUI::Widget>();
+
+ widget.load_from_gml(file_manager_window_gml);
+
+ auto& main_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("main_toolbar");
+ auto& location_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("location_toolbar");
+ location_toolbar.layout()->set_margins({ 6, 3, 6, 3 });
+
+ auto& location_textbox = *widget.find_descendant_of_type_named<GUI::TextBox>("location_textbox");
+
+ auto& breadcrumb_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("breadcrumb_toolbar");
+ breadcrumb_toolbar.layout()->set_margins({});
+ auto& breadcrumb_bar = *widget.find_descendant_of_type_named<GUI::BreadcrumbBar>("breadcrumb_bar");
+
+ location_textbox.on_focusout = [&] {
+ location_toolbar.set_visible(false);
+ breadcrumb_toolbar.set_visible(true);
+ };
+
+ auto& splitter = *widget.find_descendant_of_type_named<GUI::HorizontalSplitter>("splitter");
+ auto& tree_view = *widget.find_descendant_of_type_named<GUI::TreeView>("tree_view");
+
+ auto directories_model = GUI::FileSystemModel::create({}, GUI::FileSystemModel::Mode::DirectoriesOnly);
+ tree_view.set_model(directories_model);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Icon, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Size, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Owner, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Group, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Permissions, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::ModificationTime, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::Inode, true);
+ tree_view.set_column_hidden(GUI::FileSystemModel::Column::SymlinkTarget, true);
+ bool is_reacting_to_tree_view_selection_change = false;
+
+ auto& directory_view = splitter.add<DirectoryView>(DirectoryView::Mode::Normal);
+
+ location_textbox.on_escape_pressed = [&] {
+ directory_view.set_focus(true);
+ };
+
+ // Open the root directory. FIXME: This is awkward.
+ tree_view.toggle_index(directories_model->index(0, 0, {}));
+
+ auto& statusbar = *widget.find_descendant_of_type_named<GUI::StatusBar>("statusbar");
+
+ auto& progressbar = *widget.find_descendant_of_type_named<GUI::ProgressBar>("progressbar");
+ progressbar.set_format(GUI::ProgressBar::Format::ValueSlashMax);
+ progressbar.set_frame_shape(Gfx::FrameShape::Panel);
+ progressbar.set_frame_shadow(Gfx::FrameShadow::Sunken);
+ progressbar.set_frame_thickness(1);
+
+ location_textbox.on_return_pressed = [&] {
+ directory_view.open(location_textbox.text());
+ };
+
+ auto refresh_tree_view = [&] {
+ directories_model->update();
+
+ auto current_path = directory_view.path();
+
+ struct stat st;
+ // If the directory no longer exists, we find a parent that does.
+ while (stat(current_path.characters(), &st) != 0) {
+ directory_view.open_parent_directory();
+ current_path = directory_view.path();
+ if (current_path == directories_model->root_path()) {
+ break;
+ }
+ }
+
+ // Reselect the existing folder in the tree.
+ auto new_index = directories_model->index(current_path, GUI::FileSystemModel::Column::Name);
+ if (new_index.is_valid()) {
+ tree_view.expand_all_parents_of(new_index);
+ tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set, true);
+ }
+
+ directory_view.refresh();
+ };
+
+ auto directory_context_menu = GUI::Menu::construct("Directory View Directory");
+ auto directory_view_context_menu = GUI::Menu::construct("Directory View");
+ auto tree_view_directory_context_menu = GUI::Menu::construct("Tree View Directory");
+ auto tree_view_context_menu = GUI::Menu::construct("Tree View");
+
+ auto open_parent_directory_action = GUI::Action::create("Open parent directory", { Mod_Alt, Key_Up }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [&](const GUI::Action&) {
+ directory_view.open_parent_directory();
+ });
+
+ RefPtr<GUI::Action> view_as_table_action;
+ RefPtr<GUI::Action> view_as_icons_action;
+ RefPtr<GUI::Action> view_as_columns_action;
+
+ view_as_icons_action = GUI::Action::create_checkable(
+ "Icon view", { Mod_Ctrl, KeyCode::Key_1 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"), [&](const GUI::Action&) {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
+ config->write_entry("DirectoryView", "ViewMode", "Icon");
+ config->sync();
+ },
+ window);
+
+ view_as_table_action = GUI::Action::create_checkable(
+ "Table view", { Mod_Ctrl, KeyCode::Key_2 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/table-view.png"), [&](const GUI::Action&) {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Table);
+ config->write_entry("DirectoryView", "ViewMode", "Table");
+ config->sync();
+ },
+ window);
+
+ view_as_columns_action = GUI::Action::create_checkable(
+ "Columns view", { Mod_Ctrl, KeyCode::Key_3 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/columns-view.png"), [&](const GUI::Action&) {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
+ config->write_entry("DirectoryView", "ViewMode", "Columns");
+ config->sync();
+ },
+ window);
+
+ auto view_type_action_group = make<GUI::ActionGroup>();
+ view_type_action_group->set_exclusive(true);
+ view_type_action_group->add_action(*view_as_icons_action);
+ view_type_action_group->add_action(*view_as_table_action);
+ view_type_action_group->add_action(*view_as_columns_action);
+
+ auto tree_view_selected_file_paths = [&] {
+ Vector<String> paths;
+ auto& view = tree_view;
+ view.selection().for_each_index([&](const GUI::ModelIndex& index) {
+ paths.append(directories_model->full_path(index));
+ });
+ return paths;
+ };
+
+ auto select_all_action = GUI::Action::create("Select all", { Mod_Ctrl, KeyCode::Key_A }, [&](const GUI::Action&) {
+ directory_view.current_view().select_all();
+ });
+
+ auto copy_action = GUI::CommonActions::make_copy_action(
+ [&](auto&) {
+ auto paths = directory_view.selected_file_paths();
+
+ if (paths.is_empty())
+ paths = tree_view_selected_file_paths();
+
+ if (paths.is_empty())
+ ASSERT_NOT_REACHED();
+
+ do_copy(paths, FileUtils::FileOperation::Copy);
+ refresh_tree_view();
+ },
+ window);
+ copy_action->set_enabled(false);
+
+ auto cut_action = GUI::CommonActions::make_cut_action(
+ [&](auto&) {
+ auto paths = directory_view.selected_file_paths();
+
+ if (paths.is_empty())
+ paths = tree_view_selected_file_paths();
+
+ if (paths.is_empty())
+ ASSERT_NOT_REACHED();
+
+ do_copy(paths, FileUtils::FileOperation::Cut);
+ refresh_tree_view();
+ },
+ window);
+ cut_action->set_enabled(false);
+
+ auto shortcut_action
+ = GUI::Action::create(
+ "Create desktop shortcut",
+ {},
+ Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-symlink.png"),
+ [&](const GUI::Action&) {
+ auto paths = directory_view.selected_file_paths();
+ if (paths.is_empty()) {
+ return;
+ }
+ do_create_link(paths, directory_view.window());
+ },
+ window);
+
+ auto properties_action
+ = GUI::Action::create(
+ "Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action& action) {
+ String container_dir_path;
+ String path;
+ Vector<String> selected;
+ if (action.activator() == directory_context_menu || directory_view.active_widget()->is_focused()) {
+ path = directory_view.path();
+ container_dir_path = path;
+ selected = directory_view.selected_file_paths();
+ } else {
+ path = directories_model->full_path(tree_view.selection().first());
+ container_dir_path = LexicalPath(path).basename();
+ selected = tree_view_selected_file_paths();
+ }
+
+ show_properties(container_dir_path, path, selected, directory_view.window());
+ },
+ window);
+
+ auto paste_action = GUI::CommonActions::make_paste_action(
+ [&](const GUI::Action& action) {
+ String target_directory;
+ if (action.activator() == directory_context_menu)
+ target_directory = directory_view.selected_file_paths()[0];
+ else
+ target_directory = directory_view.path();
+ do_paste(target_directory, directory_view.window());
+ refresh_tree_view();
+ },
+ window);
+
+ auto folder_specific_paste_action = GUI::CommonActions::make_paste_action(
+ [&](const GUI::Action& action) {
+ String target_directory;
+ if (action.activator() == directory_context_menu)
+ target_directory = directory_view.selected_file_paths()[0];
+ else
+ target_directory = directory_view.path();
+ do_paste(target_directory, directory_view.window());
+ refresh_tree_view();
+ },
+ window);
+
+ auto go_back_action = GUI::CommonActions::make_go_back_action(
+ [&](auto&) {
+ directory_view.open_previous_directory();
+ },
+ window);
+
+ auto go_forward_action = GUI::CommonActions::make_go_forward_action(
+ [&](auto&) {
+ directory_view.open_next_directory();
+ },
+ window);
+
+ auto go_home_action = GUI::CommonActions::make_go_home_action(
+ [&](auto&) {
+ directory_view.open(Core::StandardPaths::home_directory());
+ },
+ window);
+
+ GUI::Clipboard::the().on_change = [&](const String& data_type) {
+ auto current_location = directory_view.path();
+ paste_action->set_enabled(data_type == "text/uri-list" && access(current_location.characters(), W_OK) == 0);
+ };
+
+ auto tree_view_delete_action = GUI::CommonActions::make_delete_action(
+ [&](auto&) {
+ FileUtils::delete_paths(tree_view_selected_file_paths(), true, window);
+ refresh_tree_view();
+ },
+ &tree_view);
+
+ // This is a little awkward. The menu action does something different depending on which view has focus.
+ // It would be nice to find a good abstraction for this instead of creating a branching action like this.
+ auto focus_dependent_delete_action = GUI::CommonActions::make_delete_action([&](auto&) {
+ if (tree_view.is_focused())
+ tree_view_delete_action->activate();
+ else
+ directory_view.delete_action().activate();
+ refresh_tree_view();
+ });
+ focus_dependent_delete_action->set_enabled(false);
+
+ auto mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
+ directory_view.mkdir_action().activate();
+ refresh_tree_view();
+ });
+
+ auto touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
+ directory_view.touch_action().activate();
+ refresh_tree_view();
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("File Manager");
+ app_menu.add_action(mkdir_action);
+ app_menu.add_action(touch_action);
+ app_menu.add_action(copy_action);
+ app_menu.add_action(cut_action);
+ app_menu.add_action(paste_action);
+ app_menu.add_action(focus_dependent_delete_action);
+ app_menu.add_action(directory_view.open_terminal_action());
+ app_menu.add_separator();
+ app_menu.add_action(properties_action);
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto action_show_dotfiles = GUI::Action::create_checkable("Show dotfiles", { Mod_Ctrl, Key_H }, [&](auto& action) {
+ directory_view.set_should_show_dotfiles(action.is_checked());
+ refresh_tree_view();
+ });
+
+ auto& view_menu = menubar->add_menu("View");
+ view_menu.add_action(*view_as_icons_action);
+ view_menu.add_action(*view_as_table_action);
+ view_menu.add_action(*view_as_columns_action);
+ view_menu.add_separator();
+ view_menu.add_action(action_show_dotfiles);
+
+ auto go_to_location_action = GUI::Action::create("Go to location...", { Mod_Ctrl, Key_L }, [&](auto&) {
+ location_toolbar.set_visible(true);
+ breadcrumb_toolbar.set_visible(false);
+ location_textbox.select_all();
+ location_textbox.set_focus(true);
+ });
+
+ auto& go_menu = menubar->add_menu("Go");
+ go_menu.add_action(go_back_action);
+ go_menu.add_action(go_forward_action);
+ go_menu.add_action(open_parent_directory_action);
+ go_menu.add_action(go_home_action);
+ go_menu.add_action(go_to_location_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("File Manager", GUI::Icon::default_icon("filetype-folder")));
+
+ GUI::Application::the()->set_menubar(move(menubar));
+
+ main_toolbar.add_action(go_back_action);
+ main_toolbar.add_action(go_forward_action);
+ main_toolbar.add_action(open_parent_directory_action);
+ main_toolbar.add_action(go_home_action);
+
+ main_toolbar.add_separator();
+ main_toolbar.add_action(mkdir_action);
+ main_toolbar.add_action(touch_action);
+ main_toolbar.add_action(copy_action);
+ main_toolbar.add_action(cut_action);
+ main_toolbar.add_action(paste_action);
+ main_toolbar.add_action(focus_dependent_delete_action);
+ main_toolbar.add_action(directory_view.open_terminal_action());
+
+ main_toolbar.add_separator();
+ main_toolbar.add_action(*view_as_icons_action);
+ main_toolbar.add_action(*view_as_table_action);
+ main_toolbar.add_action(*view_as_columns_action);
+
+ directory_view.on_path_change = [&](const String& new_path, bool can_write_in_path) {
+ auto icon = GUI::FileIconProvider::icon_for_path(new_path);
+ auto* bitmap = icon.bitmap_for_size(16);
+ window->set_icon(bitmap);
+ location_textbox.set_icon(bitmap);
+
+ window->set_title(String::formatted("{} - File Manager", new_path));
+ location_textbox.set_text(new_path);
+
+ {
+ LexicalPath lexical_path(new_path);
+
+ auto segment_index_of_new_path_in_breadcrumb_bar = [&]() -> Optional<size_t> {
+ for (size_t i = 0; i < breadcrumb_bar.segment_count(); ++i) {
+ if (breadcrumb_bar.segment_data(i) == new_path)
+ return i;
+ }
+ return {};
+ }();
+
+ if (segment_index_of_new_path_in_breadcrumb_bar.has_value()) {
+ breadcrumb_bar.set_selected_segment(segment_index_of_new_path_in_breadcrumb_bar.value());
+ } else {
+ breadcrumb_bar.clear_segments();
+
+ breadcrumb_bar.append_segment("/", GUI::FileIconProvider::icon_for_path("/").bitmap_for_size(16), "/");
+ StringBuilder builder;
+
+ for (auto& part : lexical_path.parts()) {
+ // NOTE: We rebuild the path as we go, so we have something to pass to GUI::FileIconProvider.
+ builder.append('/');
+ builder.append(part);
+
+ breadcrumb_bar.append_segment(part, GUI::FileIconProvider::icon_for_path(builder.string_view()).bitmap_for_size(16), builder.string_view());
+ }
+
+ breadcrumb_bar.set_selected_segment(breadcrumb_bar.segment_count() - 1);
+
+ breadcrumb_bar.on_segment_click = [&](size_t segment_index) {
+ directory_view.open(breadcrumb_bar.segment_data(segment_index));
+ };
+ }
+ }
+
+ if (!is_reacting_to_tree_view_selection_change) {
+ auto new_index = directories_model->index(new_path, GUI::FileSystemModel::Column::Name);
+ if (new_index.is_valid()) {
+ tree_view.expand_all_parents_of(new_index);
+ tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set);
+ }
+ }
+
+ struct stat st;
+ if (lstat(new_path.characters(), &st)) {
+ perror("stat");
+ return;
+ }
+
+ paste_action->set_enabled(can_write_in_path && GUI::Clipboard::the().mime_type() == "text/uri-list");
+ go_forward_action->set_enabled(directory_view.path_history_position() < directory_view.path_history_size() - 1);
+ go_back_action->set_enabled(directory_view.path_history_position() > 0);
+ open_parent_directory_action->set_enabled(new_path != "/");
+ };
+
+ directory_view.on_accepted_drop = [&]() {
+ refresh_tree_view();
+ };
+
+ directory_view.on_status_message = [&](const StringView& message) {
+ statusbar.set_text(message);
+ };
+
+ directory_view.on_thumbnail_progress = [&](int done, int total) {
+ if (done == total) {
+ progressbar.set_visible(false);
+ return;
+ }
+ progressbar.set_range(0, total);
+ progressbar.set_value(done);
+ progressbar.set_visible(true);
+ };
+
+ directory_view.on_selection_change = [&](GUI::AbstractView& view) {
+ auto& selection = view.selection();
+ copy_action->set_enabled(!selection.is_empty());
+ cut_action->set_enabled(!selection.is_empty());
+ focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
+ || !directory_view.current_view().selection().is_empty());
+ };
+
+ directory_context_menu->add_action(copy_action);
+ directory_context_menu->add_action(cut_action);
+ directory_context_menu->add_action(folder_specific_paste_action);
+ directory_context_menu->add_action(directory_view.delete_action());
+ directory_context_menu->add_action(shortcut_action);
+ directory_context_menu->add_separator();
+ directory_context_menu->add_action(properties_action);
+
+ directory_view_context_menu->add_action(mkdir_action);
+ directory_view_context_menu->add_action(touch_action);
+ directory_view_context_menu->add_action(paste_action);
+ directory_view_context_menu->add_action(directory_view.open_terminal_action());
+ directory_view_context_menu->add_separator();
+ directory_view_context_menu->add_action(action_show_dotfiles);
+ directory_view_context_menu->add_separator();
+ directory_view_context_menu->add_action(properties_action);
+
+ tree_view_directory_context_menu->add_action(copy_action);
+ tree_view_directory_context_menu->add_action(cut_action);
+ tree_view_directory_context_menu->add_action(paste_action);
+ tree_view_directory_context_menu->add_action(tree_view_delete_action);
+ tree_view_directory_context_menu->add_separator();
+ tree_view_directory_context_menu->add_action(properties_action);
+ tree_view_directory_context_menu->add_separator();
+ tree_view_directory_context_menu->add_action(mkdir_action);
+ tree_view_directory_context_menu->add_action(touch_action);
+
+ RefPtr<GUI::Menu> file_context_menu;
+ NonnullRefPtrVector<LauncherHandler> current_file_handlers;
+ RefPtr<GUI::Action> file_context_menu_action_default_action;
+
+ directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
+ if (index.is_valid()) {
+ auto& node = directory_view.node(index);
+
+ if (node.is_directory()) {
+ auto should_get_enabled = access(node.full_path().characters(), W_OK) == 0 && GUI::Clipboard::the().mime_type() == "text/uri-list";
+ folder_specific_paste_action->set_enabled(should_get_enabled);
+ directory_context_menu->popup(event.screen_position());
+ } else {
+ auto full_path = node.full_path();
+ current_file_handlers = directory_view.get_launch_handlers(full_path);
+
+ file_context_menu = GUI::Menu::construct("Directory View File");
+ file_context_menu->add_action(copy_action);
+ file_context_menu->add_action(cut_action);
+ file_context_menu->add_action(paste_action);
+ file_context_menu->add_action(directory_view.delete_action());
+ file_context_menu->add_action(shortcut_action);
+
+ file_context_menu->add_separator();
+ bool added_open_menu_items = false;
+ auto default_file_handler = directory_view.get_default_launch_handler(current_file_handlers);
+ if (default_file_handler) {
+ auto file_open_action = default_file_handler->create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
+ directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
+ });
+ if (default_file_handler->details().launcher_type == Desktop::Launcher::LauncherType::Application)
+ file_open_action->set_text(String::formatted("Run {}", file_open_action->text()));
+ else
+ file_open_action->set_text(String::formatted("Open in {}", file_open_action->text()));
+
+ file_context_menu_action_default_action = file_open_action;
+
+ file_context_menu->add_action(move(file_open_action));
+ added_open_menu_items = true;
+ } else {
+ file_context_menu_action_default_action.clear();
+ }
+
+ if (current_file_handlers.size() > 1) {
+ added_open_menu_items = true;
+ auto& file_open_with_menu = file_context_menu->add_submenu("Open with");
+ for (auto& handler : current_file_handlers) {
+ if (&handler == default_file_handler.ptr())
+ continue;
+ file_open_with_menu.add_action(handler.create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
+ directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
+ }));
+ }
+ }
+
+ if (added_open_menu_items)
+ file_context_menu->add_separator();
+
+ file_context_menu->add_action(properties_action);
+ file_context_menu->popup(event.screen_position(), file_context_menu_action_default_action);
+ }
+ } else {
+ directory_view_context_menu->popup(event.screen_position());
+ }
+ };
+
+ tree_view.on_selection = [&](const GUI::ModelIndex& index) {
+ if (directories_model->m_previously_selected_index.is_valid())
+ directories_model->update_node_on_selection(directories_model->m_previously_selected_index, false);
+
+ directories_model->update_node_on_selection(index, true);
+ directories_model->m_previously_selected_index = index;
+ };
+
+ tree_view.on_selection_change = [&] {
+ focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
+ || !directory_view.current_view().selection().is_empty());
+
+ if (tree_view.selection().is_empty())
+ return;
+ auto path = directories_model->full_path(tree_view.selection().first());
+ if (directory_view.path() == path)
+ return;
+ TemporaryChange change(is_reacting_to_tree_view_selection_change, true);
+ directory_view.open(path);
+ copy_action->set_enabled(!tree_view.selection().is_empty());
+ cut_action->set_enabled(!tree_view.selection().is_empty());
+ directory_view.delete_action().set_enabled(!tree_view.selection().is_empty());
+ };
+
+ tree_view.on_focus_change = [&]([[maybe_unused]] const bool has_focus, [[maybe_unused]] const GUI::FocusSource source) {
+ focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && has_focus)
+ || !directory_view.current_view().selection().is_empty());
+ };
+
+ tree_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
+ if (index.is_valid()) {
+ tree_view_directory_context_menu->popup(event.screen_position());
+ }
+ };
+
+ auto copy_urls_to_directory = [&](const Vector<URL>& urls, const String& directory) {
+ if (urls.is_empty()) {
+ dbgln("No files to copy");
+ return;
+ }
+ bool had_accepted_copy = false;
+ for (auto& url_to_copy : urls) {
+ if (!url_to_copy.is_valid() || url_to_copy.path() == directory)
+ continue;
+ auto new_path = String::formatted("{}/{}", directory, LexicalPath(url_to_copy.path()).basename());
+ if (url_to_copy.path() == new_path)
+ continue;
+
+ if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
+ auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
+ GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
+ } else {
+ had_accepted_copy = true;
+ }
+ }
+ if (had_accepted_copy)
+ refresh_tree_view();
+ };
+
+ breadcrumb_bar.on_segment_drop = [&](size_t segment_index, const GUI::DropEvent& event) {
+ if (!event.mime_data().has_urls())
+ return;
+ copy_urls_to_directory(event.mime_data().urls(), breadcrumb_bar.segment_data(segment_index));
+ };
+
+ breadcrumb_bar.on_segment_drag_enter = [&](size_t, GUI::DragEvent& event) {
+ if (event.mime_types().contains_slow("text/uri-list"))
+ event.accept();
+ };
+
+ breadcrumb_bar.on_doubleclick = [&](const GUI::MouseEvent&) {
+ go_to_location_action->activate();
+ };
+
+ tree_view.on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
+ if (!event.mime_data().has_urls())
+ return;
+ auto& target_node = directories_model->node(index);
+ if (!target_node.is_directory())
+ return;
+ copy_urls_to_directory(event.mime_data().urls(), target_node.full_path());
+ };
+
+ directory_view.open(initial_location);
+ directory_view.set_focus(true);
+
+ paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(initial_location.characters(), W_OK) == 0);
+
+ window->show();
+
+ // Read directory read mode from config.
+ auto dir_view_mode = config->read_entry("DirectoryView", "ViewMode", "Icon");
+
+ if (dir_view_mode.contains("Table")) {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Table);
+ view_as_table_action->set_checked(true);
+ } else if (dir_view_mode.contains("Columns")) {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
+ view_as_columns_action->set_checked(true);
+ } else {
+ directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
+ view_as_icons_action->set_checked(true);
+ }
+
+ // Write window position to config file on close request.
+ window->on_close_request = [&] {
+ config->write_num_entry("Window", "Left", window->x());
+ config->write_num_entry("Window", "Top", window->y());
+ config->write_num_entry("Window", "Width", window->width());
+ config->write_num_entry("Window", "Height", window->height());
+ config->sync();
+
+ return GUI::Window::CloseRequestDecision::Close;
+ };
+
+ return GUI::Application::the()->exec();
+}
diff --git a/Userland/Applications/FontEditor/.gitignore b/Userland/Applications/FontEditor/.gitignore
new file mode 100644
index 0000000000..0e7af5b54f
--- /dev/null
+++ b/Userland/Applications/FontEditor/.gitignore
@@ -0,0 +1 @@
+UI_*.h
diff --git a/Userland/Applications/FontEditor/CMakeLists.txt b/Userland/Applications/FontEditor/CMakeLists.txt
new file mode 100644
index 0000000000..569ab9a9bd
--- /dev/null
+++ b/Userland/Applications/FontEditor/CMakeLists.txt
@@ -0,0 +1,11 @@
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
+
+set(SOURCES
+ FontEditor.cpp
+ GlyphEditorWidget.cpp
+ GlyphMapWidget.cpp
+ main.cpp
+)
+
+serenity_app(FontEditor ICON app-font-editor)
+target_link_libraries(FontEditor LibGUI LibDesktop LibGfx)
diff --git a/Userland/Applications/FontEditor/FontEditor.cpp b/Userland/Applications/FontEditor/FontEditor.cpp
new file mode 100644
index 0000000000..56cbf3e06b
--- /dev/null
+++ b/Userland/Applications/FontEditor/FontEditor.cpp
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FontEditor.h"
+#include "GlyphEditorWidget.h"
+#include "GlyphMapWidget.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/BitmapFont.h>
+#include <LibGfx/Palette.h>
+#include <stdlib.h>
+
+FontEditorWidget::FontEditorWidget(const String& path, RefPtr<Gfx::BitmapFont>&& edited_font)
+ : m_edited_font(move(edited_font))
+ , m_path(path)
+{
+ set_fill_with_background_color(true);
+ set_layout<GUI::VerticalBoxLayout>();
+
+ // Top
+ auto& main_container = add<GUI::Widget>();
+ main_container.set_layout<GUI::HorizontalBoxLayout>();
+ main_container.layout()->set_margins({ 4, 4, 4, 4 });
+ main_container.set_background_role(Gfx::ColorRole::SyntaxKeyword);
+
+ // Top-Left Glyph Editor and info
+ auto& editor_container = main_container.add<GUI::Widget>();
+ editor_container.set_layout<GUI::VerticalBoxLayout>();
+ editor_container.layout()->set_margins({ 4, 4, 4, 4 });
+ editor_container.set_background_role(Gfx::ColorRole::SyntaxKeyword);
+
+ m_glyph_editor_widget = editor_container.add<GlyphEditorWidget>(*m_edited_font);
+ m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height());
+
+ editor_container.set_fixed_width(m_glyph_editor_widget->preferred_width());
+
+ auto& glyph_width_label = editor_container.add<GUI::Label>();
+ glyph_width_label.set_fixed_height(22);
+ glyph_width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ glyph_width_label.set_text("Glyph width:");
+
+ auto& glyph_width_spinbox = editor_container.add<GUI::SpinBox>();
+ glyph_width_spinbox.set_min(0);
+ glyph_width_spinbox.set_max(32);
+ glyph_width_spinbox.set_value(0);
+ glyph_width_spinbox.set_enabled(!m_edited_font->is_fixed_width());
+
+ auto& info_label = editor_container.add<GUI::Label>();
+ info_label.set_fixed_height(22);
+ info_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ info_label.set_text("info_label");
+
+ /// Top-Right glyph map and font meta data
+
+ auto& map_and_test_container = main_container.add<GUI::Widget>();
+ map_and_test_container.set_layout<GUI::VerticalBoxLayout>();
+ map_and_test_container.layout()->set_margins({ 4, 4, 4, 4 });
+
+ m_glyph_map_widget = map_and_test_container.add<GlyphMapWidget>(*m_edited_font);
+ m_glyph_map_widget->set_fixed_size(m_glyph_map_widget->preferred_width(), m_glyph_map_widget->preferred_height());
+
+ auto& font_mtest_group_box = map_and_test_container.add<GUI::GroupBox>();
+ font_mtest_group_box.set_layout<GUI::VerticalBoxLayout>();
+ font_mtest_group_box.layout()->set_margins({ 5, 15, 5, 5 });
+ font_mtest_group_box.set_fixed_height(2 * m_edited_font->glyph_height() + 50);
+ font_mtest_group_box.set_title("Test");
+
+ auto& demo_label_1 = font_mtest_group_box.add<GUI::Label>();
+ demo_label_1.set_font(m_edited_font);
+ demo_label_1.set_text("quick fox jumps nightly above wizard.");
+
+ auto& demo_label_2 = font_mtest_group_box.add<GUI::Label>();
+ demo_label_2.set_font(m_edited_font);
+ demo_label_2.set_text("QUICK FOX JUMPS NIGHTLY ABOVE WIZARD!");
+
+ auto& font_metadata_group_box = map_and_test_container.add<GUI::GroupBox>();
+ font_metadata_group_box.set_layout<GUI::VerticalBoxLayout>();
+ font_metadata_group_box.layout()->set_margins({ 5, 15, 5, 5 });
+ font_metadata_group_box.set_fixed_height(275);
+ font_metadata_group_box.set_title("Font metadata");
+
+ //// Name Row
+ auto& namecontainer = font_metadata_group_box.add<GUI::Widget>();
+ namecontainer.set_layout<GUI::HorizontalBoxLayout>();
+ namecontainer.set_fixed_height(22);
+
+ auto& name_label = namecontainer.add<GUI::Label>();
+ name_label.set_fixed_width(100);
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ name_label.set_text("Name:");
+
+ auto& name_textbox = namecontainer.add<GUI::TextBox>();
+ name_textbox.set_text(m_edited_font->name());
+ name_textbox.on_change = [&] {
+ m_edited_font->set_name(name_textbox.text());
+ };
+
+ //// Family Row
+ auto& family_container = font_metadata_group_box.add<GUI::Widget>();
+ family_container.set_layout<GUI::HorizontalBoxLayout>();
+ family_container.set_fixed_height(22);
+
+ auto& family_label = family_container.add<GUI::Label>();
+ family_label.set_fixed_width(100);
+ family_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ family_label.set_text("Family:");
+
+ auto& family_textbox = family_container.add<GUI::TextBox>();
+ family_textbox.set_text(m_edited_font->family());
+ family_textbox.on_change = [&] {
+ m_edited_font->set_family(family_textbox.text());
+ };
+
+ //// Presentation size Row
+ auto& presentation_size_container = font_metadata_group_box.add<GUI::Widget>();
+ presentation_size_container.set_layout<GUI::HorizontalBoxLayout>();
+ presentation_size_container.set_fixed_height(22);
+
+ auto& presentation_size_label = presentation_size_container.add<GUI::Label>();
+ presentation_size_label.set_fixed_width(100);
+ presentation_size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ presentation_size_label.set_text("Presentation size:");
+
+ auto& presentation_size_spinbox = presentation_size_container.add<GUI::SpinBox>();
+ presentation_size_spinbox.set_min(0);
+ presentation_size_spinbox.set_max(255);
+ presentation_size_spinbox.set_value(m_edited_font->presentation_size());
+
+ //// Weight Row
+ auto& weight_container = font_metadata_group_box.add<GUI::Widget>();
+ weight_container.set_layout<GUI::HorizontalBoxLayout>();
+ weight_container.set_fixed_height(22);
+
+ auto& weight_label = weight_container.add<GUI::Label>();
+ weight_label.set_fixed_width(100);
+ weight_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ weight_label.set_text("Weight:");
+
+ auto& weight_spinbox = weight_container.add<GUI::SpinBox>();
+ weight_spinbox.set_min(0);
+ weight_spinbox.set_max(65535);
+ weight_spinbox.set_value(m_edited_font->weight());
+
+ //// Glyph spacing Row
+ auto& glyph_spacing_container = font_metadata_group_box.add<GUI::Widget>();
+ glyph_spacing_container.set_layout<GUI::HorizontalBoxLayout>();
+ glyph_spacing_container.set_fixed_height(22);
+
+ auto& glyph_spacing = glyph_spacing_container.add<GUI::Label>();
+ glyph_spacing.set_fixed_width(100);
+ glyph_spacing.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ glyph_spacing.set_text("Glyph spacing:");
+
+ auto& spacing_spinbox = glyph_spacing_container.add<GUI::SpinBox>();
+ spacing_spinbox.set_min(0);
+ spacing_spinbox.set_max(255);
+ spacing_spinbox.set_value(m_edited_font->glyph_spacing());
+
+ //// Glyph Height Row
+ auto& glyph_height_container = font_metadata_group_box.add<GUI::Widget>();
+ glyph_height_container.set_layout<GUI::HorizontalBoxLayout>();
+ glyph_height_container.set_fixed_height(22);
+
+ auto& glyph_height = glyph_height_container.add<GUI::Label>();
+ glyph_height.set_fixed_width(100);
+ glyph_height.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ glyph_height.set_text("Glyph height:");
+
+ auto& glyph_height_spinbox = glyph_height_container.add<GUI::SpinBox>();
+ glyph_height_spinbox.set_min(0);
+ glyph_height_spinbox.set_max(255);
+ glyph_height_spinbox.set_value(m_edited_font->glyph_height());
+ glyph_height_spinbox.set_enabled(false);
+
+ //// Glyph width Row
+ auto& glyph_weight_container = font_metadata_group_box.add<GUI::Widget>();
+ glyph_weight_container.set_layout<GUI::HorizontalBoxLayout>();
+ glyph_weight_container.set_fixed_height(22);
+
+ auto& glyph_header_width_label = glyph_weight_container.add<GUI::Label>();
+ glyph_header_width_label.set_fixed_width(100);
+ glyph_header_width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ glyph_header_width_label.set_text("Glyph width:");
+
+ auto& glyph_header_width_spinbox = glyph_weight_container.add<GUI::SpinBox>();
+ glyph_header_width_spinbox.set_min(0);
+ glyph_header_width_spinbox.set_max(255);
+ glyph_header_width_spinbox.set_value(m_edited_font->glyph_fixed_width());
+ glyph_header_width_spinbox.set_enabled(false);
+
+ //// Baseline Row
+ auto& baseline_container = font_metadata_group_box.add<GUI::Widget>();
+ baseline_container.set_layout<GUI::HorizontalBoxLayout>();
+ baseline_container.set_fixed_height(22);
+
+ auto& baseline_label = baseline_container.add<GUI::Label>();
+ baseline_label.set_fixed_width(100);
+ baseline_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ baseline_label.set_text("Baseline:");
+
+ auto& baseline_spinbox = baseline_container.add<GUI::SpinBox>();
+ baseline_spinbox.set_min(0);
+ baseline_spinbox.set_max(m_edited_font->glyph_height() - 1);
+ baseline_spinbox.set_value(m_edited_font->baseline());
+
+ //// Mean line Row
+ auto& mean_line_container = font_metadata_group_box.add<GUI::Widget>();
+ mean_line_container.set_layout<GUI::HorizontalBoxLayout>();
+ mean_line_container.set_fixed_height(22);
+
+ auto& mean_line_label = mean_line_container.add<GUI::Label>();
+ mean_line_label.set_fixed_width(100);
+ mean_line_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ mean_line_label.set_text("Mean Line:");
+
+ auto& mean_line_spinbox = mean_line_container.add<GUI::SpinBox>();
+ mean_line_spinbox.set_min(0);
+ mean_line_spinbox.set_max(m_edited_font->glyph_height() - 1);
+ mean_line_spinbox.set_value(m_edited_font->mean_line());
+
+ //// Fixed checkbox Row
+ auto& fixed_width_checkbox = font_metadata_group_box.add<GUI::CheckBox>();
+ fixed_width_checkbox.set_text("Fixed width");
+ fixed_width_checkbox.set_checked(m_edited_font->is_fixed_width());
+
+ // Bottom
+ auto& bottom_container = add<GUI::Widget>();
+ bottom_container.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_container.layout()->set_margins({ 8, 0, 8, 8 });
+ bottom_container.set_fixed_height(32);
+
+ bottom_container.layout()->add_spacer();
+
+ auto& save_button = bottom_container.add<GUI::Button>();
+ save_button.set_fixed_size(80, 22);
+ save_button.set_text("Save");
+ save_button.on_click = [this](auto) { save_as(m_path); };
+
+ auto& quit_button = bottom_container.add<GUI::Button>();
+ quit_button.set_fixed_size(80, 22);
+ quit_button.set_text("Quit");
+ quit_button.on_click = [](auto) {
+ exit(0);
+ };
+
+ // Event hanglers
+ auto update_demo = [&] {
+ demo_label_1.update();
+ demo_label_2.update();
+ };
+
+ auto calculate_prefed_sizes = [&] {
+ int right_site_width = m_edited_font->width("QUICK FOX JUMPS NIGHTLY ABOVE WIZARD!") + 20;
+ right_site_width = max(right_site_width, m_glyph_map_widget->preferred_width());
+
+ m_preferred_width = m_glyph_editor_widget->width() + right_site_width + 20;
+ m_preferred_height = m_glyph_map_widget->relative_rect().height() + 2 * m_edited_font->glyph_height() + 380;
+ };
+
+ m_glyph_editor_widget->on_glyph_altered = [this, update_demo](u8 glyph) {
+ m_glyph_map_widget->update_glyph(glyph);
+ update_demo();
+ };
+
+ m_glyph_map_widget->on_glyph_selected = [&](size_t glyph) {
+ m_glyph_editor_widget->set_glyph(glyph);
+ glyph_width_spinbox.set_value(m_edited_font->glyph_width(m_glyph_map_widget->selected_glyph()));
+ StringBuilder builder;
+ builder.appendff("{:#02x} (", glyph);
+ if (glyph < 128) {
+ builder.append(glyph);
+ } else {
+ builder.append(128 | 64 | (glyph / 64));
+ builder.append(128 | (glyph % 64));
+ }
+ builder.append(')');
+ info_label.set_text(builder.to_string());
+ };
+
+ fixed_width_checkbox.on_checked = [&, update_demo](bool checked) {
+ m_edited_font->set_fixed_width(checked);
+ glyph_width_spinbox.set_enabled(!m_edited_font->is_fixed_width());
+ glyph_width_spinbox.set_value(m_edited_font->glyph_width(m_glyph_map_widget->selected_glyph()));
+ m_glyph_editor_widget->update();
+ update_demo();
+ };
+
+ glyph_width_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), value);
+ m_glyph_editor_widget->update();
+ m_glyph_map_widget->update_glyph(m_glyph_map_widget->selected_glyph());
+ update_demo();
+ };
+
+ weight_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_weight(value);
+ update_demo();
+ };
+
+ presentation_size_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_presentation_size(value);
+ update_demo();
+ };
+
+ spacing_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_glyph_spacing(value);
+ update_demo();
+ };
+
+ baseline_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_baseline(value);
+ m_glyph_editor_widget->update();
+ update_demo();
+ };
+
+ mean_line_spinbox.on_change = [this, update_demo](int value) {
+ m_edited_font->set_mean_line(value);
+ m_glyph_editor_widget->update();
+ update_demo();
+ };
+
+ // init widget
+ calculate_prefed_sizes();
+ m_glyph_map_widget->set_selected_glyph('A');
+}
+
+FontEditorWidget::~FontEditorWidget()
+{
+}
+
+bool FontEditorWidget::save_as(const String& path)
+{
+ auto ret_val = m_edited_font->write_to_file(path);
+ if (!ret_val) {
+ GUI::MessageBox::show(window(), "The font file could not be saved.", "Save failed", GUI::MessageBox::Type::Error);
+ return false;
+ }
+ m_path = path;
+ return true;
+}
diff --git a/Userland/Applications/FontEditor/FontEditor.h b/Userland/Applications/FontEditor/FontEditor.h
new file mode 100644
index 0000000000..ed904e8e7d
--- /dev/null
+++ b/Userland/Applications/FontEditor/FontEditor.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/BitmapFont.h>
+
+class GlyphEditorWidget;
+class GlyphMapWidget;
+
+class FontEditorWidget final : public GUI::Widget {
+ C_OBJECT(FontEditorWidget)
+public:
+ virtual ~FontEditorWidget() override;
+
+ int preferred_width() { return m_preferred_width; }
+ int preferred_height() { return m_preferred_height; }
+
+ bool save_as(const String&);
+
+ const String& path() { return m_path; }
+
+private:
+ FontEditorWidget(const String& path, RefPtr<Gfx::BitmapFont>&&);
+ RefPtr<Gfx::BitmapFont> m_edited_font;
+
+ RefPtr<GlyphMapWidget> m_glyph_map_widget;
+ RefPtr<GlyphEditorWidget> m_glyph_editor_widget;
+
+ String m_path;
+ int m_preferred_width;
+ int m_preferred_height;
+};
diff --git a/Userland/Applications/FontEditor/GlyphEditorWidget.cpp b/Userland/Applications/FontEditor/GlyphEditorWidget.cpp
new file mode 100644
index 0000000000..6f545c1ccd
--- /dev/null
+++ b/Userland/Applications/FontEditor/GlyphEditorWidget.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "GlyphEditorWidget.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/BitmapFont.h>
+#include <LibGfx/Palette.h>
+
+GlyphEditorWidget::GlyphEditorWidget(Gfx::BitmapFont& mutable_font)
+ : m_font(mutable_font)
+{
+ set_relative_rect({ 0, 0, preferred_width(), preferred_height() });
+}
+
+GlyphEditorWidget::~GlyphEditorWidget()
+{
+}
+
+void GlyphEditorWidget::set_glyph(int glyph)
+{
+ if (m_glyph == glyph)
+ return;
+ m_glyph = glyph;
+ update();
+}
+
+void GlyphEditorWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(frame_inner_rect());
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect(frame_inner_rect(), palette().base());
+ painter.translate(frame_thickness(), frame_thickness());
+
+ painter.translate(-1, -1);
+ for (int y = 1; y < font().glyph_height(); ++y) {
+ int y_below = y - 1;
+ bool bold_line = y_below == font().baseline() || y_below == font().mean_line();
+ painter.draw_line({ 0, y * m_scale }, { font().max_glyph_width() * m_scale, y * m_scale }, palette().threed_shadow2(), bold_line ? 2 : 1);
+ }
+
+ for (int x = 1; x < font().max_glyph_width(); ++x)
+ painter.draw_line({ x * m_scale, 0 }, { x * m_scale, font().glyph_height() * m_scale }, palette().threed_shadow2());
+
+ auto bitmap = font().glyph_bitmap(m_glyph);
+
+ for (int y = 0; y < font().glyph_height(); ++y) {
+ for (int x = 0; x < font().max_glyph_width(); ++x) {
+ Gfx::IntRect rect { x * m_scale, y * m_scale, m_scale, m_scale };
+ if (x >= font().glyph_width(m_glyph)) {
+ painter.fill_rect(rect, palette().threed_shadow1());
+ } else {
+ if (bitmap.bit_at(x, y))
+ painter.fill_rect(rect, palette().base_text());
+ }
+ }
+ }
+}
+
+void GlyphEditorWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ draw_at_mouse(event);
+}
+
+void GlyphEditorWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (event.buttons() & (GUI::MouseButton::Left | GUI::MouseButton::Right))
+ draw_at_mouse(event);
+}
+
+void GlyphEditorWidget::draw_at_mouse(const GUI::MouseEvent& event)
+{
+ bool set = event.buttons() & GUI::MouseButton::Left;
+ bool unset = event.buttons() & GUI::MouseButton::Right;
+ if (!(set ^ unset))
+ return;
+ int x = (event.x() - 1) / m_scale;
+ int y = (event.y() - 1) / m_scale;
+ auto bitmap = font().glyph_bitmap(m_glyph);
+ if (x < 0 || x >= bitmap.width())
+ return;
+ if (y < 0 || y >= bitmap.height())
+ return;
+ if (bitmap.bit_at(x, y) == set)
+ return;
+ bitmap.set_bit_at(x, y, set);
+ if (on_glyph_altered)
+ on_glyph_altered(m_glyph);
+ update();
+}
+
+int GlyphEditorWidget::preferred_width() const
+{
+ return frame_thickness() * 2 + font().max_glyph_width() * m_scale - 1;
+}
+
+int GlyphEditorWidget::preferred_height() const
+{
+ return frame_thickness() * 2 + font().glyph_height() * m_scale - 1;
+}
diff --git a/Userland/Applications/FontEditor/GlyphEditorWidget.h b/Userland/Applications/FontEditor/GlyphEditorWidget.h
new file mode 100644
index 0000000000..a54c551642
--- /dev/null
+++ b/Userland/Applications/FontEditor/GlyphEditorWidget.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibGUI/Frame.h>
+#include <LibGfx/BitmapFont.h>
+
+class GlyphEditorWidget final : public GUI::Frame {
+ C_OBJECT(GlyphEditorWidget)
+public:
+ virtual ~GlyphEditorWidget() override;
+
+ int glyph() const { return m_glyph; }
+ void set_glyph(int);
+
+ int preferred_width() const;
+ int preferred_height() const;
+
+ Gfx::BitmapFont& font() { return *m_font; }
+ const Gfx::BitmapFont& font() const { return *m_font; }
+
+ Function<void(u8)> on_glyph_altered;
+
+private:
+ GlyphEditorWidget(Gfx::BitmapFont&);
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+
+ void draw_at_mouse(const GUI::MouseEvent&);
+
+ RefPtr<Gfx::BitmapFont> m_font;
+ int m_glyph { 0 };
+ int m_scale { 10 };
+};
diff --git a/Userland/Applications/FontEditor/GlyphMapWidget.cpp b/Userland/Applications/FontEditor/GlyphMapWidget.cpp
new file mode 100644
index 0000000000..ed6a65493f
--- /dev/null
+++ b/Userland/Applications/FontEditor/GlyphMapWidget.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "GlyphMapWidget.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/BitmapFont.h>
+#include <LibGfx/Palette.h>
+
+GlyphMapWidget::GlyphMapWidget(Gfx::BitmapFont& mutable_font)
+ : m_font(mutable_font)
+{
+ m_glyph_count = mutable_font.glyph_count();
+ set_relative_rect({ 0, 0, preferred_width(), preferred_height() });
+ set_focus_policy(GUI::FocusPolicy::StrongFocus);
+}
+
+GlyphMapWidget::~GlyphMapWidget()
+{
+}
+
+int GlyphMapWidget::preferred_width() const
+{
+ return columns() * (font().max_glyph_width() + m_horizontal_spacing) + 2 + frame_thickness() * 2;
+}
+
+int GlyphMapWidget::preferred_height() const
+{
+ return rows() * (font().glyph_height() + m_vertical_spacing) + 2 + frame_thickness() * 2;
+}
+
+void GlyphMapWidget::set_selected_glyph(int glyph)
+{
+ if (m_selected_glyph == glyph)
+ return;
+ m_selected_glyph = glyph;
+ if (on_glyph_selected)
+ on_glyph_selected(glyph);
+ update();
+}
+
+Gfx::IntRect GlyphMapWidget::get_outer_rect(int glyph) const
+{
+ int row = glyph / columns();
+ int column = glyph % columns();
+ return Gfx::IntRect {
+ column * (font().max_glyph_width() + m_horizontal_spacing) + 1,
+ row * (font().glyph_height() + m_vertical_spacing) + 1,
+ font().max_glyph_width() + m_horizontal_spacing,
+ font().glyph_height() + m_horizontal_spacing
+ }
+ .translated(frame_thickness(), frame_thickness());
+}
+
+void GlyphMapWidget::update_glyph(int glyph)
+{
+ update(get_outer_rect(glyph));
+}
+
+void GlyphMapWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.set_font(font());
+ painter.fill_rect(frame_inner_rect(), palette().base());
+
+ for (int glyph = 0; glyph < m_glyph_count; ++glyph) {
+ Gfx::IntRect outer_rect = get_outer_rect(glyph);
+ Gfx::IntRect inner_rect(
+ outer_rect.x() + m_horizontal_spacing / 2,
+ outer_rect.y() + m_vertical_spacing / 2,
+ font().max_glyph_width(),
+ font().glyph_height());
+ if (glyph == m_selected_glyph) {
+ painter.fill_rect(outer_rect, is_focused() ? palette().selection() : palette().inactive_selection());
+ painter.draw_glyph(inner_rect.location(), glyph, is_focused() ? palette().selection_text() : palette().inactive_selection_text());
+ } else {
+ painter.draw_glyph(inner_rect.location(), glyph, palette().base_text());
+ }
+ }
+}
+
+void GlyphMapWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ GUI::Frame::mousedown_event(event);
+
+ // FIXME: This is a silly loop.
+ for (int glyph = 0; glyph < m_glyph_count; ++glyph) {
+ if (get_outer_rect(glyph).contains(event.position())) {
+ set_selected_glyph(glyph);
+ break;
+ }
+ }
+}
+
+void GlyphMapWidget::keydown_event(GUI::KeyEvent& event)
+{
+ GUI::Frame::keydown_event(event);
+
+ if (event.key() == KeyCode::Key_Up) {
+ if (selected_glyph() >= m_columns) {
+ set_selected_glyph(selected_glyph() - m_columns);
+ return;
+ }
+ }
+ if (event.key() == KeyCode::Key_Down) {
+ if (selected_glyph() < m_glyph_count - m_columns) {
+ set_selected_glyph(selected_glyph() + m_columns);
+ return;
+ }
+ }
+ if (event.key() == KeyCode::Key_Left) {
+ if (selected_glyph() > 0) {
+ set_selected_glyph(selected_glyph() - 1);
+ return;
+ }
+ }
+ if (event.key() == KeyCode::Key_Right) {
+ if (selected_glyph() < m_glyph_count - 1) {
+ set_selected_glyph(selected_glyph() + 1);
+ return;
+ }
+ }
+ if (event.ctrl() && event.key() == KeyCode::Key_Home) {
+ set_selected_glyph(0);
+ return;
+ }
+ if (event.ctrl() && event.key() == KeyCode::Key_End) {
+ set_selected_glyph(m_glyph_count - 1);
+ return;
+ }
+ if (!event.ctrl() && event.key() == KeyCode::Key_Home) {
+ set_selected_glyph(selected_glyph() / m_columns * m_columns);
+ return;
+ }
+ if (!event.ctrl() && event.key() == KeyCode::Key_End) {
+ int new_selection = selected_glyph() / m_columns * m_columns + (m_columns - 1);
+ int max = m_glyph_count - 1;
+ new_selection = clamp(new_selection, 0, max);
+ set_selected_glyph(new_selection);
+ return;
+ }
+}
diff --git a/Userland/Applications/FontEditor/GlyphMapWidget.h b/Userland/Applications/FontEditor/GlyphMapWidget.h
new file mode 100644
index 0000000000..2c8443a251
--- /dev/null
+++ b/Userland/Applications/FontEditor/GlyphMapWidget.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/StdLibExtras.h>
+#include <LibGUI/Frame.h>
+#include <LibGfx/BitmapFont.h>
+
+class GlyphMapWidget final : public GUI::Frame {
+ C_OBJECT(GlyphMapWidget)
+public:
+ virtual ~GlyphMapWidget() override;
+
+ int selected_glyph() const { return m_selected_glyph; }
+ void set_selected_glyph(int);
+
+ int rows() const { return ceil_div(m_glyph_count, m_columns); }
+ int columns() const { return m_columns; }
+
+ int preferred_width() const;
+ int preferred_height() const;
+
+ Gfx::BitmapFont& font() { return *m_font; }
+ const Gfx::BitmapFont& font() const { return *m_font; }
+
+ void update_glyph(int);
+
+ Function<void(int)> on_glyph_selected;
+
+private:
+ explicit GlyphMapWidget(Gfx::BitmapFont&);
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+ Gfx::IntRect get_outer_rect(int glyph) const;
+
+ RefPtr<Gfx::BitmapFont> m_font;
+ int m_glyph_count;
+ int m_columns { 32 };
+ int m_horizontal_spacing { 2 };
+ int m_vertical_spacing { 2 };
+ int m_selected_glyph { 0 };
+};
diff --git a/Userland/Applications/FontEditor/main.cpp b/Userland/Applications/FontEditor/main.cpp
new file mode 100644
index 0000000000..f0a72d1d29
--- /dev/null
+++ b/Userland/Applications/FontEditor/main.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FontEditor.h"
+#include <AK/URL.h>
+#include <LibCore/ArgsParser.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/BitmapFont.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/Point.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer thread rpath accept unix cpath wpath fattr unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer thread rpath accept cpath wpath unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (!Desktop::Launcher::add_allowed_handler_with_only_specific_urls(
+ "/bin/Help",
+ { URL::create_with_file_protocol("/usr/share/man/man1/FontEditor.md") })
+ || !Desktop::Launcher::seal_allowlist()) {
+ warnln("Failed to set up allowed launch URLs");
+ return 1;
+ }
+
+ if (pledge("stdio shared_buffer thread rpath accept cpath wpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* path = nullptr;
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(path, "The font file for editing.", "file", Core::ArgsParser::Required::No);
+ args_parser.parse(argc, argv);
+
+ RefPtr<Gfx::BitmapFont> edited_font;
+ if (path == nullptr) {
+ path = "/tmp/saved.font";
+ edited_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::FontDatabase::default_font().clone());
+ } else {
+ edited_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::Font::load_from_file(path)->clone());
+ if (!edited_font) {
+ String message = String::formatted("Couldn't load font: {}\n", path);
+ GUI::MessageBox::show(nullptr, message, "Font Editor", GUI::MessageBox::Type::Error);
+ return 1;
+ }
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-font-editor");
+
+ auto window = GUI::Window::construct();
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto set_edited_font = [&](const String& path, RefPtr<Gfx::BitmapFont>&& font, Gfx::IntPoint point) {
+ // Convert 256 char font to 384 char font.
+ if (font->type() == Gfx::FontTypes::Default)
+ font->set_type(Gfx::FontTypes::LatinExtendedA);
+
+ window->set_title(String::formatted("{} - Font Editor", path));
+ auto& font_editor_widget = window->set_main_widget<FontEditorWidget>(path, move(font));
+ window->set_rect({ point, { font_editor_widget.preferred_width(), font_editor_widget.preferred_height() } });
+ };
+ set_edited_font(path, move(edited_font), window->position());
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Font Editor");
+ app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window);
+ if (!open_path.has_value())
+ return;
+
+ RefPtr<Gfx::BitmapFont> new_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::Font::load_from_file(open_path.value())->clone());
+ if (!new_font) {
+ String message = String::formatted("Couldn't load font: {}\n", open_path.value());
+ GUI::MessageBox::show(window, message, "Font Editor", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ set_edited_font(open_path.value(), move(new_font), window->position());
+ }));
+ app_menu.add_action(GUI::CommonActions::make_save_action([&](auto&) {
+ FontEditorWidget* editor = static_cast<FontEditorWidget*>(window->main_widget());
+ editor->save_as(editor->path());
+ }));
+ app_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
+ FontEditorWidget* editor = static_cast<FontEditorWidget*>(window->main_widget());
+ LexicalPath lexical_path(editor->path());
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, lexical_path.title(), lexical_path.extension());
+ if (!save_path.has_value())
+ return;
+
+ if (editor->save_as(save_path.value()))
+ window->set_title(String::formatted("{} - Font Editor", save_path.value()));
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
+ app->quit();
+ return;
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) {
+ Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/FontEditor.md"), "/bin/Help");
+ }));
+
+ help_menu.add_action(GUI::CommonActions::make_about_action("Font Editor", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/Help/CMakeLists.txt b/Userland/Applications/Help/CMakeLists.txt
new file mode 100644
index 0000000000..46a832c044
--- /dev/null
+++ b/Userland/Applications/Help/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES
+ History.cpp
+ main.cpp
+ ManualModel.cpp
+ ManualPageNode.cpp
+ ManualSectionNode.cpp
+)
+
+serenity_app(Help ICON app-help)
+target_link_libraries(Help LibWeb LibMarkdown LibGUI LibDesktop)
diff --git a/Userland/Applications/Help/History.cpp b/Userland/Applications/Help/History.cpp
new file mode 100644
index 0000000000..bb4c822dcb
--- /dev/null
+++ b/Userland/Applications/Help/History.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "History.h"
+
+void History::push(const StringView& history_item)
+{
+ m_items.shrink(m_current_history_item + 1);
+ m_items.append(history_item);
+ m_current_history_item++;
+}
+
+String History::current()
+{
+ if (m_current_history_item == -1)
+ return {};
+ return m_items[m_current_history_item];
+}
+
+void History::go_back()
+{
+ ASSERT(can_go_back());
+ m_current_history_item--;
+}
+
+void History::go_forward()
+{
+ ASSERT(can_go_forward());
+ m_current_history_item++;
+}
+
+void History::clear()
+{
+ m_items = {};
+ m_current_history_item = -1;
+}
diff --git a/Userland/Applications/Help/History.h b/Userland/Applications/Help/History.h
new file mode 100644
index 0000000000..c4058fabfc
--- /dev/null
+++ b/Userland/Applications/Help/History.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Vector.h>
+
+class History final {
+public:
+ void push(const StringView& history_item);
+ String current();
+
+ void go_back();
+ void go_forward();
+
+ bool can_go_back() { return m_current_history_item > 0; }
+ bool can_go_forward() { return m_current_history_item + 1 < static_cast<int>(m_items.size()); }
+
+ void clear();
+
+private:
+ Vector<String> m_items;
+ int m_current_history_item { -1 };
+};
diff --git a/Userland/Applications/Help/ManualModel.cpp b/Userland/Applications/Help/ManualModel.cpp
new file mode 100644
index 0000000000..484b1b22c1
--- /dev/null
+++ b/Userland/Applications/Help/ManualModel.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ManualModel.h"
+#include "ManualNode.h"
+#include "ManualPageNode.h"
+#include "ManualSectionNode.h"
+#include <AK/ByteBuffer.h>
+#include <LibCore/File.h>
+#include <LibGUI/FilteringProxyModel.h>
+
+static ManualSectionNode s_sections[] = {
+ { "1", "User programs" },
+ { "2", "System calls" },
+ { "3", "Libraries" },
+ { "4", "Special files" },
+ { "5", "File formats" },
+ { "6", "Games" },
+ { "7", "Miscellanea" },
+ { "8", "Sysadmin tools" }
+};
+
+ManualModel::ManualModel()
+{
+ m_section_open_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book-open.png"));
+ m_section_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book.png"));
+ m_page_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"));
+}
+
+Optional<GUI::ModelIndex> ManualModel::index_from_path(const StringView& path) const
+{
+ for (int section = 0; section < row_count(); ++section) {
+ auto parent_index = index(section, 0);
+ for (int row = 0; row < row_count(parent_index); ++row) {
+ auto child_index = index(row, 0, parent_index);
+ auto* node = static_cast<const ManualNode*>(child_index.internal_data());
+ if (!node->is_page())
+ continue;
+ auto* page = static_cast<const ManualPageNode*>(node);
+ if (page->path() != path)
+ continue;
+ return child_index;
+ }
+ }
+ return {};
+}
+
+String ManualModel::page_path(const GUI::ModelIndex& index) const
+{
+ if (!index.is_valid())
+ return {};
+ auto* node = static_cast<const ManualNode*>(index.internal_data());
+ if (!node->is_page())
+ return {};
+ auto* page = static_cast<const ManualPageNode*>(node);
+ return page->path();
+}
+
+Result<StringView, OSError> ManualModel::page_view(const String& path) const
+{
+ if (path.is_empty())
+ return StringView {};
+
+ {
+ // Check if we've got it cached already.
+ auto mapped_file = m_mapped_files.get(path);
+ if (mapped_file.has_value())
+ return StringView { mapped_file.value()->bytes() };
+ }
+
+ auto file_or_error = MappedFile::map(path);
+ if (file_or_error.is_error())
+ return file_or_error.error();
+
+ StringView view { file_or_error.value()->bytes() };
+ m_mapped_files.set(path, file_or_error.release_value());
+ return view;
+}
+
+String ManualModel::page_and_section(const GUI::ModelIndex& index) const
+{
+ if (!index.is_valid())
+ return {};
+ auto* node = static_cast<const ManualNode*>(index.internal_data());
+ if (!node->is_page())
+ return {};
+ auto* page = static_cast<const ManualPageNode*>(node);
+ auto* section = static_cast<const ManualSectionNode*>(page->parent());
+ return String::formatted("{}({})", page->name(), section->section_name());
+}
+
+GUI::ModelIndex ManualModel::index(int row, int column, const GUI::ModelIndex& parent_index) const
+{
+ if (!parent_index.is_valid())
+ return create_index(row, column, &s_sections[row]);
+ auto* parent = static_cast<const ManualNode*>(parent_index.internal_data());
+ auto* child = &parent->children()[row];
+ return create_index(row, column, child);
+}
+
+GUI::ModelIndex ManualModel::parent_index(const GUI::ModelIndex& index) const
+{
+ if (!index.is_valid())
+ return {};
+ auto* child = static_cast<const ManualNode*>(index.internal_data());
+ auto* parent = child->parent();
+ if (parent == nullptr)
+ return {};
+
+ if (parent->parent() == nullptr) {
+ for (size_t row = 0; row < sizeof(s_sections) / sizeof(s_sections[0]); row++)
+ if (&s_sections[row] == parent)
+ return create_index(row, 0, parent);
+ ASSERT_NOT_REACHED();
+ }
+ for (size_t row = 0; row < parent->parent()->children().size(); row++) {
+ ManualNode* child_at_row = &parent->parent()->children()[row];
+ if (child_at_row == parent)
+ return create_index(row, 0, parent);
+ }
+ ASSERT_NOT_REACHED();
+}
+
+int ManualModel::row_count(const GUI::ModelIndex& index) const
+{
+ if (!index.is_valid())
+ return sizeof(s_sections) / sizeof(s_sections[0]);
+ auto* node = static_cast<const ManualNode*>(index.internal_data());
+ return node->children().size();
+}
+
+int ManualModel::column_count(const GUI::ModelIndex&) const
+{
+ return 1;
+}
+
+GUI::Variant ManualModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ auto* node = static_cast<const ManualNode*>(index.internal_data());
+ switch (role) {
+ case GUI::ModelRole::Search:
+ if (!node->is_page())
+ return {};
+ return String(page_view(page_path(index)).value());
+ case GUI::ModelRole::Display:
+ return node->name();
+ case GUI::ModelRole::Icon:
+ if (node->is_page())
+ return m_page_icon;
+ if (node->is_open())
+ return m_section_open_icon;
+ return m_section_icon;
+ default:
+ return {};
+ }
+}
+
+void ManualModel::update_section_node_on_toggle(const GUI::ModelIndex& index, const bool open)
+{
+ auto* node = static_cast<ManualSectionNode*>(index.internal_data());
+ node->set_open(open);
+}
+
+TriState ManualModel::data_matches(const GUI::ModelIndex& index, GUI::Variant term) const
+{
+ auto view_result = page_view(page_path(index));
+ if (view_result.is_error() || view_result.value().is_empty())
+ return TriState::False;
+
+ return view_result.value().contains(term.as_string()) ? TriState::True : TriState::False;
+}
+
+void ManualModel::update()
+{
+ did_update();
+}
diff --git a/Userland/Applications/Help/ManualModel.h b/Userland/Applications/Help/ManualModel.h
new file mode 100644
index 0000000000..02efecaff0
--- /dev/null
+++ b/Userland/Applications/Help/ManualModel.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/NonnullRefPtr.h>
+#include <AK/Optional.h>
+#include <AK/Result.h>
+#include <AK/String.h>
+#include <LibGUI/Model.h>
+
+class ManualModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<ManualModel> create()
+ {
+ return adopt(*new ManualModel);
+ }
+
+ virtual ~ManualModel() override {};
+
+ Optional<GUI::ModelIndex> index_from_path(const StringView&) const;
+
+ String page_path(const GUI::ModelIndex&) const;
+ String page_and_section(const GUI::ModelIndex&) const;
+ Result<StringView, OSError> page_view(const String& path) const;
+
+ void update_section_node_on_toggle(const GUI::ModelIndex&, const bool);
+ virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+ virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual TriState data_matches(const GUI::ModelIndex&, GUI::Variant) const override;
+ virtual void update() override;
+ virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
+ virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override;
+
+private:
+ ManualModel();
+
+ GUI::Icon m_section_open_icon;
+ GUI::Icon m_section_icon;
+ GUI::Icon m_page_icon;
+ mutable HashMap<String, NonnullRefPtr<MappedFile>> m_mapped_files;
+};
diff --git a/Userland/Applications/Help/ManualNode.h b/Userland/Applications/Help/ManualNode.h
new file mode 100644
index 0000000000..b5901f9787
--- /dev/null
+++ b/Userland/Applications/Help/ManualNode.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/String.h>
+
+class ManualNode {
+public:
+ virtual ~ManualNode() { }
+
+ virtual NonnullOwnPtrVector<ManualNode>& children() const = 0;
+ virtual const ManualNode* parent() const = 0;
+ virtual String name() const = 0;
+ virtual bool is_page() const { return false; }
+ virtual bool is_open() const { return false; }
+};
diff --git a/Userland/Applications/Help/ManualPageNode.cpp b/Userland/Applications/Help/ManualPageNode.cpp
new file mode 100644
index 0000000000..4b30c4dca4
--- /dev/null
+++ b/Userland/Applications/Help/ManualPageNode.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ManualPageNode.h"
+#include "ManualSectionNode.h"
+
+const ManualNode* ManualPageNode::parent() const
+{
+ return &m_section;
+}
+
+NonnullOwnPtrVector<ManualNode>& ManualPageNode::children() const
+{
+ static NonnullOwnPtrVector<ManualNode> empty_vector;
+ return empty_vector;
+}
+
+String ManualPageNode::path() const
+{
+ return String::formatted("{}/{}.md", m_section.path(), m_page);
+}
diff --git a/Userland/Applications/Help/ManualPageNode.h b/Userland/Applications/Help/ManualPageNode.h
new file mode 100644
index 0000000000..6de744e0e4
--- /dev/null
+++ b/Userland/Applications/Help/ManualPageNode.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ManualNode.h"
+
+class ManualSectionNode;
+
+class ManualPageNode : public ManualNode {
+public:
+ virtual ~ManualPageNode() override { }
+
+ ManualPageNode(const ManualSectionNode& section, const StringView& page)
+ : m_section(section)
+ , m_page(page)
+ {
+ }
+
+ virtual NonnullOwnPtrVector<ManualNode>& children() const override;
+ virtual const ManualNode* parent() const override;
+ virtual String name() const override { return m_page; };
+ virtual bool is_page() const override { return true; }
+
+ String path() const;
+
+private:
+ const ManualSectionNode& m_section;
+ String m_page;
+};
diff --git a/Userland/Applications/Help/ManualSectionNode.cpp b/Userland/Applications/Help/ManualSectionNode.cpp
new file mode 100644
index 0000000000..ce34eb3286
--- /dev/null
+++ b/Userland/Applications/Help/ManualSectionNode.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ManualSectionNode.h"
+#include "ManualPageNode.h"
+#include <AK/LexicalPath.h>
+#include <AK/QuickSort.h>
+#include <AK/String.h>
+#include <LibCore/DirIterator.h>
+
+String ManualSectionNode::path() const
+{
+ return String::formatted("/usr/share/man/man{}", m_section);
+}
+
+void ManualSectionNode::reify_if_needed() const
+{
+ if (m_reified)
+ return;
+ m_reified = true;
+
+ Core::DirIterator dir_iter { path(), Core::DirIterator::Flags::SkipDots };
+
+ Vector<String> page_names;
+ while (dir_iter.has_next()) {
+ LexicalPath lexical_path(dir_iter.next_path());
+ if (lexical_path.extension() != "md")
+ continue;
+ page_names.append(lexical_path.title());
+ }
+
+ quick_sort(page_names);
+
+ for (auto& page_name : page_names)
+ m_children.append(make<ManualPageNode>(*this, move(page_name)));
+}
+
+void ManualSectionNode::set_open(bool open)
+{
+ if (m_open == open)
+ return;
+ m_open = open;
+}
diff --git a/Userland/Applications/Help/ManualSectionNode.h b/Userland/Applications/Help/ManualSectionNode.h
new file mode 100644
index 0000000000..9b70ea9812
--- /dev/null
+++ b/Userland/Applications/Help/ManualSectionNode.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ManualNode.h"
+
+class ManualSectionNode : public ManualNode {
+public:
+ virtual ~ManualSectionNode() override { }
+
+ ManualSectionNode(String section, String name)
+ : m_section(section)
+ , m_full_name(String::formatted("{}. {}", section, name))
+ {
+ }
+
+ virtual NonnullOwnPtrVector<ManualNode>& children() const override
+ {
+ reify_if_needed();
+ return m_children;
+ }
+
+ virtual const ManualNode* parent() const override { return nullptr; }
+ virtual String name() const override { return m_full_name; }
+ virtual bool is_open() const override { return m_open; }
+ void set_open(bool open);
+
+ const String& section_name() const { return m_section; }
+ String path() const;
+
+private:
+ void reify_if_needed() const;
+
+ String m_section;
+ String m_full_name;
+ mutable NonnullOwnPtrVector<ManualNode> m_children;
+ mutable bool m_reified { false };
+ bool m_open { false };
+};
diff --git a/Userland/Applications/Help/main.cpp b/Userland/Applications/Help/main.cpp
new file mode 100644
index 0000000000..d5a6405fbf
--- /dev/null
+++ b/Userland/Applications/Help/main.cpp
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "History.h"
+#include "ManualModel.h"
+#include <AK/URL.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/File.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/FilteringProxyModel.h>
+#include <LibGUI/ListView.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/TreeView.h>
+#include <LibGUI/Window.h>
+#include <LibMarkdown/Document.h>
+#include <LibWeb/OutOfProcessWebView.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <string.h>
+
+int main(int argc, char* argv[])
+{
+ if (pledge("stdio shared_buffer accept rpath unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept rpath unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/usr/share/man", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/launch", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/webcontent", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ const char* start_page = nullptr;
+
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(start_page, "Page to open at launch", "page", Core::ArgsParser::Required::No);
+
+ args_parser.parse(argc, argv);
+
+ auto app_icon = GUI::Icon::default_icon("app-help");
+
+ auto window = GUI::Window::construct();
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->set_title("Help");
+ window->resize(570, 500);
+
+ auto& widget = window->set_main_widget<GUI::Widget>();
+ widget.set_layout<GUI::VerticalBoxLayout>();
+ widget.set_fill_with_background_color(true);
+ widget.layout()->set_spacing(2);
+
+ auto& toolbar_container = widget.add<GUI::ToolBarContainer>();
+ auto& toolbar = toolbar_container.add<GUI::ToolBar>();
+
+ auto& splitter = widget.add<GUI::HorizontalSplitter>();
+
+ auto model = ManualModel::create();
+
+ auto& left_tab_bar = splitter.add<GUI::TabWidget>();
+ auto& tree_view_container = left_tab_bar.add_tab<GUI::Widget>("Browse");
+ tree_view_container.set_layout<GUI::VerticalBoxLayout>();
+ tree_view_container.layout()->set_margins({ 4, 4, 4, 4 });
+ auto& tree_view = tree_view_container.add<GUI::TreeView>();
+ auto& search_view = left_tab_bar.add_tab<GUI::Widget>("Search");
+ search_view.set_layout<GUI::VerticalBoxLayout>();
+ search_view.layout()->set_margins({ 4, 4, 4, 4 });
+ auto& search_box = search_view.add<GUI::TextBox>();
+ auto& search_list_view = search_view.add<GUI::ListView>();
+ search_box.set_fixed_height(20);
+ search_box.set_placeholder("Search...");
+ search_box.on_change = [&] {
+ if (auto model = search_list_view.model()) {
+ auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
+ search_model.set_filter_term(search_box.text());
+ search_model.update();
+ }
+ };
+ search_list_view.set_model(GUI::FilteringProxyModel::construct(model));
+ search_list_view.model()->update();
+
+ tree_view.set_model(model);
+ left_tab_bar.set_fixed_width(200);
+
+ auto& page_view = splitter.add<Web::OutOfProcessWebView>();
+
+ History history;
+
+ RefPtr<GUI::Action> go_back_action;
+ RefPtr<GUI::Action> go_forward_action;
+
+ auto update_actions = [&]() {
+ go_back_action->set_enabled(history.can_go_back());
+ go_forward_action->set_enabled(history.can_go_forward());
+ };
+
+ auto open_page = [&](const String& path) {
+ if (path.is_null()) {
+ window->set_title("Help");
+ page_view.load_empty_document();
+ return;
+ }
+
+ auto source_result = model->page_view(path);
+ if (source_result.is_error()) {
+ GUI::MessageBox::show(window, source_result.error().string(), "Failed to open man page", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ auto source = source_result.value();
+ String html;
+ {
+ auto md_document = Markdown::Document::parse(source);
+ ASSERT(md_document);
+ html = md_document->render_to_html();
+ }
+
+ auto url = URL::create_with_file_protocol(path);
+ page_view.load_html(html, url);
+
+ auto tree_view_index = model->index_from_path(path);
+ if (tree_view_index.has_value())
+ tree_view.expand_tree(tree_view_index.value().parent());
+
+ String page_and_section = model->page_and_section(tree_view_index.value());
+ window->set_title(String::formatted("{} - Help", page_and_section));
+ };
+
+ tree_view.on_selection_change = [&] {
+ String path = model->page_path(tree_view.selection().first());
+ history.push(path);
+ update_actions();
+ open_page(path);
+ };
+
+ tree_view.on_toggle = [&](const GUI::ModelIndex& index, const bool open) {
+ model->update_section_node_on_toggle(index, open);
+ };
+
+ auto open_external = [&](auto& url) {
+ if (!Desktop::Launcher::open(url)) {
+ GUI::MessageBox::show(window,
+ String::formatted("The link to '{}' could not be opened.", url),
+ "Failed to open link",
+ GUI::MessageBox::Type::Error);
+ }
+ };
+ search_list_view.on_selection = [&](auto index) {
+ if (!index.is_valid())
+ return;
+
+ if (auto model = search_list_view.model()) {
+ auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
+ index = search_model.map(index);
+ } else {
+ page_view.load_empty_document();
+ return;
+ }
+ String path = model->page_path(index);
+ if (path.is_null()) {
+ page_view.load_empty_document();
+ return;
+ }
+ tree_view.selection().clear();
+ tree_view.selection().add(index);
+ history.push(path);
+ update_actions();
+ open_page(path);
+ };
+
+ page_view.on_link_click = [&](auto& url, auto&, unsigned) {
+ if (url.protocol() != "file") {
+ open_external(url);
+ return;
+ }
+ auto path = Core::File::real_path_for(url.path());
+ if (!path.starts_with("/usr/share/man/")) {
+ open_external(url);
+ return;
+ }
+ auto tree_view_index = model->index_from_path(path);
+ if (tree_view_index.has_value()) {
+ dbgln("Found path _{}_ in model at index {}", path, tree_view_index.value());
+ tree_view.selection().set(tree_view_index.value());
+ return;
+ }
+ history.push(path);
+ update_actions();
+ open_page(path);
+ };
+
+ go_back_action = GUI::CommonActions::make_go_back_action([&](auto&) {
+ history.go_back();
+ update_actions();
+ open_page(history.current());
+ });
+
+ go_forward_action = GUI::CommonActions::make_go_forward_action([&](auto&) {
+ history.go_forward();
+ update_actions();
+ open_page(history.current());
+ });
+
+ go_back_action->set_enabled(false);
+ go_forward_action->set_enabled(false);
+
+ auto go_home_action = GUI::CommonActions::make_go_home_action([&](auto&) {
+ String path = "/usr/share/man/man7/Help-index.md";
+ history.push(path);
+ update_actions();
+ open_page(path);
+ });
+
+ toolbar.add_action(*go_back_action);
+ toolbar.add_action(*go_forward_action);
+ toolbar.add_action(*go_home_action);
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Help");
+ app_menu.add_action(GUI::CommonActions::make_about_action("Help", app_icon, window));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ }));
+
+ auto& go_menu = menubar->add_menu("Go");
+ go_menu.add_action(*go_back_action);
+ go_menu.add_action(*go_forward_action);
+ go_menu.add_action(*go_home_action);
+
+ app->set_menubar(move(menubar));
+
+ if (start_page) {
+ URL url = URL::create_with_url_or_path(start_page);
+ if (url.is_valid() && url.path().ends_with(".md")) {
+ history.push(url.path());
+ update_actions();
+ open_page(url.path());
+ } else {
+ left_tab_bar.set_active_widget(&search_view);
+ search_box.set_text(start_page);
+ if (auto model = search_list_view.model()) {
+ auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
+ search_model.set_filter_term(search_box.text());
+ }
+ }
+ } else {
+ go_home_action->activate();
+ }
+
+ window->set_focused_widget(&left_tab_bar);
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/HexEditor/CMakeLists.txt b/Userland/Applications/HexEditor/CMakeLists.txt
new file mode 100644
index 0000000000..ebe134863f
--- /dev/null
+++ b/Userland/Applications/HexEditor/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES
+ HexEditor.cpp
+ HexEditorWidget.cpp
+ main.cpp
+)
+
+serenity_app(HexEditor ICON app-hexeditor)
+target_link_libraries(HexEditor LibGUI)
diff --git a/Userland/Applications/HexEditor/HexEditor.cpp b/Userland/Applications/HexEditor/HexEditor.cpp
new file mode 100644
index 0000000000..f4999661f8
--- /dev/null
+++ b/Userland/Applications/HexEditor/HexEditor.cpp
@@ -0,0 +1,583 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HexEditor.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/ScrollBar.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/Palette.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+HexEditor::HexEditor()
+{
+ set_focus_policy(GUI::FocusPolicy::StrongFocus);
+ set_scrollbars_enabled(true);
+ set_font(Gfx::FontDatabase::default_fixed_width_font());
+ set_background_role(ColorRole::Base);
+ set_foreground_role(ColorRole::BaseText);
+ vertical_scrollbar().set_step(line_height());
+}
+
+HexEditor::~HexEditor()
+{
+}
+
+void HexEditor::set_readonly(bool readonly)
+{
+ if (m_readonly == readonly)
+ return;
+ m_readonly = readonly;
+}
+
+void HexEditor::set_buffer(const ByteBuffer& buffer)
+{
+ m_buffer = buffer;
+ set_content_length(buffer.size());
+ m_tracked_changes.clear();
+ m_position = 0;
+ m_byte_position = 0;
+ update();
+ update_status();
+}
+
+void HexEditor::fill_selection(u8 fill_byte)
+{
+ if (!has_selection())
+ return;
+
+ for (int i = m_selection_start; i <= m_selection_end; i++) {
+ m_tracked_changes.set(i, m_buffer.data()[i]);
+ m_buffer.data()[i] = fill_byte;
+ }
+
+ update();
+ did_change();
+}
+
+void HexEditor::set_position(int position)
+{
+ if (position > static_cast<int>(m_buffer.size()))
+ return;
+
+ m_position = position;
+ m_byte_position = 0;
+ scroll_position_into_view(position);
+ update_status();
+}
+
+bool HexEditor::write_to_file(const StringView& path)
+{
+ if (m_buffer.is_empty())
+ return true;
+
+ int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (fd < 0) {
+ perror("open");
+ return false;
+ }
+
+ int rc = ftruncate(fd, m_buffer.size());
+ if (rc < 0) {
+ perror("ftruncate");
+ return false;
+ }
+
+ ssize_t nwritten = write(fd, m_buffer.data(), m_buffer.size());
+ if (nwritten < 0) {
+ perror("write");
+ close(fd);
+ return false;
+ }
+
+ if (static_cast<size_t>(nwritten) == m_buffer.size()) {
+ m_tracked_changes.clear();
+ update();
+ }
+
+ close(fd);
+ return true;
+}
+
+bool HexEditor::copy_selected_hex_to_clipboard()
+{
+ if (!has_selection())
+ return false;
+
+ StringBuilder output_string_builder;
+ for (int i = m_selection_start; i <= m_selection_end; i++)
+ output_string_builder.appendff("{:02X} ", m_buffer.data()[i]);
+
+ GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
+ return true;
+}
+
+bool HexEditor::copy_selected_text_to_clipboard()
+{
+ if (!has_selection())
+ return false;
+
+ StringBuilder output_string_builder;
+ for (int i = m_selection_start; i <= m_selection_end; i++)
+ output_string_builder.append(isprint(m_buffer.data()[i]) ? m_buffer[i] : '.');
+
+ GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
+ return true;
+}
+
+bool HexEditor::copy_selected_hex_to_clipboard_as_c_code()
+{
+ if (!has_selection())
+ return false;
+
+ StringBuilder output_string_builder;
+ output_string_builder.appendff("unsigned char raw_data[{}] = {{\n", (m_selection_end - m_selection_start) + 1);
+ output_string_builder.append(" ");
+ for (int i = m_selection_start, j = 1; i <= m_selection_end; i++, j++) {
+ output_string_builder.appendff("{:#02X}", m_buffer.data()[i]);
+ if (i != m_selection_end)
+ output_string_builder.append(", ");
+ if ((j % 12) == 0) {
+ output_string_builder.append("\n");
+ output_string_builder.append(" ");
+ }
+ }
+ output_string_builder.append("\n};\n");
+
+ GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
+ return true;
+}
+
+void HexEditor::set_bytes_per_row(int bytes_per_row)
+{
+ m_bytes_per_row = bytes_per_row;
+ set_content_size({ offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 10 + (m_bytes_per_row * character_width()) + 20, total_rows() * line_height() + 10 });
+ update();
+}
+
+void HexEditor::set_content_length(int length)
+{
+ if (length == m_content_length)
+ return;
+ m_content_length = length;
+ set_content_size({ offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 10 + (m_bytes_per_row * character_width()) + 20, total_rows() * line_height() + 10 });
+}
+
+void HexEditor::mousedown_event(GUI::MouseEvent& event)
+{
+ if (event.button() != GUI::MouseButton::Left) {
+ return;
+ }
+
+ auto absolute_x = horizontal_scrollbar().value() + event.x();
+ auto absolute_y = vertical_scrollbar().value() + event.y();
+
+ auto hex_start_x = frame_thickness() + 90;
+ auto hex_start_y = frame_thickness() + 5;
+ auto hex_end_x = hex_start_x + (bytes_per_row() * (character_width() * 3));
+ auto hex_end_y = hex_start_y + 5 + (total_rows() * line_height());
+
+ auto text_start_x = frame_thickness() + 100 + (bytes_per_row() * (character_width() * 3));
+ auto text_start_y = frame_thickness() + 5;
+ auto text_end_x = text_start_x + (bytes_per_row() * character_width());
+ auto text_end_y = text_start_y + 5 + (total_rows() * line_height());
+
+ if (absolute_x >= hex_start_x && absolute_x <= hex_end_x && absolute_y >= hex_start_y && absolute_y <= hex_end_y) {
+ auto byte_x = (absolute_x - hex_start_x) / (character_width() * 3);
+ auto byte_y = (absolute_y - hex_start_y) / line_height();
+ auto offset = (byte_y * m_bytes_per_row) + byte_x;
+
+ if (offset < 0 || offset >= static_cast<int>(m_buffer.size()))
+ return;
+
+#ifdef HEX_DEBUG
+ outln("HexEditor::mousedown_event(hex): offset={}", offset);
+#endif
+
+ m_edit_mode = EditMode::Hex;
+ m_byte_position = 0;
+ m_position = offset;
+ m_in_drag_select = true;
+ m_selection_start = offset;
+ m_selection_end = offset;
+ update();
+ update_status();
+ }
+
+ if (absolute_x >= text_start_x && absolute_x <= text_end_x && absolute_y >= text_start_y && absolute_y <= text_end_y) {
+ auto byte_x = (absolute_x - text_start_x) / character_width();
+ auto byte_y = (absolute_y - text_start_y) / line_height();
+ auto offset = (byte_y * m_bytes_per_row) + byte_x;
+
+ if (offset < 0 || offset >= static_cast<int>(m_buffer.size()))
+ return;
+
+#ifdef HEX_DEBUG
+ outln("HexEditor::mousedown_event(text): offset={}", offset);
+#endif
+
+ m_position = offset;
+ m_byte_position = 0;
+ m_in_drag_select = true;
+ m_selection_start = offset;
+ m_selection_end = offset;
+ m_edit_mode = EditMode::Text;
+ update();
+ update_status();
+ }
+}
+
+void HexEditor::mousemove_event(GUI::MouseEvent& event)
+{
+ auto absolute_x = horizontal_scrollbar().value() + event.x();
+ auto absolute_y = vertical_scrollbar().value() + event.y();
+
+ auto hex_start_x = frame_thickness() + 90;
+ auto hex_start_y = frame_thickness() + 5;
+ auto hex_end_x = hex_start_x + (bytes_per_row() * (character_width() * 3));
+ auto hex_end_y = hex_start_y + 5 + (total_rows() * line_height());
+
+ auto text_start_x = frame_thickness() + 100 + (bytes_per_row() * (character_width() * 3));
+ auto text_start_y = frame_thickness() + 5;
+ auto text_end_x = text_start_x + (bytes_per_row() * character_width());
+ auto text_end_y = text_start_y + 5 + (total_rows() * line_height());
+
+ if ((absolute_x >= hex_start_x && absolute_x <= hex_end_x
+ && absolute_y >= hex_start_y && absolute_y <= hex_end_y)
+ || (absolute_x >= text_start_x && absolute_x <= text_end_x
+ && absolute_y >= text_start_y && absolute_y <= text_end_y)) {
+ set_override_cursor(Gfx::StandardCursor::IBeam);
+ } else {
+ set_override_cursor(Gfx::StandardCursor::None);
+ }
+
+ if (m_in_drag_select) {
+ if (absolute_x >= hex_start_x && absolute_x <= hex_end_x && absolute_y >= hex_start_y && absolute_y <= hex_end_y) {
+ auto byte_x = (absolute_x - hex_start_x) / (character_width() * 3);
+ auto byte_y = (absolute_y - hex_start_y) / line_height();
+ auto offset = (byte_y * m_bytes_per_row) + byte_x;
+
+ if (offset < 0 || offset > static_cast<int>(m_buffer.size()))
+ return;
+
+ m_selection_end = offset;
+ scroll_position_into_view(offset);
+ }
+
+ if (absolute_x >= text_start_x && absolute_x <= text_end_x && absolute_y >= text_start_y && absolute_y <= text_end_y) {
+ auto byte_x = (absolute_x - text_start_x) / character_width();
+ auto byte_y = (absolute_y - text_start_y) / line_height();
+ auto offset = (byte_y * m_bytes_per_row) + byte_x;
+ if (offset < 0 || offset > static_cast<int>(m_buffer.size()))
+ return;
+
+ m_selection_end = offset;
+ scroll_position_into_view(offset);
+ }
+ update_status();
+ update();
+ return;
+ }
+}
+
+void HexEditor::mouseup_event(GUI::MouseEvent& event)
+{
+ if (event.button() == GUI::MouseButton::Left) {
+ if (m_in_drag_select) {
+ if (m_selection_end < m_selection_start) {
+ // lets flip these around
+ auto start = m_selection_end;
+ m_selection_end = m_selection_start;
+ m_selection_start = start;
+ }
+ m_in_drag_select = false;
+ }
+ update();
+ update_status();
+ }
+}
+
+void HexEditor::scroll_position_into_view(int position)
+{
+ int y = position / bytes_per_row();
+ int x = position % bytes_per_row();
+ Gfx::IntRect rect {
+ frame_thickness() + offset_margin_width() + (x * (character_width() * 3)) + 10,
+ frame_thickness() + 5 + (y * line_height()),
+ (character_width() * 3),
+ line_height() - m_line_spacing
+ };
+ scroll_into_view(rect, true, true);
+}
+
+void HexEditor::keydown_event(GUI::KeyEvent& event)
+{
+#ifdef HEX_DEBUG
+ outln("HexEditor::keydown_event key={}", static_cast<u8>(event.key()));
+#endif
+
+ if (event.key() == KeyCode::Key_Up) {
+ if (m_position - bytes_per_row() >= 0) {
+ m_position -= bytes_per_row();
+ m_byte_position = 0;
+ scroll_position_into_view(m_position);
+ update();
+ update_status();
+ }
+ return;
+ }
+
+ if (event.key() == KeyCode::Key_Down) {
+ if (m_position + bytes_per_row() < static_cast<int>(m_buffer.size())) {
+ m_position += bytes_per_row();
+ m_byte_position = 0;
+ scroll_position_into_view(m_position);
+ update();
+ update_status();
+ }
+ return;
+ }
+
+ if (event.key() == KeyCode::Key_Left) {
+ if (m_position - 1 >= 0) {
+ m_position--;
+ m_byte_position = 0;
+ scroll_position_into_view(m_position);
+ update();
+ update_status();
+ }
+ return;
+ }
+
+ if (event.key() == KeyCode::Key_Right) {
+ if (m_position + 1 < static_cast<int>(m_buffer.size())) {
+ m_position++;
+ m_byte_position = 0;
+ scroll_position_into_view(m_position);
+ update();
+ update_status();
+ }
+ return;
+ }
+
+ if (event.key() == KeyCode::Key_Backspace) {
+ if (m_position > 0) {
+ m_position--;
+ m_byte_position = 0;
+ scroll_position_into_view(m_position);
+ update();
+ update_status();
+ }
+ return;
+ }
+
+ if (!is_readonly() && !event.ctrl() && !event.alt() && !event.text().is_empty()) {
+ if (m_edit_mode == EditMode::Hex) {
+ hex_mode_keydown_event(event);
+ } else {
+ text_mode_keydown_event(event);
+ }
+ }
+}
+
+void HexEditor::hex_mode_keydown_event(GUI::KeyEvent& event)
+{
+ if ((event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9) || (event.key() >= KeyCode::Key_A && event.key() <= KeyCode::Key_F)) {
+ if (m_buffer.is_empty())
+ return;
+ ASSERT(m_position >= 0);
+ ASSERT(m_position < static_cast<int>(m_buffer.size()));
+
+ // yes, this is terrible... but it works.
+ auto value = (event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9)
+ ? event.key() - KeyCode::Key_0
+ : (event.key() - KeyCode::Key_A) + 0xA;
+
+ if (m_byte_position == 0) {
+ m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
+ m_buffer.data()[m_position] = value << 4 | (m_buffer.data()[m_position] & 0xF); // shift new value left 4 bits, OR with existing last 4 bits
+ m_byte_position++;
+ } else {
+ m_buffer.data()[m_position] = (m_buffer.data()[m_position] & 0xF0) | value; // save the first 4 bits, OR the new value in the last 4
+ if (m_position + 1 < static_cast<int>(m_buffer.size()))
+ m_position++;
+ m_byte_position = 0;
+ }
+
+ update();
+ update_status();
+ did_change();
+ }
+}
+
+void HexEditor::text_mode_keydown_event(GUI::KeyEvent& event)
+{
+ if (m_buffer.is_empty())
+ return;
+ ASSERT(m_position >= 0);
+ ASSERT(m_position < static_cast<int>(m_buffer.size()));
+
+ if (event.code_point() == 0) // This is a control key
+ return;
+
+ m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
+ m_buffer.data()[m_position] = event.code_point();
+ if (m_position + 1 < static_cast<int>(m_buffer.size()))
+ m_position++;
+ m_byte_position = 0;
+
+ update();
+ update_status();
+ did_change();
+}
+
+void HexEditor::update_status()
+{
+ if (on_status_change)
+ on_status_change(m_position, m_edit_mode, m_selection_start, m_selection_end);
+}
+
+void HexEditor::did_change()
+{
+ if (on_change)
+ on_change();
+}
+
+void HexEditor::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(widget_inner_rect());
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect(event.rect(), palette().color(background_role()));
+
+ if (m_buffer.is_empty())
+ return;
+
+ painter.translate(frame_thickness(), frame_thickness());
+ painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
+
+ Gfx::IntRect offset_clip_rect {
+ 0,
+ vertical_scrollbar().value(),
+ 85,
+ height() - height_occupied_by_horizontal_scrollbar() //(total_rows() * line_height()) + 5
+ };
+ painter.fill_rect(offset_clip_rect, palette().ruler());
+ painter.draw_line(offset_clip_rect.top_right(), offset_clip_rect.bottom_right(), palette().ruler_border());
+
+ auto margin_and_hex_width = offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 15;
+ painter.draw_line({ margin_and_hex_width, 0 },
+ { margin_and_hex_width, vertical_scrollbar().value() + (height() - height_occupied_by_horizontal_scrollbar()) },
+ palette().ruler_border());
+
+ auto view_height = (height() - height_occupied_by_horizontal_scrollbar());
+ auto min_row = max(0, vertical_scrollbar().value() / line_height()); // if below 0 then use 0
+ auto max_row = min(total_rows(), min_row + ceil_div(view_height, line_height())); // if above calculated rows, use calculated rows
+
+ // paint offsets
+ for (int i = min_row; i < max_row; i++) {
+ Gfx::IntRect side_offset_rect {
+ frame_thickness() + 5,
+ frame_thickness() + 5 + (i * line_height()),
+ width() - width_occupied_by_vertical_scrollbar(),
+ height() - height_occupied_by_horizontal_scrollbar()
+ };
+
+ bool is_current_line = (m_position / bytes_per_row()) == i;
+ auto line = String::formatted("{:#08X}", i * bytes_per_row());
+ painter.draw_text(
+ side_offset_rect,
+ line,
+ is_current_line ? Gfx::FontDatabase::default_bold_font() : font(),
+ Gfx::TextAlignment::TopLeft,
+ is_current_line ? palette().ruler_active_text() : palette().ruler_inactive_text());
+ }
+
+ for (int i = min_row; i < max_row; i++) {
+ for (int j = 0; j < bytes_per_row(); j++) {
+ auto byte_position = (i * bytes_per_row()) + j;
+ if (byte_position >= static_cast<int>(m_buffer.size()))
+ return;
+
+ Color text_color = palette().color(foreground_role());
+ if (m_tracked_changes.contains(byte_position)) {
+ text_color = Color::Red;
+ }
+
+ auto highlight_flag = false;
+ if (m_selection_start > -1 && m_selection_end > -1) {
+ if (byte_position >= m_selection_start && byte_position <= m_selection_end) {
+ highlight_flag = true;
+ }
+ if (byte_position >= m_selection_end && byte_position <= m_selection_start) {
+ highlight_flag = true;
+ }
+ }
+
+ Gfx::IntRect hex_display_rect {
+ frame_thickness() + offset_margin_width() + (j * (character_width() * 3)) + 10,
+ frame_thickness() + 5 + (i * line_height()),
+ (character_width() * 3),
+ line_height() - m_line_spacing
+ };
+ if (highlight_flag) {
+ painter.fill_rect(hex_display_rect, palette().selection());
+ text_color = text_color == Color::Red ? Color::from_rgb(0xFFC0CB) : palette().selection_text();
+ } else if (byte_position == m_position) {
+ painter.fill_rect(hex_display_rect, palette().inactive_selection());
+ text_color = palette().inactive_selection_text();
+ }
+
+ auto line = String::formatted("{:02X}", m_buffer[byte_position]);
+ painter.draw_text(hex_display_rect, line, Gfx::TextAlignment::TopLeft, text_color);
+
+ Gfx::IntRect text_display_rect {
+ frame_thickness() + offset_margin_width() + (bytes_per_row() * (character_width() * 3)) + (j * character_width()) + 20,
+ frame_thickness() + 5 + (i * line_height()),
+ character_width(),
+ line_height() - m_line_spacing
+ };
+ // selection highlighting.
+ if (highlight_flag) {
+ painter.fill_rect(text_display_rect, palette().selection());
+ } else if (byte_position == m_position) {
+ painter.fill_rect(text_display_rect, palette().inactive_selection());
+ }
+
+ painter.draw_text(text_display_rect, String::formatted("{:c}", isprint(m_buffer[byte_position]) ? m_buffer[byte_position] : '.'), Gfx::TextAlignment::TopLeft, text_color);
+ }
+ }
+}
diff --git a/Userland/Applications/HexEditor/HexEditor.h b/Userland/Applications/HexEditor/HexEditor.h
new file mode 100644
index 0000000000..e47eedeec3
--- /dev/null
+++ b/Userland/Applications/HexEditor/HexEditor.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <AK/Function.h>
+#include <AK/HashMap.h>
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/NonnullRefPtrVector.h>
+#include <AK/StdLibExtras.h>
+#include <LibGUI/ScrollableWidget.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/TextAlignment.h>
+
+class HexEditor : public GUI::ScrollableWidget {
+ C_OBJECT(HexEditor)
+public:
+ enum EditMode {
+ Hex,
+ Text
+ };
+
+ virtual ~HexEditor() override;
+
+ bool is_readonly() const { return m_readonly; }
+ void set_readonly(bool);
+
+ void set_buffer(const ByteBuffer&);
+ void fill_selection(u8 fill_byte);
+ bool write_to_file(const StringView& path);
+
+ bool has_selection() const { return !(m_selection_start == -1 || m_selection_end == -1 || (m_selection_end - m_selection_start) < 0 || m_buffer.is_empty()); }
+ bool copy_selected_text_to_clipboard();
+ bool copy_selected_hex_to_clipboard();
+ bool copy_selected_hex_to_clipboard_as_c_code();
+
+ int bytes_per_row() const { return m_bytes_per_row; }
+ void set_bytes_per_row(int);
+
+ void set_position(int position);
+
+ Function<void(int, EditMode, int, int)> on_status_change; // position, edit mode, selection start, selection end
+ Function<void()> on_change;
+
+protected:
+ HexEditor();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+
+private:
+ bool m_readonly { false };
+ int m_line_spacing { 4 };
+ int m_content_length { 0 };
+ int m_bytes_per_row { 16 };
+ ByteBuffer m_buffer;
+ bool m_in_drag_select { false };
+ int m_selection_start { 0 };
+ int m_selection_end { 0 };
+ HashMap<int, u8> m_tracked_changes;
+ int m_position { 0 };
+ int m_byte_position { 0 }; // 0 or 1
+ EditMode m_edit_mode { Hex };
+
+ void scroll_position_into_view(int position);
+
+ int total_rows() const { return ceil_div(m_content_length, m_bytes_per_row); }
+ int line_height() const { return font().glyph_height() + m_line_spacing; }
+ int character_width() const { return font().glyph_width('W'); }
+ int offset_margin_width() const { return 80; }
+
+ void hex_mode_keydown_event(GUI::KeyEvent&);
+ void text_mode_keydown_event(GUI::KeyEvent&);
+
+ void set_content_length(int); // I might make this public if I add fetching data on demand.
+ void update_status();
+ void did_change();
+};
diff --git a/Userland/Applications/HexEditor/HexEditorWidget.cpp b/Userland/Applications/HexEditor/HexEditorWidget.cpp
new file mode 100644
index 0000000000..e6f6711884
--- /dev/null
+++ b/Userland/Applications/HexEditor/HexEditorWidget.cpp
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HexEditorWidget.h"
+#include <AK/Optional.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/File.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/ToolBar.h>
+#include <stdio.h>
+#include <string.h>
+
+HexEditorWidget::HexEditorWidget()
+{
+ set_fill_with_background_color(true);
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_spacing(2);
+
+ m_editor = add<HexEditor>();
+
+ m_editor->on_status_change = [this](int position, HexEditor::EditMode edit_mode, int selection_start, int selection_end) {
+ m_statusbar->set_text(0, String::formatted("Offset: {:#08X}", position));
+ m_statusbar->set_text(1, String::formatted("Edit Mode: {}", edit_mode == HexEditor::EditMode::Hex ? "Hex" : "Text"));
+ m_statusbar->set_text(2, String::formatted("Selection Start: {}", selection_start));
+ m_statusbar->set_text(3, String::formatted("Selection End: {}", selection_end));
+ m_statusbar->set_text(4, String::formatted("Selected Bytes: {}", abs(selection_end - selection_start) + 1));
+ };
+
+ m_editor->on_change = [this] {
+ bool was_dirty = m_document_dirty;
+ m_document_dirty = true;
+ if (!was_dirty)
+ update_title();
+ };
+
+ m_statusbar = add<GUI::StatusBar>(5);
+
+ m_new_action = GUI::Action::create("New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
+ if (m_document_dirty) {
+ if (GUI::MessageBox::show(window(), "Save changes to current file first?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel) != GUI::Dialog::ExecResult::ExecOK)
+ return;
+ m_save_action->activate();
+ }
+
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter new file size:", "New file size") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto file_size = value.to_int();
+ if (file_size.has_value() && file_size.value() > 0) {
+ m_document_dirty = false;
+ m_editor->set_buffer(ByteBuffer::create_zeroed(file_size.value()));
+ set_path(LexicalPath());
+ update_title();
+ } else {
+ GUI::MessageBox::show(window(), "Invalid file size entered.", "Error", GUI::MessageBox::Type::Error);
+ }
+ }
+ });
+
+ m_open_action = GUI::CommonActions::make_open_action([this](auto&) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window());
+
+ if (!open_path.has_value())
+ return;
+
+ open_file(open_path.value());
+ });
+
+ m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
+ if (!m_path.is_empty()) {
+ if (!m_editor->write_to_file(m_path)) {
+ GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
+ } else {
+ m_document_dirty = false;
+ update_title();
+ }
+ return;
+ }
+
+ m_save_as_action->activate();
+ });
+
+ m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), m_name.is_null() ? "Untitled" : m_name, m_extension.is_null() ? "bin" : m_extension);
+ if (!save_path.has_value())
+ return;
+
+ if (!m_editor->write_to_file(save_path.value())) {
+ GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_document_dirty = false;
+ set_path(LexicalPath(save_path.value()));
+ dbgln("Wrote document to {}", save_path.value());
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Hex Editor");
+ app_menu.add_action(*m_new_action);
+ app_menu.add_action(*m_open_action);
+ app_menu.add_action(*m_save_action);
+ app_menu.add_action(*m_save_as_action);
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) {
+ if (!request_close())
+ return;
+ GUI::Application::the()->quit();
+ }));
+
+ m_goto_decimal_offset_action = GUI::Action::create("Go To Offset (Decimal)...", { Mod_Ctrl | Mod_Shift, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](const GUI::Action&) {
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter Decimal offset:", "Go To") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto new_offset = value.to_int();
+ if (new_offset.has_value())
+ m_editor->set_position(new_offset.value());
+ }
+ });
+
+ m_goto_hex_offset_action = GUI::Action::create("Go To Offset (Hex)...", { Mod_Ctrl, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](const GUI::Action&) {
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter Hex offset:", "Go To") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto new_offset = strtol(value.characters(), nullptr, 16);
+ m_editor->set_position(new_offset);
+ }
+ });
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ edit_menu.add_action(GUI::Action::create("Fill selection...", { Mod_Ctrl, Key_B }, [&](const GUI::Action&) {
+ String value;
+ if (GUI::InputBox::show(value, window(), "Fill byte (hex):", "Fill Selection") == GUI::InputBox::ExecOK && !value.is_empty()) {
+ auto fill_byte = strtol(value.characters(), nullptr, 16);
+ m_editor->fill_selection(fill_byte);
+ }
+ }));
+ edit_menu.add_separator();
+ edit_menu.add_action(*m_goto_decimal_offset_action);
+ edit_menu.add_action(*m_goto_hex_offset_action);
+ edit_menu.add_separator();
+ edit_menu.add_action(GUI::Action::create("Copy Hex", { Mod_Ctrl, Key_C }, [&](const GUI::Action&) {
+ m_editor->copy_selected_hex_to_clipboard();
+ }));
+ edit_menu.add_action(GUI::Action::create("Copy Text", { Mod_Ctrl | Mod_Shift, Key_C }, [&](const GUI::Action&) {
+ m_editor->copy_selected_text_to_clipboard();
+ }));
+ edit_menu.add_separator();
+ edit_menu.add_action(GUI::Action::create("Copy As C Code", { Mod_Alt | Mod_Shift, Key_C }, [&](const GUI::Action&) {
+ m_editor->copy_selected_hex_to_clipboard_as_c_code();
+ }));
+
+ auto& view_menu = menubar->add_menu("View");
+ auto& bytes_per_row_menu = view_menu.add_submenu("Bytes per row");
+ for (int i = 8; i <= 32; i += 8) {
+ bytes_per_row_menu.add_action(GUI::Action::create(String::number(i), [this, i](auto&) {
+ m_editor->set_bytes_per_row(i);
+ m_editor->update();
+ }));
+ }
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Hex Editor", GUI::Icon::default_icon("Hex Editor"), window()));
+
+ GUI::Application::the()->set_menubar(move(menubar));
+
+ m_editor->set_focus(true);
+}
+
+HexEditorWidget::~HexEditorWidget()
+{
+}
+
+void HexEditorWidget::set_path(const LexicalPath& lexical_path)
+{
+ m_path = lexical_path.string();
+ m_name = lexical_path.title();
+ m_extension = lexical_path.extension();
+ update_title();
+}
+
+void HexEditorWidget::update_title()
+{
+ StringBuilder builder;
+ builder.append(m_path);
+ if (m_document_dirty)
+ builder.append(" (*)");
+ builder.append(" - Hex Editor");
+ window()->set_title(builder.to_string());
+}
+
+void HexEditorWidget::open_file(const String& path)
+{
+ auto file = Core::File::construct(path);
+ if (!file->open(Core::IODevice::ReadOnly)) {
+ GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: {}", path, strerror(errno)), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_document_dirty = false;
+ m_editor->set_buffer(file->read_all()); // FIXME: On really huge files, this is never going to work. Should really create a framework to fetch data from the file on-demand.
+ set_path(LexicalPath(path));
+}
+
+bool HexEditorWidget::request_close()
+{
+ if (!m_document_dirty)
+ return true;
+ auto result = GUI::MessageBox::show(window(), "The file has been modified. Quit without saving?", "Quit without saving?", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel);
+ return result == GUI::MessageBox::ExecOK;
+}
diff --git a/Userland/Applications/HexEditor/HexEditorWidget.h b/Userland/Applications/HexEditor/HexEditorWidget.h
new file mode 100644
index 0000000000..55152518ab
--- /dev/null
+++ b/Userland/Applications/HexEditor/HexEditorWidget.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "HexEditor.h"
+#include <AK/Function.h>
+#include <AK/LexicalPath.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+
+class HexEditor;
+
+class HexEditorWidget final : public GUI::Widget {
+ C_OBJECT(HexEditorWidget)
+public:
+ virtual ~HexEditorWidget() override;
+ void open_file(const String& path);
+ bool request_close();
+
+private:
+ HexEditorWidget();
+ void set_path(const LexicalPath& file);
+ void update_title();
+
+ RefPtr<HexEditor> m_editor;
+ String m_path;
+ String m_name;
+ String m_extension;
+ RefPtr<GUI::Action> m_new_action;
+ RefPtr<GUI::Action> m_open_action;
+ RefPtr<GUI::Action> m_save_action;
+ RefPtr<GUI::Action> m_save_as_action;
+ RefPtr<GUI::Action> m_goto_decimal_offset_action;
+ RefPtr<GUI::Action> m_goto_hex_offset_action;
+
+ RefPtr<GUI::StatusBar> m_statusbar;
+
+ bool m_document_dirty { false };
+};
diff --git a/Userland/Applications/HexEditor/main.cpp b/Userland/Applications/HexEditor/main.cpp
new file mode 100644
index 0000000000..65e358726e
--- /dev/null
+++ b/Userland/Applications/HexEditor/main.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HexEditorWidget.h"
+#include <LibGUI/Icon.h>
+#include <LibGfx/Bitmap.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept rpath unix cpath wpath fattr thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept rpath cpath wpath thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-hexeditor");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Hex Editor");
+ window->resize(640, 400);
+
+ auto& hex_editor_widget = window->set_main_widget<HexEditorWidget>();
+
+ window->on_close_request = [&]() -> GUI::Window::CloseRequestDecision {
+ if (hex_editor_widget.request_close())
+ return GUI::Window::CloseRequestDecision::Close;
+ return GUI::Window::CloseRequestDecision::StayOpen;
+ };
+
+ window->show();
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ if (argc >= 2)
+ hex_editor_widget.open_file(argv[1]);
+
+ return app->exec();
+}
diff --git a/Userland/Applications/IRCClient/CMakeLists.txt b/Userland/Applications/IRCClient/CMakeLists.txt
new file mode 100644
index 0000000000..3926bbd5dc
--- /dev/null
+++ b/Userland/Applications/IRCClient/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES
+ IRCAppWindow.cpp
+ IRCChannel.cpp
+ IRCChannelMemberListModel.cpp
+ IRCClient.cpp
+ IRCLogBuffer.cpp
+ IRCQuery.cpp
+ IRCWindow.cpp
+ IRCWindowListModel.cpp
+ main.cpp
+)
+
+serenity_app(IRCClient ICON app-irc-client)
+target_link_libraries(IRCClient LibWeb LibGUI)
diff --git a/Userland/Applications/IRCClient/IRCAppWindow.cpp b/Userland/Applications/IRCClient/IRCAppWindow.cpp
new file mode 100644
index 0000000000..8d4f931025
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCAppWindow.cpp
@@ -0,0 +1,375 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCAppWindow.h"
+#include "IRCChannel.h"
+#include "IRCWindow.h"
+#include "IRCWindowListModel.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/StackWidget.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+
+static IRCAppWindow* s_the;
+
+IRCAppWindow& IRCAppWindow::the()
+{
+ return *s_the;
+}
+
+IRCAppWindow::IRCAppWindow(String server, int port)
+ : m_client(IRCClient::construct(server, port))
+{
+ ASSERT(!s_the);
+ s_the = this;
+
+ set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-irc-client.png"));
+
+ update_title();
+ resize(600, 400);
+ setup_actions();
+ setup_menus();
+ setup_widgets();
+
+ setup_client();
+}
+
+IRCAppWindow::~IRCAppWindow()
+{
+}
+
+void IRCAppWindow::update_title()
+{
+ set_title(String::formatted("{}@{}:{} - IRC Client", m_client->nickname(), m_client->hostname(), m_client->port()));
+}
+
+void IRCAppWindow::setup_client()
+{
+ m_client->aid_create_window = [this](void* owner, IRCWindow::Type type, const String& name) {
+ return create_window(owner, type, name);
+ };
+ m_client->aid_get_active_window = [this] {
+ return static_cast<IRCWindow*>(m_container->active_widget());
+ };
+ m_client->aid_update_window_list = [this] {
+ m_window_list->model()->update();
+ };
+ m_client->on_nickname_changed = [this](const String&) {
+ update_title();
+ };
+ m_client->on_part_from_channel = [this](auto&) {
+ update_gui_actions();
+ };
+
+ if (m_client->hostname().is_empty()) {
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter server:", "Connect to server") == GUI::InputBox::ExecCancel)
+ ::exit(0);
+
+ m_client->set_server(value, 6667);
+ }
+ update_title();
+ bool success = m_client->connect();
+ ASSERT(success);
+}
+
+void IRCAppWindow::setup_actions()
+{
+ m_join_action = GUI::Action::create("Join channel", { Mod_Ctrl, Key_J }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-join.png"), [&](auto&) {
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter channel name:", "Join channel") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_join_action(value);
+ });
+
+ m_list_channels_action = GUI::Action::create("List channels", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-list.png"), [&](auto&) {
+ m_client->handle_list_channels_action();
+ });
+
+ m_part_action = GUI::Action::create("Part from channel", { Mod_Ctrl, Key_P }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-part.png"), [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ m_client->handle_part_action(window->channel().name());
+ });
+
+ m_whois_action = GUI::Action::create("Whois user", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-whois.png"), [&](auto&) {
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nickname:", "IRC WHOIS lookup") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_whois_action(value);
+ });
+
+ m_open_query_action = GUI::Action::create("Open query", { Mod_Ctrl, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-open-query.png"), [&](auto&) {
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nickname:", "Open IRC query with...") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_open_query_action(value);
+ });
+
+ m_close_query_action = GUI::Action::create("Close query", { Mod_Ctrl, Key_D }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-close-query.png"), [](auto&) {
+ outln("FIXME: Implement close-query action");
+ });
+
+ m_change_nick_action = GUI::Action::create("Change nickname", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-nick.png"), [this](auto&) {
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nickname:", "Change nickname") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_change_nick_action(value);
+ });
+
+ m_change_topic_action = GUI::Action::create("Change topic", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-topic.png"), [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter topic:", "Change topic") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_change_topic_action(window->channel().name(), value);
+ });
+
+ m_invite_user_action = GUI::Action::create("Invite user", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-invite.png"), [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "Invite user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_invite_user_action(window->channel().name(), value);
+ });
+
+ m_banlist_action = GUI::Action::create("Ban list", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ m_client->handle_banlist_action(window->channel().name());
+ });
+
+ m_voice_user_action = GUI::Action::create("Voice user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "Voice user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_voice_user_action(window->channel().name(), value);
+ });
+
+ m_devoice_user_action = GUI::Action::create("DeVoice user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "DeVoice user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_devoice_user_action(window->channel().name(), value);
+ });
+
+ m_hop_user_action = GUI::Action::create("Hop user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "Hop user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_hop_user_action(window->channel().name(), value);
+ });
+
+ m_dehop_user_action = GUI::Action::create("DeHop user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "DeHop user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_dehop_user_action(window->channel().name(), value);
+ });
+
+ m_op_user_action = GUI::Action::create("Op user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "Op user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_op_user_action(window->channel().name(), value);
+ });
+
+ m_deop_user_action = GUI::Action::create("DeOp user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String value;
+ if (GUI::InputBox::show(value, this, "Enter nick:", "DeOp user") == GUI::InputBox::ExecOK && !value.is_empty())
+ m_client->handle_deop_user_action(window->channel().name(), value);
+ });
+
+ m_kick_user_action = GUI::Action::create("Kick user", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ String nick_value;
+ if (GUI::InputBox::show(nick_value, this, "Enter nick:", "Kick user") != GUI::InputBox::ExecOK || nick_value.is_empty())
+ return;
+ String reason_value;
+ if (GUI::InputBox::show(reason_value, this, "Enter reason:", "Reason") == GUI::InputBox::ExecOK)
+ m_client->handle_kick_user_action(window->channel().name(), nick_value, reason_value.characters());
+ });
+
+ m_cycle_channel_action = GUI::Action::create("Cycle channel", [this](auto&) {
+ auto* window = m_client->current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel) {
+ return;
+ }
+ m_client->handle_cycle_channel_action(window->channel().name());
+ });
+}
+
+void IRCAppWindow::setup_menus()
+{
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("IRC Client");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ dbgln("Terminal: Quit menu activated!");
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& server_menu = menubar->add_menu("Server");
+ server_menu.add_action(*m_change_nick_action);
+ server_menu.add_separator();
+ server_menu.add_action(*m_join_action);
+ server_menu.add_action(*m_list_channels_action);
+ server_menu.add_separator();
+ server_menu.add_action(*m_whois_action);
+ server_menu.add_action(*m_open_query_action);
+ server_menu.add_action(*m_close_query_action);
+
+ auto& channel_menu = menubar->add_menu("Channel");
+ channel_menu.add_action(*m_change_topic_action);
+ channel_menu.add_action(*m_invite_user_action);
+ channel_menu.add_action(*m_banlist_action);
+
+ auto& channel_control_menu = channel_menu.add_submenu("Control");
+ channel_control_menu.add_action(*m_voice_user_action);
+ channel_control_menu.add_action(*m_devoice_user_action);
+ channel_control_menu.add_action(*m_hop_user_action);
+ channel_control_menu.add_action(*m_dehop_user_action);
+ channel_control_menu.add_action(*m_op_user_action);
+ channel_control_menu.add_action(*m_deop_user_action);
+ channel_control_menu.add_separator();
+ channel_control_menu.add_action(*m_kick_user_action);
+
+ channel_menu.add_separator();
+ channel_menu.add_action(*m_cycle_channel_action);
+ channel_menu.add_action(*m_part_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("IRC Client", GUI::Icon::default_icon("app-irc-client"), this));
+
+ GUI::Application::the()->set_menubar(move(menubar));
+}
+
+void IRCAppWindow::setup_widgets()
+{
+ auto& widget = set_main_widget<GUI::Widget>();
+ widget.set_fill_with_background_color(true);
+ widget.set_layout<GUI::VerticalBoxLayout>();
+ widget.layout()->set_spacing(0);
+
+ auto& toolbar_container = widget.add<GUI::ToolBarContainer>();
+ auto& toolbar = toolbar_container.add<GUI::ToolBar>();
+ toolbar.set_has_frame(false);
+ toolbar.add_action(*m_change_nick_action);
+ toolbar.add_separator();
+ toolbar.add_action(*m_join_action);
+ toolbar.add_action(*m_part_action);
+ toolbar.add_separator();
+ toolbar.add_action(*m_whois_action);
+ toolbar.add_action(*m_open_query_action);
+ toolbar.add_action(*m_close_query_action);
+
+ auto& outer_container = widget.add<GUI::Widget>();
+ outer_container.set_layout<GUI::VerticalBoxLayout>();
+ outer_container.layout()->set_margins({ 2, 0, 2, 2 });
+
+ auto& horizontal_container = outer_container.add<GUI::HorizontalSplitter>();
+
+ m_window_list = horizontal_container.add<GUI::TableView>();
+ m_window_list->set_column_headers_visible(false);
+ m_window_list->set_alternating_row_colors(false);
+ m_window_list->set_model(m_client->client_window_list_model());
+ m_window_list->set_activates_on_selection(true);
+ m_window_list->set_fixed_width(100);
+ m_window_list->on_activation = [this](auto& index) {
+ set_active_window(m_client->window_at(index.row()));
+ };
+
+ m_container = horizontal_container.add<GUI::StackWidget>();
+ m_container->on_active_widget_change = [this](auto*) {
+ update_gui_actions();
+ };
+
+ create_window(&m_client, IRCWindow::Server, "Server");
+}
+
+void IRCAppWindow::set_active_window(IRCWindow& window)
+{
+ m_container->set_active_widget(&window);
+ window.clear_unread_count();
+ auto index = m_window_list->model()->index(m_client->window_index(window));
+ m_window_list->selection().set(index);
+}
+
+void IRCAppWindow::update_gui_actions()
+{
+ auto* window = static_cast<IRCWindow*>(m_container->active_widget());
+ bool is_open_channel = window && window->type() == IRCWindow::Type::Channel && window->channel().is_open();
+ m_change_topic_action->set_enabled(is_open_channel);
+ m_invite_user_action->set_enabled(is_open_channel);
+ m_banlist_action->set_enabled(is_open_channel);
+ m_voice_user_action->set_enabled(is_open_channel);
+ m_devoice_user_action->set_enabled(is_open_channel);
+ m_hop_user_action->set_enabled(is_open_channel);
+ m_dehop_user_action->set_enabled(is_open_channel);
+ m_op_user_action->set_enabled(is_open_channel);
+ m_deop_user_action->set_enabled(is_open_channel);
+ m_kick_user_action->set_enabled(is_open_channel);
+ m_cycle_channel_action->set_enabled(is_open_channel);
+ m_part_action->set_enabled(is_open_channel);
+}
+
+NonnullRefPtr<IRCWindow> IRCAppWindow::create_window(void* owner, IRCWindow::Type type, const String& name)
+{
+ return m_container->add<IRCWindow>(m_client, owner, type, name);
+}
diff --git a/Userland/Applications/IRCClient/IRCAppWindow.h b/Userland/Applications/IRCClient/IRCAppWindow.h
new file mode 100644
index 0000000000..10f0f5a4ee
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCAppWindow.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "IRCClient.h"
+#include "IRCWindow.h"
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+
+class IRCAppWindow : public GUI::Window {
+ C_OBJECT(IRCAppWindow);
+
+public:
+ virtual ~IRCAppWindow() override;
+
+ static IRCAppWindow& the();
+
+ void set_active_window(IRCWindow&);
+
+private:
+ IRCAppWindow(String server, int port);
+
+ void setup_client();
+ void setup_actions();
+ void setup_menus();
+ void setup_widgets();
+ void update_title();
+ void update_gui_actions();
+
+ NonnullRefPtr<IRCWindow> create_window(void* owner, IRCWindow::Type, const String& name);
+ NonnullRefPtr<IRCClient> m_client;
+ RefPtr<GUI::StackWidget> m_container;
+ RefPtr<GUI::TableView> m_window_list;
+ RefPtr<GUI::Action> m_join_action;
+ RefPtr<GUI::Action> m_list_channels_action;
+ RefPtr<GUI::Action> m_part_action;
+ RefPtr<GUI::Action> m_cycle_channel_action;
+ RefPtr<GUI::Action> m_whois_action;
+ RefPtr<GUI::Action> m_open_query_action;
+ RefPtr<GUI::Action> m_close_query_action;
+ RefPtr<GUI::Action> m_change_nick_action;
+ RefPtr<GUI::Action> m_change_topic_action;
+ RefPtr<GUI::Action> m_invite_user_action;
+ RefPtr<GUI::Action> m_banlist_action;
+ RefPtr<GUI::Action> m_voice_user_action;
+ RefPtr<GUI::Action> m_devoice_user_action;
+ RefPtr<GUI::Action> m_hop_user_action;
+ RefPtr<GUI::Action> m_dehop_user_action;
+ RefPtr<GUI::Action> m_op_user_action;
+ RefPtr<GUI::Action> m_deop_user_action;
+ RefPtr<GUI::Action> m_kick_user_action;
+};
diff --git a/Userland/Applications/IRCClient/IRCChannel.cpp b/Userland/Applications/IRCClient/IRCChannel.cpp
new file mode 100644
index 0000000000..81d9173e31
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCChannel.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCChannel.h"
+#include "IRCChannelMemberListModel.h"
+#include "IRCClient.h"
+#include <stdio.h>
+
+IRCChannel::IRCChannel(IRCClient& client, const String& name)
+ : m_client(client)
+ , m_name(name)
+ , m_log(IRCLogBuffer::create())
+ , m_member_model(IRCChannelMemberListModel::create(*this))
+{
+ m_window = m_client.aid_create_window(this, IRCWindow::Channel, m_name);
+ m_window->set_log_buffer(*m_log);
+}
+
+IRCChannel::~IRCChannel()
+{
+}
+
+NonnullRefPtr<IRCChannel> IRCChannel::create(IRCClient& client, const String& name)
+{
+ return adopt(*new IRCChannel(client, name));
+}
+
+void IRCChannel::add_member(const String& name, char prefix)
+{
+ for (auto& member : m_members) {
+ if (member.name == name) {
+ member.prefix = prefix;
+ return;
+ }
+ }
+ m_members.append({ name, prefix });
+ m_member_model->update();
+}
+
+void IRCChannel::remove_member(const String& name)
+{
+ m_members.remove_first_matching([&](auto& member) { return name == member.name; });
+}
+
+void IRCChannel::add_message(char prefix, const String& name, const String& text, Color color)
+{
+ log().add_message(prefix, name, text, color);
+ window().did_add_message(name, text);
+}
+
+void IRCChannel::add_message(const String& text, Color color)
+{
+ log().add_message(text, color);
+ window().did_add_message();
+}
+
+void IRCChannel::say(const String& text)
+{
+ m_client.send_privmsg(m_name, text);
+ add_message(' ', m_client.nickname(), text);
+}
+
+void IRCChannel::handle_join(const String& nick, const String& hostmask)
+{
+ if (nick == m_client.nickname()) {
+ m_open = true;
+ return;
+ }
+ add_member(nick, (char)0);
+ m_member_model->update();
+ if (m_client.show_join_part_messages())
+ add_message(String::formatted("*** {} [{}] has joined {}", nick, hostmask, m_name), Color::MidGreen);
+}
+
+void IRCChannel::handle_part(const String& nick, const String& hostmask)
+{
+ if (nick == m_client.nickname()) {
+ m_open = false;
+ m_members.clear();
+ m_client.did_part_from_channel({}, *this);
+ } else {
+ remove_member(nick);
+ }
+ m_member_model->update();
+ if (m_client.show_join_part_messages())
+ add_message(String::formatted("*** {} [{}] has parted from {}", nick, hostmask, m_name), Color::MidGreen);
+}
+
+void IRCChannel::handle_quit(const String& nick, const String& hostmask, const String& message)
+{
+ if (nick == m_client.nickname()) {
+ m_open = false;
+ m_members.clear();
+ m_client.did_part_from_channel({}, *this);
+ } else {
+ remove_member(nick);
+ }
+ m_member_model->update();
+ add_message(String::formatted("*** {} [{}] has quit ({})", nick, hostmask, message), Color::MidGreen);
+}
+
+void IRCChannel::handle_topic(const String& nick, const String& topic)
+{
+ if (nick.is_null())
+ add_message(String::formatted("*** Topic is \"{}\"", topic), Color::MidBlue);
+ else
+ add_message(String::formatted("*** {} set topic to \"{}\"", nick, topic), Color::MidBlue);
+}
+
+void IRCChannel::notify_nick_changed(const String& old_nick, const String& new_nick)
+{
+ for (auto& member : m_members) {
+ if (member.name == old_nick) {
+ member.name = new_nick;
+ m_member_model->update();
+ if (m_client.show_nick_change_messages())
+ add_message(String::formatted("~ {} changed nickname to {}", old_nick, new_nick), Color::MidMagenta);
+ return;
+ }
+ }
+}
diff --git a/Userland/Applications/IRCClient/IRCChannel.h b/Userland/Applications/IRCClient/IRCChannel.h
new file mode 100644
index 0000000000..1d0eb8657b
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCChannel.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "IRCLogBuffer.h"
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <AK/Vector.h>
+
+class IRCClient;
+class IRCChannelMemberListModel;
+class IRCWindow;
+
+class IRCChannel : public RefCounted<IRCChannel> {
+public:
+ static NonnullRefPtr<IRCChannel> create(IRCClient&, const String&);
+ ~IRCChannel();
+
+ bool is_open() const { return m_open; }
+ void set_open(bool b) { m_open = b; }
+
+ String name() const { return m_name; }
+
+ void add_member(const String& name, char prefix);
+ void remove_member(const String& name);
+
+ void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
+ void add_message(const String& text, Color = Color::Black);
+
+ void say(const String&);
+
+ const IRCLogBuffer& log() const { return *m_log; }
+ IRCLogBuffer& log() { return *m_log; }
+
+ IRCChannelMemberListModel* member_model() { return m_member_model.ptr(); }
+ const IRCChannelMemberListModel* member_model() const { return m_member_model.ptr(); }
+
+ int member_count() const { return m_members.size(); }
+ String member_at(int i) { return m_members[i].name; }
+
+ void handle_join(const String& nick, const String& hostmask);
+ void handle_part(const String& nick, const String& hostmask);
+ void handle_quit(const String& nick, const String& hostmask, const String& message);
+ void handle_topic(const String& nick, const String& topic);
+
+ IRCWindow& window() { return *m_window; }
+ const IRCWindow& window() const { return *m_window; }
+
+ String topic() const { return m_topic; }
+
+ void notify_nick_changed(const String& old_nick, const String& new_nick);
+
+private:
+ IRCChannel(IRCClient&, const String&);
+
+ IRCClient& m_client;
+ String m_name;
+ String m_topic;
+ struct Member {
+ String name;
+ char prefix { 0 };
+ };
+ Vector<Member> m_members;
+ bool m_open { false };
+
+ NonnullRefPtr<IRCLogBuffer> m_log;
+ NonnullRefPtr<IRCChannelMemberListModel> m_member_model;
+ IRCWindow* m_window { nullptr };
+};
diff --git a/Userland/Applications/IRCClient/IRCChannelMemberListModel.cpp b/Userland/Applications/IRCClient/IRCChannelMemberListModel.cpp
new file mode 100644
index 0000000000..22cdcd710f
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCChannelMemberListModel.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCChannelMemberListModel.h"
+#include "IRCChannel.h"
+#include <stdio.h>
+#include <time.h>
+
+IRCChannelMemberListModel::IRCChannelMemberListModel(IRCChannel& channel)
+ : m_channel(channel)
+{
+}
+
+IRCChannelMemberListModel::~IRCChannelMemberListModel()
+{
+}
+
+int IRCChannelMemberListModel::row_count(const GUI::ModelIndex&) const
+{
+ return m_channel.member_count();
+}
+
+int IRCChannelMemberListModel::column_count(const GUI::ModelIndex&) const
+{
+ return 1;
+}
+
+String IRCChannelMemberListModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Name:
+ return "Name";
+ }
+ ASSERT_NOT_REACHED();
+}
+
+GUI::Variant IRCChannelMemberListModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ if (role == GUI::ModelRole::TextAlignment)
+ return Gfx::TextAlignment::CenterLeft;
+ if (role == GUI::ModelRole::Display) {
+ switch (index.column()) {
+ case Column::Name:
+ return m_channel.member_at(index.row());
+ }
+ }
+ return {};
+}
+
+void IRCChannelMemberListModel::update()
+{
+ did_update();
+}
+
+String IRCChannelMemberListModel::nick_at(const GUI::ModelIndex& index) const
+{
+ return data(index, GUI::ModelRole::Display).to_string();
+}
diff --git a/Userland/Applications/IRCClient/IRCChannelMemberListModel.h b/Userland/Applications/IRCClient/IRCChannelMemberListModel.h
new file mode 100644
index 0000000000..99e4128fc6
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCChannelMemberListModel.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibGUI/Model.h>
+
+class IRCChannel;
+
+class IRCChannelMemberListModel final : public GUI::Model {
+public:
+ enum Column {
+ Name
+ };
+ static NonnullRefPtr<IRCChannelMemberListModel> create(IRCChannel& channel) { return adopt(*new IRCChannelMemberListModel(channel)); }
+ virtual ~IRCChannelMemberListModel() override;
+
+ virtual int row_count(const GUI::ModelIndex&) const override;
+ virtual int column_count(const GUI::ModelIndex&) const override;
+ virtual String column_name(int column) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+ virtual String nick_at(const GUI::ModelIndex& index) const;
+
+private:
+ explicit IRCChannelMemberListModel(IRCChannel&);
+
+ IRCChannel& m_channel;
+};
diff --git a/Userland/Applications/IRCClient/IRCClient.cpp b/Userland/Applications/IRCClient/IRCClient.cpp
new file mode 100644
index 0000000000..c0da630b8f
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCClient.cpp
@@ -0,0 +1,1188 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCClient.h"
+#include "IRCAppWindow.h"
+#include "IRCChannel.h"
+#include "IRCLogBuffer.h"
+#include "IRCQuery.h"
+#include "IRCWindow.h"
+#include "IRCWindowListModel.h"
+#include <AK/QuickSort.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/DateTime.h>
+#include <LibCore/Notifier.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <strings.h>
+
+#ifndef IRC_DEBUG
+# define IRC_DEBUG
+#endif
+
+enum IRCNumeric {
+ RPL_WELCOME = 1,
+ RPL_WHOISUSER = 311,
+ RPL_WHOISSERVER = 312,
+ RPL_WHOISOPERATOR = 313,
+ RPL_ENDOFWHO = 315,
+ RPL_WHOISIDLE = 317,
+ RPL_ENDOFWHOIS = 318,
+ RPL_WHOISCHANNELS = 319,
+ RPL_TOPIC = 332,
+ RPL_TOPICWHOTIME = 333,
+ RPL_NAMREPLY = 353,
+ RPL_ENDOFNAMES = 366,
+ RPL_BANLIST = 367,
+ RPL_ENDOFBANLIST = 368,
+ RPL_ENDOFWHOWAS = 369,
+ RPL_ENDOFMOTD = 376,
+ ERR_NOSUCHNICK = 401,
+ ERR_UNKNOWNCOMMAND = 421,
+ ERR_NICKNAMEINUSE = 433,
+};
+
+IRCClient::IRCClient(String server, int port)
+ : m_nickname("seren1ty")
+ , m_client_window_list_model(IRCWindowListModel::create(*this))
+ , m_log(IRCLogBuffer::create())
+ , m_config(Core::ConfigFile::get_for_app("IRCClient"))
+{
+ struct passwd* user_pw = getpwuid(getuid());
+ m_socket = Core::TCPSocket::construct(this);
+ m_nickname = m_config->read_entry("User", "Nickname", String::formatted("{}_seren1ty", user_pw->pw_name));
+
+ if (server.is_empty()) {
+ m_hostname = m_config->read_entry("Connection", "Server", "");
+ m_port = m_config->read_num_entry("Connection", "Port", 6667);
+ } else {
+ m_hostname = server;
+ m_port = port ? port : 6667;
+ }
+
+ m_show_join_part_messages = m_config->read_bool_entry("Messaging", "ShowJoinPartMessages", 1);
+ m_show_nick_change_messages = m_config->read_bool_entry("Messaging", "ShowNickChangeMessages", 1);
+
+ m_notify_on_message = m_config->read_bool_entry("Notifications", "NotifyOnMessage", 1);
+ m_notify_on_mention = m_config->read_bool_entry("Notifications", "NotifyOnMention", 1);
+
+ m_ctcp_version_reply = m_config->read_entry("CTCP", "VersionReply", "IRC Client [x86] / Serenity OS");
+ m_ctcp_userinfo_reply = m_config->read_entry("CTCP", "UserInfoReply", user_pw->pw_name);
+ m_ctcp_finger_reply = m_config->read_entry("CTCP", "FingerReply", user_pw->pw_name);
+}
+
+IRCClient::~IRCClient()
+{
+}
+
+void IRCClient::set_server(const String& hostname, int port)
+{
+ m_hostname = hostname;
+ m_port = port;
+ m_config->write_entry("Connection", "Server", hostname);
+ m_config->write_num_entry("Connection", "Port", port);
+ m_config->sync();
+}
+
+void IRCClient::on_socket_connected()
+{
+ m_notifier = Core::Notifier::construct(m_socket->fd(), Core::Notifier::Read);
+ m_notifier->on_ready_to_read = [this] { receive_from_server(); };
+
+ send_user();
+ send_nick();
+}
+
+bool IRCClient::connect()
+{
+ if (m_socket->is_connected())
+ ASSERT_NOT_REACHED();
+
+ m_socket->on_connected = [this] { on_socket_connected(); };
+
+ return m_socket->connect(m_hostname, m_port);
+}
+
+void IRCClient::receive_from_server()
+{
+ while (m_socket->can_read_line()) {
+ auto line = m_socket->read_line();
+ if (line.is_null()) {
+ if (!m_socket->is_connected()) {
+ outln("IRCClient: Connection closed!");
+ exit(1);
+ }
+ ASSERT_NOT_REACHED();
+ }
+ process_line(line);
+ }
+}
+
+void IRCClient::process_line(const String& line)
+{
+ Message msg;
+ Vector<char, 32> prefix;
+ Vector<char, 32> command;
+ Vector<char, 256> current_parameter;
+ enum {
+ Start,
+ InPrefix,
+ InCommand,
+ InStartOfParameter,
+ InParameter,
+ InTrailingParameter,
+ } state
+ = Start;
+
+ for (size_t i = 0; i < line.length(); ++i) {
+ char ch = line[i];
+ if (ch == '\r')
+ continue;
+ if (ch == '\n')
+ break;
+ switch (state) {
+ case Start:
+ if (ch == ':') {
+ state = InPrefix;
+ continue;
+ }
+ state = InCommand;
+ [[fallthrough]];
+ case InCommand:
+ if (ch == ' ') {
+ state = InStartOfParameter;
+ continue;
+ }
+ command.append(ch);
+ continue;
+ case InPrefix:
+ if (ch == ' ') {
+ state = InCommand;
+ continue;
+ }
+ prefix.append(ch);
+ continue;
+ case InStartOfParameter:
+ if (ch == ':') {
+ state = InTrailingParameter;
+ continue;
+ }
+ state = InParameter;
+ [[fallthrough]];
+ case InParameter:
+ if (ch == ' ') {
+ if (!current_parameter.is_empty())
+ msg.arguments.append(String(current_parameter.data(), current_parameter.size()));
+ current_parameter.clear_with_capacity();
+ state = InStartOfParameter;
+ continue;
+ }
+ current_parameter.append(ch);
+ continue;
+ case InTrailingParameter:
+ current_parameter.append(ch);
+ continue;
+ }
+ }
+ if (!current_parameter.is_empty())
+ msg.arguments.append(String::copy(current_parameter));
+ msg.prefix = String::copy(prefix);
+ msg.command = String::copy(command);
+ handle(msg);
+}
+
+void IRCClient::send(const String& text)
+{
+ if (!m_socket->send(text.bytes())) {
+ perror("send");
+ exit(1);
+ }
+}
+
+void IRCClient::send_user()
+{
+ send(String::formatted("USER {} 0 * :{}\r\n", m_nickname, m_nickname));
+}
+
+void IRCClient::send_nick()
+{
+ send(String::formatted("NICK {}\r\n", m_nickname));
+}
+
+void IRCClient::send_pong(const String& server)
+{
+ send(String::formatted("PONG {}\r\n", server));
+ sleep(1);
+}
+
+void IRCClient::join_channel(const String& channel_name)
+{
+ send(String::formatted("JOIN {}\r\n", channel_name));
+}
+
+void IRCClient::part_channel(const String& channel_name)
+{
+ send(String::formatted("PART {}\r\n", channel_name));
+}
+
+void IRCClient::send_whois(const String& nick)
+{
+ send(String::formatted("WHOIS {}\r\n", nick));
+}
+
+void IRCClient::handle(const Message& msg)
+{
+#ifdef IRC_DEBUG
+ outln("IRCClient::execute: prefix='{}', command='{}', arguments={}",
+ msg.prefix,
+ msg.command,
+ msg.arguments.size());
+
+ size_t index = 0;
+ for (auto& arg : msg.arguments)
+ outln(" [{}]: {}", index++, arg);
+#endif
+
+ auto numeric = msg.command.to_uint();
+
+ if (numeric.has_value()) {
+ switch (numeric.value()) {
+ case RPL_WELCOME:
+ return handle_rpl_welcome(msg);
+ case RPL_WHOISCHANNELS:
+ return handle_rpl_whoischannels(msg);
+ case RPL_ENDOFWHO:
+ return handle_rpl_endofwho(msg);
+ case RPL_ENDOFWHOIS:
+ return handle_rpl_endofwhois(msg);
+ case RPL_ENDOFWHOWAS:
+ return handle_rpl_endofwhowas(msg);
+ case RPL_ENDOFMOTD:
+ return handle_rpl_endofmotd(msg);
+ case RPL_WHOISOPERATOR:
+ return handle_rpl_whoisoperator(msg);
+ case RPL_WHOISSERVER:
+ return handle_rpl_whoisserver(msg);
+ case RPL_WHOISUSER:
+ return handle_rpl_whoisuser(msg);
+ case RPL_WHOISIDLE:
+ return handle_rpl_whoisidle(msg);
+ case RPL_TOPICWHOTIME:
+ return handle_rpl_topicwhotime(msg);
+ case RPL_TOPIC:
+ return handle_rpl_topic(msg);
+ case RPL_NAMREPLY:
+ return handle_rpl_namreply(msg);
+ case RPL_ENDOFNAMES:
+ return handle_rpl_endofnames(msg);
+ case RPL_BANLIST:
+ return handle_rpl_banlist(msg);
+ case RPL_ENDOFBANLIST:
+ return handle_rpl_endofbanlist(msg);
+ case ERR_NOSUCHNICK:
+ return handle_err_nosuchnick(msg);
+ case ERR_UNKNOWNCOMMAND:
+ return handle_err_unknowncommand(msg);
+ case ERR_NICKNAMEINUSE:
+ return handle_err_nicknameinuse(msg);
+ }
+ }
+
+ if (msg.command == "PING")
+ return handle_ping(msg);
+
+ if (msg.command == "JOIN")
+ return handle_join(msg);
+
+ if (msg.command == "PART")
+ return handle_part(msg);
+
+ if (msg.command == "QUIT")
+ return handle_quit(msg);
+
+ if (msg.command == "TOPIC")
+ return handle_topic(msg);
+
+ if (msg.command == "PRIVMSG")
+ return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Privmsg);
+
+ if (msg.command == "NOTICE")
+ return handle_privmsg_or_notice(msg, PrivmsgOrNotice::Notice);
+
+ if (msg.command == "NICK")
+ return handle_nick(msg);
+
+ if (msg.arguments.size() >= 2)
+ add_server_message(String::formatted("[{}] {}", msg.command, msg.arguments[1]));
+}
+
+void IRCClient::add_server_message(const String& text, Color color)
+{
+ m_log->add_message(0, "", text, color);
+ m_server_subwindow->did_add_message();
+}
+
+void IRCClient::send_topic(const String& channel_name, const String& text)
+{
+ send(String::formatted("TOPIC {} :{}\r\n", channel_name, text));
+}
+
+void IRCClient::send_invite(const String& channel_name, const String& nick)
+{
+ send(String::formatted("INVITE {} {}\r\n", nick, channel_name));
+}
+
+void IRCClient::send_banlist(const String& channel_name)
+{
+ send(String::formatted("MODE {} +b\r\n", channel_name));
+}
+
+void IRCClient::send_voice_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} +v {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_devoice_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} -v {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_hop_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} +h {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_dehop_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} -h {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_op_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} +o {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_deop_user(const String& channel_name, const String& nick)
+{
+ send(String::formatted("MODE {} -o {}\r\n", channel_name, nick));
+}
+
+void IRCClient::send_kick(const String& channel_name, const String& nick, const String& comment)
+{
+ send(String::formatted("KICK {} {} :{}\r\n", channel_name, nick, comment));
+}
+
+void IRCClient::send_list()
+{
+ send("LIST\r\n");
+}
+
+void IRCClient::send_privmsg(const String& target, const String& text)
+{
+ send(String::formatted("PRIVMSG {} :{}\r\n", target, text));
+}
+
+void IRCClient::send_notice(const String& target, const String& text)
+{
+ send(String::formatted("NOTICE {} :{}\r\n", target, text));
+}
+
+void IRCClient::handle_user_input_in_channel(const String& channel_name, const String& input)
+{
+ if (input.is_empty())
+ return;
+ if (input[0] == '/')
+ return handle_user_command(input);
+ ensure_channel(channel_name).say(input);
+}
+
+void IRCClient::handle_user_input_in_query(const String& query_name, const String& input)
+{
+ if (input.is_empty())
+ return;
+ if (input[0] == '/')
+ return handle_user_command(input);
+ ensure_query(query_name).say(input);
+}
+
+void IRCClient::handle_user_input_in_server(const String& input)
+{
+ if (input.is_empty())
+ return;
+ if (input[0] == '/')
+ return handle_user_command(input);
+}
+
+String IRCClient::nick_without_prefix(const String& nick)
+{
+ assert(!nick.is_empty());
+ if (IRCClient::is_nick_prefix(nick[0]))
+ return nick.substring(1, nick.length() - 1);
+ return nick;
+}
+
+bool IRCClient::is_nick_prefix(char ch)
+{
+ switch (ch) {
+ case '@':
+ case '+':
+ case '~':
+ case '&':
+ case '%':
+ return true;
+ }
+ return false;
+}
+
+bool IRCClient::is_channel_prefix(char ch)
+{
+ switch (ch) {
+ case '&':
+ case '#':
+ case '+':
+ case '!':
+ return true;
+ }
+ return false;
+}
+
+static bool has_ctcp_payload(const StringView& string)
+{
+ return string.length() >= 2 && string[0] == 0x01 && string[string.length() - 1] == 0x01;
+}
+
+void IRCClient::handle_privmsg_or_notice(const Message& msg, PrivmsgOrNotice type)
+{
+ if (msg.arguments.size() < 2)
+ return;
+ if (msg.prefix.is_empty())
+ return;
+ auto parts = msg.prefix.split('!');
+ auto sender_nick = parts[0];
+ auto target = msg.arguments[0];
+
+ bool is_ctcp = has_ctcp_payload(msg.arguments[1]);
+
+#ifdef IRC_DEBUG
+ outln("handle_privmsg_or_notice: type='{}'{}, sender_nick='{}', target='{}'",
+ type == PrivmsgOrNotice::Privmsg ? "privmsg" : "notice",
+ is_ctcp ? " (ctcp)" : "",
+ sender_nick,
+ target);
+#endif
+
+ if (sender_nick.is_empty())
+ return;
+
+ char sender_prefix = 0;
+ if (is_nick_prefix(sender_nick[0])) {
+ sender_prefix = sender_nick[0];
+ sender_nick = sender_nick.substring(1, sender_nick.length() - 1);
+ }
+
+ String message_text = msg.arguments[1];
+ auto message_color = Color::Black;
+
+ bool insert_as_raw_message = false;
+
+ if (is_ctcp) {
+ auto ctcp_payload = msg.arguments[1].substring_view(1, msg.arguments[1].length() - 2);
+ if (type == PrivmsgOrNotice::Privmsg)
+ handle_ctcp_request(sender_nick, ctcp_payload);
+ else
+ handle_ctcp_response(sender_nick, ctcp_payload);
+
+ if (ctcp_payload.starts_with("ACTION")) {
+ insert_as_raw_message = true;
+ message_text = String::formatted("* {}{}", sender_nick, ctcp_payload.substring_view(6, ctcp_payload.length() - 6));
+ message_color = Color::Magenta;
+ } else {
+ message_text = String::formatted("(CTCP) {}", ctcp_payload);
+ message_color = Color::Blue;
+ }
+ }
+
+ {
+ auto it = m_channels.find(target);
+ if (it != m_channels.end()) {
+ if (insert_as_raw_message)
+ (*it).value->add_message(message_text, message_color);
+ else
+ (*it).value->add_message(sender_prefix, sender_nick, message_text, message_color);
+ return;
+ }
+ }
+
+ // For NOTICE or CTCP messages, only put them in query if one already exists.
+ // Otherwise, put them in the server window. This seems to match other clients.
+ IRCQuery* query = nullptr;
+ if (is_ctcp || type == PrivmsgOrNotice::Notice) {
+ query = query_with_name(sender_nick);
+ } else {
+ query = &ensure_query(sender_nick);
+ }
+ if (query) {
+ if (insert_as_raw_message)
+ query->add_message(message_text, message_color);
+ else
+ query->add_message(sender_prefix, sender_nick, message_text, message_color);
+ } else {
+ add_server_message(String::formatted("<{}> {}", sender_nick, message_text), message_color);
+ }
+}
+
+IRCQuery* IRCClient::query_with_name(const String& name)
+{
+ return const_cast<IRCQuery*>(m_queries.get(name).value_or(nullptr));
+}
+
+IRCQuery& IRCClient::ensure_query(const String& name)
+{
+ auto it = m_queries.find(name);
+ if (it != m_queries.end())
+ return *(*it).value;
+ auto query = IRCQuery::create(*this, name);
+ auto& query_reference = *query;
+ m_queries.set(name, query);
+ return query_reference;
+}
+
+IRCChannel& IRCClient::ensure_channel(const String& name)
+{
+ auto it = m_channels.find(name);
+ if (it != m_channels.end())
+ return *(*it).value;
+ auto channel = IRCChannel::create(*this, name);
+ auto& channel_reference = *channel;
+ m_channels.set(name, channel);
+ return channel_reference;
+}
+
+void IRCClient::handle_ping(const Message& msg)
+{
+ if (msg.arguments.size() < 1)
+ return;
+ m_log->add_message(0, "", "Ping? Pong!");
+ send_pong(msg.arguments[0]);
+}
+
+void IRCClient::handle_join(const Message& msg)
+{
+ if (msg.arguments.size() != 1)
+ return;
+ auto prefix_parts = msg.prefix.split('!');
+ if (prefix_parts.size() < 1)
+ return;
+ auto nick = prefix_parts[0];
+ auto& channel_name = msg.arguments[0];
+ ensure_channel(channel_name).handle_join(nick, msg.prefix);
+}
+
+void IRCClient::handle_part(const Message& msg)
+{
+ if (msg.arguments.size() < 1)
+ return;
+ auto prefix_parts = msg.prefix.split('!');
+ if (prefix_parts.size() < 1)
+ return;
+ auto nick = prefix_parts[0];
+ auto& channel_name = msg.arguments[0];
+ ensure_channel(channel_name).handle_part(nick, msg.prefix);
+}
+
+void IRCClient::handle_quit(const Message& msg)
+{
+ if (msg.arguments.size() < 1)
+ return;
+ auto prefix_parts = msg.prefix.split('!');
+ if (prefix_parts.size() < 1)
+ return;
+ auto nick = prefix_parts[0];
+ auto& message = msg.arguments[0];
+ for (auto& it : m_channels) {
+ it.value->handle_quit(nick, msg.prefix, message);
+ }
+}
+
+void IRCClient::handle_nick(const Message& msg)
+{
+ auto prefix_parts = msg.prefix.split('!');
+ if (prefix_parts.size() < 1)
+ return;
+ auto old_nick = prefix_parts[0];
+ if (msg.arguments.size() != 1)
+ return;
+ auto& new_nick = msg.arguments[0];
+ if (old_nick == m_nickname)
+ m_nickname = new_nick;
+ if (m_show_nick_change_messages)
+ add_server_message(String::formatted("~ {} changed nickname to {}", old_nick, new_nick));
+ if (on_nickname_changed)
+ on_nickname_changed(new_nick);
+ for (auto& it : m_channels) {
+ it.value->notify_nick_changed(old_nick, new_nick);
+ }
+}
+
+void IRCClient::handle_topic(const Message& msg)
+{
+ if (msg.arguments.size() != 2)
+ return;
+ auto prefix_parts = msg.prefix.split('!');
+ if (prefix_parts.size() < 1)
+ return;
+ auto nick = prefix_parts[0];
+ auto& channel_name = msg.arguments[0];
+ ensure_channel(channel_name).handle_topic(nick, msg.arguments[1]);
+}
+
+void IRCClient::handle_rpl_welcome(const Message& msg)
+{
+ if (msg.arguments.size() < 2)
+ return;
+ auto& welcome_message = msg.arguments[1];
+ add_server_message(welcome_message);
+
+ auto channel_str = m_config->read_entry("Connection", "AutoJoinChannels", "");
+ if (channel_str.is_empty())
+ return;
+ dbgln("IRCClient: Channels to autojoin: {}", channel_str);
+ auto channels = channel_str.split(',');
+ for (auto& channel : channels) {
+ join_channel(channel);
+ dbgln("IRCClient: Auto joining channel: {}", channel);
+ }
+}
+
+void IRCClient::handle_rpl_topic(const Message& msg)
+{
+ if (msg.arguments.size() < 3)
+ return;
+ auto& channel_name = msg.arguments[1];
+ auto& topic = msg.arguments[2];
+ ensure_channel(channel_name).handle_topic({}, topic);
+}
+
+void IRCClient::handle_rpl_namreply(const Message& msg)
+{
+ if (msg.arguments.size() < 4)
+ return;
+ auto& channel_name = msg.arguments[2];
+ auto& channel = ensure_channel(channel_name);
+ auto members = msg.arguments[3].split(' ');
+
+ quick_sort(members, [](auto& a, auto& b) {
+ return strcasecmp(a.characters(), b.characters()) < 0;
+ });
+
+ for (auto& member : members) {
+ if (member.is_empty())
+ continue;
+ char prefix = 0;
+ if (is_nick_prefix(member[0]))
+ prefix = member[0];
+ channel.add_member(member, prefix);
+ }
+}
+
+void IRCClient::handle_rpl_endofnames(const Message&)
+{
+ add_server_message("// End of NAMES");
+}
+
+void IRCClient::handle_rpl_banlist(const Message& msg)
+{
+ if (msg.arguments.size() < 5)
+ return;
+ auto& channel = msg.arguments[1];
+ auto& mask = msg.arguments[2];
+ auto& user = msg.arguments[3];
+ auto& datestamp = msg.arguments[4];
+ add_server_message(String::formatted("* {}: {} on {} by {}", channel, mask, datestamp, user));
+}
+
+void IRCClient::handle_rpl_endofbanlist(const Message&)
+{
+ add_server_message("// End of BANLIST");
+}
+
+void IRCClient::handle_rpl_endofwho(const Message&)
+{
+ add_server_message("// End of WHO");
+}
+
+void IRCClient::handle_rpl_endofwhois(const Message&)
+{
+ add_server_message("// End of WHOIS");
+}
+
+void IRCClient::handle_rpl_endofwhowas(const Message&)
+{
+ add_server_message("// End of WHOWAS");
+}
+
+void IRCClient::handle_rpl_endofmotd(const Message&)
+{
+ add_server_message("// End of MOTD");
+}
+
+void IRCClient::handle_rpl_whoisoperator(const Message& msg)
+{
+ if (msg.arguments.size() < 2)
+ return;
+ auto& nick = msg.arguments[1];
+ add_server_message(String::formatted("* {} is an IRC operator", nick));
+}
+
+void IRCClient::handle_rpl_whoisserver(const Message& msg)
+{
+ if (msg.arguments.size() < 3)
+ return;
+ auto& nick = msg.arguments[1];
+ auto& server = msg.arguments[2];
+ add_server_message(String::formatted("* {} is using server {}", nick, server));
+}
+
+void IRCClient::handle_rpl_whoisuser(const Message& msg)
+{
+ if (msg.arguments.size() < 6)
+ return;
+ auto& nick = msg.arguments[1];
+ auto& username = msg.arguments[2];
+ auto& host = msg.arguments[3];
+ [[maybe_unused]] auto& asterisk = msg.arguments[4];
+ auto& realname = msg.arguments[5];
+ add_server_message(String::formatted("* {} is {}@{}, real name: {}", nick, username, host, realname));
+}
+
+void IRCClient::handle_rpl_whoisidle(const Message& msg)
+{
+ if (msg.arguments.size() < 3)
+ return;
+ auto& nick = msg.arguments[1];
+ auto& secs = msg.arguments[2];
+ add_server_message(String::formatted("* {} is {} seconds idle", nick, secs));
+}
+
+void IRCClient::handle_rpl_whoischannels(const Message& msg)
+{
+ if (msg.arguments.size() < 3)
+ return;
+ auto& nick = msg.arguments[1];
+ auto& channel_list = msg.arguments[2];
+ add_server_message(String::formatted("* {} is in channels {}", nick, channel_list));
+}
+
+void IRCClient::handle_rpl_topicwhotime(const Message& msg)
+{
+ if (msg.arguments.size() < 4)
+ return;
+ auto& channel_name = msg.arguments[1];
+ auto& nick = msg.arguments[2];
+ auto setat = msg.arguments[3];
+ auto setat_time = setat.to_uint();
+ if (setat_time.has_value())
+ setat = Core::DateTime::from_timestamp(setat_time.value()).to_string();
+ ensure_channel(channel_name).add_message(String::formatted("*** (set by {} at {})", nick, setat), Color::Blue);
+}
+
+void IRCClient::handle_err_nosuchnick(const Message& msg)
+{
+ if (msg.arguments.size() < 3)
+ return;
+ auto& nick = msg.arguments[1];
+ auto& message = msg.arguments[2];
+ add_server_message(String::formatted("* {} :{}", nick, message));
+}
+
+void IRCClient::handle_err_unknowncommand(const Message& msg)
+{
+ if (msg.arguments.size() < 2)
+ return;
+ auto& cmd = msg.arguments[1];
+ add_server_message(String::formatted("* Unknown command: {}", cmd));
+}
+
+void IRCClient::handle_err_nicknameinuse(const Message& msg)
+{
+ if (msg.arguments.size() < 2)
+ return;
+ auto& nick = msg.arguments[1];
+ add_server_message(String::formatted("* {} :Nickname in use", nick));
+}
+
+void IRCClient::register_subwindow(IRCWindow& subwindow)
+{
+ if (subwindow.type() == IRCWindow::Server) {
+ m_server_subwindow = &subwindow;
+ subwindow.set_log_buffer(*m_log);
+ }
+ m_windows.append(&subwindow);
+ m_client_window_list_model->update();
+}
+
+void IRCClient::unregister_subwindow(IRCWindow& subwindow)
+{
+ if (subwindow.type() == IRCWindow::Server) {
+ m_server_subwindow = &subwindow;
+ }
+ for (size_t i = 0; i < m_windows.size(); ++i) {
+ if (m_windows.at(i) == &subwindow) {
+ m_windows.remove(i);
+ break;
+ }
+ }
+ m_client_window_list_model->update();
+}
+
+void IRCClient::handle_user_command(const String& input)
+{
+ auto parts = input.split_view(' ');
+ if (parts.is_empty())
+ return;
+ auto command = String(parts[0]).to_uppercase();
+ if (command == "/RAW") {
+ if (parts.size() <= 1)
+ return;
+ int command_length = command.length() + 1;
+ StringView raw_message = input.view().substring_view(command_length, input.view().length() - command_length);
+ send(String::formatted("{}\r\n", raw_message));
+ return;
+ }
+ if (command == "/NICK") {
+ if (parts.size() >= 2)
+ change_nick(parts[1]);
+ return;
+ }
+ if (command == "/JOIN") {
+ if (parts.size() >= 2)
+ join_channel(parts[1]);
+ return;
+ }
+ if (command == "/PART") {
+ if (parts.size() >= 2) {
+ auto channel = parts[1];
+ part_channel(channel);
+ } else {
+ auto* window = current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel)
+ return;
+ auto channel = window->channel().name();
+ join_channel(channel);
+ }
+ return;
+ }
+ if (command == "/CYCLE") {
+ if (parts.size() >= 2) {
+ auto channel = parts[1];
+ part_channel(channel);
+ join_channel(channel);
+ } else {
+ auto* window = current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel)
+ return;
+ auto channel = window->channel().name();
+ part_channel(channel);
+ join_channel(channel);
+ }
+ return;
+ }
+ if (command == "/BANLIST") {
+ if (parts.size() >= 2) {
+ auto channel = parts[1];
+ send_banlist(channel);
+ } else {
+ auto* window = current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel)
+ return;
+ auto channel = window->channel().name();
+ send_banlist(channel);
+ }
+ return;
+ }
+ if (command == "/ME") {
+ if (parts.size() < 2)
+ return;
+
+ auto* window = current_window();
+ if (!window)
+ return;
+
+ auto emote = input.view().substring_view_starting_after_substring(parts[0]);
+ auto action_string = String::formatted("ACTION{}", emote);
+ String peer;
+ if (window->type() == IRCWindow::Type::Channel) {
+ peer = window->channel().name();
+ window->channel().add_message(String::formatted("* {}{}", m_nickname, emote), Gfx::Color::Magenta);
+ } else if (window->type() == IRCWindow::Type::Query) {
+ peer = window->query().name();
+ window->query().add_message(String::formatted("* {}{}", m_nickname, emote), Gfx::Color::Magenta);
+ } else {
+ return;
+ }
+
+ send_ctcp_request(peer, action_string);
+ return;
+ }
+ if (command == "/TOPIC") {
+ if (parts.size() < 2)
+ return;
+ if (parts[1].is_empty())
+ return;
+
+ if (is_channel_prefix(parts[1][0])) {
+ if (parts.size() < 3)
+ return;
+ auto channel = parts[1];
+ auto topic = input.view().substring_view_starting_after_substring(channel);
+ send_topic(channel, topic);
+ } else {
+ auto* window = current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel)
+ return;
+ auto channel = window->channel().name();
+ auto topic = input.view().substring_view_starting_after_substring(parts[0]);
+ send_topic(channel, topic);
+ }
+ return;
+ }
+ if (command == "/KICK") {
+ if (parts.size() < 2)
+ return;
+ if (parts[1].is_empty())
+ return;
+
+ if (is_channel_prefix(parts[1][0])) {
+ if (parts.size() < 3)
+ return;
+ auto channel = parts[1];
+ auto nick = parts[2];
+ auto reason = input.view().substring_view_starting_after_substring(nick);
+ send_kick(channel, nick, reason);
+ } else {
+ auto* window = current_window();
+ if (!window || window->type() != IRCWindow::Type::Channel)
+ return;
+ auto channel = window->channel().name();
+ auto nick = parts[1];
+ auto reason = input.view().substring_view_starting_after_substring(nick);
+ send_kick(channel, nick, reason);
+ }
+ return;
+ }
+ if (command == "/LIST") {
+ send_list();
+ return;
+ }
+ if (command == "/QUERY") {
+ if (parts.size() >= 2) {
+ auto& query = ensure_query(parts[1]);
+ IRCAppWindow::the().set_active_window(query.window());
+ }
+ return;
+ }
+ if (command == "/MSG") {
+ if (parts.size() < 3)
+ return;
+ auto nick = parts[1];
+ auto& query = ensure_query(nick);
+ IRCAppWindow::the().set_active_window(query.window());
+ query.say(input.view().substring_view_starting_after_substring(nick));
+ return;
+ }
+ if (command == "/WHOIS") {
+ if (parts.size() >= 2)
+ send_whois(parts[1]);
+ return;
+ }
+}
+
+void IRCClient::change_nick(const String& nick)
+{
+ send(String::formatted("NICK {}\r\n", nick));
+}
+
+void IRCClient::handle_list_channels_action()
+{
+ send_list();
+}
+
+void IRCClient::handle_whois_action(const String& nick)
+{
+ send_whois(nick);
+}
+
+void IRCClient::handle_ctcp_user_action(const String& nick, const String& message)
+{
+ send_ctcp_request(nick, message);
+}
+
+void IRCClient::handle_open_query_action(const String& nick)
+{
+ ensure_query(nick);
+}
+
+void IRCClient::handle_change_nick_action(const String& nick)
+{
+ change_nick(nick);
+}
+
+void IRCClient::handle_change_topic_action(const String& channel, const String& topic)
+{
+ send_topic(channel, topic);
+}
+
+void IRCClient::handle_invite_user_action(const String& channel, const String& nick)
+{
+ send_invite(channel, nick);
+}
+
+void IRCClient::handle_banlist_action(const String& channel)
+{
+ send_banlist(channel);
+}
+
+void IRCClient::handle_voice_user_action(const String& channel, const String& nick)
+{
+ send_voice_user(channel, nick);
+}
+
+void IRCClient::handle_devoice_user_action(const String& channel, const String& nick)
+{
+ send_devoice_user(channel, nick);
+}
+
+void IRCClient::handle_hop_user_action(const String& channel, const String& nick)
+{
+ send_hop_user(channel, nick);
+}
+
+void IRCClient::handle_dehop_user_action(const String& channel, const String& nick)
+{
+ send_dehop_user(channel, nick);
+}
+
+void IRCClient::handle_op_user_action(const String& channel, const String& nick)
+{
+ send_op_user(channel, nick);
+}
+
+void IRCClient::handle_deop_user_action(const String& channel, const String& nick)
+{
+ send_deop_user(channel, nick);
+}
+
+void IRCClient::handle_kick_user_action(const String& channel, const String& nick, const String& message)
+{
+ send_kick(channel, nick, message);
+}
+
+void IRCClient::handle_close_query_action(const String& nick)
+{
+ m_queries.remove(nick);
+ m_client_window_list_model->update();
+}
+
+void IRCClient::handle_join_action(const String& channel)
+{
+ join_channel(channel);
+}
+
+void IRCClient::handle_part_action(const String& channel)
+{
+ part_channel(channel);
+}
+
+void IRCClient::handle_cycle_channel_action(const String& channel)
+{
+ part_channel(channel);
+ join_channel(channel);
+}
+
+void IRCClient::did_part_from_channel(Badge<IRCChannel>, IRCChannel& channel)
+{
+ if (on_part_from_channel)
+ on_part_from_channel(channel);
+}
+
+void IRCClient::send_ctcp_response(const StringView& peer, const StringView& payload)
+{
+ StringBuilder builder;
+ builder.append(0x01);
+ builder.append(payload);
+ builder.append(0x01);
+ auto message = builder.to_string();
+ send_notice(peer, message);
+}
+
+void IRCClient::send_ctcp_request(const StringView& peer, const StringView& payload)
+{
+ StringBuilder builder;
+ builder.append(0x01);
+ builder.append(payload);
+ builder.append(0x01);
+ auto message = builder.to_string();
+ send_privmsg(peer, message);
+}
+
+void IRCClient::handle_ctcp_request(const StringView& peer, const StringView& payload)
+{
+ dbgln("handle_ctcp_request: {}", payload);
+
+ if (payload == "VERSION") {
+ auto version = ctcp_version_reply();
+ if (version.is_empty())
+ return;
+ send_ctcp_response(peer, String::formatted("VERSION {}", version));
+ return;
+ }
+
+ if (payload == "USERINFO") {
+ auto userinfo = ctcp_userinfo_reply();
+ if (userinfo.is_empty())
+ return;
+ send_ctcp_response(peer, String::formatted("USERINFO {}", userinfo));
+ return;
+ }
+
+ if (payload == "FINGER") {
+ auto finger = ctcp_finger_reply();
+ if (finger.is_empty())
+ return;
+ send_ctcp_response(peer, String::formatted("FINGER {}", finger));
+ return;
+ }
+
+ if (payload.starts_with("PING")) {
+ send_ctcp_response(peer, payload);
+ return;
+ }
+}
+
+void IRCClient::handle_ctcp_response(const StringView& peer, const StringView& payload)
+{
+ dbgln("handle_ctcp_response({}): {}", peer, payload);
+}
diff --git a/Userland/Applications/IRCClient/IRCClient.h b/Userland/Applications/IRCClient/IRCClient.h
new file mode 100644
index 0000000000..06f7e59289
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCClient.h
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "IRCLogBuffer.h"
+#include "IRCWindow.h"
+#include <AK/CircularQueue.h>
+#include <AK/Function.h>
+#include <AK/HashMap.h>
+#include <AK/String.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/TCPSocket.h>
+
+class IRCChannel;
+class IRCQuery;
+class IRCWindowListModel;
+
+class IRCClient final : public Core::Object {
+ C_OBJECT(IRCClient)
+ friend class IRCChannel;
+ friend class IRCQuery;
+
+public:
+ virtual ~IRCClient() override;
+
+ void set_server(const String& hostname, int port = 6667);
+
+ bool connect();
+
+ String hostname() const { return m_hostname; }
+ int port() const { return m_port; }
+
+ String nickname() const { return m_nickname; }
+
+ String ctcp_version_reply() const { return m_ctcp_version_reply; }
+ String ctcp_userinfo_reply() const { return m_ctcp_userinfo_reply; }
+ String ctcp_finger_reply() const { return m_ctcp_finger_reply; }
+
+ bool show_join_part_messages() const { return m_show_join_part_messages; }
+ bool show_nick_change_messages() const { return m_show_nick_change_messages; }
+
+ bool notify_on_message() const { return m_notify_on_message; }
+ bool notify_on_mention() const { return m_notify_on_mention; }
+
+ void join_channel(const String&);
+ void part_channel(const String&);
+ void change_nick(const String&);
+
+ static bool is_nick_prefix(char);
+ static bool is_channel_prefix(char);
+ String nick_without_prefix(const String& nick);
+
+ IRCWindow* current_window() { return aid_get_active_window(); }
+ const IRCWindow* current_window() const { return aid_get_active_window(); }
+
+ Function<void()> on_disconnect;
+ Function<void()> on_server_message;
+ Function<void(const String&)> on_nickname_changed;
+ Function<void(IRCChannel&)> on_part_from_channel;
+
+ Function<NonnullRefPtr<IRCWindow>(void*, IRCWindow::Type, const String&)> aid_create_window;
+ Function<IRCWindow*()> aid_get_active_window;
+ Function<void()> aid_update_window_list;
+
+ void register_subwindow(IRCWindow&);
+ void unregister_subwindow(IRCWindow&);
+
+ IRCWindowListModel* client_window_list_model() { return m_client_window_list_model.ptr(); }
+ const IRCWindowListModel* client_window_list_model() const { return m_client_window_list_model.ptr(); }
+
+ int window_count() const { return m_windows.size(); }
+ const IRCWindow& window_at(int index) const { return *m_windows.at(index); }
+ IRCWindow& window_at(int index) { return *m_windows.at(index); }
+
+ size_t window_index(const IRCWindow& window) const
+ {
+ for (size_t i = 0; i < m_windows.size(); ++i) {
+ if (m_windows[i] == &window)
+ return i;
+ }
+ ASSERT_NOT_REACHED();
+ }
+
+ void did_part_from_channel(Badge<IRCChannel>, IRCChannel&);
+
+ void handle_user_input_in_channel(const String& channel_name, const String&);
+ void handle_user_input_in_query(const String& query_name, const String&);
+ void handle_user_input_in_server(const String&);
+
+ void handle_list_channels_action();
+ void handle_whois_action(const String& nick);
+ void handle_ctcp_user_action(const String& nick, const String& message);
+ void handle_open_query_action(const String&);
+ void handle_close_query_action(const String&);
+ void handle_join_action(const String& channel_name);
+ void handle_part_action(const String& channel_name);
+ void handle_cycle_channel_action(const String& channel_name);
+ void handle_change_nick_action(const String& nick);
+ void handle_change_topic_action(const String& channel_name, const String&);
+ void handle_invite_user_action(const String& channel_name, const String& nick);
+ void handle_banlist_action(const String& channel_name);
+ void handle_voice_user_action(const String& channel_name, const String& nick);
+ void handle_devoice_user_action(const String& channel_name, const String& nick);
+ void handle_hop_user_action(const String& channel_name, const String& nick);
+ void handle_dehop_user_action(const String& channel_name, const String& nick);
+ void handle_op_user_action(const String& channel_name, const String& nick);
+ void handle_deop_user_action(const String& channel_name, const String& nick);
+ void handle_kick_user_action(const String& channel_name, const String& nick, const String&);
+
+ IRCQuery* query_with_name(const String&);
+ IRCQuery& ensure_query(const String& name);
+ IRCChannel& ensure_channel(const String& name);
+
+ void add_server_message(const String&, Color = Color::Black);
+
+private:
+ IRCClient(String server, int port);
+
+ struct Message {
+ String prefix;
+ String command;
+ Vector<String> arguments;
+ };
+
+ enum class PrivmsgOrNotice {
+ Privmsg,
+ Notice,
+ };
+
+ void receive_from_server();
+ void send(const String&);
+ void send_user();
+ void send_nick();
+ void send_pong(const String& server);
+ void send_privmsg(const String& target, const String&);
+ void send_notice(const String& target, const String&);
+ void send_topic(const String& channel_name, const String&);
+ void send_invite(const String& channel_name, const String& nick);
+ void send_banlist(const String& channel_name);
+ void send_voice_user(const String& channel_name, const String& nick);
+ void send_devoice_user(const String& channel_name, const String& nick);
+ void send_hop_user(const String& channel_name, const String& nick);
+ void send_dehop_user(const String& channel_name, const String& nick);
+ void send_op_user(const String& channel_name, const String& nick);
+ void send_deop_user(const String& channel_name, const String& nick);
+ void send_kick(const String& channel_name, const String& nick, const String&);
+ void send_list();
+ void send_whois(const String&);
+ void process_line(const String&);
+ void handle_join(const Message&);
+ void handle_part(const Message&);
+ void handle_quit(const Message&);
+ void handle_ping(const Message&);
+ void handle_topic(const Message&);
+ void handle_rpl_welcome(const Message&);
+ void handle_rpl_topic(const Message&);
+ void handle_rpl_whoisuser(const Message&);
+ void handle_rpl_whoisserver(const Message&);
+ void handle_rpl_whoisoperator(const Message&);
+ void handle_rpl_whoisidle(const Message&);
+ void handle_rpl_endofwho(const Message&);
+ void handle_rpl_endofwhois(const Message&);
+ void handle_rpl_endofwhowas(const Message&);
+ void handle_rpl_endofmotd(const Message&);
+ void handle_rpl_whoischannels(const Message&);
+ void handle_rpl_topicwhotime(const Message&);
+ void handle_rpl_endofnames(const Message&);
+ void handle_rpl_endofbanlist(const Message&);
+ void handle_rpl_namreply(const Message&);
+ void handle_rpl_banlist(const Message&);
+ void handle_err_nosuchnick(const Message&);
+ void handle_err_unknowncommand(const Message&);
+ void handle_err_nicknameinuse(const Message&);
+ void handle_privmsg_or_notice(const Message&, PrivmsgOrNotice);
+ void handle_nick(const Message&);
+ void handle(const Message&);
+ void handle_user_command(const String&);
+ void handle_ctcp_request(const StringView& peer, const StringView& payload);
+ void handle_ctcp_response(const StringView& peer, const StringView& payload);
+ void send_ctcp_request(const StringView& peer, const StringView& payload);
+ void send_ctcp_response(const StringView& peer, const StringView& payload);
+
+ void on_socket_connected();
+
+ String m_hostname;
+ int m_port { 6667 };
+
+ RefPtr<Core::TCPSocket> m_socket;
+
+ String m_nickname;
+ RefPtr<Core::Notifier> m_notifier;
+ HashMap<String, RefPtr<IRCChannel>, CaseInsensitiveStringTraits> m_channels;
+ HashMap<String, RefPtr<IRCQuery>, CaseInsensitiveStringTraits> m_queries;
+
+ bool m_show_join_part_messages { 1 };
+ bool m_show_nick_change_messages { 1 };
+
+ bool m_notify_on_message { 1 };
+ bool m_notify_on_mention { 1 };
+
+ String m_ctcp_version_reply;
+ String m_ctcp_userinfo_reply;
+ String m_ctcp_finger_reply;
+
+ Vector<IRCWindow*> m_windows;
+
+ IRCWindow* m_server_subwindow { nullptr };
+
+ NonnullRefPtr<IRCWindowListModel> m_client_window_list_model;
+ NonnullRefPtr<IRCLogBuffer> m_log;
+ NonnullRefPtr<Core::ConfigFile> m_config;
+};
diff --git a/Userland/Applications/IRCClient/IRCLogBuffer.cpp b/Userland/Applications/IRCClient/IRCLogBuffer.cpp
new file mode 100644
index 0000000000..b292e0a4ee
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCLogBuffer.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCLogBuffer.h"
+#include <AK/StringBuilder.h>
+#include <LibWeb/DOM/DocumentFragment.h>
+#include <LibWeb/DOM/DocumentType.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/HTML/HTMLBodyElement.h>
+#include <time.h>
+
+NonnullRefPtr<IRCLogBuffer> IRCLogBuffer::create()
+{
+ return adopt(*new IRCLogBuffer);
+}
+
+IRCLogBuffer::IRCLogBuffer()
+{
+ m_document = Web::DOM::Document::create();
+ m_document->append_child(adopt(*new Web::DOM::DocumentType(document())));
+ auto html_element = m_document->create_element("html");
+ m_document->append_child(html_element);
+ auto head_element = m_document->create_element("head");
+ html_element->append_child(head_element);
+ auto style_element = m_document->create_element("style");
+ style_element->append_child(adopt(*new Web::DOM::Text(document(), "div { font-family: Csilla; font-weight: lighter; }")));
+ head_element->append_child(style_element);
+ auto body_element = m_document->create_element("body");
+ html_element->append_child(body_element);
+ m_container_element = body_element;
+}
+
+IRCLogBuffer::~IRCLogBuffer()
+{
+}
+
+static String timestamp_string()
+{
+ auto now = time(nullptr);
+ auto* tm = localtime(&now);
+ return String::formatted("{:02}:{:02}:{:02} ", tm->tm_hour, tm->tm_min, tm->tm_sec);
+}
+
+void IRCLogBuffer::add_message(char prefix, const String& name, const String& text, Color color)
+{
+ auto nick_string = String::formatted("<{}{}> ", prefix ? prefix : ' ', name.characters());
+ auto html = String::formatted(
+ "<span>{}</span>"
+ "<b>{}</b>"
+ "<span>{}</span>",
+ timestamp_string(),
+ escape_html_entities(nick_string),
+ escape_html_entities(text));
+
+ auto wrapper = m_document->create_element(Web::HTML::TagNames::div);
+ wrapper->set_attribute(Web::HTML::AttributeNames::style, String::formatted("color: {}", color.to_string()));
+ wrapper->set_inner_html(html);
+ m_container_element->append_child(wrapper);
+ m_document->force_layout();
+}
+
+void IRCLogBuffer::add_message(const String& text, Color color)
+{
+ auto html = String::formatted(
+ "<span>{}</span>"
+ "<span>{}</span>",
+ timestamp_string(),
+ escape_html_entities(text));
+ auto wrapper = m_document->create_element(Web::HTML::TagNames::div);
+ wrapper->set_attribute(Web::HTML::AttributeNames::style, String::formatted("color: {}", color.to_string()));
+ wrapper->set_inner_html(html);
+ m_container_element->append_child(wrapper);
+ m_document->force_layout();
+}
diff --git a/Userland/Applications/IRCClient/IRCLogBuffer.h b/Userland/Applications/IRCClient/IRCLogBuffer.h
new file mode 100644
index 0000000000..db4636f0ee
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCLogBuffer.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <LibGfx/Color.h>
+#include <LibWeb/DOM/Document.h>
+
+class IRCLogBuffer : public RefCounted<IRCLogBuffer> {
+public:
+ static NonnullRefPtr<IRCLogBuffer> create();
+ ~IRCLogBuffer();
+
+ struct Message {
+ time_t timestamp { 0 };
+ char prefix { 0 };
+ String sender;
+ String text;
+ Color color { Color::Black };
+ };
+
+ void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
+ void add_message(const String& text, Color = Color::Black);
+
+ const Web::DOM::Document& document() const { return *m_document; }
+ Web::DOM::Document& document() { return *m_document; }
+
+private:
+ IRCLogBuffer();
+ RefPtr<Web::DOM::Document> m_document;
+ RefPtr<Web::DOM::Element> m_container_element;
+};
diff --git a/Userland/Applications/IRCClient/IRCQuery.cpp b/Userland/Applications/IRCClient/IRCQuery.cpp
new file mode 100644
index 0000000000..e2795f05d8
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCQuery.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCQuery.h"
+#include "IRCClient.h"
+#include <stdio.h>
+
+IRCQuery::IRCQuery(IRCClient& client, const String& name)
+ : m_client(client)
+ , m_name(name)
+ , m_log(IRCLogBuffer::create())
+{
+ m_window = m_client->aid_create_window(this, IRCWindow::Query, m_name);
+ m_window->set_log_buffer(*m_log);
+}
+
+IRCQuery::~IRCQuery()
+{
+}
+
+NonnullRefPtr<IRCQuery> IRCQuery::create(IRCClient& client, const String& name)
+{
+ return adopt(*new IRCQuery(client, name));
+}
+
+void IRCQuery::add_message(char prefix, const String& name, const String& text, Color color)
+{
+ log().add_message(prefix, name, text, color);
+ window().did_add_message(name, text);
+}
+
+void IRCQuery::add_message(const String& text, Color color)
+{
+ log().add_message(text, color);
+ window().did_add_message();
+}
+
+void IRCQuery::say(const String& text)
+{
+ m_client->send_privmsg(m_name, text);
+ add_message(' ', m_client->nickname(), text);
+}
diff --git a/Userland/Applications/IRCClient/IRCQuery.h b/Userland/Applications/IRCClient/IRCQuery.h
new file mode 100644
index 0000000000..379d9d6492
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCQuery.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "IRCLogBuffer.h"
+#include <AK/CircularQueue.h>
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <AK/Vector.h>
+
+class IRCClient;
+class IRCWindow;
+
+class IRCQuery : public RefCounted<IRCQuery> {
+public:
+ static NonnullRefPtr<IRCQuery> create(IRCClient&, const String& name);
+ ~IRCQuery();
+
+ String name() const { return m_name; }
+ void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
+ void add_message(const String& text, Color = Color::Black);
+
+ const IRCLogBuffer& log() const { return *m_log; }
+ IRCLogBuffer& log() { return *m_log; }
+
+ void say(const String&);
+
+ IRCWindow& window() { return *m_window; }
+ const IRCWindow& window() const { return *m_window; }
+
+private:
+ IRCQuery(IRCClient&, const String& name);
+
+ NonnullRefPtr<IRCClient> m_client;
+ String m_name;
+ RefPtr<IRCWindow> m_window;
+
+ NonnullRefPtr<IRCLogBuffer> m_log;
+};
diff --git a/Userland/Applications/IRCClient/IRCWindow.cpp b/Userland/Applications/IRCClient/IRCWindow.cpp
new file mode 100644
index 0000000000..7a84795b8f
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCWindow.cpp
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCWindow.h"
+#include "IRCChannel.h"
+#include "IRCChannelMemberListModel.h"
+#include "IRCClient.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Notification.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Window.h>
+#include <LibWeb/InProcessWebView.h>
+
+IRCWindow::IRCWindow(IRCClient& client, void* owner, Type type, const String& name)
+ : m_client(client)
+ , m_owner(owner)
+ , m_type(type)
+ , m_name(name)
+{
+ set_layout<GUI::VerticalBoxLayout>();
+
+ // Make a container for the log buffer view + (optional) member list.
+ auto& container = add<GUI::HorizontalSplitter>();
+
+ m_page_view = container.add<Web::InProcessWebView>();
+
+ if (m_type == Channel) {
+ auto& member_view = container.add<GUI::TableView>();
+ member_view.set_column_headers_visible(false);
+ member_view.set_fixed_width(100);
+ member_view.set_alternating_row_colors(false);
+ member_view.set_model(channel().member_model());
+ member_view.set_activates_on_selection(true);
+ member_view.on_activation = [&](auto& index) {
+ if (!index.is_valid())
+ return;
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_open_query_action(m_client->nick_without_prefix(nick.characters()));
+ };
+ member_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
+ if (!index.is_valid())
+ return;
+
+ m_context_menu = GUI::Menu::construct();
+
+ m_context_menu->add_action(GUI::Action::create("Open query", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-open-query.png"), [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_open_query_action(m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ m_context_menu->add_action(GUI::Action::create("Whois", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-whois.png"), [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_whois_action(m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ auto& context_control_menu = m_context_menu->add_submenu("Control");
+
+ context_control_menu.add_action(GUI::Action::create("Voice", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_voice_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_action(GUI::Action::create("DeVoice", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_devoice_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_action(GUI::Action::create("Hop", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_hop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_action(GUI::Action::create("DeHop", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_dehop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_action(GUI::Action::create("Op", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_op_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_action(GUI::Action::create("DeOp", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_deop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
+ }));
+
+ context_control_menu.add_separator();
+
+ context_control_menu.add_action(GUI::Action::create("Kick", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ if (IRCClient::is_nick_prefix(nick[0]))
+ nick = nick.substring(1, nick.length() - 1);
+ String value;
+ if (GUI::InputBox::show(value, window(), "Enter reason:", "Reason") == GUI::InputBox::ExecOK)
+ m_client->handle_kick_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()), value);
+ }));
+
+ auto& context_ctcp_menu = m_context_menu->add_submenu("CTCP");
+
+ context_ctcp_menu.add_action(GUI::Action::create("User info", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "USERINFO");
+ }));
+
+ context_ctcp_menu.add_action(GUI::Action::create("Finger", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "FINGER");
+ }));
+
+ context_ctcp_menu.add_action(GUI::Action::create("Time", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "TIME");
+ }));
+
+ context_ctcp_menu.add_action(GUI::Action::create("Version", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "VERSION");
+ }));
+
+ context_ctcp_menu.add_action(GUI::Action::create("Client info", [&](const GUI::Action&) {
+ auto nick = channel().member_model()->nick_at(member_view.selection().first());
+ if (nick.is_empty())
+ return;
+ m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "CLIENTINFO");
+ }));
+
+ m_context_menu->popup(event.screen_position());
+ };
+ }
+
+ m_text_box = add<GUI::TextBox>();
+ m_text_box->set_fixed_height(19);
+ m_text_box->on_return_pressed = [this] {
+ if (m_type == Channel)
+ m_client->handle_user_input_in_channel(m_name, m_text_box->text());
+ else if (m_type == Query)
+ m_client->handle_user_input_in_query(m_name, m_text_box->text());
+ else if (m_type == Server)
+ m_client->handle_user_input_in_server(m_text_box->text());
+ m_text_box->add_current_text_to_history();
+ m_text_box->clear();
+ };
+ m_text_box->set_history_enabled(true);
+ m_text_box->set_placeholder("Message");
+
+ m_client->register_subwindow(*this);
+}
+
+IRCWindow::~IRCWindow()
+{
+ m_client->unregister_subwindow(*this);
+}
+
+void IRCWindow::set_log_buffer(const IRCLogBuffer& log_buffer)
+{
+ m_log_buffer = &log_buffer;
+ m_page_view->set_document(const_cast<Web::DOM::Document*>(&log_buffer.document()));
+}
+
+bool IRCWindow::is_active() const
+{
+ return m_client->current_window() == this;
+}
+
+void IRCWindow::post_notification_if_needed(const String& name, const String& message)
+{
+ if (name.is_null() || message.is_null())
+ return;
+ if (is_active() && window()->is_active())
+ return;
+
+ auto notification = GUI::Notification::construct();
+
+ if (type() == Type::Channel) {
+ if (!m_client->notify_on_mention())
+ return;
+ if (!message.contains(m_client->nickname()))
+ return;
+
+ StringBuilder builder;
+ builder.append(name);
+ builder.append(" in ");
+ builder.append(m_name);
+ notification->set_title(builder.to_string());
+ } else {
+ if (!m_client->notify_on_message())
+ return;
+ notification->set_title(name);
+ }
+
+ notification->set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/app-irc-client.png"));
+ notification->set_text(message);
+ notification->show();
+}
+
+void IRCWindow::did_add_message(const String& name, const String& message)
+{
+ post_notification_if_needed(name, message);
+
+ if (!is_active()) {
+ ++m_unread_count;
+ m_client->aid_update_window_list();
+ return;
+ }
+ m_page_view->scroll_to_bottom();
+}
+
+void IRCWindow::clear_unread_count()
+{
+ if (!m_unread_count)
+ return;
+ m_unread_count = 0;
+ m_client->aid_update_window_list();
+}
+
+int IRCWindow::unread_count() const
+{
+ return m_unread_count;
+}
diff --git a/Userland/Applications/IRCClient/IRCWindow.h b/Userland/Applications/IRCClient/IRCWindow.h
new file mode 100644
index 0000000000..a26e00d65e
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCWindow.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+#include <LibWeb/Forward.h>
+
+class IRCChannel;
+class IRCClient;
+class IRCQuery;
+class IRCLogBuffer;
+
+class IRCWindow : public GUI::Widget {
+ C_OBJECT(IRCWindow)
+public:
+ enum Type {
+ Server,
+ Channel,
+ Query,
+ };
+
+ virtual ~IRCWindow() override;
+
+ String name() const { return m_name; }
+ void set_name(const String& name) { m_name = name; }
+
+ Type type() const { return m_type; }
+
+ void set_log_buffer(const IRCLogBuffer&);
+
+ bool is_active() const;
+
+ int unread_count() const;
+ void clear_unread_count();
+
+ void did_add_message(const String& name = {}, const String& message = {});
+
+ IRCChannel& channel() { return *(IRCChannel*)m_owner; }
+ const IRCChannel& channel() const { return *(const IRCChannel*)m_owner; }
+
+ IRCQuery& query() { return *(IRCQuery*)m_owner; }
+ const IRCQuery& query() const { return *(const IRCQuery*)m_owner; }
+
+private:
+ IRCWindow(IRCClient&, void* owner, Type, const String& name);
+
+ void post_notification_if_needed(const String& name, const String& message);
+
+ NonnullRefPtr<IRCClient> m_client;
+ void* m_owner { nullptr };
+ Type m_type;
+ String m_name;
+ RefPtr<Web::InProcessWebView> m_page_view;
+ RefPtr<GUI::TextBox> m_text_box;
+ RefPtr<IRCLogBuffer> m_log_buffer;
+ RefPtr<GUI::Menu> m_context_menu;
+ int m_unread_count { 0 };
+};
diff --git a/Userland/Applications/IRCClient/IRCWindowListModel.cpp b/Userland/Applications/IRCClient/IRCWindowListModel.cpp
new file mode 100644
index 0000000000..7ee68c429e
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCWindowListModel.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCWindowListModel.h"
+#include "IRCChannel.h"
+#include "IRCClient.h"
+
+IRCWindowListModel::IRCWindowListModel(IRCClient& client)
+ : m_client(client)
+{
+}
+
+IRCWindowListModel::~IRCWindowListModel()
+{
+}
+
+int IRCWindowListModel::row_count(const GUI::ModelIndex&) const
+{
+ return m_client->window_count();
+}
+
+int IRCWindowListModel::column_count(const GUI::ModelIndex&) const
+{
+ return 1;
+}
+
+String IRCWindowListModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Name:
+ return "Name";
+ }
+ ASSERT_NOT_REACHED();
+}
+
+GUI::Variant IRCWindowListModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ if (role == GUI::ModelRole::TextAlignment)
+ return Gfx::TextAlignment::CenterLeft;
+ if (role == GUI::ModelRole::Display) {
+ switch (index.column()) {
+ case Column::Name: {
+ auto& window = m_client->window_at(index.row());
+ if (window.unread_count())
+ return String::formatted("{} ({})", window.name(), window.unread_count());
+ return window.name();
+ }
+ }
+ }
+ if (role == GUI::ModelRole::ForegroundColor) {
+ switch (index.column()) {
+ case Column::Name: {
+ auto& window = m_client->window_at(index.row());
+ if (window.unread_count())
+ return Color(Color::Red);
+ if (!window.channel().is_open())
+ return Color(Color::WarmGray);
+ return Color(Color::Black);
+ }
+ }
+ }
+ return {};
+}
+
+void IRCWindowListModel::update()
+{
+ did_update();
+}
diff --git a/Userland/Applications/IRCClient/IRCWindowListModel.h b/Userland/Applications/IRCClient/IRCWindowListModel.h
new file mode 100644
index 0000000000..88e5463b82
--- /dev/null
+++ b/Userland/Applications/IRCClient/IRCWindowListModel.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibGUI/Model.h>
+
+class IRCClient;
+class IRCWindow;
+
+class IRCWindowListModel final : public GUI::Model {
+public:
+ enum Column {
+ Name,
+ };
+
+ static NonnullRefPtr<IRCWindowListModel> create(IRCClient& client) { return adopt(*new IRCWindowListModel(client)); }
+ virtual ~IRCWindowListModel() override;
+
+ virtual int row_count(const GUI::ModelIndex&) const override;
+ virtual int column_count(const GUI::ModelIndex&) const override;
+ virtual String column_name(int column) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+
+private:
+ explicit IRCWindowListModel(IRCClient&);
+
+ NonnullRefPtr<IRCClient> m_client;
+};
diff --git a/Userland/Applications/IRCClient/main.cpp b/Userland/Applications/IRCClient/main.cpp
new file mode 100644
index 0000000000..a0adf92c0d
--- /dev/null
+++ b/Userland/Applications/IRCClient/main.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IRCAppWindow.h"
+#include "IRCClient.h"
+#include <LibCore/StandardPaths.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/MessageBox.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio inet dns unix shared_buffer cpath rpath fattr wpath cpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (getuid() == 0) {
+ warnln("Refusing to run as root");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio inet dns unix shared_buffer rpath wpath cpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/lookup", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/notify", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc/passwd", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(Core::StandardPaths::home_directory().characters(), "rwc") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ URL url = "";
+ if (app->args().size() >= 1) {
+ url = URL::create_with_url_or_path(app->args()[0]);
+
+ if (url.protocol().to_lowercase() == "ircs") {
+ warnln("Secure IRC over SSL/TLS (ircs) is not supported");
+ return 1;
+ }
+
+ if (url.protocol().to_lowercase() != "irc") {
+ warnln("Unsupported protocol");
+ return 1;
+ }
+
+ if (url.host().is_empty()) {
+ warnln("Invalid URL");
+ return 1;
+ }
+
+ if (!url.port() || url.port() == 80)
+ url.set_port(6667);
+ }
+
+ auto app_window = IRCAppWindow::construct(url.host(), url.port());
+ app_window->show();
+ return app->exec();
+}
diff --git a/Userland/Applications/KeyboardMapper/CMakeLists.txt b/Userland/Applications/KeyboardMapper/CMakeLists.txt
new file mode 100644
index 0000000000..fe24376eb1
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES
+ KeyboardMapperWidget.cpp
+ KeyButton.cpp
+ main.cpp
+)
+
+serenity_bin(KeyboardMapper)
+target_link_libraries(KeyboardMapper LibGUI LibKeyboard)
diff --git a/Userland/Applications/KeyboardMapper/KeyButton.cpp b/Userland/Applications/KeyboardMapper/KeyButton.cpp
new file mode 100644
index 0000000000..58a687faca
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/KeyButton.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyButton.h"
+#include <LibGUI/Button.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+
+KeyButton::~KeyButton()
+{
+}
+
+void KeyButton::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ auto cont_rect = rect();
+ auto& font = this->font();
+
+ Color color;
+ if (m_pressed) {
+ color = Color::Cyan;
+ } else if (!is_enabled()) {
+ color = Color::LightGray;
+ } else {
+ color = Color::White;
+ }
+
+ painter.fill_rect(cont_rect, Color::Black);
+ painter.fill_rect({ cont_rect.x() + 1, cont_rect.y() + 1, cont_rect.width() - 2, cont_rect.height() - 2 }, Color::from_rgb(0x999999));
+ painter.fill_rect({ cont_rect.x() + 6, cont_rect.y() + 3, cont_rect.width() - 12, cont_rect.height() - 12 }, Color::from_rgb(0x8C7272));
+ painter.fill_rect({ cont_rect.x() + 7, cont_rect.y() + 4, cont_rect.width() - 14, cont_rect.height() - 14 }, color);
+
+ if (!text().is_empty()) {
+ Gfx::IntRect text_rect { 0, 0, font.width(text()), font.glyph_height() };
+ text_rect.align_within({ cont_rect.x() + 7, cont_rect.y() + 4, cont_rect.width() - 14, cont_rect.height() - 14 }, Gfx::TextAlignment::Center);
+
+ painter.draw_text(text_rect, text(), font, Gfx::TextAlignment::Center, palette().button_text(), Gfx::TextElision::Right);
+ if (is_focused())
+ painter.draw_rect(text_rect.inflated(6, 4), palette().focus_outline());
+ }
+}
+
+void KeyButton::click(unsigned)
+{
+ if (on_click)
+ on_click();
+}
+
+void KeyButton::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!is_enabled())
+ return;
+
+ Gfx::IntRect c = { rect().x() + 7, rect().y() + 4, rect().width() - 14, rect().height() - 14 };
+
+ if (c.contains(event.position())) {
+ window()->set_cursor(Gfx::StandardCursor::Hand);
+ return;
+ }
+ window()->set_cursor(Gfx::StandardCursor::Arrow);
+
+ AbstractButton::mousemove_event(event);
+}
+
+void KeyButton::leave_event(Core::Event& event)
+{
+ window()->set_cursor(Gfx::StandardCursor::Arrow);
+ AbstractButton::leave_event(event);
+}
diff --git a/Userland/Applications/KeyboardMapper/KeyButton.h b/Userland/Applications/KeyboardMapper/KeyButton.h
new file mode 100644
index 0000000000..afb57033b3
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/KeyButton.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/AbstractButton.h>
+
+class KeyButton : public GUI::AbstractButton {
+ C_OBJECT(KeyButton)
+
+public:
+ virtual ~KeyButton() override;
+
+ void set_pressed(bool value) { m_pressed = value; }
+
+ Function<void()> on_click;
+
+protected:
+ virtual void click(unsigned modifiers = 0) override;
+ virtual void leave_event(Core::Event&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+private:
+ bool m_pressed { false };
+};
diff --git a/Userland/Applications/KeyboardMapper/KeyPositions.h b/Userland/Applications/KeyboardMapper/KeyPositions.h
new file mode 100644
index 0000000000..3cd9dbaaf7
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/KeyPositions.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+
+struct KeyPosition {
+ u32 scancode;
+ int x;
+ int y;
+ int width;
+ int height;
+ bool enabled;
+ int map_index;
+ AK::String name;
+};
+
+#define KEY_COUNT 63
+
+struct KeyPosition keys[KEY_COUNT] = {
+ // clang-format off
+ [ 0] = { 0, 0, 0, 0, 0, false, 0, ""},
+
+ [ 1] = { 0x29, 0, 0, 50, 50, true, 41, "`"},
+ [ 2] = { 0x02, 51, 0, 50, 50, true, 2, "1"},
+ [ 3] = { 0x03, 102, 0, 50, 50, true, 3, "2"},
+ [ 4] = { 0x04, 153, 0, 50, 50, true, 4, "3"},
+ [ 5] = { 0x05, 204, 0, 50, 50, true, 5, "4"},
+ [ 6] = { 0x06, 255, 0, 50, 50, true, 6, "5"},
+ [ 7] = { 0x07, 306, 0, 50, 50, true, 7, "6"},
+ [ 8] = { 0x08, 357, 0, 50, 50, true, 8, "7"},
+ [ 9] = { 0x09, 408, 0, 50, 50, true, 9, "8"},
+ [10] = { 0x0A, 459, 0, 50, 50, true, 10, "9"},
+ [11] = { 0x0B, 510, 0, 50, 50, true, 11, "0"},
+ [12] = { 0x0C, 561, 0, 50, 50, true, 12, "-"},
+ [13] = { 0x0D, 612, 0, 50, 50, true, 13, "="},
+ [14] = { 0x0E, 663, 0, 100, 50, false, 0, "back space"},
+
+ [15] = { 0x0F, 0, 52, 76, 50, false, 0, "tab"},
+ [16] = { 0x10, 77, 52, 50, 50, true, 16, "q"},
+ [17] = { 0x11, 128, 52, 50, 50, true, 17, "w"},
+ [18] = { 0x12, 179, 52, 50, 50, true, 18, "e"},
+ [19] = { 0x13, 230, 52, 50, 50, true, 19, "r"},
+ [20] = { 0x14, 281, 52, 50, 50, true, 20, "t"},
+ [21] = { 0x15, 332, 52, 50, 50, true, 21, "y"},
+ [22] = { 0x16, 383, 52, 50, 50, true, 22, "u"},
+ [23] = { 0x17, 434, 52, 50, 50, true, 23, "ı"},
+ [24] = { 0x18, 485, 52, 50, 50, true, 24, "o"},
+ [25] = { 0x19, 536, 52, 50, 50, true, 25, "p"},
+ [26] = { 0x1A, 587, 52, 50, 50, true, 26, "["},
+ [27] = { 0x1B, 638, 52, 50, 50, true, 27, "]"},
+ [28] = { 0x1C, 689, 52, 74, 50, false, 0, "enter"},
+
+ [29] = { 0x3A, 0, 104, 101, 50, false, 0, "caps lock"},
+ [30] = { 0x1E, 103, 104, 50, 50, true, 30, "a"},
+ [31] = { 0x1F, 154, 104, 50, 50, true, 31, "s"},
+ [32] = { 0x20, 205, 104, 50, 50, true, 32, "d"},
+ [33] = { 0x21, 256, 104, 50, 50, true, 33, "f"},
+ [34] = { 0x22, 307, 104, 50, 50, true, 34, "g"},
+ [35] = { 0x23, 358, 104, 50, 50, true, 35, "h"},
+ [36] = { 0x24, 409, 104, 50, 50, true, 36, "j"},
+ [37] = { 0x25, 460, 104, 50, 50, true, 37, "k"},
+ [38] = { 0x26, 511, 104, 50, 50, true, 38, "l"},
+ [39] = { 0x27, 562, 104, 50, 50, true, 39, ";"},
+ [40] = { 0x28, 614, 104, 50, 50, true, 40, "\""},
+ [41] = { 0x2B, 665, 104, 50, 50, true, 43, "\\"},
+
+ [42] = { 0x2A, 0, 156, 76, 50, false, 0, "left shift"},
+ [43] = { 0x56, 77, 156, 50, 50, true, 86, "\\"},
+ [44] = { 0x2C, 128, 156, 50, 50, true, 44, "z"},
+ [45] = { 0x2D, 179, 156, 50, 50, true, 45, "x"},
+ [46] = { 0x2E, 230, 156, 50, 50, true, 46, "c"},
+ [47] = { 0x2F, 281, 156, 50, 50, true, 47, "v"},
+ [48] = { 0x30, 332, 156, 50, 50, true, 48, "b"},
+ [49] = { 0x31, 383, 156, 50, 50, true, 49, "n"},
+ [50] = { 0x32, 434, 156, 50, 50, true, 50, "m"},
+ [51] = { 0x33, 485, 156, 50, 50, true, 51, ","},
+ [52] = { 0x34, 536, 156, 50, 50, true, 52, "."},
+ [53] = { 0x35, 587, 156, 50, 50, true, 53, "/"},
+ [54] = { 0x36, 638, 156, 125, 50, false, 0, "right shift"},
+
+ [55] = { 0x1D, 0, 208, 76, 50, false, 0, "left ctrl"},
+ [56] = {0xE05B, 77, 208, 50, 50, false, 0, "left\nsuper"},
+ [57] = { 0x38, 128, 208, 50, 50, false, 0, "alt"},
+ [58] = { 0x39, 179, 208, 356, 50, false, 0, "space"},
+ [59] = {0xE038, 536, 208, 50, 50, false, 0, "alt gr"},
+ [60] = {0xE05C, 587, 208, 50, 50, false, 0, "right\nsuper"},
+ [61] = {0xE05D, 638, 208, 50, 50, false, 0, "menu"},
+ [62] = {0xE01D, 689, 208, 74, 50, false, 0, "right ctrl"}
+ // clang-format on
+};
diff --git a/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.cpp b/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.cpp
new file mode 100644
index 0000000000..24df141a01
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.cpp
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyboardMapperWidget.h"
+#include "KeyPositions.h"
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/RadioButton.h>
+#include <LibKeyboard/CharacterMapFile.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+
+KeyboardMapperWidget::KeyboardMapperWidget()
+{
+ create_frame();
+}
+
+KeyboardMapperWidget::~KeyboardMapperWidget()
+{
+}
+
+void KeyboardMapperWidget::create_frame()
+{
+ set_fill_with_background_color(true);
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& main_widget = add<GUI::Widget>();
+ main_widget.set_relative_rect(0, 0, 200, 200);
+
+ m_keys.resize(KEY_COUNT);
+
+ for (unsigned i = 0; i < KEY_COUNT; i++) {
+ Gfx::IntRect rect = { keys[i].x, keys[i].y, keys[i].width, keys[i].height };
+
+ auto& tmp_button = main_widget.add<KeyButton>();
+ tmp_button.set_relative_rect(rect);
+ tmp_button.set_text(keys[i].name);
+ tmp_button.set_enabled(keys[i].enabled);
+
+ tmp_button.on_click = [this, &tmp_button]() {
+ String value;
+ if (GUI::InputBox::show(value, window(), "New Character:", "Select Character") == GUI::InputBox::ExecOK) {
+ int i = m_keys.find_first_index(&tmp_button).value_or(0);
+ ASSERT(i > 0);
+
+ auto index = keys[i].map_index;
+ ASSERT(index > 0);
+
+ tmp_button.set_text(value);
+ u32* map;
+
+ if (m_current_map_name == "map") {
+ map = m_character_map.map;
+ } else if (m_current_map_name == "shift_map") {
+ map = m_character_map.shift_map;
+ } else if (m_current_map_name == "alt_map") {
+ map = m_character_map.alt_map;
+ } else if (m_current_map_name == "altgr_map") {
+ map = m_character_map.altgr_map;
+ } else if (m_current_map_name == "shift_altgr_map") {
+ map = m_character_map.shift_altgr_map;
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+
+ if (value.length() == 0)
+ map[index] = '\0'; // Empty string
+ else
+ map[index] = value[0];
+
+ m_modified = true;
+ update_window_title();
+ }
+ };
+
+ m_keys.insert(i, &tmp_button);
+ }
+
+ // Action Buttons
+ auto& bottom_widget = add<GUI::Widget>();
+ bottom_widget.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_widget.set_fixed_height(40);
+
+ // Map Selection
+ m_map_group = bottom_widget.add<GUI::Widget>();
+ m_map_group->set_layout<GUI::HorizontalBoxLayout>();
+ m_map_group->set_fixed_width(250);
+
+ auto& radio_map = m_map_group->add<GUI::RadioButton>("Default");
+ radio_map.set_name("map");
+ radio_map.on_checked = [&](bool) {
+ set_current_map("map");
+ };
+ auto& radio_shift = m_map_group->add<GUI::RadioButton>("Shift");
+ radio_shift.set_name("shift_map");
+ radio_shift.on_checked = [this](bool) {
+ set_current_map("shift_map");
+ };
+ auto& radio_altgr = m_map_group->add<GUI::RadioButton>("AltGr");
+ radio_altgr.set_name("altgr_map");
+ radio_altgr.on_checked = [this](bool) {
+ set_current_map("altgr_map");
+ };
+ auto& radio_alt = m_map_group->add<GUI::RadioButton>("Alt");
+ radio_alt.set_name("alt_map");
+ radio_alt.on_checked = [this](bool) {
+ set_current_map("alt_map");
+ };
+ auto& radio_shift_altgr = m_map_group->add<GUI::RadioButton>("Shift+AltGr");
+ radio_shift_altgr.set_name("shift_altgr_map");
+ radio_shift_altgr.on_checked = [this](bool) {
+ set_current_map("shift_altgr_map");
+ };
+
+ bottom_widget.layout()->add_spacer();
+
+ auto& ok_button = bottom_widget.add<GUI::Button>();
+ ok_button.set_text("Save");
+ ok_button.set_fixed_width(80);
+ ok_button.on_click = [this](auto) {
+ save();
+ };
+}
+
+void KeyboardMapperWidget::load_from_file(String file_name)
+{
+ auto result = Keyboard::CharacterMapFile::load_from_file(file_name);
+ if (!result.has_value()) {
+ ASSERT_NOT_REACHED();
+ }
+
+ m_file_name = file_name;
+ m_character_map = result.value();
+ set_current_map("map");
+
+ for (Widget* widget : m_map_group->child_widgets()) {
+ auto radio_button = (GUI::RadioButton*)widget;
+ radio_button->set_checked(radio_button->name() == "map");
+ }
+
+ update_window_title();
+}
+
+void KeyboardMapperWidget::save()
+{
+ save_to_file(m_file_name);
+}
+
+void KeyboardMapperWidget::save_to_file(const StringView& file_name)
+{
+ JsonObject map_json;
+
+ auto add_array = [&](String name, u32* values) {
+ JsonArray items;
+ for (int i = 0; i < 90; i++) {
+ AK::StringBuilder sb;
+ sb.append(values[i]);
+
+ JsonValue val(sb.to_string());
+ items.append(move(val));
+ }
+ map_json.set(name, move(items));
+ };
+
+ add_array("map", m_character_map.map);
+ add_array("shift_map", m_character_map.shift_map);
+ add_array("alt_map", m_character_map.alt_map);
+ add_array("altgr_map", m_character_map.altgr_map);
+ add_array("shift_altgr_map", m_character_map.shift_altgr_map);
+
+ // Write to file.
+ String file_content = map_json.to_string();
+
+ auto file = Core::File::construct(file_name);
+ file->open(Core::IODevice::WriteOnly);
+ if (!file->is_open()) {
+ StringBuilder sb;
+ sb.append("Failed to open ");
+ sb.append(file_name);
+ sb.append(" for write. Error: ");
+ sb.append(file->error_string());
+
+ GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ bool result = file->write(file_content);
+ if (!result) {
+ int error_number = errno;
+ StringBuilder sb;
+ sb.append("Unable to save file. Error: ");
+ sb.append(strerror(error_number));
+
+ GUI::MessageBox::show(window(), sb.to_string(), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_modified = false;
+ m_file_name = file_name;
+ update_window_title();
+}
+
+void KeyboardMapperWidget::keydown_event(GUI::KeyEvent& event)
+{
+ for (int i = 0; i < KEY_COUNT; i++) {
+ auto& tmp_button = m_keys.at(i);
+ tmp_button->set_pressed(keys[i].scancode == event.scancode());
+ tmp_button->update();
+ }
+}
+
+void KeyboardMapperWidget::keyup_event(GUI::KeyEvent& event)
+{
+ for (int i = 0; i < KEY_COUNT; i++) {
+ if (keys[i].scancode == event.scancode()) {
+ auto& tmp_button = m_keys.at(i);
+ tmp_button->set_pressed(false);
+ tmp_button->update();
+ break;
+ }
+ }
+}
+
+void KeyboardMapperWidget::set_current_map(const String current_map)
+{
+ m_current_map_name = current_map;
+ u32* map;
+
+ if (m_current_map_name == "map") {
+ map = m_character_map.map;
+ } else if (m_current_map_name == "shift_map") {
+ map = m_character_map.shift_map;
+ } else if (m_current_map_name == "alt_map") {
+ map = m_character_map.alt_map;
+ } else if (m_current_map_name == "altgr_map") {
+ map = m_character_map.altgr_map;
+ } else if (m_current_map_name == "shift_altgr_map") {
+ map = m_character_map.shift_altgr_map;
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+
+ for (unsigned k = 0; k < KEY_COUNT; k++) {
+ auto index = keys[k].map_index;
+ if (index == 0)
+ continue;
+
+ AK::StringBuilder sb;
+ sb.append_code_point(map[index]);
+
+ m_keys.at(k)->set_text(sb.to_string());
+ }
+
+ this->update();
+}
+
+void KeyboardMapperWidget::update_window_title()
+{
+ StringBuilder sb;
+ sb.append(m_file_name);
+ if (m_modified)
+ sb.append(" (*)");
+ sb.append(" - KeyboardMapper");
+
+ window()->set_title(sb.to_string());
+}
diff --git a/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.h b/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.h
new file mode 100644
index 0000000000..e0df34ed78
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/KeyboardMapperWidget.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "KeyButton.h"
+#include <LibGUI/Button.h>
+#include <LibKeyboard/CharacterMapData.h>
+
+class KeyboardMapperWidget : public GUI::Widget {
+ C_OBJECT(KeyboardMapperWidget)
+
+public:
+ KeyboardMapperWidget();
+ virtual ~KeyboardMapperWidget() override;
+
+ void create_frame();
+ void load_from_file(const String);
+ void save();
+ void save_to_file(const StringView&);
+
+protected:
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+
+ void set_current_map(const String);
+ void update_window_title();
+
+private:
+ Vector<KeyButton*> m_keys;
+ RefPtr<GUI::Widget> m_map_group;
+
+ String m_file_name;
+ Keyboard::CharacterMapData m_character_map;
+ String m_current_map_name;
+ bool m_modified { false };
+};
diff --git a/Userland/Applications/KeyboardMapper/main.cpp b/Userland/Applications/KeyboardMapper/main.cpp
new file mode 100644
index 0000000000..0edeef72fe
--- /dev/null
+++ b/Userland/Applications/KeyboardMapper/main.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyboardMapperWidget.h"
+#include <LibCore/ArgsParser.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+
+int main(int argc, char** argv)
+{
+ const char* path = nullptr;
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(path, "Keyboard character mapping file.", "file", Core::ArgsParser::Required::No);
+ args_parser.parse(argc, argv);
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-keyboard-mapper");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Keyboard Mapper");
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->set_main_widget<KeyboardMapperWidget>();
+ window->resize(775, 315);
+ window->set_resizable(false);
+ window->show();
+
+ auto keyboard_mapper_widget = (KeyboardMapperWidget*)window->main_widget();
+ if (path != nullptr) {
+ keyboard_mapper_widget->load_from_file(path);
+ } else {
+ keyboard_mapper_widget->load_from_file("/res/keymaps/en.json");
+ }
+
+ // Actions
+ auto open_action = GUI::CommonActions::make_open_action(
+ [&](auto&) {
+ Optional<String> path = GUI::FilePicker::get_open_filepath(window, "Open");
+ if (path.has_value()) {
+ keyboard_mapper_widget->load_from_file(path.value());
+ }
+ });
+
+ auto save_action = GUI::CommonActions::make_save_action(
+ [&](auto&) {
+ keyboard_mapper_widget->save();
+ });
+
+ auto save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
+ String m_name = "Unnamed";
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, m_name, "json");
+ if (!save_path.has_value())
+ return;
+
+ keyboard_mapper_widget->save_to_file(save_path.value());
+ });
+
+ auto quit_action = GUI::CommonActions::make_quit_action(
+ [&](auto&) {
+ app->quit();
+ });
+
+ // Menu
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Keyboard Mapper");
+ app_menu.add_action(open_action);
+ app_menu.add_action(save_action);
+ app_menu.add_action(save_as_action);
+ app_menu.add_separator();
+ app_menu.add_action(quit_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Keyboard Mapper", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ return app->exec();
+}
diff --git a/Userland/Applications/KeyboardSettings/CMakeLists.txt b/Userland/Applications/KeyboardSettings/CMakeLists.txt
new file mode 100644
index 0000000000..62212cda36
--- /dev/null
+++ b/Userland/Applications/KeyboardSettings/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES
+ main.cpp
+)
+
+serenity_app(KeyboardSettings ICON app-keyboard-settings)
+target_link_libraries(KeyboardSettings LibGUI LibKeyboard)
diff --git a/Userland/Applications/KeyboardSettings/CharacterMapFileListModel.h b/Userland/Applications/KeyboardSettings/CharacterMapFileListModel.h
new file mode 100644
index 0000000000..4fcd1d8123
--- /dev/null
+++ b/Userland/Applications/KeyboardSettings/CharacterMapFileListModel.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+#include <LibGUI/Model.h>
+
+class CharacterMapFileListModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<CharacterMapFileListModel> create(Vector<String>& file_names)
+ {
+ return adopt(*new CharacterMapFileListModel(file_names));
+ }
+
+ virtual ~CharacterMapFileListModel() override { }
+
+ virtual int row_count(const GUI::ModelIndex&) const override
+ {
+ return m_file_names.size();
+ }
+
+ virtual int column_count(const GUI::ModelIndex&) const override
+ {
+ return 1;
+ }
+
+ virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
+ {
+ ASSERT(index.is_valid());
+ ASSERT(index.column() == 0);
+
+ if (role == GUI::ModelRole::Display)
+ return m_file_names.at(index.row());
+
+ return {};
+ }
+
+ virtual void update() override
+ {
+ did_update();
+ }
+
+private:
+ explicit CharacterMapFileListModel(Vector<String>& file_names)
+ : m_file_names(file_names)
+ {
+ }
+
+ Vector<String>& m_file_names;
+};
diff --git a/Userland/Applications/KeyboardSettings/main.cpp b/Userland/Applications/KeyboardSettings/main.cpp
new file mode 100644
index 0000000000..8b515d815e
--- /dev/null
+++ b/Userland/Applications/KeyboardSettings/main.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CharacterMapFileListModel.h"
+#include <AK/JsonObject.h>
+#include <AK/QuickSort.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/ComboBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/WindowServerConnection.h>
+#include <LibKeyboard/CharacterMap.h>
+#include <spawn.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio rpath accept cpath wpath shared_buffer unix fattr proc exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ // If there is no command line parameter go for GUI.
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio rpath accept shared_buffer proc exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/keymap", "x") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/proc/keymap", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr)) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-keyboard-settings");
+
+ auto proc_keymap = Core::File::construct("/proc/keymap");
+ if (!proc_keymap->open(Core::IODevice::OpenMode::ReadOnly))
+ ASSERT_NOT_REACHED();
+
+ auto json = JsonValue::from_string(proc_keymap->read_all());
+ ASSERT(json.has_value());
+ JsonObject keymap_object = json.value().as_object();
+ ASSERT(keymap_object.has("keymap"));
+ String current_keymap = keymap_object.get("keymap").to_string();
+ dbgln("KeyboardSettings thinks the current keymap is: {}", current_keymap);
+
+ Vector<String> character_map_files;
+ Core::DirIterator iterator("/res/keymaps/", Core::DirIterator::Flags::SkipDots);
+ if (iterator.has_error()) {
+ GUI::MessageBox::show(nullptr, String::formatted("Error on reading mapping file list: {}", iterator.error_string()), "Keyboard settings", GUI::MessageBox::Type::Error);
+ return -1;
+ }
+
+ while (iterator.has_next()) {
+ auto name = iterator.next_path();
+ name.replace(".json", "");
+ character_map_files.append(name);
+ }
+ quick_sort(character_map_files);
+
+ size_t initial_keymap_index = SIZE_MAX;
+ for (size_t i = 0; i < character_map_files.size(); ++i) {
+ if (character_map_files[i].equals_ignoring_case(current_keymap))
+ initial_keymap_index = i;
+ }
+ ASSERT(initial_keymap_index < character_map_files.size());
+
+ auto window = GUI::Window::construct();
+ window->set_title("Keyboard Settings");
+ window->resize(300, 70);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& root_widget = window->set_main_widget<GUI::Widget>();
+ root_widget.set_layout<GUI::VerticalBoxLayout>();
+ root_widget.set_fill_with_background_color(true);
+ root_widget.layout()->set_spacing(0);
+ root_widget.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& character_map_file_selection_container = root_widget.add<GUI::Widget>();
+ character_map_file_selection_container.set_layout<GUI::HorizontalBoxLayout>();
+ character_map_file_selection_container.set_fixed_height(22);
+
+ auto& character_map_file_label = character_map_file_selection_container.add<GUI::Label>();
+ character_map_file_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ character_map_file_label.set_fixed_width(130);
+ character_map_file_label.set_text("Character Mapping File:");
+
+ auto& character_map_file_combo = character_map_file_selection_container.add<GUI::ComboBox>();
+ character_map_file_combo.set_only_allow_values_from_model(true);
+ character_map_file_combo.set_model(*CharacterMapFileListModel::create(character_map_files));
+ character_map_file_combo.set_selected_index(initial_keymap_index);
+
+ root_widget.layout()->add_spacer();
+
+ auto apply_settings = [&](bool quit) {
+ String character_map_file = character_map_file_combo.text();
+ if (character_map_file.is_empty()) {
+ GUI::MessageBox::show(window, "Please select character mapping file.", "Keyboard settings", GUI::MessageBox::Type::Error);
+ return;
+ }
+ pid_t child_pid;
+ const char* argv[] = { "/bin/keymap", character_map_file.characters(), nullptr };
+ if ((errno = posix_spawn(&child_pid, "/bin/keymap", nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ exit(1);
+ }
+ if (quit)
+ app->quit();
+ };
+
+ auto& bottom_widget = root_widget.add<GUI::Widget>();
+ bottom_widget.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_widget.layout()->add_spacer();
+ bottom_widget.set_fixed_height(22);
+
+ auto& apply_button = bottom_widget.add<GUI::Button>();
+ apply_button.set_text("Apply");
+ apply_button.set_fixed_width(60);
+ apply_button.on_click = [&](auto) {
+ apply_settings(false);
+ };
+
+ auto& ok_button = bottom_widget.add<GUI::Button>();
+ ok_button.set_text("OK");
+ ok_button.set_fixed_width(60);
+ ok_button.on_click = [&](auto) {
+ apply_settings(true);
+ };
+
+ auto& cancel_button = bottom_widget.add<GUI::Button>();
+ cancel_button.set_text("Cancel");
+ cancel_button.set_fixed_width(60);
+ cancel_button.on_click = [&](auto) {
+ app->quit();
+ };
+
+ auto quit_action = GUI::CommonActions::make_quit_action(
+ [&](auto&) {
+ app->quit();
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Keyboard Settings");
+ app_menu.add_action(quit_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Keyboard Settings", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/MouseSettings/CMakeLists.txt b/Userland/Applications/MouseSettings/CMakeLists.txt
new file mode 100644
index 0000000000..f52df8d76c
--- /dev/null
+++ b/Userland/Applications/MouseSettings/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES
+ main.cpp
+)
+
+serenity_app(MouseSettings ICON app-mouse)
+target_link_libraries(MouseSettings LibGUI)
diff --git a/Userland/Applications/MouseSettings/main.cpp b/Userland/Applications/MouseSettings/main.cpp
new file mode 100644
index 0000000000..74b07d0b0a
--- /dev/null
+++ b/Userland/Applications/MouseSettings/main.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2020, Idan Horowitz <idan.horowitz@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Slider.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGUI/WindowServerConnection.h>
+#include <LibGfx/SystemTheme.h>
+#include <WindowServer/Screen.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio cpath rpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio cpath rpath shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-mouse");
+ auto window = GUI::Window::construct();
+ window->set_title("Mouse Settings");
+ window->resize(200, 130);
+ window->set_resizable(false);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& settings = window->set_main_widget<GUI::Widget>();
+ settings.set_fill_with_background_color(true);
+ settings.set_background_role(ColorRole::Button);
+ settings.set_layout<GUI::VerticalBoxLayout>();
+ settings.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& speed_container = settings.add<GUI::GroupBox>("Mouse speed");
+ speed_container.set_layout<GUI::VerticalBoxLayout>();
+ speed_container.layout()->set_margins({ 6, 16, 6, 6 });
+ speed_container.set_fixed_height(50);
+
+ auto& speed_slider = speed_container.add<GUI::HorizontalSlider>();
+ const auto scalar = 1000.0;
+ speed_slider.set_range(WindowServer::mouse_accel_min * scalar, WindowServer::mouse_accel_max * scalar); // These values are scaled down (by a factor of 1000) to get fractional values
+ int current_value = GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::GetMouseAcceleration>()->factor() * scalar;
+ speed_slider.set_value(current_value);
+
+ auto& scroll_container = settings.add<GUI::GroupBox>("Scroll length");
+ scroll_container.set_layout<GUI::VerticalBoxLayout>();
+ scroll_container.layout()->set_margins({ 6, 16, 6, 6 });
+ scroll_container.set_fixed_height(46);
+
+ auto& scroll_spinbox = scroll_container.add<GUI::SpinBox>();
+ scroll_spinbox.set_min(WindowServer::scroll_step_size_min);
+ scroll_spinbox.set_value(GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::GetScrollStepSize>()->step_size());
+
+ auto update_window_server = [&]() {
+ float factor = speed_slider.value() / scalar;
+ GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::SetMouseAcceleration>(factor);
+ GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::SetScrollStepSize>(scroll_spinbox.value());
+ };
+
+ auto& prompt_buttons = settings.add<GUI::Widget>();
+ prompt_buttons.set_layout<GUI::HorizontalBoxLayout>();
+ prompt_buttons.set_fixed_height(22);
+
+ auto& ok_button = prompt_buttons.add<GUI::Button>();
+ ok_button.set_text("OK");
+ prompt_buttons.set_fixed_height(22);
+ ok_button.on_click = [&](auto) {
+ update_window_server();
+ app->quit();
+ };
+ auto& apply_button = prompt_buttons.add<GUI::Button>();
+ apply_button.set_text("Apply");
+ prompt_buttons.set_fixed_height(22);
+ apply_button.on_click = [&](auto) {
+ update_window_server();
+ };
+ auto& reset_button = prompt_buttons.add<GUI::Button>();
+ reset_button.set_text("Reset");
+ prompt_buttons.set_fixed_height(22);
+ reset_button.on_click = [&](auto) {
+ speed_slider.set_value(scalar);
+ scroll_spinbox.set_value(4);
+ update_window_server();
+ };
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Mouse Settings");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
+ app->quit();
+ }));
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Mouse Settings", app_icon, window));
+ app->set_menubar(move(menubar));
+
+ window->show();
+ return app->exec();
+}
diff --git a/Userland/Applications/Piano/CMakeLists.txt b/Userland/Applications/Piano/CMakeLists.txt
new file mode 100644
index 0000000000..382e023c3b
--- /dev/null
+++ b/Userland/Applications/Piano/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES
+ Track.cpp
+ TrackManager.cpp
+ KeysWidget.cpp
+ KnobsWidget.cpp
+ main.cpp
+ MainWidget.cpp
+ RollWidget.cpp
+ SamplerWidget.cpp
+ WaveWidget.cpp
+)
+
+serenity_app(Piano ICON app-piano)
+target_link_libraries(Piano LibAudio LibGUI)
diff --git a/Userland/Applications/Piano/KeysWidget.cpp b/Userland/Applications/Piano/KeysWidget.cpp
new file mode 100644
index 0000000000..65fcdc39b5
--- /dev/null
+++ b/Userland/Applications/Piano/KeysWidget.cpp
@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeysWidget.h"
+#include "TrackManager.h"
+#include <LibGUI/Painter.h>
+
+KeysWidget::KeysWidget(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+ set_fill_with_background_color(true);
+}
+
+KeysWidget::~KeysWidget()
+{
+}
+
+int KeysWidget::mouse_note() const
+{
+ if (m_mouse_down && m_mouse_note + m_track_manager.octave_base() < note_count)
+ return m_mouse_note; // Can be -1.
+ else
+ return -1;
+}
+
+void KeysWidget::set_key(int key, Switch switch_key)
+{
+ if (key == -1 || key + m_track_manager.octave_base() >= note_count)
+ return;
+
+ if (switch_key == On) {
+ ++m_key_on[key];
+ } else {
+ if (m_key_on[key] >= 1)
+ --m_key_on[key];
+ }
+ ASSERT(m_key_on[key] <= 2);
+
+ m_track_manager.set_note_current_octave(key, switch_key);
+}
+
+bool KeysWidget::note_is_set(int note) const
+{
+ if (note < m_track_manager.octave_base())
+ return false;
+
+ if (note >= m_track_manager.octave_base() + note_count)
+ return false;
+
+ return m_key_on[note - m_track_manager.octave_base()] != 0;
+}
+
+int KeysWidget::key_code_to_key(int key_code) const
+{
+ switch (key_code) {
+ case Key_A:
+ return 0;
+ case Key_W:
+ return 1;
+ case Key_S:
+ return 2;
+ case Key_E:
+ return 3;
+ case Key_D:
+ return 4;
+ case Key_F:
+ return 5;
+ case Key_T:
+ return 6;
+ case Key_G:
+ return 7;
+ case Key_Y:
+ return 8;
+ case Key_H:
+ return 9;
+ case Key_U:
+ return 10;
+ case Key_J:
+ return 11;
+ case Key_K:
+ return 12;
+ case Key_O:
+ return 13;
+ case Key_L:
+ return 14;
+ case Key_P:
+ return 15;
+ case Key_Semicolon:
+ return 16;
+ case Key_Apostrophe:
+ return 17;
+ case Key_RightBracket:
+ return 18;
+ case Key_Return:
+ return 19;
+ default:
+ return -1;
+ }
+}
+
+constexpr int white_key_width = 24;
+constexpr int black_key_width = 16;
+constexpr int black_key_x_offset = black_key_width / 2;
+constexpr int black_key_height = 60;
+
+constexpr char white_key_labels[] = {
+ 'A',
+ 'S',
+ 'D',
+ 'F',
+ 'G',
+ 'H',
+ 'J',
+ 'K',
+ 'L',
+ ';',
+ '\'',
+ 'r',
+};
+constexpr int white_key_labels_count = sizeof(white_key_labels) / sizeof(char);
+
+constexpr char black_key_labels[] = {
+ 'W',
+ 'E',
+ 'T',
+ 'Y',
+ 'U',
+ 'O',
+ 'P',
+ ']',
+};
+constexpr int black_key_labels_count = sizeof(black_key_labels) / sizeof(char);
+
+constexpr int black_key_offsets[] = {
+ white_key_width,
+ white_key_width * 2,
+ white_key_width,
+ white_key_width,
+ white_key_width * 2,
+};
+
+constexpr int white_key_note_accumulator[] = {
+ 2,
+ 2,
+ 1,
+ 2,
+ 2,
+ 2,
+ 1,
+};
+
+constexpr int black_key_note_accumulator[] = {
+ 2,
+ 3,
+ 2,
+ 2,
+ 3,
+};
+
+void KeysWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.translate(frame_thickness(), frame_thickness());
+
+ int note = 0;
+ int x = 0;
+ int i = 0;
+ for (;;) {
+ Gfx::IntRect rect(x, 0, white_key_width, frame_inner_rect().height());
+ painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::White);
+ painter.draw_rect(rect, Color::Black);
+ if (i < white_key_labels_count) {
+ rect.set_height(rect.height() * 1.5);
+ painter.draw_text(rect, StringView(&white_key_labels[i], 1), Gfx::TextAlignment::Center, Color::Black);
+ }
+
+ note += white_key_note_accumulator[i % white_keys_per_octave];
+ x += white_key_width;
+ ++i;
+
+ if (note + m_track_manager.octave_base() >= note_count)
+ break;
+ if (x >= frame_inner_rect().width())
+ break;
+ }
+
+ note = 1;
+ x = white_key_width - black_key_x_offset;
+ i = 0;
+ for (;;) {
+ Gfx::IntRect rect(x, 0, black_key_width, black_key_height);
+ painter.fill_rect(rect, m_key_on[note] ? note_pressed_color : Color::Black);
+ painter.draw_rect(rect, Color::Black);
+ if (i < black_key_labels_count) {
+ rect.set_height(rect.height() * 1.5);
+ painter.draw_text(rect, StringView(&black_key_labels[i], 1), Gfx::TextAlignment::Center, Color::White);
+ }
+
+ note += black_key_note_accumulator[i % black_keys_per_octave];
+ x += black_key_offsets[i % black_keys_per_octave];
+ ++i;
+
+ if (note + m_track_manager.octave_base() >= note_count)
+ break;
+ if (x >= frame_inner_rect().width())
+ break;
+ }
+
+ GUI::Frame::paint_event(event);
+}
+
+constexpr int notes_per_white_key[] = {
+ 1,
+ 3,
+ 5,
+ 6,
+ 8,
+ 10,
+ 12,
+};
+
+// Keep in mind that in any of these functions a note value can be out of
+// bounds. Bounds checking is done in set_key().
+
+static inline int note_from_white_keys(int white_keys)
+{
+ int octaves = white_keys / white_keys_per_octave;
+ int remainder = white_keys % white_keys_per_octave;
+ int notes_from_octaves = octaves * notes_per_octave;
+ int notes_from_remainder = notes_per_white_key[remainder];
+ int note = (notes_from_octaves + notes_from_remainder) - 1;
+ return note;
+}
+
+int KeysWidget::note_for_event_position(const Gfx::IntPoint& a_point) const
+{
+ if (!frame_inner_rect().contains(a_point))
+ return -1;
+
+ auto point = a_point;
+ point.move_by(-frame_thickness(), -frame_thickness());
+
+ int white_keys = point.x() / white_key_width;
+ int note = note_from_white_keys(white_keys);
+
+ bool black_key_on_left = note != 0 && key_pattern[(note - 1) % notes_per_octave] == Black;
+ if (black_key_on_left) {
+ int black_key_x = (white_keys * white_key_width) - black_key_x_offset;
+ Gfx::IntRect black_key(black_key_x, 0, black_key_width, black_key_height);
+ if (black_key.contains(point))
+ return note - 1;
+ }
+
+ bool black_key_on_right = key_pattern[(note + 1) % notes_per_octave] == Black;
+ if (black_key_on_right) {
+ int black_key_x = ((white_keys + 1) * white_key_width) - black_key_x_offset;
+ Gfx::IntRect black_key(black_key_x, 0, black_key_width, black_key_height);
+ if (black_key.contains(point))
+ return note + 1;
+ }
+
+ return note;
+}
+
+void KeysWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+
+ m_mouse_down = true;
+
+ m_mouse_note = note_for_event_position(event.position());
+
+ set_key(m_mouse_note, On);
+ update();
+}
+
+void KeysWidget::mouseup_event(GUI::MouseEvent& event)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+
+ m_mouse_down = false;
+
+ set_key(m_mouse_note, Off);
+ update();
+}
+
+void KeysWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!m_mouse_down)
+ return;
+
+ int new_mouse_note = note_for_event_position(event.position());
+
+ if (m_mouse_note == new_mouse_note)
+ return;
+
+ set_key(m_mouse_note, Off);
+ set_key(new_mouse_note, On);
+ update();
+
+ m_mouse_note = new_mouse_note;
+}
diff --git a/Userland/Applications/Piano/KeysWidget.h b/Userland/Applications/Piano/KeysWidget.h
new file mode 100644
index 0000000000..d0b5a833ce
--- /dev/null
+++ b/Userland/Applications/Piano/KeysWidget.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Music.h"
+#include <LibGUI/Frame.h>
+
+class TrackManager;
+
+class KeysWidget final : public GUI::Frame {
+ C_OBJECT(KeysWidget)
+public:
+ virtual ~KeysWidget() override;
+
+ int key_code_to_key(int key_code) const;
+ int mouse_note() const;
+
+ void set_key(int key, Switch);
+ bool note_is_set(int note) const;
+
+private:
+ explicit KeysWidget(TrackManager&);
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+
+ int note_for_event_position(const Gfx::IntPoint&) const;
+
+ TrackManager& m_track_manager;
+
+ u8 m_key_on[note_count] { 0 };
+
+ bool m_mouse_down { false };
+ int m_mouse_note { -1 };
+};
diff --git a/Userland/Applications/Piano/KnobsWidget.cpp b/Userland/Applications/Piano/KnobsWidget.cpp
new file mode 100644
index 0000000000..4b9e4360c2
--- /dev/null
+++ b/Userland/Applications/Piano/KnobsWidget.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KnobsWidget.h"
+#include "MainWidget.h"
+#include "TrackManager.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Slider.h>
+
+constexpr int max_attack = 1000;
+constexpr int max_decay = 1000;
+constexpr int max_sustain = 1000;
+constexpr int max_release = 1000;
+constexpr int max_delay = 8;
+
+KnobsWidget::KnobsWidget(TrackManager& track_manager, MainWidget& main_widget)
+ : m_track_manager(track_manager)
+ , m_main_widget(main_widget)
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ set_fill_with_background_color(true);
+
+ m_labels_container = add<GUI::Widget>();
+ m_labels_container->set_layout<GUI::HorizontalBoxLayout>();
+ m_labels_container->set_fixed_height(20);
+
+ m_octave_label = m_labels_container->add<GUI::Label>("Octave");
+ m_wave_label = m_labels_container->add<GUI::Label>("Wave");
+ m_attack_label = m_labels_container->add<GUI::Label>("Attack");
+ m_decay_label = m_labels_container->add<GUI::Label>("Decay");
+ m_sustain_label = m_labels_container->add<GUI::Label>("Sustain");
+ m_release_label = m_labels_container->add<GUI::Label>("Release");
+ m_delay_label = m_labels_container->add<GUI::Label>("Delay");
+
+ m_values_container = add<GUI::Widget>();
+ m_values_container->set_layout<GUI::HorizontalBoxLayout>();
+ m_values_container->set_fixed_height(10);
+
+ m_octave_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.octave()));
+ m_wave_value = m_values_container->add<GUI::Label>(wave_strings[m_track_manager.current_track().wave()]);
+ m_attack_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().attack()));
+ m_decay_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().decay()));
+ m_sustain_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().sustain()));
+ m_release_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().release()));
+ m_delay_value = m_values_container->add<GUI::Label>(String::number(m_track_manager.current_track().delay()));
+
+ m_knobs_container = add<GUI::Widget>();
+ m_knobs_container->set_layout<GUI::HorizontalBoxLayout>();
+
+ // FIXME: Implement vertical flipping in GUI::Slider, not here.
+
+ m_octave_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_octave_knob->set_tooltip("Z: octave down, X: octave up");
+ m_octave_knob->set_range(octave_min - 1, octave_max - 1);
+ m_octave_knob->set_value((octave_max - 1) - (m_track_manager.octave() - 1));
+ m_octave_knob->on_change = [this](int value) {
+ int new_octave = octave_max - value;
+ if (m_change_underlying)
+ m_main_widget.set_octave_and_ensure_note_change(new_octave);
+ ASSERT(new_octave == m_track_manager.octave());
+ m_octave_value->set_text(String::number(new_octave));
+ };
+
+ m_wave_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_wave_knob->set_tooltip("C: cycle through waveforms");
+ m_wave_knob->set_range(0, last_wave);
+ m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave());
+ m_wave_knob->on_change = [this](int value) {
+ int new_wave = last_wave - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_wave(new_wave);
+ ASSERT(new_wave == m_track_manager.current_track().wave());
+ m_wave_value->set_text(wave_strings[new_wave]);
+ };
+
+ m_attack_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_attack_knob->set_range(0, max_attack);
+ m_attack_knob->set_value(max_attack - m_track_manager.current_track().attack());
+ m_attack_knob->set_step(100);
+ m_attack_knob->on_change = [this](int value) {
+ int new_attack = max_attack - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_attack(new_attack);
+ ASSERT(new_attack == m_track_manager.current_track().attack());
+ m_attack_value->set_text(String::number(new_attack));
+ };
+
+ m_decay_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_decay_knob->set_range(0, max_decay);
+ m_decay_knob->set_value(max_decay - m_track_manager.current_track().decay());
+ m_decay_knob->set_step(100);
+ m_decay_knob->on_change = [this](int value) {
+ int new_decay = max_decay - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_decay(new_decay);
+ ASSERT(new_decay == m_track_manager.current_track().decay());
+ m_decay_value->set_text(String::number(new_decay));
+ };
+
+ m_sustain_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_sustain_knob->set_range(0, max_sustain);
+ m_sustain_knob->set_value(max_sustain - m_track_manager.current_track().sustain());
+ m_sustain_knob->set_step(100);
+ m_sustain_knob->on_change = [this](int value) {
+ int new_sustain = max_sustain - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_sustain(new_sustain);
+ ASSERT(new_sustain == m_track_manager.current_track().sustain());
+ m_sustain_value->set_text(String::number(new_sustain));
+ };
+
+ m_release_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_release_knob->set_range(0, max_release);
+ m_release_knob->set_value(max_release - m_track_manager.current_track().release());
+ m_release_knob->set_step(100);
+ m_release_knob->on_change = [this](int value) {
+ int new_release = max_release - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_release(new_release);
+ ASSERT(new_release == m_track_manager.current_track().release());
+ m_release_value->set_text(String::number(new_release));
+ };
+
+ m_delay_knob = m_knobs_container->add<GUI::VerticalSlider>();
+ m_delay_knob->set_range(0, max_delay);
+ m_delay_knob->set_value(max_delay - m_track_manager.current_track().delay());
+ m_delay_knob->on_change = [this](int value) {
+ int new_delay = max_delay - value;
+ if (m_change_underlying)
+ m_track_manager.current_track().set_delay(new_delay);
+ ASSERT(new_delay == m_track_manager.current_track().delay());
+ m_delay_value->set_text(String::number(new_delay));
+ };
+}
+
+KnobsWidget::~KnobsWidget()
+{
+}
+
+void KnobsWidget::update_knobs()
+{
+ m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave());
+
+ // FIXME: This is needed because when the slider is changed normally, we
+ // need to change the underlying value, but if the keyboard was used, we
+ // need to change the slider without changing the underlying value.
+ m_change_underlying = false;
+
+ m_octave_knob->set_value(octave_max - m_track_manager.octave());
+ m_wave_knob->set_value(last_wave - m_track_manager.current_track().wave());
+ m_attack_knob->set_value(max_attack - m_track_manager.current_track().attack());
+ m_decay_knob->set_value(max_decay - m_track_manager.current_track().decay());
+ m_sustain_knob->set_value(max_sustain - m_track_manager.current_track().sustain());
+ m_release_knob->set_value(max_release - m_track_manager.current_track().release());
+ m_delay_knob->set_value(max_delay - m_track_manager.current_track().delay());
+
+ m_change_underlying = true;
+}
diff --git a/Userland/Applications/Piano/KnobsWidget.h b/Userland/Applications/Piano/KnobsWidget.h
new file mode 100644
index 0000000000..d5abea5be0
--- /dev/null
+++ b/Userland/Applications/Piano/KnobsWidget.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+class TrackManager;
+class MainWidget;
+
+class KnobsWidget final : public GUI::Frame {
+ C_OBJECT(KnobsWidget)
+public:
+ virtual ~KnobsWidget() override;
+
+ void update_knobs();
+
+private:
+ KnobsWidget(TrackManager&, MainWidget&);
+
+ TrackManager& m_track_manager;
+ MainWidget& m_main_widget;
+
+ RefPtr<GUI::Widget> m_labels_container;
+ RefPtr<GUI::Label> m_octave_label;
+ RefPtr<GUI::Label> m_wave_label;
+ RefPtr<GUI::Label> m_attack_label;
+ RefPtr<GUI::Label> m_decay_label;
+ RefPtr<GUI::Label> m_sustain_label;
+ RefPtr<GUI::Label> m_release_label;
+ RefPtr<GUI::Label> m_delay_label;
+
+ RefPtr<GUI::Widget> m_values_container;
+ RefPtr<GUI::Label> m_octave_value;
+ RefPtr<GUI::Label> m_wave_value;
+ RefPtr<GUI::Label> m_attack_value;
+ RefPtr<GUI::Label> m_decay_value;
+ RefPtr<GUI::Label> m_sustain_value;
+ RefPtr<GUI::Label> m_release_value;
+ RefPtr<GUI::Label> m_delay_value;
+
+ RefPtr<GUI::Widget> m_knobs_container;
+ RefPtr<GUI::Slider> m_octave_knob;
+ RefPtr<GUI::Slider> m_wave_knob;
+ RefPtr<GUI::Slider> m_attack_knob;
+ RefPtr<GUI::Slider> m_decay_knob;
+ RefPtr<GUI::Slider> m_sustain_knob;
+ RefPtr<GUI::Slider> m_release_knob;
+ RefPtr<GUI::Slider> m_delay_knob;
+
+ bool m_change_underlying { true };
+};
diff --git a/Userland/Applications/Piano/MainWidget.cpp b/Userland/Applications/Piano/MainWidget.cpp
new file mode 100644
index 0000000000..ef40a4fd1a
--- /dev/null
+++ b/Userland/Applications/Piano/MainWidget.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MainWidget.h"
+#include "KeysWidget.h"
+#include "KnobsWidget.h"
+#include "RollWidget.h"
+#include "SamplerWidget.h"
+#include "TrackManager.h"
+#include "WaveWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/TabWidget.h>
+
+MainWidget::MainWidget(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_spacing(2);
+ layout()->set_margins({ 2, 2, 2, 2 });
+ set_fill_with_background_color(true);
+
+ m_wave_widget = add<WaveWidget>(track_manager);
+ m_wave_widget->set_fixed_height(100);
+
+ m_tab_widget = add<GUI::TabWidget>();
+ m_roll_widget = m_tab_widget->add_tab<RollWidget>("Piano Roll", track_manager);
+
+ m_roll_widget->set_fixed_height(300);
+
+ m_tab_widget->add_tab<SamplerWidget>("Sampler", track_manager);
+
+ m_keys_and_knobs_container = add<GUI::Widget>();
+ m_keys_and_knobs_container->set_layout<GUI::HorizontalBoxLayout>();
+ m_keys_and_knobs_container->layout()->set_spacing(2);
+ m_keys_and_knobs_container->set_fixed_height(100);
+ m_keys_and_knobs_container->set_fill_with_background_color(true);
+
+ m_keys_widget = m_keys_and_knobs_container->add<KeysWidget>(track_manager);
+
+ m_knobs_widget = m_keys_and_knobs_container->add<KnobsWidget>(track_manager, *this);
+ m_knobs_widget->set_fixed_width(350);
+
+ m_roll_widget->set_keys_widget(m_keys_widget);
+}
+
+MainWidget::~MainWidget()
+{
+}
+
+void MainWidget::add_actions(GUI::Menu& menu)
+{
+ menu.add_action(GUI::Action::create("Add track", { Mod_Ctrl, Key_T }, [&](auto&) {
+ m_track_manager.add_track();
+ }));
+
+ menu.add_action(GUI::Action::create("Next track", { Mod_Ctrl, Key_N }, [&](auto&) {
+ turn_off_pressed_keys();
+ m_track_manager.next_track();
+ turn_on_pressed_keys();
+
+ m_knobs_widget->update_knobs();
+ }));
+}
+
+// FIXME: There are some unnecessary calls to update() throughout this program,
+// which are an easy target for optimization.
+
+void MainWidget::custom_event(Core::CustomEvent&)
+{
+ m_wave_widget->update();
+ m_roll_widget->update();
+}
+
+void MainWidget::keydown_event(GUI::KeyEvent& event)
+{
+ // This is to stop held-down keys from creating multiple events.
+ if (m_keys_pressed[event.key()])
+ return;
+ else
+ m_keys_pressed[event.key()] = true;
+
+ note_key_action(event.key(), On);
+ special_key_action(event.key());
+ m_keys_widget->update();
+}
+
+void MainWidget::keyup_event(GUI::KeyEvent& event)
+{
+ m_keys_pressed[event.key()] = false;
+
+ note_key_action(event.key(), Off);
+ m_keys_widget->update();
+}
+
+void MainWidget::note_key_action(int key_code, Switch switch_note)
+{
+ int key = m_keys_widget->key_code_to_key(key_code);
+ m_keys_widget->set_key(key, switch_note);
+}
+
+void MainWidget::special_key_action(int key_code)
+{
+ switch (key_code) {
+ case Key_Z:
+ set_octave_and_ensure_note_change(Down);
+ break;
+ case Key_X:
+ set_octave_and_ensure_note_change(Up);
+ break;
+ case Key_C:
+ m_track_manager.current_track().set_wave(Up);
+ m_knobs_widget->update_knobs();
+ break;
+ }
+}
+
+void MainWidget::turn_off_pressed_keys()
+{
+ m_keys_widget->set_key(m_keys_widget->mouse_note(), Off);
+ for (int i = 0; i < key_code_count; ++i) {
+ if (m_keys_pressed[i])
+ note_key_action(i, Off);
+ }
+}
+
+void MainWidget::turn_on_pressed_keys()
+{
+ m_keys_widget->set_key(m_keys_widget->mouse_note(), On);
+ for (int i = 0; i < key_code_count; ++i) {
+ if (m_keys_pressed[i])
+ note_key_action(i, On);
+ }
+}
+
+void MainWidget::set_octave_and_ensure_note_change(int octave)
+{
+ turn_off_pressed_keys();
+ m_track_manager.set_octave(octave);
+ turn_on_pressed_keys();
+
+ m_knobs_widget->update_knobs();
+ m_keys_widget->update();
+}
+
+void MainWidget::set_octave_and_ensure_note_change(Direction direction)
+{
+ turn_off_pressed_keys();
+ m_track_manager.set_octave(direction);
+ turn_on_pressed_keys();
+
+ m_knobs_widget->update_knobs();
+ m_keys_widget->update();
+}
diff --git a/Userland/Applications/Piano/MainWidget.h b/Userland/Applications/Piano/MainWidget.h
new file mode 100644
index 0000000000..a2a6a35a0c
--- /dev/null
+++ b/Userland/Applications/Piano/MainWidget.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Music.h"
+#include <LibGUI/Widget.h>
+
+class TrackManager;
+class WaveWidget;
+class RollWidget;
+class SamplerWidget;
+class KeysWidget;
+class KnobsWidget;
+
+class MainWidget final : public GUI::Widget {
+ C_OBJECT(MainWidget)
+public:
+ virtual ~MainWidget() override;
+
+ void add_actions(GUI::Menu&);
+
+ void set_octave_and_ensure_note_change(Direction);
+ void set_octave_and_ensure_note_change(int);
+
+private:
+ explicit MainWidget(TrackManager&);
+
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+ virtual void custom_event(Core::CustomEvent&) override;
+
+ void note_key_action(int key_code, Switch);
+ void special_key_action(int key_code);
+
+ void turn_off_pressed_keys();
+ void turn_on_pressed_keys();
+
+ TrackManager& m_track_manager;
+
+ RefPtr<WaveWidget> m_wave_widget;
+ RefPtr<RollWidget> m_roll_widget;
+ RefPtr<SamplerWidget> m_sampler_widget;
+ RefPtr<GUI::TabWidget> m_tab_widget;
+ RefPtr<GUI::Widget> m_keys_and_knobs_container;
+ RefPtr<KeysWidget> m_keys_widget;
+ RefPtr<KnobsWidget> m_knobs_widget;
+
+ bool m_keys_pressed[key_code_count] { false };
+};
diff --git a/Userland/Applications/Piano/Music.h b/Userland/Applications/Piano/Music.h
new file mode 100644
index 0000000000..3a150944ec
--- /dev/null
+++ b/Userland/Applications/Piano/Music.h
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Types.h>
+#include <LibGfx/Color.h>
+
+namespace Music {
+
+// CD quality
+// - Stereo
+// - 16 bit
+// - 44,100 samples/sec
+// - 1,411.2 kbps
+
+struct Sample {
+ i16 left;
+ i16 right;
+};
+
+constexpr int sample_count = 1024;
+
+constexpr int buffer_size = sample_count * sizeof(Sample);
+
+constexpr double sample_rate = 44100;
+
+constexpr double volume = 1800;
+
+enum Switch {
+ Off,
+ On,
+};
+
+struct RollNote {
+ u32 length() const { return (off_sample - on_sample) + 1; }
+
+ u32 on_sample;
+ u32 off_sample;
+};
+
+enum Direction {
+ Down,
+ Up,
+};
+
+enum Wave {
+ Sine,
+ Triangle,
+ Square,
+ Saw,
+ Noise,
+ RecordedSample,
+};
+
+constexpr const char* wave_strings[] = {
+ "Sine",
+ "Triangle",
+ "Square",
+ "Saw",
+ "Noise",
+ "Sample",
+};
+
+constexpr int first_wave = Sine;
+constexpr int last_wave = RecordedSample;
+
+enum Envelope {
+ Done,
+ Attack,
+ Decay,
+ Release,
+};
+
+enum KeyColor {
+ White,
+ Black,
+};
+
+constexpr KeyColor key_pattern[] = {
+ White,
+ Black,
+ White,
+ Black,
+ White,
+ White,
+ Black,
+ White,
+ Black,
+ White,
+ Black,
+ White,
+};
+
+const Color note_pressed_color(64, 64, 255);
+const Color column_playing_color(128, 128, 255);
+
+const Color left_wave_colors[] = {
+ // Sine
+ {
+ 255,
+ 192,
+ 0,
+ },
+ // Triangle
+ {
+ 35,
+ 171,
+ 35,
+ },
+ // Square
+ {
+ 128,
+ 160,
+ 255,
+ },
+ // Saw
+ {
+ 240,
+ 100,
+ 128,
+ },
+ // Noise
+ {
+ 197,
+ 214,
+ 225,
+ },
+ // RecordedSample
+ {
+ 227,
+ 39,
+ 39,
+ },
+};
+
+const Color right_wave_colors[] = {
+ // Sine
+ {
+ 255,
+ 223,
+ 0,
+ },
+ // Triangle
+ {
+ 35,
+ 171,
+ 90,
+ },
+ // Square
+ {
+ 139,
+ 128,
+ 255,
+ },
+ // Saw
+ {
+ 240,
+ 100,
+ 220,
+ },
+ // Noise
+ {
+ 197,
+ 223,
+ 225,
+ },
+ // RecordedSample
+ {
+ 227,
+ 105,
+ 39,
+ },
+};
+
+constexpr int notes_per_octave = 12;
+constexpr int white_keys_per_octave = 7;
+constexpr int black_keys_per_octave = 5;
+constexpr int octave_min = 1;
+constexpr int octave_max = 7;
+
+constexpr double beats_per_minute = 60;
+constexpr int beats_per_bar = 4;
+constexpr int notes_per_beat = 4;
+constexpr int roll_length = (sample_rate / (beats_per_minute / 60)) * beats_per_bar;
+
+constexpr const char* note_names[] = {
+ "C",
+ "C#",
+ "D",
+ "D#",
+ "E",
+ "F",
+ "F#",
+ "G",
+ "G#",
+ "A",
+ "A#",
+ "B",
+};
+
+// Equal temperament, A = 440Hz
+// We calculate note frequencies relative to A4:
+// 440.0 * pow(pow(2.0, 1.0 / 12.0), N)
+// Where N is the note distance from A.
+constexpr double note_frequencies[] = {
+ // Octave 1
+ 32.703195662574764,
+ 34.647828872108946,
+ 36.708095989675876,
+ 38.890872965260044,
+ 41.203444614108669,
+ 43.653528929125407,
+ 46.249302838954222,
+ 48.99942949771858,
+ 51.913087197493056,
+ 54.999999999999915,
+ 58.270470189761156,
+ 61.735412657015416,
+ // Octave 2
+ 65.406391325149571,
+ 69.295657744217934,
+ 73.416191979351794,
+ 77.781745930520117,
+ 82.406889228217381,
+ 87.307057858250872,
+ 92.4986056779085,
+ 97.998858995437217,
+ 103.82617439498618,
+ 109.99999999999989,
+ 116.54094037952237,
+ 123.4708253140309,
+ // Octave 3
+ 130.8127826502992,
+ 138.59131548843592,
+ 146.83238395870364,
+ 155.56349186104035,
+ 164.81377845643485,
+ 174.61411571650183,
+ 184.99721135581709,
+ 195.99771799087452,
+ 207.65234878997245,
+ 219.99999999999989,
+ 233.08188075904488,
+ 246.94165062806198,
+ // Octave 4
+ 261.62556530059851,
+ 277.18263097687202,
+ 293.66476791740746,
+ 311.12698372208081,
+ 329.62755691286986,
+ 349.22823143300383,
+ 369.99442271163434,
+ 391.99543598174927,
+ 415.30469757994513,
+ 440,
+ 466.16376151808993,
+ 493.88330125612413,
+ // Octave 5
+ 523.25113060119736,
+ 554.36526195374427,
+ 587.32953583481526,
+ 622.25396744416196,
+ 659.25511382574007,
+ 698.456462866008,
+ 739.98884542326903,
+ 783.99087196349899,
+ 830.60939515989071,
+ 880.00000000000034,
+ 932.32752303618031,
+ 987.76660251224882,
+ // Octave 6
+ 1046.5022612023952,
+ 1108.7305239074892,
+ 1174.659071669631,
+ 1244.5079348883246,
+ 1318.5102276514808,
+ 1396.9129257320169,
+ 1479.977690846539,
+ 1567.9817439269987,
+ 1661.2187903197821,
+ 1760.000000000002,
+ 1864.6550460723618,
+ 1975.5332050244986,
+ // Octave 7
+ 2093.0045224047913,
+ 2217.4610478149793,
+ 2349.3181433392633,
+ 2489.0158697766506,
+ 2637.020455302963,
+ 2793.8258514640347,
+ 2959.9553816930793,
+ 3135.9634878539991,
+ 3322.437580639566,
+ 3520.0000000000055,
+ 3729.3100921447249,
+ 3951.0664100489994,
+};
+constexpr int note_count = sizeof(note_frequencies) / sizeof(double);
+
+constexpr double middle_c = note_frequencies[36];
+
+}
+
+using namespace Music;
diff --git a/Userland/Applications/Piano/RollWidget.cpp b/Userland/Applications/Piano/RollWidget.cpp
new file mode 100644
index 0000000000..2677fed384
--- /dev/null
+++ b/Userland/Applications/Piano/RollWidget.cpp
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "RollWidget.h"
+#include "TrackManager.h"
+#include <LibGUI/Painter.h>
+#include <LibGUI/ScrollBar.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <math.h>
+
+constexpr int note_height = 20;
+constexpr int max_note_width = note_height * 2;
+constexpr int roll_height = note_count * note_height;
+constexpr int horizontal_scroll_sensitivity = 20;
+constexpr int max_zoom = 1 << 8;
+
+RollWidget::RollWidget(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+ set_should_hide_unnecessary_scrollbars(true);
+ set_content_size({ 0, roll_height });
+ vertical_scrollbar().set_value(roll_height / 2);
+}
+
+RollWidget::~RollWidget()
+{
+}
+
+void RollWidget::paint_event(GUI::PaintEvent& event)
+{
+ m_roll_width = widget_inner_rect().width() * m_zoom_level;
+ set_content_size({ m_roll_width, roll_height });
+
+ // Divide the roll by the maximum note width. If we get fewer notes than
+ // our time signature requires, round up. Otherwise, round down to the
+ // nearest x*(2^y), where x is the base number of notes of our time
+ // signature. In other words, find a number that is a double of our time
+ // signature. For 4/4 that would be 16, 32, 64, 128 ...
+ m_num_notes = m_roll_width / max_note_width;
+ int time_signature_notes = beats_per_bar * notes_per_beat;
+ if (m_num_notes < time_signature_notes)
+ m_num_notes = time_signature_notes;
+ else
+ m_num_notes = time_signature_notes * pow(2, static_cast<int>(log2(m_num_notes / time_signature_notes)));
+ m_note_width = static_cast<double>(m_roll_width) / m_num_notes;
+
+ // This calculates the minimum number of rows needed. We account for a
+ // partial row at the top and/or bottom.
+ int y_offset = vertical_scrollbar().value();
+ int note_offset = y_offset / note_height;
+ int note_offset_remainder = y_offset % note_height;
+ int paint_area = widget_inner_rect().height() + note_offset_remainder;
+ if (paint_area % note_height != 0)
+ paint_area += note_height;
+ int notes_to_paint = paint_area / note_height;
+ int key_pattern_index = (notes_per_octave - 1) - (note_offset % notes_per_octave);
+
+ int x_offset = horizontal_scrollbar().value();
+ int horizontal_note_offset_remainder = fmod(x_offset, m_note_width);
+ int horizontal_paint_area = widget_inner_rect().width() + horizontal_note_offset_remainder;
+ if (fmod(horizontal_paint_area, m_note_width) != 0)
+ horizontal_paint_area += m_note_width;
+ int horizontal_notes_to_paint = horizontal_paint_area / m_note_width;
+
+ GUI::Painter painter(*this);
+ painter.translate(frame_thickness(), frame_thickness());
+ painter.add_clip_rect(event.rect());
+ painter.translate(-horizontal_note_offset_remainder, -note_offset_remainder);
+
+ for (int y = 0; y < notes_to_paint; ++y) {
+ int y_pos = y * note_height;
+
+ int note = (note_count - note_offset - 1) - y;
+ for (int x = 0; x < horizontal_notes_to_paint; ++x) {
+ // This is needed to avoid rounding errors. You can't just use
+ // m_note_width as the width.
+ int x_pos = x * m_note_width;
+ int next_x_pos = (x + 1) * m_note_width;
+ int distance_to_next_x = next_x_pos - x_pos;
+ Gfx::IntRect rect(x_pos, y_pos, distance_to_next_x, note_height);
+
+ if (key_pattern[key_pattern_index] == Black)
+ painter.fill_rect(rect, Color::LightGray);
+ else
+ painter.fill_rect(rect, Color::White);
+
+ if (keys_widget() && keys_widget()->note_is_set(note))
+ painter.fill_rect(rect, note_pressed_color.with_alpha(128));
+
+ painter.draw_line(rect.top_right(), rect.bottom_right(), Color::Black);
+ painter.draw_line(rect.bottom_left(), rect.bottom_right(), Color::Black);
+ }
+
+ if (--key_pattern_index == -1)
+ key_pattern_index = notes_per_octave - 1;
+ }
+
+ painter.translate(-x_offset, -y_offset);
+ painter.translate(horizontal_note_offset_remainder, note_offset_remainder);
+
+ for (int note = note_count - (note_offset + notes_to_paint); note <= (note_count - 1) - note_offset; ++note) {
+ int y = ((note_count - 1) - note) * note_height;
+ for (auto roll_note : m_track_manager.current_track().roll_notes(note)) {
+ int x = m_roll_width * (static_cast<double>(roll_note.on_sample) / roll_length);
+ int width = m_roll_width * (static_cast<double>(roll_note.length()) / roll_length);
+ if (x + width < x_offset || x > x_offset + widget_inner_rect().width())
+ continue;
+ if (width < 2)
+ width = 2;
+
+ int height = note_height;
+
+ Gfx::IntRect rect(x, y, width, height);
+ painter.fill_rect(rect, note_pressed_color);
+ painter.draw_rect(rect, Color::Black);
+ }
+ Gfx::IntRect note_name_rect(3, y, 1, note_height);
+ const char* note_name = note_names[note % notes_per_octave];
+
+ painter.draw_text(note_name_rect, note_name, Gfx::TextAlignment::CenterLeft);
+ note_name_rect.move_by(Gfx::FontDatabase::default_font().width(note_name) + 2, 0);
+ if (note % notes_per_octave == 0)
+ painter.draw_text(note_name_rect, String::formatted("{}", note / notes_per_octave + 1), Gfx::TextAlignment::CenterLeft);
+ }
+
+ int x = m_roll_width * (static_cast<double>(m_track_manager.time()) / roll_length);
+ if (x > x_offset && x <= x_offset + widget_inner_rect().width())
+ painter.draw_line({ x, 0 }, { x, roll_height }, Gfx::Color::Black);
+
+ GUI::Frame::paint_event(event);
+}
+
+void RollWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ if (!widget_inner_rect().contains(event.x(), event.y()))
+ return;
+
+ m_note_drag_start = event.position();
+
+ int y = (m_note_drag_start.value().y() + vertical_scrollbar().value()) - frame_thickness();
+ y /= note_height;
+ m_drag_note = (note_count - 1) - y;
+
+ mousemove_event(event);
+}
+
+void RollWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!m_note_drag_start.has_value())
+ return;
+
+ if (m_note_drag_location.has_value()) {
+ // Clear previous note
+ m_track_manager.current_track().set_roll_note(m_drag_note, m_note_drag_location.value().on_sample, m_note_drag_location.value().off_sample);
+ }
+
+ auto get_note_x = [&](int x0) {
+ // There's a case where we can't just use x / m_note_width. For example, if
+ // your m_note_width is 3.1 you will have a rect starting at 3. When that
+ // leftmost pixel of the rect is clicked you will do 3 / 3.1 which is 0
+ // and not 1. We can avoid that case by shifting x by 1 if m_note_width is
+ // fractional, being careful not to shift out of bounds.
+ int x = (x0 + horizontal_scrollbar().value()) - frame_thickness();
+ bool note_width_is_fractional = m_note_width - static_cast<int>(m_note_width) != 0;
+ bool x_is_not_last = x != widget_inner_rect().width() - 1;
+ if (note_width_is_fractional && x_is_not_last)
+ ++x;
+ x /= m_note_width;
+ return x;
+ };
+
+ int x0 = get_note_x(m_note_drag_start.value().x());
+ int x1 = get_note_x(event.x());
+
+ u32 on_sample = roll_length * (static_cast<double>(min(x0, x1)) / m_num_notes);
+ u32 off_sample = (roll_length * (static_cast<double>(max(x0, x1) + 1) / m_num_notes)) - 1;
+ m_track_manager.current_track().set_roll_note(m_drag_note, on_sample, off_sample);
+ m_note_drag_location = RollNote({ on_sample, off_sample });
+
+ update();
+}
+
+void RollWidget::mouseup_event([[maybe_unused]] GUI::MouseEvent& event)
+{
+ m_note_drag_start = {};
+ m_note_drag_location = {};
+}
+
+// FIXME: Implement zoom and horizontal scroll events in LibGUI, not here.
+void RollWidget::mousewheel_event(GUI::MouseEvent& event)
+{
+ if (event.modifiers() & KeyModifier::Mod_Shift) {
+ horizontal_scrollbar().set_value(horizontal_scrollbar().value() + (event.wheel_delta() * horizontal_scroll_sensitivity));
+ return;
+ }
+
+ if (!(event.modifiers() & KeyModifier::Mod_Ctrl)) {
+ GUI::ScrollableWidget::mousewheel_event(event);
+ return;
+ }
+
+ double multiplier = event.wheel_delta() >= 0 ? 0.5 : 2;
+
+ if (m_zoom_level * multiplier > max_zoom)
+ return;
+
+ if (m_zoom_level * multiplier < 1) {
+ if (m_zoom_level == 1)
+ return;
+ m_zoom_level = 1;
+ } else {
+ m_zoom_level *= multiplier;
+ }
+
+ int absolute_x_of_pixel_at_cursor = horizontal_scrollbar().value() + event.position().x();
+ int absolute_x_of_pixel_at_cursor_after_resize = absolute_x_of_pixel_at_cursor * multiplier;
+ int new_scrollbar = absolute_x_of_pixel_at_cursor_after_resize - event.position().x();
+
+ m_roll_width = widget_inner_rect().width() * m_zoom_level;
+ set_content_size({ m_roll_width, roll_height });
+
+ horizontal_scrollbar().set_value(new_scrollbar);
+}
diff --git a/Userland/Applications/Piano/RollWidget.h b/Userland/Applications/Piano/RollWidget.h
new file mode 100644
index 0000000000..bbfcc8bb62
--- /dev/null
+++ b/Userland/Applications/Piano/RollWidget.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "KeysWidget.h"
+#include "Music.h"
+#include <LibGUI/ScrollableWidget.h>
+
+class TrackManager;
+
+class RollWidget final : public GUI::ScrollableWidget {
+ C_OBJECT(RollWidget)
+public:
+ virtual ~RollWidget() override;
+
+ const KeysWidget* keys_widget() const { return m_keys_widget; }
+ void set_keys_widget(const KeysWidget* widget) { m_keys_widget = widget; }
+
+private:
+ explicit RollWidget(TrackManager&);
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent& event) override;
+ virtual void mousemove_event(GUI::MouseEvent& event) override;
+ virtual void mouseup_event(GUI::MouseEvent& event) override;
+ virtual void mousewheel_event(GUI::MouseEvent&) override;
+
+ TrackManager& m_track_manager;
+ const KeysWidget* m_keys_widget;
+
+ int m_roll_width { 0 };
+ int m_num_notes { 0 };
+ double m_note_width { 0.0 };
+ int m_zoom_level { 1 };
+
+ Optional<Gfx::IntPoint> m_note_drag_start;
+ Optional<RollNote> m_note_drag_location;
+ int m_drag_note;
+};
diff --git a/Userland/Applications/Piano/SamplerWidget.cpp b/Userland/Applications/Piano/SamplerWidget.cpp
new file mode 100644
index 0000000000..72704f28c7
--- /dev/null
+++ b/Userland/Applications/Piano/SamplerWidget.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SamplerWidget.h"
+#include "TrackManager.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+
+WaveEditor::WaveEditor(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+}
+
+WaveEditor::~WaveEditor()
+{
+}
+
+int WaveEditor::sample_to_y(double percentage) const
+{
+ double portion_of_half_height = percentage * ((frame_inner_rect().height() - 1) / 2.0);
+ double y = (frame_inner_rect().height() / 2.0) + portion_of_half_height;
+ return y;
+}
+
+void WaveEditor::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.fill_rect(frame_inner_rect(), Color::Black);
+
+ auto recorded_sample = m_track_manager.current_track().recorded_sample();
+ if (recorded_sample.is_empty())
+ return;
+
+ double width_scale = static_cast<double>(frame_inner_rect().width()) / recorded_sample.size();
+
+ painter.translate(frame_thickness(), frame_thickness());
+
+ int prev_x = 0;
+ int left_prev_y = sample_to_y(recorded_sample[0].left);
+ int right_prev_y = sample_to_y(recorded_sample[0].right);
+ painter.set_pixel({ prev_x, left_prev_y }, left_wave_colors[RecordedSample]);
+ painter.set_pixel({ prev_x, right_prev_y }, right_wave_colors[RecordedSample]);
+
+ for (size_t x = 1; x < recorded_sample.size(); ++x) {
+ int left_y = sample_to_y(recorded_sample[x].left);
+ int right_y = sample_to_y(recorded_sample[x].right);
+
+ Gfx::IntPoint left_point1(prev_x * width_scale, left_prev_y);
+ Gfx::IntPoint left_point2(x * width_scale, left_y);
+ painter.draw_line(left_point1, left_point2, left_wave_colors[RecordedSample]);
+
+ Gfx::IntPoint right_point1(prev_x * width_scale, right_prev_y);
+ Gfx::IntPoint right_point2(x * width_scale, right_y);
+ painter.draw_line(right_point1, right_point2, right_wave_colors[RecordedSample]);
+
+ prev_x = x;
+ left_prev_y = left_y;
+ right_prev_y = right_y;
+ }
+}
+
+SamplerWidget::SamplerWidget(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 10, 10, 10, 10 });
+ layout()->set_spacing(10);
+ set_fill_with_background_color(true);
+
+ m_open_button_and_recorded_sample_name_container = add<GUI::Widget>();
+ m_open_button_and_recorded_sample_name_container->set_layout<GUI::HorizontalBoxLayout>();
+ m_open_button_and_recorded_sample_name_container->layout()->set_spacing(10);
+ m_open_button_and_recorded_sample_name_container->set_fixed_height(24);
+
+ m_open_button = m_open_button_and_recorded_sample_name_container->add<GUI::Button>();
+ m_open_button->set_fixed_size(24, 24);
+ m_open_button->set_focus_policy(GUI::FocusPolicy::TabFocus);
+ m_open_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"));
+ m_open_button->on_click = [this](auto) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window());
+ if (!open_path.has_value())
+ return;
+ String error_string = m_track_manager.current_track().set_recorded_sample(open_path.value());
+ if (!error_string.is_empty()) {
+ GUI::MessageBox::show(window(), String::formatted("Failed to load WAV file: {}", error_string.characters()), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+ m_recorded_sample_name->set_text(open_path.value());
+ m_wave_editor->update();
+ };
+
+ m_recorded_sample_name = m_open_button_and_recorded_sample_name_container->add<GUI::Label>("No sample loaded");
+ m_recorded_sample_name->set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ m_wave_editor = add<WaveEditor>(m_track_manager);
+ m_wave_editor->set_fixed_height(100);
+}
+
+SamplerWidget::~SamplerWidget()
+{
+}
diff --git a/Userland/Applications/Piano/SamplerWidget.h b/Userland/Applications/Piano/SamplerWidget.h
new file mode 100644
index 0000000000..b99dbcbe02
--- /dev/null
+++ b/Userland/Applications/Piano/SamplerWidget.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+class TrackManager;
+
+class WaveEditor final : public GUI::Frame {
+ C_OBJECT(WaveEditor)
+public:
+ virtual ~WaveEditor() override;
+
+private:
+ explicit WaveEditor(TrackManager&);
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ int sample_to_y(double percentage) const;
+
+ TrackManager& m_track_manager;
+};
+
+class SamplerWidget final : public GUI::Frame {
+ C_OBJECT(SamplerWidget)
+public:
+ virtual ~SamplerWidget() override;
+
+private:
+ explicit SamplerWidget(TrackManager&);
+
+ TrackManager& m_track_manager;
+
+ RefPtr<GUI::Widget> m_open_button_and_recorded_sample_name_container;
+ RefPtr<GUI::Button> m_open_button;
+ RefPtr<GUI::Label> m_recorded_sample_name;
+ RefPtr<WaveEditor> m_wave_editor;
+};
diff --git a/Userland/Applications/Piano/Track.cpp b/Userland/Applications/Piano/Track.cpp
new file mode 100644
index 0000000000..61d4351d06
--- /dev/null
+++ b/Userland/Applications/Piano/Track.cpp
@@ -0,0 +1,369 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Track.h"
+#include <AK/NumericLimits.h>
+#include <LibAudio/Loader.h>
+#include <math.h>
+
+Track::Track(const u32& time)
+ : m_time(time)
+{
+ set_sustain_impl(1000);
+ set_attack(5);
+ set_decay(1000);
+ set_release(5);
+}
+
+Track::~Track()
+{
+}
+
+void Track::fill_sample(Sample& sample)
+{
+ Audio::Sample new_sample;
+
+ for (size_t note = 0; note < note_count; ++note) {
+ if (!m_roll_iters[note].is_end()) {
+ if (m_roll_iters[note]->on_sample == m_time) {
+ set_note(note, On);
+ } else if (m_roll_iters[note]->off_sample == m_time) {
+ set_note(note, Off);
+ ++m_roll_iters[note];
+ if (m_roll_iters[note].is_end())
+ m_roll_iters[note] = m_roll_notes[note].begin();
+ }
+ }
+
+ switch (m_envelope[note]) {
+ case Done:
+ continue;
+ case Attack:
+ m_power[note] += m_attack_step;
+ if (m_power[note] >= 1) {
+ m_power[note] = 1;
+ m_envelope[note] = Decay;
+ }
+ break;
+ case Decay:
+ m_power[note] -= m_decay_step;
+ if (m_power[note] < m_sustain_level)
+ m_power[note] = m_sustain_level;
+ break;
+ case Release:
+ m_power[note] -= m_release_step[note];
+ if (m_power[note] <= 0) {
+ m_power[note] = 0;
+ m_envelope[note] = Done;
+ continue;
+ }
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ Audio::Sample note_sample;
+ switch (m_wave) {
+ case Wave::Sine:
+ note_sample = sine(note);
+ break;
+ case Wave::Saw:
+ note_sample = saw(note);
+ break;
+ case Wave::Square:
+ note_sample = square(note);
+ break;
+ case Wave::Triangle:
+ note_sample = triangle(note);
+ break;
+ case Wave::Noise:
+ note_sample = noise();
+ break;
+ case Wave::RecordedSample:
+ note_sample = recorded_sample(note);
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ new_sample.left += note_sample.left * m_power[note] * volume;
+ new_sample.right += note_sample.right * m_power[note] * volume;
+ }
+
+ if (m_delay) {
+ new_sample.left += m_delay_buffer[m_delay_index].left * 0.333333;
+ new_sample.right += m_delay_buffer[m_delay_index].right * 0.333333;
+ m_delay_buffer[m_delay_index].left = new_sample.left;
+ m_delay_buffer[m_delay_index].right = new_sample.right;
+ if (++m_delay_index >= m_delay_samples)
+ m_delay_index = 0;
+ }
+
+ sample.left += new_sample.left;
+ sample.right += new_sample.right;
+}
+
+void Track::reset()
+{
+ memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample));
+ m_delay_index = 0;
+
+ memset(m_note_on, 0, sizeof(m_note_on));
+ memset(m_power, 0, sizeof(m_power));
+ memset(m_envelope, 0, sizeof(m_envelope));
+}
+
+String Track::set_recorded_sample(const StringView& path)
+{
+ NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
+ if (loader->has_error())
+ return String(loader->error_string());
+ auto buffer = loader->get_more_samples(60 * sample_rate * sizeof(Sample)); // 1 minute maximum
+
+ if (!m_recorded_sample.is_empty())
+ m_recorded_sample.clear();
+ m_recorded_sample.resize(buffer->sample_count());
+
+ double peak = 0;
+ for (int i = 0; i < buffer->sample_count(); ++i) {
+ double left_abs = fabs(buffer->samples()[i].left);
+ double right_abs = fabs(buffer->samples()[i].right);
+ if (left_abs > peak)
+ peak = left_abs;
+ if (right_abs > peak)
+ peak = right_abs;
+ }
+
+ if (peak) {
+ for (int i = 0; i < buffer->sample_count(); ++i) {
+ m_recorded_sample[i].left = buffer->samples()[i].left / peak;
+ m_recorded_sample[i].right = buffer->samples()[i].right / peak;
+ }
+ }
+
+ return String::empty();
+}
+
+// All of the information for these waves is on Wikipedia.
+
+Audio::Sample Track::sine(size_t note)
+{
+ double pos = note_frequencies[note] / sample_rate;
+ double sin_step = pos * 2 * M_PI;
+ double w = sin(m_pos[note]);
+ m_pos[note] += sin_step;
+ return w;
+}
+
+Audio::Sample Track::saw(size_t note)
+{
+ double saw_step = note_frequencies[note] / sample_rate;
+ double t = m_pos[note];
+ double w = (0.5 - (t - floor(t))) * 2;
+ m_pos[note] += saw_step;
+ return w;
+}
+
+Audio::Sample Track::square(size_t note)
+{
+ double pos = note_frequencies[note] / sample_rate;
+ double square_step = pos * 2 * M_PI;
+ double w = sin(m_pos[note]) >= 0 ? 1 : -1;
+ m_pos[note] += square_step;
+ return w;
+}
+
+Audio::Sample Track::triangle(size_t note)
+{
+ double triangle_step = note_frequencies[note] / sample_rate;
+ double t = m_pos[note];
+ double w = fabs(fmod((4 * t) + 1, 4) - 2) - 1;
+ m_pos[note] += triangle_step;
+ return w;
+}
+
+Audio::Sample Track::noise() const
+{
+ double random_percentage = static_cast<double>(rand()) / RAND_MAX;
+ double w = (random_percentage * 2) - 1;
+ return w;
+}
+
+Audio::Sample Track::recorded_sample(size_t note)
+{
+ int t = m_pos[note];
+ if (t >= static_cast<int>(m_recorded_sample.size()))
+ return 0;
+ double w_left = m_recorded_sample[t].left;
+ double w_right = m_recorded_sample[t].right;
+ if (t + 1 < static_cast<int>(m_recorded_sample.size())) {
+ double t_fraction = m_pos[note] - t;
+ w_left += (m_recorded_sample[t + 1].left - m_recorded_sample[t].left) * t_fraction;
+ w_right += (m_recorded_sample[t + 1].right - m_recorded_sample[t].right) * t_fraction;
+ }
+ double recorded_sample_step = note_frequencies[note] / middle_c;
+ m_pos[note] += recorded_sample_step;
+ return { w_left, w_right };
+}
+
+static inline double calculate_step(double distance, int milliseconds)
+{
+ if (milliseconds == 0)
+ return distance;
+
+ constexpr double samples_per_millisecond = sample_rate / 1000.0;
+ double samples = milliseconds * samples_per_millisecond;
+ double step = distance / samples;
+ return step;
+}
+
+void Track::set_note(int note, Switch switch_note)
+{
+ ASSERT(note >= 0 && note < note_count);
+
+ if (switch_note == On) {
+ if (m_note_on[note] == 0) {
+ m_pos[note] = 0;
+ m_envelope[note] = Attack;
+ }
+ ++m_note_on[note];
+ } else {
+ if (m_note_on[note] >= 1) {
+ if (m_note_on[note] == 1) {
+ m_release_step[note] = calculate_step(m_power[note], m_release);
+ m_envelope[note] = Release;
+ }
+ --m_note_on[note];
+ }
+ }
+
+ ASSERT(m_note_on[note] != NumericLimits<u8>::max());
+ ASSERT(m_power[note] >= 0);
+}
+
+void Track::sync_roll(int note)
+{
+ auto it = m_roll_notes[note].find_if([&](auto& roll_note) { return roll_note.off_sample > m_time; });
+ if (it.is_end())
+ m_roll_iters[note] = m_roll_notes[note].begin();
+ else
+ m_roll_iters[note] = it;
+}
+
+void Track::set_roll_note(int note, u32 on_sample, u32 off_sample)
+{
+ RollNote new_roll_note = { on_sample, off_sample };
+
+ ASSERT(note >= 0 && note < note_count);
+ ASSERT(new_roll_note.off_sample < roll_length);
+ ASSERT(new_roll_note.length() >= 2);
+
+ for (auto it = m_roll_notes[note].begin(); !it.is_end();) {
+ if (it->on_sample > new_roll_note.off_sample) {
+ m_roll_notes[note].insert_before(it, new_roll_note);
+ sync_roll(note);
+ return;
+ }
+ if (it->on_sample <= new_roll_note.on_sample && it->off_sample >= new_roll_note.on_sample) {
+ if (m_time >= it->on_sample && m_time <= it->off_sample)
+ set_note(note, Off);
+ m_roll_notes[note].remove(it);
+ sync_roll(note);
+ return;
+ }
+ if ((new_roll_note.on_sample == 0 || it->on_sample >= new_roll_note.on_sample - 1) && it->on_sample <= new_roll_note.off_sample) {
+ if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample)
+ set_note(note, Off);
+ m_roll_notes[note].remove(it);
+ it = m_roll_notes[note].begin();
+ continue;
+ }
+ ++it;
+ }
+
+ m_roll_notes[note].append(new_roll_note);
+ sync_roll(note);
+}
+
+void Track::set_wave(int wave)
+{
+ ASSERT(wave >= first_wave && wave <= last_wave);
+ m_wave = wave;
+}
+
+void Track::set_wave(Direction direction)
+{
+ if (direction == Up) {
+ if (++m_wave > last_wave)
+ m_wave = first_wave;
+ } else {
+ if (--m_wave < first_wave)
+ m_wave = last_wave;
+ }
+}
+
+void Track::set_attack(int attack)
+{
+ ASSERT(attack >= 0);
+ m_attack = attack;
+ m_attack_step = calculate_step(1, m_attack);
+}
+
+void Track::set_decay(int decay)
+{
+ ASSERT(decay >= 0);
+ m_decay = decay;
+ m_decay_step = calculate_step(1 - m_sustain_level, m_decay);
+}
+
+void Track::set_sustain_impl(int sustain)
+{
+ ASSERT(sustain >= 0);
+ m_sustain = sustain;
+ m_sustain_level = sustain / 1000.0;
+}
+
+void Track::set_sustain(int sustain)
+{
+ set_sustain_impl(sustain);
+ set_decay(m_decay);
+}
+
+void Track::set_release(int release)
+{
+ ASSERT(release >= 0);
+ m_release = release;
+}
+
+void Track::set_delay(int delay)
+{
+ ASSERT(delay >= 0);
+ m_delay = delay;
+ m_delay_samples = m_delay == 0 ? 0 : (sample_rate / (beats_per_minute / 60)) / m_delay;
+ m_delay_buffer.resize(m_delay_samples);
+ memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample));
+ m_delay_index = 0;
+}
diff --git a/Userland/Applications/Piano/Track.h b/Userland/Applications/Piano/Track.h
new file mode 100644
index 0000000000..0acca429b0
--- /dev/null
+++ b/Userland/Applications/Piano/Track.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Music.h"
+#include <AK/Noncopyable.h>
+#include <AK/SinglyLinkedList.h>
+#include <LibAudio/Buffer.h>
+
+typedef AK::SinglyLinkedListIterator<SinglyLinkedList<RollNote>, RollNote> RollIter;
+
+class Track {
+ AK_MAKE_NONCOPYABLE(Track);
+ AK_MAKE_NONMOVABLE(Track);
+
+public:
+ explicit Track(const u32& time);
+ ~Track();
+
+ const Vector<Audio::Sample>& recorded_sample() const { return m_recorded_sample; }
+ const SinglyLinkedList<RollNote>& roll_notes(int note) const { return m_roll_notes[note]; }
+ int wave() const { return m_wave; }
+ int attack() const { return m_attack; }
+ int decay() const { return m_decay; }
+ int sustain() const { return m_sustain; }
+ int release() const { return m_release; }
+ int delay() const { return m_delay; }
+
+ void fill_sample(Sample& sample);
+ void reset();
+ String set_recorded_sample(const StringView& path);
+ void set_note(int note, Switch);
+ void set_roll_note(int note, u32 on_sample, u32 off_sample);
+ void set_wave(int wave);
+ void set_wave(Direction);
+ void set_attack(int attack);
+ void set_decay(int decay);
+ void set_sustain(int sustain);
+ void set_release(int release);
+ void set_delay(int delay);
+
+private:
+ Audio::Sample sine(size_t note);
+ Audio::Sample saw(size_t note);
+ Audio::Sample square(size_t note);
+ Audio::Sample triangle(size_t note);
+ Audio::Sample noise() const;
+ Audio::Sample recorded_sample(size_t note);
+
+ void sync_roll(int note);
+ void set_sustain_impl(int sustain);
+
+ Vector<Sample> m_delay_buffer;
+
+ Vector<Audio::Sample> m_recorded_sample;
+
+ u8 m_note_on[note_count] { 0 };
+ double m_power[note_count] { 0 };
+ double m_pos[note_count]; // Initialized lazily.
+ Envelope m_envelope[note_count] { Done };
+
+ int m_wave { first_wave };
+ int m_attack;
+ double m_attack_step;
+ int m_decay;
+ double m_decay_step;
+ int m_sustain;
+ double m_sustain_level;
+ int m_release;
+ double m_release_step[note_count];
+ int m_delay { 0 };
+ size_t m_delay_samples { 0 };
+ size_t m_delay_index { 0 };
+
+ const u32& m_time;
+
+ SinglyLinkedList<RollNote> m_roll_notes[note_count];
+ RollIter m_roll_iters[note_count];
+};
diff --git a/Userland/Applications/Piano/TrackManager.cpp b/Userland/Applications/Piano/TrackManager.cpp
new file mode 100644
index 0000000000..5aa1856a11
--- /dev/null
+++ b/Userland/Applications/Piano/TrackManager.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TrackManager.h"
+
+TrackManager::TrackManager()
+{
+ add_track();
+}
+
+TrackManager::~TrackManager()
+{
+}
+
+void TrackManager::fill_buffer(Span<Sample> buffer)
+{
+ memset(buffer.data(), 0, buffer_size);
+
+ for (size_t i = 0; i < buffer.size(); ++i) {
+ for (auto& track : m_tracks)
+ track->fill_sample(buffer[i]);
+
+ if (++m_time >= roll_length) {
+ m_time = 0;
+ if (!m_should_loop)
+ break;
+ }
+ }
+
+ memcpy(m_current_back_buffer.data(), buffer.data(), buffer_size);
+ swap(m_current_front_buffer, m_current_back_buffer);
+}
+
+void TrackManager::reset()
+{
+ memset(m_front_buffer.data(), 0, buffer_size);
+ memset(m_back_buffer.data(), 0, buffer_size);
+
+ m_current_front_buffer = m_front_buffer.span();
+ m_current_back_buffer = m_back_buffer.span();
+
+ m_time = 0;
+
+ for (auto& track : m_tracks)
+ track->reset();
+}
+
+void TrackManager::set_note_current_octave(int note, Switch switch_note)
+{
+ current_track().set_note(note + octave_base(), switch_note);
+}
+
+void TrackManager::set_octave(Direction direction)
+{
+ if (direction == Up) {
+ if (m_octave < octave_max)
+ ++m_octave;
+ } else {
+ if (m_octave > octave_min)
+ --m_octave;
+ }
+}
+
+void TrackManager::set_octave(int octave)
+{
+ if (octave <= octave_max && octave >= octave_min) {
+ m_octave = octave;
+ }
+}
+
+void TrackManager::add_track()
+{
+ m_tracks.append(make<Track>(m_time));
+}
+
+void TrackManager::next_track()
+{
+ if (++m_current_track >= m_tracks.size())
+ m_current_track = 0;
+}
diff --git a/Userland/Applications/Piano/TrackManager.h b/Userland/Applications/Piano/TrackManager.h
new file mode 100644
index 0000000000..70cb661bc4
--- /dev/null
+++ b/Userland/Applications/Piano/TrackManager.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Music.h"
+#include "Track.h"
+#include <AK/Array.h>
+#include <AK/Noncopyable.h>
+#include <AK/NonnullOwnPtr.h>
+#include <AK/Vector.h>
+
+class TrackManager {
+ AK_MAKE_NONCOPYABLE(TrackManager);
+ AK_MAKE_NONMOVABLE(TrackManager);
+
+public:
+ TrackManager();
+ ~TrackManager();
+
+ Track& current_track() { return *m_tracks[m_current_track]; }
+ Span<const Sample> buffer() const { return m_current_front_buffer; }
+ int octave() const { return m_octave; }
+ int octave_base() const { return (m_octave - octave_min) * 12; }
+ int time() const { return m_time; }
+
+ void fill_buffer(Span<Sample>);
+ void reset();
+ void set_should_loop(bool b) { m_should_loop = b; }
+ void set_note_current_octave(int note, Switch);
+ void set_octave(Direction);
+ void set_octave(int octave);
+ void add_track();
+ void next_track();
+
+private:
+ Vector<NonnullOwnPtr<Track>> m_tracks;
+ size_t m_current_track { 0 };
+
+ Array<Sample, sample_count> m_front_buffer;
+ Array<Sample, sample_count> m_back_buffer;
+ Span<Sample> m_current_front_buffer { m_front_buffer.span() };
+ Span<Sample> m_current_back_buffer { m_back_buffer.span() };
+
+ int m_octave { 4 };
+
+ u32 m_time { 0 };
+
+ bool m_should_loop { true };
+};
diff --git a/Userland/Applications/Piano/WaveWidget.cpp b/Userland/Applications/Piano/WaveWidget.cpp
new file mode 100644
index 0000000000..9c40b46bde
--- /dev/null
+++ b/Userland/Applications/Piano/WaveWidget.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "WaveWidget.h"
+#include "TrackManager.h"
+#include <AK/NumericLimits.h>
+#include <LibGUI/Painter.h>
+
+WaveWidget::WaveWidget(TrackManager& track_manager)
+ : m_track_manager(track_manager)
+{
+}
+
+WaveWidget::~WaveWidget()
+{
+}
+
+int WaveWidget::sample_to_y(int sample) const
+{
+ constexpr int nice_scale_factor = 4;
+ sample *= nice_scale_factor;
+ constexpr double sample_max = NumericLimits<i16>::max();
+ double percentage = sample / sample_max;
+ double portion_of_half_height = percentage * ((frame_inner_rect().height() - 1) / 2.0);
+ double y = (frame_inner_rect().height() / 2.0) + portion_of_half_height;
+ return y;
+}
+
+void WaveWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.fill_rect(frame_inner_rect(), Color::Black);
+ painter.translate(frame_thickness(), frame_thickness());
+
+ Color left_wave_color = left_wave_colors[m_track_manager.current_track().wave()];
+ Color right_wave_color = right_wave_colors[m_track_manager.current_track().wave()];
+ auto buffer = m_track_manager.buffer();
+ double width_scale = static_cast<double>(frame_inner_rect().width()) / buffer.size();
+
+ int prev_x = 0;
+ int prev_y_left = sample_to_y(buffer[0].left);
+ int prev_y_right = sample_to_y(buffer[0].right);
+ painter.set_pixel({ prev_x, prev_y_left }, left_wave_color);
+ painter.set_pixel({ prev_x, prev_y_right }, right_wave_color);
+
+ for (size_t x = 1; x < buffer.size(); ++x) {
+ int y_left = sample_to_y(buffer[x].left);
+ int y_right = sample_to_y(buffer[x].right);
+
+ Gfx::IntPoint point1_left(prev_x * width_scale, prev_y_left);
+ Gfx::IntPoint point2_left(x * width_scale, y_left);
+ painter.draw_line(point1_left, point2_left, left_wave_color);
+
+ Gfx::IntPoint point1_right(prev_x * width_scale, prev_y_right);
+ Gfx::IntPoint point2_right(x * width_scale, y_right);
+ painter.draw_line(point1_right, point2_right, right_wave_color);
+
+ prev_x = x;
+ prev_y_left = y_left;
+ prev_y_right = y_right;
+ }
+
+ GUI::Frame::paint_event(event);
+}
diff --git a/Userland/Applications/Piano/WaveWidget.h b/Userland/Applications/Piano/WaveWidget.h
new file mode 100644
index 0000000000..e8b9cd4d5f
--- /dev/null
+++ b/Userland/Applications/Piano/WaveWidget.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+class TrackManager;
+
+class WaveWidget final : public GUI::Frame {
+ C_OBJECT(WaveWidget)
+public:
+ virtual ~WaveWidget() override;
+
+private:
+ explicit WaveWidget(TrackManager&);
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ int sample_to_y(int sample) const;
+
+ TrackManager& m_track_manager;
+};
diff --git a/Userland/Applications/Piano/main.cpp b/Userland/Applications/Piano/main.cpp
new file mode 100644
index 0000000000..dc75e2944d
--- /dev/null
+++ b/Userland/Applications/Piano/main.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2019-2020, William McPherson <willmcpherson2@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MainWidget.h"
+#include "TrackManager.h"
+#include <AK/Array.h>
+#include <LibAudio/ClientConnection.h>
+#include <LibAudio/WavWriter.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/File.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibThread/Thread.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto audio_client = Audio::ClientConnection::construct();
+ audio_client->handshake();
+
+ TrackManager track_manager;
+
+ auto app_icon = GUI::Icon::default_icon("app-piano");
+ auto window = GUI::Window::construct();
+ auto& main_widget = window->set_main_widget<MainWidget>(track_manager);
+ window->set_title("Piano");
+ window->resize(840, 600);
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->show();
+
+ Audio::WavWriter wav_writer;
+ Optional<String> save_path;
+ bool need_to_write_wav = false;
+
+ auto audio_thread = LibThread::Thread::construct([&] {
+ auto audio = Core::File::construct("/dev/audio");
+ if (!audio->open(Core::IODevice::WriteOnly)) {
+ dbgln("Can't open audio device: {}", audio->error_string());
+ return 1;
+ }
+
+ Array<Sample, sample_count> buffer;
+ while (!Core::EventLoop::current().was_exit_requested()) {
+ track_manager.fill_buffer(buffer);
+ audio->write(reinterpret_cast<u8*>(buffer.data()), buffer_size);
+ Core::EventLoop::current().post_event(main_widget, make<Core::CustomEvent>(0));
+ Core::EventLoop::wake();
+
+ if (need_to_write_wav) {
+ need_to_write_wav = false;
+ track_manager.reset();
+ track_manager.set_should_loop(false);
+ do {
+ track_manager.fill_buffer(buffer);
+ wav_writer.write_samples(reinterpret_cast<u8*>(buffer.data()), buffer_size);
+ } while (track_manager.time());
+ track_manager.reset();
+ track_manager.set_should_loop(true);
+ wav_writer.finalize();
+ }
+ }
+ return 0;
+ });
+ audio_thread->start();
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Piano");
+ app_menu.add_action(GUI::Action::create("Export", { Mod_Ctrl, Key_E }, [&](const GUI::Action&) {
+ save_path = GUI::FilePicker::get_save_filepath(window, "Untitled", "wav");
+ if (!save_path.has_value())
+ return;
+ wav_writer.set_file(save_path.value());
+ if (wav_writer.has_error()) {
+ GUI::MessageBox::show(window, String::formatted("Failed to export WAV file: {}", wav_writer.error_string()), "Error", GUI::MessageBox::Type::Error);
+ wav_writer.clear_error();
+ return;
+ }
+ need_to_write_wav = true;
+ }));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ main_widget.add_actions(edit_menu);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Piano", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ return app->exec();
+}
diff --git a/Userland/Applications/PixelPaint/BrushTool.cpp b/Userland/Applications/PixelPaint/BrushTool.cpp
new file mode 100644
index 0000000000..200e270ea7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BrushTool.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BrushTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Rect.h>
+#include <utility>
+
+namespace PixelPaint {
+
+BrushTool::BrushTool()
+{
+}
+
+BrushTool::~BrushTool()
+{
+}
+
+void BrushTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ m_last_position = event.position();
+}
+
+void BrushTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!(event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right))
+ return;
+
+ draw_line(layer.bitmap(), m_editor->color_for(event), m_last_position, event.position());
+ layer.did_modify_bitmap(*m_editor->image());
+ m_last_position = event.position();
+ m_was_drawing = true;
+}
+
+void BrushTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&)
+{
+ if (m_was_drawing) {
+ m_editor->did_complete_action();
+ m_was_drawing = false;
+ }
+}
+
+void BrushTool::draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& point)
+{
+ for (int y = point.y() - m_size; y < point.y() + m_size; y++) {
+ for (int x = point.x() - m_size; x < point.x() + m_size; x++) {
+ auto distance = point.distance_from({ x, y });
+ if (x < 0 || x >= bitmap.width() || y < 0 || y >= bitmap.height())
+ continue;
+ if (distance >= m_size)
+ continue;
+
+ auto falloff = (1.0 - (distance / (float)m_size)) * (1.0f / (100 - m_hardness));
+ auto pixel_color = color;
+ pixel_color.set_alpha(falloff * 255);
+ bitmap.set_pixel(x, y, bitmap.get_pixel(x, y).blend(pixel_color));
+ }
+ }
+}
+
+void BrushTool::draw_line(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& start, const Gfx::IntPoint& end)
+{
+ int length_x = end.x() - start.x();
+ int length_y = end.y() - start.y();
+ float y_step = length_y == 0 ? 0 : (float)(length_y) / (float)(length_x);
+ if (y_step > abs(length_y))
+ y_step = abs(length_y);
+ if (y_step < -abs(length_y))
+ y_step = -abs(length_y);
+ if (y_step == 0 && start.x() == end.x())
+ return;
+
+ int start_x = start.x();
+ int end_x = end.x();
+ int start_y = start.y();
+ int end_y = end.y();
+ if (start_x > end_x) {
+ swap(start_x, end_x);
+ swap(start_y, end_y);
+ }
+
+ float y = start_y;
+ for (int x = start_x; x <= end_x; x++) {
+ int start_step_y = y;
+ int end_step_y = y + y_step;
+ if (start_step_y > end_step_y)
+ swap(start_step_y, end_step_y);
+ for (int i = start_step_y; i <= end_step_y; i++)
+ draw_point(bitmap, color, { x, i });
+ y += y_step;
+ }
+}
+
+GUI::Widget* BrushTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& size_container = m_properties_widget->add<GUI::Widget>();
+ size_container.set_fixed_height(20);
+ size_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& size_label = size_container.add<GUI::Label>("Size:");
+ size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ size_label.set_fixed_size(80, 20);
+
+ auto& size_slider = size_container.add<GUI::HorizontalSlider>();
+ size_slider.set_fixed_height(20);
+ size_slider.set_range(1, 100);
+ size_slider.set_value(m_size);
+ size_slider.on_change = [this](int value) {
+ m_size = value;
+ };
+
+ auto& hardness_container = m_properties_widget->add<GUI::Widget>();
+ hardness_container.set_fixed_height(20);
+ hardness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& hardness_label = hardness_container.add<GUI::Label>("Hardness:");
+ hardness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ hardness_label.set_fixed_size(80, 20);
+
+ auto& hardness_slider = hardness_container.add<GUI::HorizontalSlider>();
+ hardness_slider.set_fixed_height(20);
+ hardness_slider.set_range(1, 99);
+ hardness_slider.set_value(m_hardness);
+ hardness_slider.on_change = [this](int value) {
+ m_hardness = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/BrushTool.h b/Userland/Applications/PixelPaint/BrushTool.h
new file mode 100644
index 0000000000..939ccdeef4
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BrushTool.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class BrushTool final : public Tool {
+public:
+ BrushTool();
+ virtual ~BrushTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_size { 20 };
+ int m_hardness { 80 };
+ bool m_was_drawing { false };
+ Gfx::IntPoint m_last_position;
+
+ void draw_line(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& start, const Gfx::IntPoint& end);
+ void draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& point);
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/BucketTool.cpp b/Userland/Applications/PixelPaint/BucketTool.cpp
new file mode 100644
index 0000000000..6cffddac20
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BucketTool.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BucketTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <AK/Queue.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Rect.h>
+
+namespace PixelPaint {
+
+BucketTool::BucketTool()
+{
+}
+
+BucketTool::~BucketTool()
+{
+}
+
+static float color_distance_squared(const Gfx::Color& lhs, const Gfx::Color& rhs)
+{
+ int a = rhs.red() - lhs.red();
+ int b = rhs.green() - lhs.green();
+ int c = rhs.blue() - lhs.blue();
+ return (a * a + b * b + c * c) / (255.0f * 255.0f);
+}
+
+static void flood_fill(Gfx::Bitmap& bitmap, const Gfx::IntPoint& start_position, Color target_color, Color fill_color, int threshold)
+{
+ ASSERT(bitmap.bpp() == 32);
+
+ if (target_color == fill_color)
+ return;
+
+ if (!bitmap.rect().contains(start_position))
+ return;
+
+ float threshold_normalized_squared = (threshold / 100.0f) * (threshold / 100.0f);
+
+ Queue<Gfx::IntPoint> queue;
+ queue.enqueue(start_position);
+ while (!queue.is_empty()) {
+ auto position = queue.dequeue();
+
+ auto pixel_color = bitmap.get_pixel<Gfx::StorageFormat::RGBA32>(position.x(), position.y());
+ if (color_distance_squared(pixel_color, target_color) > threshold_normalized_squared)
+ continue;
+
+ bitmap.set_pixel<Gfx::StorageFormat::RGBA32>(position.x(), position.y(), fill_color);
+
+ if (position.x() != 0)
+ queue.enqueue(position.translated(-1, 0));
+
+ if (position.x() != bitmap.width() - 1)
+ queue.enqueue(position.translated(1, 0));
+
+ if (position.y() != 0)
+ queue.enqueue(position.translated(0, -1));
+
+ if (position.y() != bitmap.height() - 1)
+ queue.enqueue(position.translated(0, 1));
+ }
+}
+
+void BucketTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!layer.rect().contains(event.position()))
+ return;
+
+ GUI::Painter painter(layer.bitmap());
+ auto target_color = layer.bitmap().get_pixel(event.x(), event.y());
+
+ flood_fill(layer.bitmap(), event.position(), target_color, m_editor->color_for(event), m_threshold);
+
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+}
+
+GUI::Widget* BucketTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& threshold_container = m_properties_widget->add<GUI::Widget>();
+ threshold_container.set_fixed_height(20);
+ threshold_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& threshold_label = threshold_container.add<GUI::Label>("Threshold:");
+ threshold_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ threshold_label.set_fixed_size(80, 20);
+
+ auto& threshold_slider = threshold_container.add<GUI::HorizontalSlider>();
+ threshold_slider.set_fixed_height(20);
+ threshold_slider.set_range(0, 100);
+ threshold_slider.set_value(m_threshold);
+ threshold_slider.on_change = [this](int value) {
+ m_threshold = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/BucketTool.h b/Userland/Applications/PixelPaint/BucketTool.h
new file mode 100644
index 0000000000..b4bf700815
--- /dev/null
+++ b/Userland/Applications/PixelPaint/BucketTool.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class BucketTool final : public Tool {
+public:
+ BucketTool();
+ virtual ~BucketTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_threshold { 0 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt
new file mode 100644
index 0000000000..713845c80e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES
+ BrushTool.cpp
+ BucketTool.cpp
+ CreateNewImageDialog.cpp
+ CreateNewLayerDialog.cpp
+ EllipseTool.cpp
+ EraseTool.cpp
+ Image.cpp
+ ImageEditor.cpp
+ Layer.cpp
+ LayerListWidget.cpp
+ LayerPropertiesWidget.cpp
+ LineTool.cpp
+ main.cpp
+ MoveTool.cpp
+ PaletteWidget.cpp
+ PenTool.cpp
+ PickerTool.cpp
+ RectangleTool.cpp
+ SprayTool.cpp
+ ToolboxWidget.cpp
+ ToolPropertiesWidget.cpp
+ Tool.cpp
+)
+
+serenity_app(PixelPaint ICON app-pixel-paint)
+target_link_libraries(PixelPaint LibGUI LibGfx)
diff --git a/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp b/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp
new file mode 100644
index 0000000000..0696b69ba8
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewImageDialog.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewImageDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+
+namespace PixelPaint {
+
+CreateNewImageDialog::CreateNewImageDialog(GUI::Window* parent_window)
+ : Dialog(parent_window)
+{
+ set_title("Create new image");
+ resize(200, 200);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& name_label = main_widget.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ m_name_textbox = main_widget.add<GUI::TextBox>();
+ m_name_textbox->on_change = [this] {
+ m_image_name = m_name_textbox->text();
+ };
+
+ auto& width_label = main_widget.add<GUI::Label>("Width:");
+ width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& width_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& height_label = main_widget.add<GUI::Label>("Height:");
+ height_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& height_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& button_container = main_widget.add<GUI::Widget>();
+ button_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& ok_button = button_container.add<GUI::Button>("OK");
+ ok_button.on_click = [this](auto) {
+ done(ExecOK);
+ };
+
+ auto& cancel_button = button_container.add<GUI::Button>("Cancel");
+ cancel_button.on_click = [this](auto) {
+ done(ExecCancel);
+ };
+
+ width_spinbox.on_change = [this](int value) {
+ m_image_size.set_width(value);
+ };
+
+ height_spinbox.on_change = [this](int value) {
+ m_image_size.set_height(value);
+ };
+
+ width_spinbox.set_range(0, 16384);
+ height_spinbox.set_range(0, 16384);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewImageDialog.h b/Userland/Applications/PixelPaint/CreateNewImageDialog.h
new file mode 100644
index 0000000000..b46b65adab
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewImageDialog.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+
+namespace PixelPaint {
+
+class CreateNewImageDialog final : public GUI::Dialog {
+ C_OBJECT(CreateNewImageDialog)
+
+public:
+ const Gfx::IntSize& image_size() const { return m_image_size; }
+ const String& image_name() const { return m_image_name; }
+
+private:
+ CreateNewImageDialog(GUI::Window* parent_window);
+
+ String m_image_name;
+ Gfx::IntSize m_image_size;
+
+ RefPtr<GUI::TextBox> m_name_textbox;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp b/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp
new file mode 100644
index 0000000000..02f65b51ad
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewLayerDialog.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewLayerDialog.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+
+namespace PixelPaint {
+
+CreateNewLayerDialog::CreateNewLayerDialog(const Gfx::IntSize& suggested_size, GUI::Window* parent_window)
+ : Dialog(parent_window)
+{
+ set_title("Create new layer");
+ set_icon(parent_window->icon());
+ resize(200, 200);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+
+ auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ auto& name_label = main_widget.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ m_name_textbox = main_widget.add<GUI::TextBox>();
+ m_name_textbox->on_change = [this] {
+ m_layer_name = m_name_textbox->text();
+ };
+
+ auto& width_label = main_widget.add<GUI::Label>("Width:");
+ width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& width_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& height_label = main_widget.add<GUI::Label>("Height:");
+ height_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ auto& height_spinbox = main_widget.add<GUI::SpinBox>();
+
+ auto& button_container = main_widget.add<GUI::Widget>();
+ button_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& ok_button = button_container.add<GUI::Button>("OK");
+ ok_button.on_click = [this](auto) {
+ done(ExecOK);
+ };
+
+ auto& cancel_button = button_container.add<GUI::Button>("Cancel");
+ cancel_button.on_click = [this](auto) {
+ done(ExecCancel);
+ };
+
+ width_spinbox.on_change = [this](int value) {
+ m_layer_size.set_width(value);
+ };
+
+ height_spinbox.on_change = [this](int value) {
+ m_layer_size.set_height(value);
+ };
+
+ width_spinbox.set_range(0, 16384);
+ height_spinbox.set_range(0, 16384);
+
+ width_spinbox.set_value(suggested_size.width());
+ height_spinbox.set_value(suggested_size.height());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/CreateNewLayerDialog.h b/Userland/Applications/PixelPaint/CreateNewLayerDialog.h
new file mode 100644
index 0000000000..bb07ff9943
--- /dev/null
+++ b/Userland/Applications/PixelPaint/CreateNewLayerDialog.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Dialog.h>
+
+namespace PixelPaint {
+
+class CreateNewLayerDialog final : public GUI::Dialog {
+ C_OBJECT(CreateNewLayerDialog);
+
+public:
+ const Gfx::IntSize& layer_size() const { return m_layer_size; }
+ const String& layer_name() const { return m_layer_name; }
+
+private:
+ CreateNewLayerDialog(const Gfx::IntSize& suggested_size, GUI::Window* parent_window);
+
+ Gfx::IntSize m_layer_size;
+ String m_layer_name;
+
+ RefPtr<GUI::TextBox> m_name_textbox;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/EllipseTool.cpp b/Userland/Applications/PixelPaint/EllipseTool.cpp
new file mode 100644
index 0000000000..0501e2fb96
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EllipseTool.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "EllipseTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Rect.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+EllipseTool::EllipseTool()
+{
+}
+
+EllipseTool::~EllipseTool()
+{
+}
+
+void EllipseTool::draw_using(GUI::Painter& painter, const Gfx::IntRect& ellipse_intersecting_rect)
+{
+ switch (m_mode) {
+ case Mode::Outline:
+ painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), m_thickness);
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void EllipseTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = event.button();
+ m_ellipse_start_position = event.position();
+ m_ellipse_end_position = event.position();
+ m_editor->update();
+}
+
+void EllipseTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ draw_using(painter, Gfx::IntRect::from_two_points(m_ellipse_start_position, m_ellipse_end_position));
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ m_editor->did_complete_action();
+ }
+}
+
+void EllipseTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ m_ellipse_end_position = event.position();
+ m_editor->update();
+}
+
+void EllipseTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto preview_start = m_editor->layer_position_to_editor_position(layer, m_ellipse_start_position).to_type<int>();
+ auto preview_end = m_editor->layer_position_to_editor_position(layer, m_ellipse_end_position).to_type<int>();
+ draw_using(painter, Gfx::IntRect::from_two_points(preview_start, preview_end));
+}
+
+void EllipseTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void EllipseTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::Action::create("Outline", [this](auto&) {
+ m_mode = Mode::Outline;
+ }));
+ m_context_menu->add_separator();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/EllipseTool.h b/Userland/Applications/PixelPaint/EllipseTool.h
new file mode 100644
index 0000000000..0b1c10c89a
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EllipseTool.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class EllipseTool final : public Tool {
+public:
+ EllipseTool();
+ virtual ~EllipseTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+
+private:
+ enum class Mode {
+ Outline,
+ // FIXME: Add Mode::Fill
+ };
+
+ void draw_using(GUI::Painter&, const Gfx::IntRect&);
+
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_ellipse_start_position;
+ Gfx::IntPoint m_ellipse_end_position;
+ RefPtr<GUI::Menu> m_context_menu;
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+ Mode m_mode { Mode::Outline };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/EraseTool.cpp b/Userland/Applications/PixelPaint/EraseTool.cpp
new file mode 100644
index 0000000000..19d1482ba8
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EraseTool.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "EraseTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+EraseTool::EraseTool()
+{
+}
+
+EraseTool::~EraseTool()
+{
+}
+
+Gfx::IntRect EraseTool::build_rect(const Gfx::IntPoint& pos, const Gfx::IntRect& widget_rect)
+{
+ const int base_eraser_size = 10;
+ const int eraser_size = (base_eraser_size * m_thickness);
+ const int eraser_radius = eraser_size / 2;
+ const auto ex = pos.x();
+ const auto ey = pos.y();
+ return Gfx::IntRect(ex - eraser_radius, ey - eraser_radius, eraser_size, eraser_size).intersected(widget_rect);
+}
+
+void EraseTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+ Gfx::IntRect r = build_rect(event.position(), layer.rect());
+ GUI::Painter painter(layer.bitmap());
+ painter.clear_rect(r, get_color());
+ layer.did_modify_bitmap(*m_editor->image());
+}
+
+void EraseTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right) {
+ Gfx::IntRect r = build_rect(event.position(), layer.rect());
+ GUI::Painter painter(layer.bitmap());
+ painter.clear_rect(r, get_color());
+ layer.did_modify_bitmap(*m_editor->image());
+ }
+}
+
+void EraseTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+ m_editor->did_complete_action();
+}
+
+void EraseTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+
+ auto eraser_color_toggler = GUI::Action::create_checkable("Use secondary color", [&](auto& action) {
+ m_use_secondary_color = action.is_checked();
+ });
+ eraser_color_toggler->set_checked(m_use_secondary_color);
+
+ m_context_menu->add_action(eraser_color_toggler);
+ m_context_menu->add_separator();
+
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+
+ m_context_menu->popup(event.screen_position());
+}
+
+Color EraseTool::get_color() const
+{
+ if (m_use_secondary_color)
+ return m_editor->secondary_color();
+ return Color(255, 255, 255, 0);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/EraseTool.h b/Userland/Applications/PixelPaint/EraseTool.h
new file mode 100644
index 0000000000..16241e87f0
--- /dev/null
+++ b/Userland/Applications/PixelPaint/EraseTool.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class EraseTool final : public Tool {
+public:
+ EraseTool();
+ virtual ~EraseTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+
+private:
+ Gfx::Color get_color() const;
+ Gfx::IntRect build_rect(const Gfx::IntPoint& pos, const Gfx::IntRect& widget_rect);
+ RefPtr<GUI::Menu> m_context_menu;
+
+ bool m_use_secondary_color { false };
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/FilterParams.h b/Userland/Applications/PixelPaint/FilterParams.h
new file mode 100644
index 0000000000..398caecf9f
--- /dev/null
+++ b/Userland/Applications/PixelPaint/FilterParams.h
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/Filters/BoxBlurFilter.h>
+#include <LibGfx/Filters/GenericConvolutionFilter.h>
+#include <LibGfx/Filters/LaplacianFilter.h>
+#include <LibGfx/Filters/SharpenFilter.h>
+#include <LibGfx/Filters/SpatialGaussianBlurFilter.h>
+
+namespace PixelPaint {
+
+template<typename Filter>
+struct FilterParameters {
+};
+
+template<size_t N>
+class GenericConvolutionFilterInputDialog : public GUI::Dialog {
+ C_OBJECT(GenericConvolutionFilterInputDialog);
+
+public:
+ const Matrix<N, float>& matrix() const { return m_matrix; }
+ bool should_wrap() const { return m_should_wrap; }
+
+private:
+ explicit GenericConvolutionFilterInputDialog(GUI::Window* parent_window)
+ : Dialog(parent_window)
+ {
+ // FIXME: Help! Make this GUI less ugly.
+ StringBuilder builder;
+ builder.appendf("%zux%zu", N, N);
+ builder.append(" Convolution");
+ set_title(builder.string_view());
+
+ resize(200, 250);
+ auto& main_widget = set_main_widget<GUI::Frame>();
+ main_widget.set_frame_shape(Gfx::FrameShape::Container);
+ main_widget.set_frame_shadow(Gfx::FrameShadow::Raised);
+ main_widget.set_fill_with_background_color(true);
+ auto& layout = main_widget.template set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 4, 4, 4, 4 });
+
+ size_t index = 0;
+ size_t columns = N;
+ size_t rows = N;
+
+ for (size_t row = 0; row < rows; ++row) {
+ auto& horizontal_container = main_widget.template add<GUI::Widget>();
+ horizontal_container.template set_layout<GUI::HorizontalBoxLayout>();
+ for (size_t column = 0; column < columns; ++column) {
+ if (index < columns * rows) {
+ auto& textbox = horizontal_container.template add<GUI::TextBox>();
+ textbox.on_change = [&, row = row, column = column] {
+ auto& element = m_matrix.elements()[row][column];
+ char* endptr = nullptr;
+ auto value = strtof(textbox.text().characters(), &endptr);
+ if (endptr != nullptr)
+ element = value;
+ else
+ textbox.set_text("");
+ };
+ } else {
+ horizontal_container.template add<GUI::Widget>();
+ }
+ }
+ }
+
+ auto& norm_checkbox = main_widget.template add<GUI::CheckBox>("Normalize");
+ norm_checkbox.set_checked(false);
+
+ auto& wrap_checkbox = main_widget.template add<GUI::CheckBox>("Wrap");
+ wrap_checkbox.set_checked(m_should_wrap);
+
+ auto& button = main_widget.template add<GUI::Button>("Done");
+ button.on_click = [&](auto) {
+ m_should_wrap = wrap_checkbox.is_checked();
+ if (norm_checkbox.is_checked())
+ normalize(m_matrix);
+ done(ExecOK);
+ };
+ }
+
+ Matrix<N, float> m_matrix {};
+ bool m_should_wrap { false };
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::SpatialGaussianBlurFilter<N>> {
+ static OwnPtr<typename Gfx::SpatialGaussianBlurFilter<N>::Parameters> get()
+ {
+ constexpr static ssize_t offset = N / 2;
+ Matrix<N, float> kernel;
+ auto sigma = 1.0f;
+ auto s = 2.0f * sigma * sigma;
+
+ for (auto x = -offset; x <= offset; x++) {
+ for (auto y = -offset; y <= offset; y++) {
+ auto r = sqrt(x * x + y * y);
+ kernel.elements()[x + offset][y + offset] = (exp(-(r * r) / s)) / (M_PI * s);
+ }
+ }
+
+ normalize(kernel);
+
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(kernel);
+ }
+};
+
+template<>
+struct FilterParameters<Gfx::SharpenFilter> {
+ static OwnPtr<Gfx::GenericConvolutionFilter<3>::Parameters> get()
+ {
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(0, -1, 0, -1, 5, -1, 0, -1, 0));
+ }
+};
+
+template<>
+struct FilterParameters<Gfx::LaplacianFilter> {
+ static OwnPtr<Gfx::GenericConvolutionFilter<3>::Parameters> get(bool diagonal)
+ {
+ if (diagonal)
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(-1, -1, -1, -1, 8, -1, -1, -1, -1));
+
+ return make<Gfx::GenericConvolutionFilter<3>::Parameters>(Matrix<3, float>(0, -1, 0, -1, 4, -1, 0, -1, 0));
+ }
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::GenericConvolutionFilter<N>> {
+ static OwnPtr<typename Gfx::GenericConvolutionFilter<N>::Parameters> get(GUI::Window* parent_window)
+ {
+ auto input = GenericConvolutionFilterInputDialog<N>::construct(parent_window);
+ input->exec();
+ if (input->result() == GUI::Dialog::ExecOK)
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(input->matrix(), input->should_wrap());
+
+ return {};
+ }
+};
+
+template<size_t N>
+struct FilterParameters<Gfx::BoxBlurFilter<N>> {
+ static OwnPtr<typename Gfx::GenericConvolutionFilter<N>::Parameters> get()
+ {
+ Matrix<N, float> kernel;
+
+ for (size_t i = 0; i < N; ++i) {
+ for (size_t j = 0; j < N; ++j) {
+ kernel.elements()[i][j] = 1;
+ }
+ }
+
+ normalize(kernel);
+
+ return make<typename Gfx::GenericConvolutionFilter<N>::Parameters>(kernel);
+ }
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Image.cpp b/Userland/Applications/PixelPaint/Image.cpp
new file mode 100644
index 0000000000..09a075dfe0
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Image.cpp
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Image.h"
+#include "Layer.h"
+#include <AK/Base64.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonObjectSerializer.h>
+#include <AK/JsonValue.h>
+#include <AK/StringBuilder.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/BMPWriter.h>
+#include <LibGfx/ImageDecoder.h>
+#include <stdio.h>
+
+//#define PAINT_DEBUG
+
+namespace PixelPaint {
+
+RefPtr<Image> Image::create_with_size(const Gfx::IntSize& size)
+{
+ if (size.is_empty())
+ return nullptr;
+
+ if (size.width() > 16384 || size.height() > 16384)
+ return nullptr;
+
+ return adopt(*new Image(size));
+}
+
+Image::Image(const Gfx::IntSize& size)
+ : m_size(size)
+{
+}
+
+void Image::paint_into(GUI::Painter& painter, const Gfx::IntRect& dest_rect)
+{
+ float scale = (float)dest_rect.width() / (float)rect().width();
+ Gfx::PainterStateSaver saver(painter);
+ painter.add_clip_rect(dest_rect);
+ for (auto& layer : m_layers) {
+ if (!layer.is_visible())
+ continue;
+ auto target = dest_rect.translated(layer.location().x() * scale, layer.location().y() * scale);
+ target.set_size(layer.size().width() * scale, layer.size().height() * scale);
+ painter.draw_scaled_bitmap(target, layer.bitmap(), layer.rect(), (float)layer.opacity_percent() / 100.0f);
+ }
+}
+
+RefPtr<Image> Image::create_from_file(const String& file_path)
+{
+ auto file = fopen(file_path.characters(), "r");
+ fseek(file, 0L, SEEK_END);
+ auto length = ftell(file);
+ rewind(file);
+
+ auto buffer = ByteBuffer::create_uninitialized(length);
+ fread(buffer.data(), sizeof(u8), length, file);
+ fclose(file);
+
+ auto json_or_error = JsonValue::from_string(String::copy(buffer));
+ if (!json_or_error.has_value())
+ return nullptr;
+
+ auto json = json_or_error.value().as_object();
+ auto image = create_with_size({ json.get("width").to_i32(), json.get("height").to_i32() });
+ json.get("layers").as_array().for_each([&](JsonValue json_layer) {
+ auto json_layer_object = json_layer.as_object();
+ auto width = json_layer_object.get("width").to_i32();
+ auto height = json_layer_object.get("height").to_i32();
+ auto name = json_layer_object.get("name").as_string();
+ auto layer = Layer::create_with_size(*image, { width, height }, name);
+ layer->set_location({ json_layer_object.get("locationx").to_i32(), json_layer_object.get("locationy").to_i32() });
+ layer->set_opacity_percent(json_layer_object.get("opacity_percent").to_i32());
+ layer->set_visible(json_layer_object.get("visible").as_bool());
+ layer->set_selected(json_layer_object.get("selected").as_bool());
+
+ auto bitmap_base64_encoded = json_layer_object.get("bitmap").as_string();
+ auto bitmap_data = decode_base64(bitmap_base64_encoded);
+ auto image_decoder = Gfx::ImageDecoder::create(bitmap_data);
+ layer->set_bitmap(*image_decoder->bitmap());
+ image->add_layer(*layer);
+ });
+
+ return image;
+}
+
+void Image::save(const String& file_path) const
+{
+ // Build json file
+ StringBuilder builder;
+ JsonObjectSerializer json(builder);
+ json.add("width", m_size.width());
+ json.add("height", m_size.height());
+ {
+ auto json_layers = json.add_array("layers");
+ for (const auto& layer : m_layers) {
+ Gfx::BMPWriter bmp_dumber;
+ auto json_layer = json_layers.add_object();
+ json_layer.add("width", layer.size().width());
+ json_layer.add("height", layer.size().height());
+ json_layer.add("name", layer.name());
+ json_layer.add("locationx", layer.location().x());
+ json_layer.add("locationy", layer.location().y());
+ json_layer.add("opacity_percent", layer.opacity_percent());
+ json_layer.add("visible", layer.is_visible());
+ json_layer.add("selected", layer.is_selected());
+ json_layer.add("bitmap", encode_base64(bmp_dumber.dump(layer.bitmap())));
+ }
+ }
+ json.finish();
+
+ // Write json to disk
+ auto file = fopen(file_path.characters(), "w");
+ auto byte_buffer = builder.to_byte_buffer();
+ fwrite(byte_buffer.data(), sizeof(u8), byte_buffer.size(), file);
+ fclose(file);
+}
+
+void Image::export_bmp(const String& file_path)
+{
+ auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, m_size);
+ GUI::Painter painter(*bitmap);
+ paint_into(painter, { 0, 0, m_size.width(), m_size.height() });
+
+ Gfx::BMPWriter dumper;
+ auto bmp = dumper.dump(bitmap);
+ auto file = fopen(file_path.characters(), "wb");
+ fwrite(bmp.data(), sizeof(u8), bmp.size(), file);
+ fclose(file);
+}
+
+void Image::add_layer(NonnullRefPtr<Layer> layer)
+{
+ for (auto& existing_layer : m_layers) {
+ ASSERT(&existing_layer != layer.ptr());
+ }
+ m_layers.append(move(layer));
+
+ for (auto* client : m_clients)
+ client->image_did_add_layer(m_layers.size() - 1);
+
+ did_modify_layer_stack();
+}
+
+RefPtr<Image> Image::take_snapshot() const
+{
+ auto snapshot = create_with_size(m_size);
+ for (const auto& layer : m_layers)
+ snapshot->add_layer(*Layer::create_snapshot(*snapshot, layer));
+ return snapshot;
+}
+
+void Image::restore_snapshot(const Image& snapshot)
+{
+ m_layers.clear();
+ select_layer(nullptr);
+ for (const auto& snapshot_layer : snapshot.m_layers) {
+ auto layer = Layer::create_snapshot(*this, snapshot_layer);
+ if (layer->is_selected())
+ select_layer(layer.ptr());
+ add_layer(*layer);
+ }
+
+ did_modify_layer_stack();
+}
+
+size_t Image::index_of(const Layer& layer) const
+{
+ for (size_t i = 0; i < m_layers.size(); ++i) {
+ if (&m_layers.at(i) == &layer)
+ return i;
+ }
+ ASSERT_NOT_REACHED();
+}
+
+void Image::move_layer_to_back(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+ m_layers.prepend(layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_to_front(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+ m_layers.append(layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_down(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ if (!index)
+ return;
+ m_layers.remove(index);
+ m_layers.insert(index - 1, layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::move_layer_up(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ if (index == m_layers.size() - 1)
+ return;
+ m_layers.remove(index);
+ m_layers.insert(index + 1, layer);
+
+ did_modify_layer_stack();
+}
+
+void Image::change_layer_index(size_t old_index, size_t new_index)
+{
+ ASSERT(old_index < m_layers.size());
+ ASSERT(new_index < m_layers.size());
+ auto layer = m_layers.take(old_index);
+ m_layers.insert(new_index, move(layer));
+ did_modify_layer_stack();
+}
+
+void Image::did_modify_layer_stack()
+{
+ for (auto* client : m_clients)
+ client->image_did_modify_layer_stack();
+
+ did_change();
+}
+
+void Image::remove_layer(Layer& layer)
+{
+ NonnullRefPtr<Layer> protector(layer);
+ auto index = index_of(layer);
+ m_layers.remove(index);
+
+ for (auto* client : m_clients)
+ client->image_did_remove_layer(index);
+
+ did_modify_layer_stack();
+}
+
+void Image::select_layer(Layer* layer)
+{
+ for (auto* client : m_clients)
+ client->image_select_layer(layer);
+}
+
+void Image::add_client(ImageClient& client)
+{
+ ASSERT(!m_clients.contains(&client));
+ m_clients.set(&client);
+}
+
+void Image::remove_client(ImageClient& client)
+{
+ ASSERT(m_clients.contains(&client));
+ m_clients.remove(&client);
+}
+
+void Image::layer_did_modify_bitmap(Badge<Layer>, const Layer& layer)
+{
+ auto layer_index = index_of(layer);
+ for (auto* client : m_clients)
+ client->image_did_modify_layer(layer_index);
+
+ did_change();
+}
+
+void Image::layer_did_modify_properties(Badge<Layer>, const Layer& layer)
+{
+ auto layer_index = index_of(layer);
+ for (auto* client : m_clients)
+ client->image_did_modify_layer(layer_index);
+
+ did_change();
+}
+
+void Image::did_change()
+{
+ for (auto* client : m_clients)
+ client->image_did_change();
+}
+
+ImageUndoCommand::ImageUndoCommand(Image& image)
+ : m_snapshot(image.take_snapshot())
+ , m_image(image)
+{
+}
+
+void ImageUndoCommand::undo()
+{
+ m_image.restore_snapshot(*m_snapshot);
+}
+
+void ImageUndoCommand::redo()
+{
+ undo();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Image.h b/Userland/Applications/PixelPaint/Image.h
new file mode 100644
index 0000000000..63184b3ad3
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Image.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashTable.h>
+#include <AK/NonnullRefPtrVector.h>
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/Vector.h>
+#include <LibGUI/Command.h>
+#include <LibGUI/Forward.h>
+#include <LibGfx/Forward.h>
+#include <LibGfx/Rect.h>
+#include <LibGfx/Size.h>
+
+namespace PixelPaint {
+
+class Layer;
+
+class ImageClient {
+public:
+ virtual void image_did_add_layer(size_t) { }
+ virtual void image_did_remove_layer(size_t) { }
+ virtual void image_did_modify_layer(size_t) { }
+ virtual void image_did_modify_layer_stack() { }
+ virtual void image_did_change() { }
+ virtual void image_select_layer(Layer*) { }
+};
+
+class Image : public RefCounted<Image> {
+public:
+ static RefPtr<Image> create_with_size(const Gfx::IntSize&);
+ static RefPtr<Image> create_from_file(const String& file_path);
+
+ size_t layer_count() const { return m_layers.size(); }
+ const Layer& layer(size_t index) const { return m_layers.at(index); }
+ Layer& layer(size_t index) { return m_layers.at(index); }
+
+ const Gfx::IntSize& size() const { return m_size; }
+ Gfx::IntRect rect() const { return { {}, m_size }; }
+
+ void add_layer(NonnullRefPtr<Layer>);
+ RefPtr<Image> take_snapshot() const;
+ void restore_snapshot(const Image&);
+
+ void paint_into(GUI::Painter&, const Gfx::IntRect& dest_rect);
+ void save(const String& file_path) const;
+ void export_bmp(const String& file_path);
+
+ void move_layer_to_front(Layer&);
+ void move_layer_to_back(Layer&);
+ void move_layer_up(Layer&);
+ void move_layer_down(Layer&);
+ void change_layer_index(size_t old_index, size_t new_index);
+ void remove_layer(Layer&);
+ void select_layer(Layer*);
+
+ void add_client(ImageClient&);
+ void remove_client(ImageClient&);
+
+ void layer_did_modify_bitmap(Badge<Layer>, const Layer&);
+ void layer_did_modify_properties(Badge<Layer>, const Layer&);
+
+ size_t index_of(const Layer&) const;
+
+private:
+ explicit Image(const Gfx::IntSize&);
+
+ void did_change();
+ void did_modify_layer_stack();
+
+ Gfx::IntSize m_size;
+ NonnullRefPtrVector<Layer> m_layers;
+
+ HashTable<ImageClient*> m_clients;
+};
+
+class ImageUndoCommand : public GUI::Command {
+public:
+ ImageUndoCommand(Image& image);
+
+ virtual void undo() override;
+ virtual void redo() override;
+
+private:
+ RefPtr<Image> m_snapshot;
+ Image& m_image;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ImageEditor.cpp b/Userland/Applications/PixelPaint/ImageEditor.cpp
new file mode 100644
index 0000000000..f29551c050
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ImageEditor.cpp
@@ -0,0 +1,418 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ImageEditor.h"
+#include "Image.h"
+#include "Layer.h"
+#include "MoveTool.h"
+#include "Tool.h"
+#include <LibGUI/Command.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/Rect.h>
+
+namespace PixelPaint {
+
+ImageEditor::ImageEditor()
+ : m_undo_stack(make<GUI::UndoStack>())
+{
+ set_focus_policy(GUI::FocusPolicy::StrongFocus);
+}
+
+ImageEditor::~ImageEditor()
+{
+ if (m_image)
+ m_image->remove_client(*this);
+}
+
+void ImageEditor::set_image(RefPtr<Image> image)
+{
+ if (m_image)
+ m_image->remove_client(*this);
+
+ m_image = move(image);
+ m_active_layer = nullptr;
+ m_undo_stack = make<GUI::UndoStack>();
+ m_undo_stack->push(make<ImageUndoCommand>(*m_image));
+ update();
+ relayout();
+
+ if (m_image)
+ m_image->add_client(*this);
+}
+
+void ImageEditor::did_complete_action()
+{
+ if (!m_image)
+ return;
+ m_undo_stack->finalize_current_combo();
+ m_undo_stack->push(make<ImageUndoCommand>(*m_image));
+}
+
+bool ImageEditor::undo()
+{
+ if (!m_image)
+ return false;
+ if (m_undo_stack->can_undo()) {
+ m_undo_stack->undo();
+ layers_did_change();
+ return true;
+ }
+ return false;
+}
+
+bool ImageEditor::redo()
+{
+ if (!m_image)
+ return false;
+ if (m_undo_stack->can_redo()) {
+ m_undo_stack->redo();
+ layers_did_change();
+ return true;
+ }
+ return false;
+}
+
+void ImageEditor::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.add_clip_rect(frame_inner_rect());
+
+ Gfx::StylePainter::paint_transparency_grid(painter, rect(), palette());
+
+ if (m_image) {
+ painter.draw_rect(m_editor_image_rect.inflated(2, 2), Color::Black);
+ m_image->paint_into(painter, m_editor_image_rect);
+ }
+
+ if (m_active_layer) {
+ painter.draw_rect(enclosing_int_rect(image_rect_to_editor_rect(m_active_layer->relative_rect())).inflated(2, 2), Color::Black);
+ }
+}
+
+Gfx::FloatRect ImageEditor::layer_rect_to_editor_rect(const Layer& layer, const Gfx::IntRect& layer_rect) const
+{
+ return image_rect_to_editor_rect(layer_rect.translated(layer.location()));
+}
+
+Gfx::FloatRect ImageEditor::image_rect_to_editor_rect(const Gfx::IntRect& image_rect) const
+{
+ Gfx::FloatRect editor_rect;
+ editor_rect.set_location(image_position_to_editor_position(image_rect.location()));
+ editor_rect.set_width((float)image_rect.width() * m_scale);
+ editor_rect.set_height((float)image_rect.height() * m_scale);
+ return editor_rect;
+}
+
+Gfx::FloatRect ImageEditor::editor_rect_to_image_rect(const Gfx::IntRect& editor_rect) const
+{
+ Gfx::FloatRect image_rect;
+ image_rect.set_location(editor_position_to_image_position(editor_rect.location()));
+ image_rect.set_width((float)editor_rect.width() / m_scale);
+ image_rect.set_height((float)editor_rect.height() / m_scale);
+ return image_rect;
+}
+
+Gfx::FloatPoint ImageEditor::layer_position_to_editor_position(const Layer& layer, const Gfx::IntPoint& layer_position) const
+{
+ return image_position_to_editor_position(layer_position.translated(layer.location()));
+}
+
+Gfx::FloatPoint ImageEditor::image_position_to_editor_position(const Gfx::IntPoint& image_position) const
+{
+ Gfx::FloatPoint editor_position;
+ editor_position.set_x(m_editor_image_rect.x() + ((float)image_position.x() * m_scale));
+ editor_position.set_y(m_editor_image_rect.y() + ((float)image_position.y() * m_scale));
+ return editor_position;
+}
+
+Gfx::FloatPoint ImageEditor::editor_position_to_image_position(const Gfx::IntPoint& editor_position) const
+{
+ Gfx::FloatPoint image_position;
+ image_position.set_x(((float)editor_position.x() - m_editor_image_rect.x()) / m_scale);
+ image_position.set_y(((float)editor_position.y() - m_editor_image_rect.y()) / m_scale);
+ return image_position;
+}
+
+void ImageEditor::second_paint_event(GUI::PaintEvent& event)
+{
+ if (m_active_tool && m_active_layer)
+ m_active_tool->on_second_paint(*m_active_layer, event);
+}
+
+GUI::MouseEvent ImageEditor::event_with_pan_and_scale_applied(const GUI::MouseEvent& event) const
+{
+ auto image_position = editor_position_to_image_position(event.position());
+ return {
+ static_cast<GUI::Event::Type>(event.type()),
+ Gfx::IntPoint(image_position.x(), image_position.y()),
+ event.buttons(),
+ event.button(),
+ event.modifiers(),
+ event.wheel_delta()
+ };
+}
+
+GUI::MouseEvent ImageEditor::event_adjusted_for_layer(const GUI::MouseEvent& event, const Layer& layer) const
+{
+ auto image_position = editor_position_to_image_position(event.position());
+ image_position.move_by(-layer.location().x(), -layer.location().y());
+ return {
+ static_cast<GUI::Event::Type>(event.type()),
+ Gfx::IntPoint(image_position.x(), image_position.y()),
+ event.buttons(),
+ event.button(),
+ event.modifiers(),
+ event.wheel_delta()
+ };
+}
+
+void ImageEditor::mousedown_event(GUI::MouseEvent& event)
+{
+ if (event.button() == GUI::MouseButton::Middle) {
+ m_click_position = event.position();
+ m_saved_pan_origin = m_pan_origin;
+ return;
+ }
+
+ if (!m_active_tool)
+ return;
+
+ if (is<MoveTool>(*m_active_tool)) {
+ if (auto* other_layer = layer_at_editor_position(event.position())) {
+ set_active_layer(other_layer);
+ }
+ }
+
+ if (!m_active_layer)
+ return;
+
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+ m_active_tool->on_mousedown(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mousemove_event(GUI::MouseEvent& event)
+{
+ if (event.buttons() & GUI::MouseButton::Middle) {
+ auto delta = event.position() - m_click_position;
+ m_pan_origin = m_saved_pan_origin.translated(
+ -delta.x() / m_scale,
+ -delta.y() / m_scale);
+
+ relayout();
+ return;
+ }
+
+ if (!m_active_layer || !m_active_tool)
+ return;
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+
+ m_active_tool->on_mousemove(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mouseup_event(GUI::MouseEvent& event)
+{
+ if (!m_active_layer || !m_active_tool)
+ return;
+ auto layer_event = event_adjusted_for_layer(event, *m_active_layer);
+ auto image_event = event_with_pan_and_scale_applied(event);
+ m_active_tool->on_mouseup(*m_active_layer, layer_event, image_event);
+}
+
+void ImageEditor::mousewheel_event(GUI::MouseEvent& event)
+{
+ auto old_scale = m_scale;
+
+ m_scale += -event.wheel_delta() * 0.1f;
+ if (m_scale < 0.1f)
+ m_scale = 0.1f;
+ if (m_scale > 100.0f)
+ m_scale = 100.0f;
+
+ auto focus_point = Gfx::FloatPoint(
+ m_pan_origin.x() - ((float)event.x() - (float)width() / 2.0) / old_scale,
+ m_pan_origin.y() - ((float)event.y() - (float)height() / 2.0) / old_scale);
+
+ m_pan_origin = Gfx::FloatPoint(
+ focus_point.x() - m_scale / old_scale * (focus_point.x() - m_pan_origin.x()),
+ focus_point.y() - m_scale / old_scale * (focus_point.y() - m_pan_origin.y()));
+
+ if (old_scale != m_scale)
+ relayout();
+}
+
+void ImageEditor::context_menu_event(GUI::ContextMenuEvent& event)
+{
+ if (!m_active_layer || !m_active_tool)
+ return;
+ m_active_tool->on_context_menu(*m_active_layer, event);
+}
+
+void ImageEditor::resize_event(GUI::ResizeEvent& event)
+{
+ relayout();
+ GUI::Frame::resize_event(event);
+}
+
+void ImageEditor::keydown_event(GUI::KeyEvent& event)
+{
+ if (m_active_tool)
+ m_active_tool->on_keydown(event);
+}
+
+void ImageEditor::keyup_event(GUI::KeyEvent& event)
+{
+ if (m_active_tool)
+ m_active_tool->on_keyup(event);
+}
+
+void ImageEditor::set_active_layer(Layer* layer)
+{
+ if (m_active_layer == layer)
+ return;
+ m_active_layer = layer;
+
+ if (m_active_layer) {
+ size_t index = 0;
+ for (; index < m_image->layer_count(); ++index) {
+ if (&m_image->layer(index) == layer)
+ break;
+ }
+ if (on_active_layer_change)
+ on_active_layer_change(layer);
+ } else {
+ if (on_active_layer_change)
+ on_active_layer_change({});
+ }
+
+ layers_did_change();
+}
+
+void ImageEditor::set_active_tool(Tool* tool)
+{
+ if (m_active_tool == tool)
+ return;
+
+ if (m_active_tool)
+ m_active_tool->clear();
+
+ m_active_tool = tool;
+
+ if (m_active_tool)
+ m_active_tool->setup(*this);
+}
+
+void ImageEditor::layers_did_change()
+{
+ update();
+}
+
+Color ImageEditor::color_for(GUI::MouseButton button) const
+{
+ if (button == GUI::MouseButton::Left)
+ return m_primary_color;
+ if (button == GUI::MouseButton::Right)
+ return m_secondary_color;
+ ASSERT_NOT_REACHED();
+}
+
+Color ImageEditor::color_for(const GUI::MouseEvent& event) const
+{
+ if (event.buttons() & GUI::MouseButton::Left)
+ return m_primary_color;
+ if (event.buttons() & GUI::MouseButton::Right)
+ return m_secondary_color;
+ ASSERT_NOT_REACHED();
+}
+
+void ImageEditor::set_primary_color(Color color)
+{
+ if (m_primary_color == color)
+ return;
+ m_primary_color = color;
+ if (on_primary_color_change)
+ on_primary_color_change(color);
+}
+
+void ImageEditor::set_secondary_color(Color color)
+{
+ if (m_secondary_color == color)
+ return;
+ m_secondary_color = color;
+ if (on_secondary_color_change)
+ on_secondary_color_change(color);
+}
+
+Layer* ImageEditor::layer_at_editor_position(const Gfx::IntPoint& editor_position)
+{
+ if (!m_image)
+ return nullptr;
+ auto image_position = editor_position_to_image_position(editor_position);
+ for (ssize_t i = m_image->layer_count() - 1; i >= 0; --i) {
+ auto& layer = m_image->layer(i);
+ if (!layer.is_visible())
+ continue;
+ if (layer.relative_rect().contains(Gfx::IntPoint(image_position.x(), image_position.y())))
+ return const_cast<Layer*>(&layer);
+ }
+ return nullptr;
+}
+
+void ImageEditor::relayout()
+{
+ if (!image())
+ return;
+ auto& image = *this->image();
+
+ Gfx::IntSize new_size;
+ new_size.set_width(image.size().width() * m_scale);
+ new_size.set_height(image.size().height() * m_scale);
+ m_editor_image_rect.set_size(new_size);
+
+ Gfx::IntPoint new_location;
+ new_location.set_x((width() / 2) - (new_size.width() / 2) - (m_pan_origin.x() * m_scale));
+ new_location.set_y((height() / 2) - (new_size.height() / 2) - (m_pan_origin.y() * m_scale));
+ m_editor_image_rect.set_location(new_location);
+
+ update();
+}
+
+void ImageEditor::image_did_change()
+{
+ update();
+}
+
+void ImageEditor::image_select_layer(Layer* layer)
+{
+ set_active_layer(layer);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ImageEditor.h b/Userland/Applications/PixelPaint/ImageEditor.h
new file mode 100644
index 0000000000..b6cdc8fb62
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ImageEditor.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Image.h"
+#include <LibGUI/Frame.h>
+#include <LibGUI/UndoStack.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class Layer;
+class Tool;
+
+class ImageEditor final
+ : public GUI::Frame
+ , public ImageClient {
+ C_OBJECT(ImageEditor);
+
+public:
+ virtual ~ImageEditor() override;
+
+ const Image* image() const { return m_image; }
+ Image* image() { return m_image; }
+
+ void set_image(RefPtr<Image>);
+
+ Layer* active_layer() { return m_active_layer; }
+ void set_active_layer(Layer*);
+
+ Tool* active_tool() { return m_active_tool; }
+ void set_active_tool(Tool*);
+
+ void did_complete_action();
+ bool undo();
+ bool redo();
+
+ void layers_did_change();
+
+ Layer* layer_at_editor_position(const Gfx::IntPoint&);
+
+ Color primary_color() const { return m_primary_color; }
+ void set_primary_color(Color);
+
+ Color secondary_color() const { return m_secondary_color; }
+ void set_secondary_color(Color);
+
+ Color color_for(const GUI::MouseEvent&) const;
+ Color color_for(GUI::MouseButton) const;
+
+ Function<void(Color)> on_primary_color_change;
+ Function<void(Color)> on_secondary_color_change;
+
+ Function<void(Layer*)> on_active_layer_change;
+
+ Gfx::FloatRect layer_rect_to_editor_rect(const Layer&, const Gfx::IntRect&) const;
+ Gfx::FloatRect image_rect_to_editor_rect(const Gfx::IntRect&) const;
+ Gfx::FloatRect editor_rect_to_image_rect(const Gfx::IntRect&) const;
+ Gfx::FloatPoint layer_position_to_editor_position(const Layer&, const Gfx::IntPoint&) const;
+ Gfx::FloatPoint image_position_to_editor_position(const Gfx::IntPoint&) const;
+ Gfx::FloatPoint editor_position_to_image_position(const Gfx::IntPoint&) const;
+
+private:
+ ImageEditor();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void second_paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousewheel_event(GUI::MouseEvent&) override;
+ virtual void keydown_event(GUI::KeyEvent&) override;
+ virtual void keyup_event(GUI::KeyEvent&) override;
+ virtual void context_menu_event(GUI::ContextMenuEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ virtual void image_did_change() override;
+ virtual void image_select_layer(Layer*) override;
+
+ GUI::MouseEvent event_adjusted_for_layer(const GUI::MouseEvent&, const Layer&) const;
+ GUI::MouseEvent event_with_pan_and_scale_applied(const GUI::MouseEvent&) const;
+
+ void relayout();
+
+ RefPtr<Image> m_image;
+ RefPtr<Layer> m_active_layer;
+ OwnPtr<GUI::UndoStack> m_undo_stack;
+
+ Tool* m_active_tool { nullptr };
+
+ Color m_primary_color { Color::Black };
+ Color m_secondary_color { Color::White };
+
+ Gfx::IntRect m_editor_image_rect;
+ float m_scale { 1 };
+ Gfx::FloatPoint m_pan_origin;
+ Gfx::FloatPoint m_saved_pan_origin;
+ Gfx::IntPoint m_click_position;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Layer.cpp b/Userland/Applications/PixelPaint/Layer.cpp
new file mode 100644
index 0000000000..e607113c32
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Layer.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Layer.h"
+#include "Image.h"
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+RefPtr<Layer> Layer::create_with_size(Image& image, const Gfx::IntSize& size, const String& name)
+{
+ if (size.is_empty())
+ return nullptr;
+
+ if (size.width() > 16384 || size.height() > 16384)
+ return nullptr;
+
+ return adopt(*new Layer(image, size, name));
+}
+
+RefPtr<Layer> Layer::create_with_bitmap(Image& image, const Gfx::Bitmap& bitmap, const String& name)
+{
+ if (bitmap.size().is_empty())
+ return nullptr;
+
+ if (bitmap.size().width() > 16384 || bitmap.size().height() > 16384)
+ return nullptr;
+
+ return adopt(*new Layer(image, bitmap, name));
+}
+
+RefPtr<Layer> Layer::create_snapshot(Image& image, const Layer& layer)
+{
+ auto snapshot = create_with_bitmap(image, *layer.bitmap().clone(), layer.name());
+ snapshot->set_opacity_percent(layer.opacity_percent());
+ snapshot->set_visible(layer.is_visible());
+ snapshot->set_selected(layer.is_selected());
+ snapshot->set_location(layer.location());
+ return snapshot;
+}
+
+Layer::Layer(Image& image, const Gfx::IntSize& size, const String& name)
+ : m_image(image)
+ , m_name(name)
+{
+ m_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGBA32, size);
+}
+
+Layer::Layer(Image& image, const Gfx::Bitmap& bitmap, const String& name)
+ : m_image(image)
+ , m_name(name)
+ , m_bitmap(bitmap)
+{
+}
+
+void Layer::did_modify_bitmap(Image& image)
+{
+ image.layer_did_modify_bitmap({}, *this);
+}
+
+void Layer::set_visible(bool visible)
+{
+ if (m_visible == visible)
+ return;
+ m_visible = visible;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+void Layer::set_opacity_percent(int opacity_percent)
+{
+ if (m_opacity_percent == opacity_percent)
+ return;
+ m_opacity_percent = opacity_percent;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+void Layer::set_name(const String& name)
+{
+ if (m_name == name)
+ return;
+ m_name = name;
+ m_image.layer_did_modify_properties({}, *this);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Layer.h b/Userland/Applications/PixelPaint/Layer.h
new file mode 100644
index 0000000000..b3b7d4d603
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Layer.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Noncopyable.h>
+#include <AK/RefCounted.h>
+#include <AK/String.h>
+#include <AK/Weakable.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+class Image;
+
+class Layer
+ : public RefCounted<Layer>
+ , public Weakable<Layer> {
+
+ AK_MAKE_NONCOPYABLE(Layer);
+ AK_MAKE_NONMOVABLE(Layer);
+
+public:
+ static RefPtr<Layer> create_with_size(Image&, const Gfx::IntSize&, const String& name);
+ static RefPtr<Layer> create_with_bitmap(Image&, const Gfx::Bitmap&, const String& name);
+ static RefPtr<Layer> create_snapshot(Image&, const Layer&);
+
+ ~Layer() { }
+
+ const Gfx::IntPoint& location() const { return m_location; }
+ void set_location(const Gfx::IntPoint& location) { m_location = location; }
+
+ const Gfx::Bitmap& bitmap() const { return *m_bitmap; }
+ Gfx::Bitmap& bitmap() { return *m_bitmap; }
+ Gfx::IntSize size() const { return bitmap().size(); }
+
+ Gfx::IntRect relative_rect() const { return { location(), size() }; }
+ Gfx::IntRect rect() const { return { {}, size() }; }
+
+ const String& name() const { return m_name; }
+ void set_name(const String&);
+
+ void set_bitmap(Gfx::Bitmap& bitmap) { m_bitmap = bitmap; }
+
+ void did_modify_bitmap(Image&);
+
+ void set_selected(bool selected) { m_selected = selected; }
+ bool is_selected() const { return m_selected; }
+
+ bool is_visible() const { return m_visible; }
+ void set_visible(bool visible);
+
+ int opacity_percent() const { return m_opacity_percent; }
+ void set_opacity_percent(int);
+
+private:
+ Layer(Image&, const Gfx::IntSize&, const String& name);
+ Layer(Image&, const Gfx::Bitmap&, const String& name);
+
+ Image& m_image;
+
+ String m_name;
+ Gfx::IntPoint m_location;
+ RefPtr<Gfx::Bitmap> m_bitmap;
+
+ bool m_selected { false };
+ bool m_visible { true };
+
+ int m_opacity_percent { 100 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerListWidget.cpp b/Userland/Applications/PixelPaint/LayerListWidget.cpp
new file mode 100644
index 0000000000..756772bb87
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerListWidget.cpp
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LayerListWidget.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Palette.h>
+
+namespace PixelPaint {
+
+LayerListWidget::LayerListWidget()
+{
+}
+
+LayerListWidget::~LayerListWidget()
+{
+ if (m_image)
+ m_image->remove_client(*this);
+}
+
+void LayerListWidget::set_image(Image* image)
+{
+ if (m_image == image)
+ return;
+ if (m_image)
+ m_image->remove_client(*this);
+ m_image = image;
+ if (m_image)
+ m_image->add_client(*this);
+
+ rebuild_gadgets();
+}
+
+void LayerListWidget::rebuild_gadgets()
+{
+ m_gadgets.clear();
+ if (m_image) {
+ for (size_t layer_index = 0; layer_index < m_image->layer_count(); ++layer_index) {
+ m_gadgets.append({ layer_index, {}, {}, false, {} });
+ }
+ }
+ relayout_gadgets();
+}
+
+void LayerListWidget::resize_event(GUI::ResizeEvent& event)
+{
+ Widget::resize_event(event);
+ relayout_gadgets();
+}
+
+void LayerListWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ painter.fill_rect(event.rect(), palette().button());
+
+ if (!m_image)
+ return;
+
+ painter.fill_rect(event.rect(), palette().button());
+
+ auto paint_gadget = [&](auto& gadget) {
+ auto& layer = m_image->layer(gadget.layer_index);
+
+ auto adjusted_rect = gadget.rect;
+
+ if (gadget.is_moving) {
+ adjusted_rect.move_by(0, gadget.movement_delta.y());
+ }
+
+ if (gadget.is_moving) {
+ painter.fill_rect(adjusted_rect, palette().selection().lightened(1.5f));
+ } else if (layer.is_selected()) {
+ painter.fill_rect(adjusted_rect, palette().selection());
+ }
+
+ painter.draw_rect(adjusted_rect, Color::Black);
+
+ Gfx::IntRect thumbnail_rect { adjusted_rect.x(), adjusted_rect.y(), adjusted_rect.height(), adjusted_rect.height() };
+ thumbnail_rect.shrink(8, 8);
+ painter.draw_scaled_bitmap(thumbnail_rect, layer.bitmap(), layer.bitmap().rect());
+
+ Gfx::IntRect text_rect { thumbnail_rect.right() + 10, adjusted_rect.y(), adjusted_rect.width(), adjusted_rect.height() };
+ text_rect.intersect(adjusted_rect);
+
+ painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, layer.is_selected() ? palette().selection_text() : palette().button_text());
+ };
+
+ for (auto& gadget : m_gadgets) {
+ if (!gadget.is_moving)
+ paint_gadget(gadget);
+ }
+
+ if (m_moving_gadget_index.has_value())
+ paint_gadget(m_gadgets[m_moving_gadget_index.value()]);
+}
+
+Optional<size_t> LayerListWidget::gadget_at(const Gfx::IntPoint& position)
+{
+ for (size_t i = 0; i < m_gadgets.size(); ++i) {
+ if (m_gadgets[i].rect.contains(position))
+ return i;
+ }
+ return {};
+}
+
+void LayerListWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ auto gadget_index = gadget_at(event.position());
+ if (!gadget_index.has_value()) {
+ if (on_layer_select)
+ on_layer_select(nullptr);
+ return;
+ }
+ m_moving_gadget_index = gadget_index;
+ m_moving_event_origin = event.position();
+ auto& gadget = m_gadgets[m_moving_gadget_index.value()];
+ auto& layer = m_image->layer(gadget_index.value());
+ set_selected_layer(&layer);
+ gadget.is_moving = true;
+ gadget.movement_delta = {};
+ update();
+}
+
+void LayerListWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (!m_moving_gadget_index.has_value())
+ return;
+
+ auto delta = event.position() - m_moving_event_origin;
+ auto& gadget = m_gadgets[m_moving_gadget_index.value()];
+ ASSERT(gadget.is_moving);
+ gadget.movement_delta = delta;
+ relayout_gadgets();
+}
+
+void LayerListWidget::mouseup_event(GUI::MouseEvent& event)
+{
+ if (!m_image)
+ return;
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ if (!m_moving_gadget_index.has_value())
+ return;
+
+ size_t old_index = m_moving_gadget_index.value();
+ size_t new_index = hole_index_during_move();
+ if (new_index >= m_image->layer_count())
+ new_index = m_image->layer_count() - 1;
+
+ m_moving_gadget_index = {};
+ m_image->change_layer_index(old_index, new_index);
+}
+
+void LayerListWidget::image_did_add_layer(size_t layer_index)
+{
+ if (m_moving_gadget_index.has_value()) {
+ m_gadgets[m_moving_gadget_index.value()].is_moving = false;
+ m_moving_gadget_index = {};
+ }
+ Gadget gadget { layer_index, {}, {}, false, {} };
+ m_gadgets.insert(layer_index, move(gadget));
+ relayout_gadgets();
+}
+
+void LayerListWidget::image_did_remove_layer(size_t layer_index)
+{
+ if (m_moving_gadget_index.has_value()) {
+ m_gadgets[m_moving_gadget_index.value()].is_moving = false;
+ m_moving_gadget_index = {};
+ }
+ m_gadgets.remove(layer_index);
+ relayout_gadgets();
+}
+
+void LayerListWidget::image_did_modify_layer(size_t layer_index)
+{
+ update(m_gadgets[layer_index].rect);
+}
+
+void LayerListWidget::image_did_modify_layer_stack()
+{
+ rebuild_gadgets();
+}
+
+static constexpr int gadget_height = 30;
+static constexpr int gadget_spacing = 1;
+static constexpr int vertical_step = gadget_height + gadget_spacing;
+
+size_t LayerListWidget::hole_index_during_move() const
+{
+ ASSERT(is_moving_gadget());
+ auto& moving_gadget = m_gadgets[m_moving_gadget_index.value()];
+ int center_y_of_moving_gadget = moving_gadget.rect.translated(0, moving_gadget.movement_delta.y()).center().y();
+ return center_y_of_moving_gadget / vertical_step;
+}
+
+void LayerListWidget::select_bottom_layer()
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ set_selected_layer(&m_image->layer(0));
+}
+
+void LayerListWidget::select_top_layer()
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ set_selected_layer(&m_image->layer(m_image->layer_count() - 1));
+}
+
+void LayerListWidget::move_selection(int delta)
+{
+ if (!m_image || !m_image->layer_count())
+ return;
+ int new_layer_index = min(max(0, (int)m_image->layer_count() + delta), (int)m_image->layer_count() - 1);
+ set_selected_layer(&m_image->layer(new_layer_index));
+}
+
+void LayerListWidget::relayout_gadgets()
+{
+ int y = 0;
+
+ Optional<size_t> hole_index;
+ if (is_moving_gadget())
+ hole_index = hole_index_during_move();
+
+ size_t index = 0;
+ for (auto& gadget : m_gadgets) {
+ if (gadget.is_moving)
+ continue;
+ if (hole_index.has_value() && index == hole_index.value())
+ y += vertical_step;
+ gadget.rect = { 0, y, width(), gadget_height };
+ y += vertical_step;
+ ++index;
+ }
+
+ update();
+}
+
+void LayerListWidget::set_selected_layer(Layer* layer)
+{
+ if (!m_image)
+ return;
+ for (size_t i = 0; i < m_image->layer_count(); ++i)
+ m_image->layer(i).set_selected(layer == &m_image->layer(i));
+ if (on_layer_select)
+ on_layer_select(layer);
+ update();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerListWidget.h b/Userland/Applications/PixelPaint/LayerListWidget.h
new file mode 100644
index 0000000000..9550913b8e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerListWidget.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Image.h"
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class LayerListWidget final
+ : public GUI::Widget
+ , ImageClient {
+ C_OBJECT(LayerListWidget);
+
+public:
+ virtual ~LayerListWidget() override;
+
+ void set_image(Image*);
+
+ void set_selected_layer(Layer*);
+ Function<void(Layer*)> on_layer_select;
+
+ void select_bottom_layer();
+ void select_top_layer();
+ void move_selection(int delta);
+
+private:
+ explicit LayerListWidget();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ virtual void image_did_add_layer(size_t) override;
+ virtual void image_did_remove_layer(size_t) override;
+ virtual void image_did_modify_layer(size_t) override;
+ virtual void image_did_modify_layer_stack() override;
+
+ void rebuild_gadgets();
+ void relayout_gadgets();
+
+ size_t hole_index_during_move() const;
+
+ struct Gadget {
+ size_t layer_index { 0 };
+ Gfx::IntRect rect;
+ Gfx::IntRect temporary_rect_during_move;
+ bool is_moving { false };
+ Gfx::IntPoint movement_delta;
+ };
+
+ bool is_moving_gadget() const { return m_moving_gadget_index.has_value(); }
+
+ Optional<size_t> gadget_at(const Gfx::IntPoint&);
+
+ Vector<Gadget> m_gadgets;
+ RefPtr<Image> m_image;
+
+ Optional<size_t> m_moving_gadget_index;
+ Gfx::IntPoint m_moving_event_origin;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp b/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp
new file mode 100644
index 0000000000..cfdee65c0a
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerPropertiesWidget.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LayerPropertiesWidget.h"
+#include "Layer.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/OpacitySlider.h>
+#include <LibGUI/TextBox.h>
+#include <LibGfx/Font.h>
+
+namespace PixelPaint {
+
+LayerPropertiesWidget::LayerPropertiesWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+
+ auto& group_box = add<GUI::GroupBox>("Layer properties");
+ auto& layout = group_box.set_layout<GUI::VerticalBoxLayout>();
+
+ layout.set_margins({ 10, 20, 10, 10 });
+
+ auto& name_container = group_box.add<GUI::Widget>();
+ name_container.set_fixed_height(20);
+ name_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& name_label = name_container.add<GUI::Label>("Name:");
+ name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ name_label.set_fixed_size(80, 20);
+
+ m_name_textbox = name_container.add<GUI::TextBox>();
+ m_name_textbox->set_fixed_height(20);
+ m_name_textbox->on_change = [this] {
+ if (m_layer)
+ m_layer->set_name(m_name_textbox->text());
+ };
+
+ auto& opacity_container = group_box.add<GUI::Widget>();
+ opacity_container.set_fixed_height(20);
+ opacity_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& opacity_label = opacity_container.add<GUI::Label>("Opacity:");
+ opacity_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ opacity_label.set_fixed_size(80, 20);
+
+ m_opacity_slider = opacity_container.add<GUI::OpacitySlider>();
+ m_opacity_slider->set_range(0, 100);
+ m_opacity_slider->on_change = [this](int value) {
+ if (m_layer)
+ m_layer->set_opacity_percent(value);
+ };
+
+ m_visibility_checkbox = group_box.add<GUI::CheckBox>("Visible");
+ m_visibility_checkbox->set_fixed_height(20);
+ m_visibility_checkbox->on_checked = [this](bool checked) {
+ if (m_layer)
+ m_layer->set_visible(checked);
+ };
+}
+
+LayerPropertiesWidget::~LayerPropertiesWidget()
+{
+}
+
+void LayerPropertiesWidget::set_layer(Layer* layer)
+{
+ if (m_layer == layer)
+ return;
+
+ if (layer) {
+ m_layer = layer->make_weak_ptr();
+ m_name_textbox->set_text(layer->name());
+ m_opacity_slider->set_value(layer->opacity_percent());
+ m_visibility_checkbox->set_checked(layer->is_visible());
+ set_enabled(true);
+ } else {
+ m_layer = nullptr;
+ set_enabled(false);
+ }
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LayerPropertiesWidget.h b/Userland/Applications/PixelPaint/LayerPropertiesWidget.h
new file mode 100644
index 0000000000..e07e02ec93
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LayerPropertiesWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class Layer;
+
+class LayerPropertiesWidget final : public GUI::Widget {
+ C_OBJECT(LayerPropertiesWidget);
+
+public:
+ virtual ~LayerPropertiesWidget() override;
+
+ void set_layer(Layer*);
+
+private:
+ LayerPropertiesWidget();
+
+ RefPtr<GUI::CheckBox> m_visibility_checkbox;
+ RefPtr<GUI::OpacitySlider> m_opacity_slider;
+ RefPtr<GUI::TextBox> m_name_textbox;
+
+ WeakPtr<Layer> m_layer;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/LineTool.cpp b/Userland/Applications/PixelPaint/LineTool.cpp
new file mode 100644
index 0000000000..7c3e37e836
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LineTool.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "LineTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+static Gfx::IntPoint constrain_line_angle(const Gfx::IntPoint& start_pos, const Gfx::IntPoint& end_pos, float angle_increment)
+{
+ float current_angle = atan2(end_pos.y() - start_pos.y(), end_pos.x() - start_pos.x()) + M_PI * 2.;
+
+ float constrained_angle = ((int)((current_angle + angle_increment / 2.) / angle_increment)) * angle_increment;
+
+ auto diff = end_pos - start_pos;
+ float line_length = sqrt(diff.x() * diff.x() + diff.y() * diff.y());
+
+ return { start_pos.x() + (int)(cos(constrained_angle) * line_length),
+ start_pos.y() + (int)(sin(constrained_angle) * line_length) };
+}
+
+LineTool::LineTool()
+{
+}
+
+LineTool::~LineTool()
+{
+}
+
+void LineTool::on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent&)
+{
+ if (layer_event.button() != GUI::MouseButton::Left && layer_event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = layer_event.button();
+
+ m_line_start_position = layer_event.position();
+ m_line_end_position = layer_event.position();
+
+ m_editor->update();
+}
+
+void LineTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ painter.draw_line(m_line_start_position, m_line_end_position, m_editor->color_for(m_drawing_button), m_thickness);
+ m_drawing_button = GUI::MouseButton::None;
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+ }
+}
+
+void LineTool::on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ if (!m_constrain_angle) {
+ m_line_end_position = layer_event.position();
+ } else {
+ const float ANGLE_STEP = M_PI / 8.0f;
+ m_line_end_position = constrain_line_angle(m_line_start_position, layer_event.position(), ANGLE_STEP);
+ }
+ m_editor->update();
+}
+
+void LineTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto preview_start = m_editor->layer_position_to_editor_position(layer, m_line_start_position).to_type<int>();
+ auto preview_end = m_editor->layer_position_to_editor_position(layer, m_line_end_position).to_type<int>();
+ painter.draw_line(preview_start, preview_end, m_editor->color_for(m_drawing_button), m_thickness);
+}
+
+void LineTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+
+ if (event.key() == Key_Shift) {
+ m_constrain_angle = true;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void LineTool::on_keyup(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Shift) {
+ m_constrain_angle = false;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void LineTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/LineTool.h b/Userland/Applications/PixelPaint/LineTool.h
new file mode 100644
index 0000000000..aa8757655c
--- /dev/null
+++ b/Userland/Applications/PixelPaint/LineTool.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class LineTool final : public Tool {
+public:
+ LineTool();
+ virtual ~LineTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+ virtual void on_keyup(GUI::KeyEvent&) override;
+
+private:
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_line_start_position;
+ Gfx::IntPoint m_line_end_position;
+
+ RefPtr<GUI::Menu> m_context_menu;
+ GUI::ActionGroup m_thickness_actions;
+ int m_thickness { 1 };
+ bool m_constrain_angle { false };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/MoveTool.cpp b/Userland/Applications/PixelPaint/MoveTool.cpp
new file mode 100644
index 0000000000..ebeba5ba89
--- /dev/null
+++ b/Userland/Applications/PixelPaint/MoveTool.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MoveTool.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+MoveTool::MoveTool()
+{
+}
+
+MoveTool::~MoveTool()
+{
+}
+
+void MoveTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent& image_event)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ if (!layer.rect().contains(event.position()))
+ return;
+ m_layer_being_moved = layer;
+ m_event_origin = image_event.position();
+ m_layer_origin = layer.location();
+ m_editor->window()->set_cursor(Gfx::StandardCursor::Move);
+}
+
+void MoveTool::on_mousemove(Layer&, GUI::MouseEvent&, GUI::MouseEvent& image_event)
+{
+ if (!m_layer_being_moved)
+ return;
+ auto delta = image_event.position() - m_event_origin;
+ m_layer_being_moved->set_location(m_layer_origin.translated(delta));
+ m_editor->layers_did_change();
+}
+
+void MoveTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ m_layer_being_moved = nullptr;
+ m_editor->window()->set_cursor(Gfx::StandardCursor::None);
+ m_editor->did_complete_action();
+}
+
+void MoveTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.modifiers() != 0)
+ return;
+
+ auto* layer = m_editor->active_layer();
+ if (!layer)
+ return;
+
+ auto new_location = layer->location();
+
+ switch (event.key()) {
+ case Key_Up:
+ new_location.move_by(0, -1);
+ break;
+ case Key_Down:
+ new_location.move_by(0, 1);
+ break;
+ case Key_Left:
+ new_location.move_by(-1, 0);
+ break;
+ case Key_Right:
+ new_location.move_by(1, 0);
+ break;
+ default:
+ return;
+ }
+
+ layer->set_location(new_location);
+ m_editor->layers_did_change();
+}
+
+void MoveTool::on_context_menu(Layer& layer, GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::CommonActions::make_move_to_front_action(
+ [this](auto&) {
+ m_editor->image()->move_layer_to_front(*m_context_menu_layer);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ m_context_menu->add_action(GUI::CommonActions::make_move_to_back_action(
+ [this](auto&) {
+ m_editor->image()->move_layer_to_back(*m_context_menu_layer);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ m_context_menu->add_separator();
+ m_context_menu->add_action(GUI::Action::create(
+ "Delete layer", Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"), [this](auto&) {
+ m_editor->image()->remove_layer(*m_context_menu_layer);
+ // FIXME: This should not be done imperatively here. Perhaps a Image::Client interface that ImageEditor can implement?
+ if (m_editor->active_layer() == m_context_menu_layer)
+ m_editor->set_active_layer(nullptr);
+ m_editor->layers_did_change();
+ },
+ m_editor));
+ }
+ m_context_menu_layer = layer;
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/MoveTool.h b/Userland/Applications/PixelPaint/MoveTool.h
new file mode 100644
index 0000000000..a0c16e5282
--- /dev/null
+++ b/Userland/Applications/PixelPaint/MoveTool.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class MoveTool final : public Tool {
+public:
+ MoveTool();
+ virtual ~MoveTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+ virtual void on_context_menu(Layer&, GUI::ContextMenuEvent&) override;
+
+private:
+ RefPtr<Layer> m_layer_being_moved;
+ Gfx::IntPoint m_event_origin;
+ Gfx::IntPoint m_layer_origin;
+ RefPtr<GUI::Menu> m_context_menu;
+ RefPtr<Layer> m_context_menu_layer;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PaletteWidget.cpp b/Userland/Applications/PixelPaint/PaletteWidget.cpp
new file mode 100644
index 0000000000..52620b4b31
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PaletteWidget.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PaletteWidget.h"
+#include "ImageEditor.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/ColorPicker.h>
+#include <LibGfx/Palette.h>
+
+namespace PixelPaint {
+
+class ColorWidget : public GUI::Frame {
+ C_OBJECT(ColorWidget);
+
+public:
+ explicit ColorWidget(Color color, PaletteWidget& palette_widget)
+ : m_palette_widget(palette_widget)
+ , m_color(color)
+ {
+ }
+
+ virtual ~ColorWidget() override
+ {
+ }
+
+ virtual void mousedown_event(GUI::MouseEvent& event) override
+ {
+ if (event.modifiers() & KeyModifier::Mod_Ctrl && event.button() == GUI::MouseButton::Left) {
+ auto dialog = GUI::ColorPicker::construct(m_color, window());
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ m_color = dialog->color();
+ auto pal = palette();
+ pal.set_color(ColorRole::Background, m_color);
+ set_palette(pal);
+ update();
+ }
+ return;
+ }
+
+ if (event.button() == GUI::MouseButton::Left)
+ m_palette_widget.set_primary_color(m_color);
+ else if (event.button() == GUI::MouseButton::Right)
+ m_palette_widget.set_secondary_color(m_color);
+ }
+
+private:
+ PaletteWidget& m_palette_widget;
+ Color m_color;
+};
+
+PaletteWidget::PaletteWidget(ImageEditor& editor)
+ : m_editor(editor)
+{
+ set_frame_shape(Gfx::FrameShape::Panel);
+ set_frame_shadow(Gfx::FrameShadow::Raised);
+ set_frame_thickness(0);
+ set_fill_with_background_color(true);
+
+ set_fixed_height(34);
+
+ m_secondary_color_widget = add<GUI::Frame>();
+ m_secondary_color_widget->set_relative_rect({ 2, 2, 60, 31 });
+ m_secondary_color_widget->set_fill_with_background_color(true);
+ set_secondary_color(m_editor.secondary_color());
+
+ m_primary_color_widget = add<GUI::Frame>();
+ Gfx::IntRect rect { 0, 0, 38, 15 };
+ rect.center_within(m_secondary_color_widget->relative_rect());
+ m_primary_color_widget->set_relative_rect(rect);
+ m_primary_color_widget->set_fill_with_background_color(true);
+ set_primary_color(m_editor.primary_color());
+
+ m_editor.on_primary_color_change = [this](Color color) {
+ set_primary_color(color);
+ };
+
+ m_editor.on_secondary_color_change = [this](Color color) {
+ set_secondary_color(color);
+ };
+
+ auto& color_container = add<GUI::Widget>();
+ color_container.set_relative_rect(m_secondary_color_widget->relative_rect().right() + 2, 2, 500, 32);
+ color_container.set_layout<GUI::VerticalBoxLayout>();
+ color_container.layout()->set_spacing(1);
+
+ auto& top_color_container = color_container.add<GUI::Widget>();
+ top_color_container.set_layout<GUI::HorizontalBoxLayout>();
+ top_color_container.layout()->set_spacing(1);
+
+ auto& bottom_color_container = color_container.add<GUI::Widget>();
+ bottom_color_container.set_layout<GUI::HorizontalBoxLayout>();
+ bottom_color_container.layout()->set_spacing(1);
+
+ auto add_color_widget = [&](GUI::Widget& container, Color color) {
+ auto& color_widget = container.add<ColorWidget>(color, *this);
+ color_widget.set_fill_with_background_color(true);
+ auto pal = color_widget.palette();
+ pal.set_color(ColorRole::Background, color);
+ color_widget.set_palette(pal);
+ };
+
+ add_color_widget(top_color_container, Color::from_rgb(0x000000));
+ add_color_widget(top_color_container, Color::from_rgb(0x808080));
+ add_color_widget(top_color_container, Color::from_rgb(0x800000));
+ add_color_widget(top_color_container, Color::from_rgb(0x808000));
+ add_color_widget(top_color_container, Color::from_rgb(0x008000));
+ add_color_widget(top_color_container, Color::from_rgb(0x008080));
+ add_color_widget(top_color_container, Color::from_rgb(0x000080));
+ add_color_widget(top_color_container, Color::from_rgb(0x800080));
+ add_color_widget(top_color_container, Color::from_rgb(0x808040));
+ add_color_widget(top_color_container, Color::from_rgb(0x004040));
+ add_color_widget(top_color_container, Color::from_rgb(0x0080ff));
+ add_color_widget(top_color_container, Color::from_rgb(0x004080));
+ add_color_widget(top_color_container, Color::from_rgb(0x8000ff));
+ add_color_widget(top_color_container, Color::from_rgb(0x804000));
+
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xc0c0c0));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff0000));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffff00));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ff00));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x0000ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff00ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xffff80));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x00ff80));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x80ffff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0x8080ff));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff0080));
+ add_color_widget(bottom_color_container, Color::from_rgb(0xff8040));
+}
+
+PaletteWidget::~PaletteWidget()
+{
+}
+
+void PaletteWidget::set_primary_color(Color color)
+{
+ m_editor.set_primary_color(color);
+ auto pal = m_primary_color_widget->palette();
+ pal.set_color(ColorRole::Background, color);
+ m_primary_color_widget->set_palette(pal);
+ m_primary_color_widget->update();
+}
+
+void PaletteWidget::set_secondary_color(Color color)
+{
+ m_editor.set_secondary_color(color);
+ auto pal = m_secondary_color_widget->palette();
+ pal.set_color(ColorRole::Background, color);
+ m_secondary_color_widget->set_palette(pal);
+ m_secondary_color_widget->update();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PaletteWidget.h b/Userland/Applications/PixelPaint/PaletteWidget.h
new file mode 100644
index 0000000000..02c843f038
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PaletteWidget.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+namespace PixelPaint {
+
+class ImageEditor;
+
+class PaletteWidget final : public GUI::Frame {
+ C_OBJECT(PaletteWidget);
+
+public:
+ virtual ~PaletteWidget() override;
+
+ void set_primary_color(Color);
+ void set_secondary_color(Color);
+
+private:
+ explicit PaletteWidget(ImageEditor&);
+
+ ImageEditor& m_editor;
+ RefPtr<GUI::Frame> m_primary_color_widget;
+ RefPtr<GUI::Frame> m_secondary_color_widget;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PenTool.cpp b/Userland/Applications/PixelPaint/PenTool.cpp
new file mode 100644
index 0000000000..579231edf7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PenTool.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PenTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+
+namespace PixelPaint {
+
+PenTool::PenTool()
+{
+}
+
+PenTool::~PenTool()
+{
+}
+
+void PenTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ GUI::Painter painter(layer.bitmap());
+ painter.draw_line(event.position(), event.position(), m_editor->color_for(event), m_thickness);
+ layer.did_modify_bitmap(*m_editor->image());
+ m_last_drawing_event_position = event.position();
+}
+
+void PenTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) {
+ m_last_drawing_event_position = { -1, -1 };
+ m_editor->did_complete_action();
+ }
+}
+
+void PenTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!(event.buttons() & GUI::MouseButton::Left || event.buttons() & GUI::MouseButton::Right))
+ return;
+ GUI::Painter painter(layer.bitmap());
+
+ if (m_last_drawing_event_position != Gfx::IntPoint(-1, -1))
+ painter.draw_line(m_last_drawing_event_position, event.position(), m_editor->color_for(event), m_thickness);
+ else
+ painter.draw_line(event.position(), event.position(), m_editor->color_for(event), m_thickness);
+ layer.did_modify_bitmap(*m_editor->image());
+
+ m_last_drawing_event_position = event.position();
+}
+
+void PenTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+GUI::Widget* PenTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& thickness_container = m_properties_widget->add<GUI::Widget>();
+ thickness_container.set_fixed_height(20);
+ thickness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& thickness_label = thickness_container.add<GUI::Label>("Thickness:");
+ thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ thickness_label.set_fixed_size(80, 20);
+
+ auto& thickness_slider = thickness_container.add<GUI::HorizontalSlider>();
+ thickness_slider.set_fixed_height(20);
+ thickness_slider.set_range(1, 20);
+ thickness_slider.set_value(m_thickness);
+ thickness_slider.on_change = [this](int value) {
+ m_thickness = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PenTool.h b/Userland/Applications/PixelPaint/PenTool.h
new file mode 100644
index 0000000000..137e58251e
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PenTool.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/ActionGroup.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class PenTool final : public Tool {
+public:
+ PenTool();
+ virtual ~PenTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ Gfx::IntPoint m_last_drawing_event_position { -1, -1 };
+ RefPtr<GUI::Menu> m_context_menu;
+ RefPtr<GUI::Widget> m_properties_widget;
+ int m_thickness { 1 };
+ GUI::ActionGroup m_thickness_actions;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/PickerTool.cpp b/Userland/Applications/PixelPaint/PickerTool.cpp
new file mode 100644
index 0000000000..86a7acf961
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PickerTool.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PickerTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGfx/Bitmap.h>
+
+namespace PixelPaint {
+
+PickerTool::PickerTool()
+{
+}
+
+PickerTool::~PickerTool()
+{
+}
+
+void PickerTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (!layer.rect().contains(event.position()))
+ return;
+ auto color = layer.bitmap().get_pixel(event.position());
+ if (event.button() == GUI::MouseButton::Left)
+ m_editor->set_primary_color(color);
+ else if (event.button() == GUI::MouseButton::Right)
+ m_editor->set_secondary_color(color);
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/PickerTool.h b/Userland/Applications/PixelPaint/PickerTool.h
new file mode 100644
index 0000000000..163a0aeeee
--- /dev/null
+++ b/Userland/Applications/PixelPaint/PickerTool.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+
+namespace PixelPaint {
+
+class PickerTool final : public Tool {
+public:
+ PickerTool();
+ virtual ~PickerTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/RectangleTool.cpp b/Userland/Applications/PixelPaint/RectangleTool.cpp
new file mode 100644
index 0000000000..a5b023f18b
--- /dev/null
+++ b/Userland/Applications/PixelPaint/RectangleTool.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "RectangleTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Rect.h>
+#include <math.h>
+
+namespace PixelPaint {
+
+RectangleTool::RectangleTool()
+{
+}
+
+RectangleTool::~RectangleTool()
+{
+}
+
+void RectangleTool::draw_using(GUI::Painter& painter, const Gfx::IntRect& rect)
+{
+ switch (m_mode) {
+ case Mode::Fill:
+ painter.fill_rect(rect, m_editor->color_for(m_drawing_button));
+ break;
+ case Mode::Outline:
+ painter.draw_rect(rect, m_editor->color_for(m_drawing_button));
+ break;
+ case Mode::Gradient:
+ painter.fill_rect_with_gradient(rect, m_editor->primary_color(), m_editor->secondary_color());
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+void RectangleTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right)
+ return;
+
+ if (m_drawing_button != GUI::MouseButton::None)
+ return;
+
+ m_drawing_button = event.button();
+ m_rectangle_start_position = event.position();
+ m_rectangle_end_position = event.position();
+ m_editor->update();
+}
+
+void RectangleTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (event.button() == m_drawing_button) {
+ GUI::Painter painter(layer.bitmap());
+ auto rect = Gfx::IntRect::from_two_points(m_rectangle_start_position, m_rectangle_end_position);
+ draw_using(painter, rect);
+ m_drawing_button = GUI::MouseButton::None;
+ layer.did_modify_bitmap(*m_editor->image());
+ m_editor->did_complete_action();
+ }
+}
+
+void RectangleTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ m_rectangle_end_position = event.position();
+ m_editor->update();
+}
+
+void RectangleTool::on_second_paint(const Layer& layer, GUI::PaintEvent& event)
+{
+ if (m_drawing_button == GUI::MouseButton::None)
+ return;
+
+ GUI::Painter painter(*m_editor);
+ painter.add_clip_rect(event.rect());
+ auto rect = Gfx::IntRect::from_two_points(
+ m_editor->layer_position_to_editor_position(layer, m_rectangle_start_position).to_type<int>(),
+ m_editor->layer_position_to_editor_position(layer, m_rectangle_end_position).to_type<int>());
+ draw_using(painter, rect);
+}
+
+void RectangleTool::on_keydown(GUI::KeyEvent& event)
+{
+ if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
+ m_drawing_button = GUI::MouseButton::None;
+ m_editor->update();
+ event.accept();
+ }
+}
+
+void RectangleTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_context_menu->add_action(GUI::Action::create("Fill", [this](auto&) {
+ m_mode = Mode::Fill;
+ }));
+ m_context_menu->add_action(GUI::Action::create("Outline", [this](auto&) {
+ m_mode = Mode::Outline;
+ }));
+ m_context_menu->add_action(GUI::Action::create("Gradient", [this](auto&) {
+ m_mode = Mode::Gradient;
+ }));
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/RectangleTool.h b/Userland/Applications/PixelPaint/RectangleTool.h
new file mode 100644
index 0000000000..f7d8c53273
--- /dev/null
+++ b/Userland/Applications/PixelPaint/RectangleTool.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibGUI/Forward.h>
+#include <LibGfx/Point.h>
+
+namespace PixelPaint {
+
+class RectangleTool final : public Tool {
+public:
+ RectangleTool();
+ virtual ~RectangleTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) override;
+ virtual void on_keydown(GUI::KeyEvent&) override;
+
+private:
+ enum class Mode {
+ Outline,
+ Fill,
+ Gradient,
+ };
+
+ void draw_using(GUI::Painter&, const Gfx::IntRect&);
+
+ GUI::MouseButton m_drawing_button { GUI::MouseButton::None };
+ Gfx::IntPoint m_rectangle_start_position;
+ Gfx::IntPoint m_rectangle_end_position;
+ RefPtr<GUI::Menu> m_context_menu;
+ Mode m_mode { Mode::Outline };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/SprayTool.cpp b/Userland/Applications/PixelPaint/SprayTool.cpp
new file mode 100644
index 0000000000..9997c29674
--- /dev/null
+++ b/Userland/Applications/PixelPaint/SprayTool.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SprayTool.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include <AK/Queue.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Slider.h>
+#include <LibGfx/Bitmap.h>
+#include <math.h>
+#include <stdio.h>
+
+namespace PixelPaint {
+
+SprayTool::SprayTool()
+{
+ m_timer = Core::Timer::construct();
+ m_timer->on_timeout = [&]() {
+ paint_it();
+ };
+ m_timer->set_interval(200);
+}
+
+SprayTool::~SprayTool()
+{
+}
+
+static double nrand()
+{
+ return double(rand()) / double(RAND_MAX);
+}
+
+void SprayTool::paint_it()
+{
+ auto* layer = m_editor->active_layer();
+ if (!layer)
+ return;
+
+ auto& bitmap = layer->bitmap();
+ GUI::Painter painter(bitmap);
+ ASSERT(bitmap.bpp() == 32);
+ m_editor->update();
+ const double minimal_radius = 2;
+ const double base_radius = minimal_radius * m_thickness;
+ for (int i = 0; i < M_PI * base_radius * base_radius * (m_density / 100.0f); i++) {
+ double radius = base_radius * nrand();
+ double angle = 2 * M_PI * nrand();
+ const int xpos = m_last_pos.x() + radius * cos(angle);
+ const int ypos = m_last_pos.y() - radius * sin(angle);
+ if (xpos < 0 || xpos >= bitmap.width())
+ continue;
+ if (ypos < 0 || ypos >= bitmap.height())
+ continue;
+ bitmap.set_pixel<Gfx::StorageFormat::RGBA32>(xpos, ypos, m_color);
+ }
+
+ layer->did_modify_bitmap(*m_editor->image());
+}
+
+void SprayTool::on_mousedown(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ m_color = m_editor->color_for(event);
+ m_last_pos = event.position();
+ m_timer->start();
+ paint_it();
+}
+
+void SprayTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&)
+{
+ m_last_pos = event.position();
+ if (m_timer->is_active()) {
+ paint_it();
+ m_timer->restart(m_timer->interval());
+ }
+}
+
+void SprayTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&)
+{
+ if (m_timer->is_active()) {
+ m_timer->stop();
+ m_editor->did_complete_action();
+ }
+}
+
+void SprayTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event)
+{
+ if (!m_context_menu) {
+ m_context_menu = GUI::Menu::construct();
+ m_thickness_actions.set_exclusive(true);
+ auto insert_action = [&](int size, bool checked = false) {
+ auto action = GUI::Action::create_checkable(String::number(size), [this, size](auto&) {
+ m_thickness = size;
+ });
+ action->set_checked(checked);
+ m_thickness_actions.add_action(*action);
+ m_context_menu->add_action(move(action));
+ };
+ insert_action(1, true);
+ insert_action(2);
+ insert_action(3);
+ insert_action(4);
+ }
+ m_context_menu->popup(event.screen_position());
+}
+
+GUI::Widget* SprayTool::get_properties_widget()
+{
+ if (!m_properties_widget) {
+ m_properties_widget = GUI::Widget::construct();
+ m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
+
+ auto& thickness_container = m_properties_widget->add<GUI::Widget>();
+ thickness_container.set_fixed_height(20);
+ thickness_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& thickness_label = thickness_container.add<GUI::Label>("Thickness:");
+ thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ thickness_label.set_fixed_size(80, 20);
+
+ auto& thickness_slider = thickness_container.add<GUI::HorizontalSlider>();
+ thickness_slider.set_fixed_height(20);
+ thickness_slider.set_range(1, 20);
+ thickness_slider.set_value(m_thickness);
+ thickness_slider.on_change = [this](int value) {
+ m_thickness = value;
+ };
+
+ auto& density_container = m_properties_widget->add<GUI::Widget>();
+ density_container.set_fixed_height(20);
+ density_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ auto& density_label = density_container.add<GUI::Label>("Density:");
+ density_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ density_label.set_fixed_size(80, 20);
+
+ auto& density_slider = density_container.add<GUI::HorizontalSlider>();
+ density_slider.set_fixed_height(30);
+ density_slider.set_range(1, 100);
+ density_slider.set_value(m_density);
+ density_slider.on_change = [this](int value) {
+ m_density = value;
+ };
+ }
+
+ return m_properties_widget.ptr();
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/SprayTool.h b/Userland/Applications/PixelPaint/SprayTool.h
new file mode 100644
index 0000000000..a3e55efd52
--- /dev/null
+++ b/Userland/Applications/PixelPaint/SprayTool.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Tool.h"
+#include <LibCore/Timer.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Painter.h>
+
+namespace PixelPaint {
+
+class SprayTool final : public Tool {
+public:
+ SprayTool();
+ virtual ~SprayTool() override;
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override;
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override;
+ virtual GUI::Widget* get_properties_widget() override;
+
+private:
+ void paint_it();
+
+ RefPtr<GUI::Widget> m_properties_widget;
+ RefPtr<Core::Timer> m_timer;
+ Gfx::IntPoint m_last_pos;
+ Color m_color;
+ RefPtr<GUI::Menu> m_context_menu;
+ GUI::ActionGroup m_thickness_actions;
+ int m_thickness { 10 };
+ int m_density { 40 };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/Tool.cpp b/Userland/Applications/PixelPaint/Tool.cpp
new file mode 100644
index 0000000000..846e017410
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Tool.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Tool.h"
+#include "ImageEditor.h"
+#include <LibGUI/Action.h>
+
+namespace PixelPaint {
+
+Tool::Tool()
+{
+}
+
+Tool::~Tool()
+{
+}
+
+void Tool::setup(ImageEditor& editor)
+{
+ m_editor = editor;
+}
+
+void Tool::set_action(GUI::Action* action)
+{
+ m_action = action;
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/Tool.h b/Userland/Applications/PixelPaint/Tool.h
new file mode 100644
index 0000000000..e7deb5f1cb
--- /dev/null
+++ b/Userland/Applications/PixelPaint/Tool.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Event.h>
+#include <LibGUI/Forward.h>
+
+namespace PixelPaint {
+
+class ImageEditor;
+class Layer;
+
+class Tool {
+public:
+ virtual ~Tool();
+
+ virtual void on_mousedown(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_mousemove(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { }
+ virtual void on_context_menu(Layer&, GUI::ContextMenuEvent&) { }
+ virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) { }
+ virtual void on_second_paint(const Layer&, GUI::PaintEvent&) { }
+ virtual void on_keydown(GUI::KeyEvent&) { }
+ virtual void on_keyup(GUI::KeyEvent&) { }
+ virtual GUI::Widget* get_properties_widget() { return nullptr; }
+
+ void clear() { m_editor = nullptr; }
+ void setup(ImageEditor&);
+
+ GUI::Action* action() { return m_action; }
+ void set_action(GUI::Action*);
+
+protected:
+ Tool();
+ WeakPtr<ImageEditor> m_editor;
+ RefPtr<GUI::Action> m_action;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp b/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp
new file mode 100644
index 0000000000..a27103d3e7
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolPropertiesWidget.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ToolPropertiesWidget.h"
+#include "Tool.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/GroupBox.h>
+
+namespace PixelPaint {
+
+ToolPropertiesWidget::ToolPropertiesWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+
+ m_group_box = add<GUI::GroupBox>("Tool properties");
+ auto& layout = m_group_box->set_layout<GUI::VerticalBoxLayout>();
+ layout.set_margins({ 10, 20, 10, 10 });
+}
+
+void ToolPropertiesWidget::set_active_tool(Tool* tool)
+{
+ if (tool == m_active_tool)
+ return;
+
+ if (m_active_tool_widget != nullptr)
+ m_group_box->remove_child(*m_active_tool_widget);
+
+ m_active_tool = tool;
+ m_active_tool_widget = tool->get_properties_widget();
+ if (m_active_tool_widget != nullptr)
+ m_group_box->add_child(*m_active_tool_widget);
+}
+
+ToolPropertiesWidget::~ToolPropertiesWidget()
+{
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolPropertiesWidget.h b/Userland/Applications/PixelPaint/ToolPropertiesWidget.h
new file mode 100644
index 0000000000..41c1675cfa
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolPropertiesWidget.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/RefPtr.h>
+#include <LibGUI/Forward.h>
+#include <LibGUI/Widget.h>
+
+namespace PixelPaint {
+
+class Tool;
+
+class ToolPropertiesWidget final : public GUI::Widget {
+ C_OBJECT(ToolPropertiesWidget);
+
+public:
+ virtual ~ToolPropertiesWidget() override;
+
+ void set_active_tool(Tool*);
+
+private:
+ ToolPropertiesWidget();
+
+ RefPtr<GUI::GroupBox> m_group_box;
+
+ Tool* m_active_tool { nullptr };
+ GUI::Widget* m_active_tool_widget { nullptr };
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.cpp b/Userland/Applications/PixelPaint/ToolboxWidget.cpp
new file mode 100644
index 0000000000..c746901887
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolboxWidget.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ToolboxWidget.h"
+#include "BrushTool.h"
+#include "BucketTool.h"
+#include "EllipseTool.h"
+#include "EraseTool.h"
+#include "LineTool.h"
+#include "MoveTool.h"
+#include "PenTool.h"
+#include "PickerTool.h"
+#include "RectangleTool.h"
+#include "SprayTool.h"
+#include <AK/StringBuilder.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Window.h>
+
+namespace PixelPaint {
+
+class ToolButton final : public GUI::Button {
+ C_OBJECT(ToolButton)
+public:
+ ToolButton(ToolboxWidget& toolbox, const String& name, const GUI::Shortcut& shortcut, OwnPtr<Tool> tool)
+ : m_toolbox(toolbox)
+ , m_tool(move(tool))
+ {
+ StringBuilder builder;
+ builder.append(name);
+ builder.append(" (");
+ builder.append(shortcut.to_string());
+ builder.append(")");
+ set_tooltip(builder.to_string());
+
+ m_action = GUI::Action::create_checkable(
+ name, shortcut, [this](auto& action) {
+ if (action.is_checked())
+ m_toolbox.on_tool_selection(m_tool);
+ else
+ m_toolbox.on_tool_selection(nullptr);
+ },
+ toolbox.window());
+
+ m_tool->set_action(m_action);
+ set_action(*m_action);
+ m_toolbox.m_action_group.add_action(*m_action);
+ }
+
+ const Tool& tool() const { return *m_tool; }
+ Tool& tool() { return *m_tool; }
+
+ virtual bool is_uncheckable() const override { return false; }
+
+ virtual void context_menu_event(GUI::ContextMenuEvent& event) override
+ {
+ m_action->activate();
+ m_tool->on_tool_button_contextmenu(event);
+ }
+
+private:
+ ToolboxWidget& m_toolbox;
+ OwnPtr<Tool> m_tool;
+ RefPtr<GUI::Action> m_action;
+};
+
+ToolboxWidget::ToolboxWidget()
+{
+ set_fill_with_background_color(true);
+
+ set_frame_thickness(1);
+ set_frame_shape(Gfx::FrameShape::Panel);
+ set_frame_shadow(Gfx::FrameShadow::Raised);
+
+ set_fixed_width(48);
+
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+
+ m_action_group.set_exclusive(true);
+ m_action_group.set_unchecking_allowed(false);
+
+ deferred_invoke([this](auto&) {
+ setup_tools();
+ });
+}
+
+ToolboxWidget::~ToolboxWidget()
+{
+}
+
+void ToolboxWidget::setup_tools()
+{
+ auto add_tool = [&](const StringView& name, const StringView& icon_name, const GUI::Shortcut& shortcut, NonnullOwnPtr<Tool> tool) -> ToolButton& {
+ m_tools.append(tool.ptr());
+ auto& button = add<ToolButton>(*this, name, shortcut, move(tool));
+ button.set_focus_policy(GUI::FocusPolicy::TabFocus);
+ button.set_fixed_height(32);
+ button.set_checkable(true);
+ button.set_icon(Gfx::Bitmap::load_from_file(String::formatted("/res/icons/pixelpaint/{}.png", icon_name)));
+ return button;
+ };
+
+ add_tool("Move", "move", { 0, Key_M }, make<MoveTool>());
+ add_tool("Pen", "pen", { 0, Key_N }, make<PenTool>());
+ add_tool("Brush", "brush", { 0, Key_P }, make<BrushTool>());
+ add_tool("Bucket Fill", "bucket", { Mod_Shift, Key_B }, make<BucketTool>());
+ add_tool("Spray", "spray", { Mod_Shift, Key_S }, make<SprayTool>());
+ add_tool("Color Picker", "picker", { 0, Key_O }, make<PickerTool>());
+ add_tool("Erase", "eraser", { Mod_Shift, Key_E }, make<EraseTool>());
+ add_tool("Line", "line", { Mod_Ctrl | Mod_Shift, Key_L }, make<LineTool>());
+ add_tool("Rectangle", "rectangle", { Mod_Ctrl | Mod_Shift, Key_R }, make<RectangleTool>());
+ add_tool("Ellipse", "circle", { Mod_Ctrl | Mod_Shift, Key_E }, make<EllipseTool>());
+}
+
+}
diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.h b/Userland/Applications/PixelPaint/ToolboxWidget.h
new file mode 100644
index 0000000000..23794179f6
--- /dev/null
+++ b/Userland/Applications/PixelPaint/ToolboxWidget.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Frame.h>
+
+namespace PixelPaint {
+
+class Tool;
+
+class ToolboxWidget final : public GUI::Frame {
+ C_OBJECT(ToolboxWidget)
+public:
+ virtual ~ToolboxWidget() override;
+
+ Function<void(Tool*)> on_tool_selection;
+
+ template<typename Callback>
+ void for_each_tool(Callback callback)
+ {
+ for (auto& tool : m_tools)
+ callback(*tool);
+ }
+
+private:
+ friend class ToolButton;
+
+ void setup_tools();
+
+ explicit ToolboxWidget();
+ GUI::ActionGroup m_action_group;
+ Vector<Tool*> m_tools;
+};
+
+}
diff --git a/Userland/Applications/PixelPaint/main.cpp b/Userland/Applications/PixelPaint/main.cpp
new file mode 100644
index 0000000000..02164a52ce
--- /dev/null
+++ b/Userland/Applications/PixelPaint/main.cpp
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CreateNewImageDialog.h"
+#include "CreateNewLayerDialog.h"
+#include "FilterParams.h"
+#include "Image.h"
+#include "ImageEditor.h"
+#include "Layer.h"
+#include "LayerListWidget.h"
+#include "LayerPropertiesWidget.h"
+#include "PaletteWidget.h"
+#include "Tool.h"
+#include "ToolPropertiesWidget.h"
+#include "ToolboxWidget.h"
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Matrix4x4.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread shared_buffer accept rpath unix wpath cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread shared_buffer accept rpath wpath cpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-pixel-paint");
+
+ auto window = GUI::Window::construct();
+ window->set_title("PixelPaint");
+ window->resize(950, 570);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& horizontal_container = window->set_main_widget<GUI::Widget>();
+ horizontal_container.set_layout<GUI::HorizontalBoxLayout>();
+ horizontal_container.layout()->set_spacing(0);
+
+ auto& toolbox = horizontal_container.add<PixelPaint::ToolboxWidget>();
+
+ auto& vertical_container = horizontal_container.add<GUI::Widget>();
+ vertical_container.set_layout<GUI::VerticalBoxLayout>();
+ vertical_container.layout()->set_spacing(0);
+
+ auto& image_editor = vertical_container.add<PixelPaint::ImageEditor>();
+ image_editor.set_focus(true);
+
+ vertical_container.add<PixelPaint::PaletteWidget>(image_editor);
+
+ auto& right_panel = horizontal_container.add<GUI::Widget>();
+ right_panel.set_fill_with_background_color(true);
+ right_panel.set_fixed_width(230);
+ right_panel.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& layer_list_widget = right_panel.add<PixelPaint::LayerListWidget>();
+
+ auto& layer_properties_widget = right_panel.add<PixelPaint::LayerPropertiesWidget>();
+
+ auto& tool_properties_widget = right_panel.add<PixelPaint::ToolPropertiesWidget>();
+
+ toolbox.on_tool_selection = [&](auto* tool) {
+ image_editor.set_active_tool(tool);
+ tool_properties_widget.set_active_tool(tool);
+ };
+
+ window->show();
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("PixelPaint");
+
+ app_menu.add_action(
+ GUI::Action::create(
+ "New", [&](auto&) {
+ auto dialog = PixelPaint::CreateNewImageDialog::construct(window);
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ auto image = PixelPaint::Image::create_with_size(dialog->image_size());
+ auto bg_layer = PixelPaint::Layer::create_with_size(*image, image->size(), "Background");
+ image->add_layer(*bg_layer);
+ bg_layer->bitmap().fill(Color::White);
+
+ image_editor.set_image(image);
+ layer_list_widget.set_image(image);
+ image_editor.set_active_layer(bg_layer);
+ }
+ },
+ window));
+ app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window);
+
+ if (!open_path.has_value())
+ return;
+
+ auto image = PixelPaint::Image::create_from_file(open_path.value());
+ image_editor.set_image(image);
+ layer_list_widget.set_image(image);
+ }));
+ app_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
+ if (!image_editor.image())
+ return;
+
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "pp");
+
+ if (!save_path.has_value())
+ return;
+
+ image_editor.image()->save(save_path.value());
+ }));
+ auto& export_submenu = app_menu.add_submenu("Export");
+ export_submenu.add_action(
+ GUI::Action::create(
+ "As BMP", [&](auto&) {
+ if (!image_editor.image())
+ return;
+
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, "untitled", "bmp");
+
+ if (!save_path.has_value())
+ return;
+
+ image_editor.image()->export_bmp(save_path.value());
+ },
+ window));
+
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ auto paste_action = GUI::CommonActions::make_paste_action([&](auto&) {
+ ASSERT(image_editor.image());
+ auto bitmap = GUI::Clipboard::the().bitmap();
+ if (!bitmap)
+ return;
+
+ auto layer = PixelPaint::Layer::create_with_bitmap(*image_editor.image(), *bitmap, "Pasted layer");
+ image_editor.image()->add_layer(layer.release_nonnull());
+ });
+ GUI::Clipboard::the().on_change = [&](auto& mime_type) {
+ paste_action->set_enabled(mime_type == "image/x-serenityos");
+ };
+ paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "image/x-serenityos");
+
+ edit_menu.add_action(paste_action);
+
+ auto undo_action = GUI::CommonActions::make_undo_action([&](auto&) {
+ ASSERT(image_editor.image());
+ image_editor.undo();
+ });
+ edit_menu.add_action(undo_action);
+
+ auto redo_action = GUI::CommonActions::make_redo_action([&](auto&) {
+ ASSERT(image_editor.image());
+ image_editor.redo();
+ });
+ edit_menu.add_action(redo_action);
+
+ auto& tool_menu = menubar->add_menu("Tool");
+ toolbox.for_each_tool([&](auto& tool) {
+ if (tool.action())
+ tool_menu.add_action(*tool.action());
+ return IterationDecision::Continue;
+ });
+
+ auto& layer_menu = menubar->add_menu("Layer");
+ layer_menu.add_action(GUI::Action::create(
+ "Create new layer...", { Mod_Ctrl | Mod_Shift, Key_N }, [&](auto&) {
+ auto dialog = PixelPaint::CreateNewLayerDialog::construct(image_editor.image()->size(), window);
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ auto layer = PixelPaint::Layer::create_with_size(*image_editor.image(), dialog->layer_size(), dialog->layer_name());
+ if (!layer) {
+ GUI::MessageBox::show_error(window, String::formatted("Unable to create layer with size {}", dialog->size().to_string()));
+ return;
+ }
+ image_editor.image()->add_layer(layer.release_nonnull());
+ image_editor.layers_did_change();
+ }
+ },
+ window));
+
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Select previous layer", { 0, Key_PageUp }, [&](auto&) {
+ layer_list_widget.move_selection(1);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select next layer", { 0, Key_PageDown }, [&](auto&) {
+ layer_list_widget.move_selection(-1);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select top layer", { 0, Key_Home }, [&](auto&) {
+ layer_list_widget.select_top_layer();
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Select bottom layer", { 0, Key_End }, [&](auto&) {
+ layer_list_widget.select_bottom_layer();
+ },
+ window));
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Move active layer up", { Mod_Ctrl, Key_PageUp }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->move_layer_up(*active_layer);
+ },
+ window));
+ layer_menu.add_action(GUI::Action::create(
+ "Move active layer down", { Mod_Ctrl, Key_PageDown }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->move_layer_down(*active_layer);
+ },
+ window));
+ layer_menu.add_separator();
+ layer_menu.add_action(GUI::Action::create(
+ "Remove active layer", { Mod_Ctrl, Key_D }, [&](auto&) {
+ auto active_layer = image_editor.active_layer();
+ if (!active_layer)
+ return;
+ image_editor.image()->remove_layer(*active_layer);
+ image_editor.set_active_layer(nullptr);
+ },
+ window));
+
+ auto& filter_menu = menubar->add_menu("Filter");
+ auto& spatial_filters_menu = filter_menu.add_submenu("Spatial");
+
+ auto& edge_detect_submenu = spatial_filters_menu.add_submenu("Edge Detect");
+ edge_detect_submenu.add_action(GUI::Action::create("Laplacian (cardinal)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::LaplacianFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::LaplacianFilter>::get(false)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ edge_detect_submenu.add_action(GUI::Action::create("Laplacian (diagonal)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::LaplacianFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::LaplacianFilter>::get(true)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ auto& blur_submenu = spatial_filters_menu.add_submenu("Blur and Sharpen");
+ blur_submenu.add_action(GUI::Action::create("Gaussian Blur (3x3)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SpatialGaussianBlurFilter<3> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SpatialGaussianBlurFilter<3>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Gaussian Blur (5x5)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SpatialGaussianBlurFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SpatialGaussianBlurFilter<5>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Box Blur (3x3)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::BoxBlurFilter<3> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::BoxBlurFilter<3>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Box Blur (5x5)", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::BoxBlurFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::BoxBlurFilter<5>>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+ blur_submenu.add_action(GUI::Action::create("Sharpen", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::SharpenFilter filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::SharpenFilter>::get()) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+
+ spatial_filters_menu.add_separator();
+ spatial_filters_menu.add_action(GUI::Action::create("Generic 5x5 Convolution", [&](auto&) {
+ if (auto* layer = image_editor.active_layer()) {
+ Gfx::GenericConvolutionFilter<5> filter;
+ if (auto parameters = PixelPaint::FilterParameters<Gfx::GenericConvolutionFilter<5>>::get(window)) {
+ filter.apply(layer->bitmap(), layer->rect(), layer->bitmap(), layer->rect(), *parameters);
+ image_editor.did_complete_action();
+ }
+ }
+ }));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("PixelPaint", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ image_editor.on_active_layer_change = [&](auto* layer) {
+ layer_list_widget.set_selected_layer(layer);
+ layer_properties_widget.set_layer(layer);
+ };
+
+ auto image = PixelPaint::Image::create_with_size({ 640, 480 });
+
+ auto bg_layer = PixelPaint::Layer::create_with_size(*image, { 640, 480 }, "Background");
+ image->add_layer(*bg_layer);
+ bg_layer->bitmap().fill(Color::White);
+
+ auto fg_layer1 = PixelPaint::Layer::create_with_size(*image, { 200, 200 }, "FG Layer 1");
+ fg_layer1->set_location({ 50, 50 });
+ image->add_layer(*fg_layer1);
+ fg_layer1->bitmap().fill(Color::Yellow);
+
+ auto fg_layer2 = PixelPaint::Layer::create_with_size(*image, { 100, 100 }, "FG Layer 2");
+ fg_layer2->set_location({ 300, 300 });
+ image->add_layer(*fg_layer2);
+ fg_layer2->bitmap().fill(Color::Blue);
+
+ layer_list_widget.on_layer_select = [&](auto* layer) {
+ image_editor.set_active_layer(layer);
+ };
+
+ layer_list_widget.set_image(image);
+
+ image_editor.set_image(image);
+ image_editor.set_active_layer(bg_layer);
+
+ return app->exec();
+}
diff --git a/Userland/Applications/QuickShow/CMakeLists.txt b/Userland/Applications/QuickShow/CMakeLists.txt
new file mode 100644
index 0000000000..8d2b46e217
--- /dev/null
+++ b/Userland/Applications/QuickShow/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ main.cpp
+ QSWidget.cpp
+)
+
+serenity_app(QuickShow ICON filetype-image)
+target_link_libraries(QuickShow LibGUI LibGfx)
diff --git a/Userland/Applications/QuickShow/QSWidget.cpp b/Userland/Applications/QuickShow/QSWidget.cpp
new file mode 100644
index 0000000000..94dbe858ea
--- /dev/null
+++ b/Userland/Applications/QuickShow/QSWidget.cpp
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "QSWidget.h"
+#include <AK/StringBuilder.h>
+#include <LibCore/DirIterator.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Orientation.h>
+#include <LibGfx/Palette.h>
+
+QSWidget::QSWidget()
+{
+ set_fill_with_background_color(false);
+}
+
+QSWidget::~QSWidget()
+{
+}
+
+void QSWidget::clear()
+{
+ m_bitmap = nullptr;
+ m_path = {};
+
+ set_scale(100);
+ update();
+}
+
+void QSWidget::flip(Gfx::Orientation orientation)
+{
+ m_bitmap = m_bitmap->flipped(orientation);
+ set_scale(m_scale);
+
+ resize_window();
+}
+
+void QSWidget::rotate(Gfx::RotationDirection rotation_direction)
+{
+ m_bitmap = m_bitmap->rotated(rotation_direction);
+ set_scale(m_scale);
+
+ resize_window();
+}
+
+void QSWidget::navigate(Directions direction)
+{
+ if (m_path == nullptr)
+ return;
+
+ auto parts = m_path.split('/');
+ parts.remove(parts.size() - 1);
+ StringBuilder sb;
+ sb.append("/");
+ sb.join("/", parts);
+ AK::String current_dir = sb.to_string();
+
+ if (m_files_in_same_dir.is_empty()) {
+ Core::DirIterator iterator(current_dir, Core::DirIterator::Flags::SkipDots);
+ while (iterator.has_next()) {
+ String file = iterator.next_full_path();
+ if (!Gfx::Bitmap::is_path_a_supported_image_format(file))
+ continue;
+ m_files_in_same_dir.append(file);
+ }
+ }
+
+ auto current_index = m_files_in_same_dir.find_first_index(m_path);
+ if (!current_index.has_value()) {
+ return;
+ }
+
+ size_t index = current_index.value();
+ if (direction == Directions::Back) {
+ if (index == 0) {
+ GUI::MessageBox::show(window(), "This is the first file.", "Cannot open image", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ index--;
+ } else if (direction == Directions::Forward) {
+ if (index == m_files_in_same_dir.size() - 1) {
+ GUI::MessageBox::show(window(), "This is the last file.", "Cannot open image", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ index++;
+ } else if (direction == Directions::First) {
+ index = 0;
+ } else if (direction == Directions::Last) {
+ index = m_files_in_same_dir.size() - 1;
+ }
+
+ this->load_from_file(m_files_in_same_dir.at(index));
+}
+
+void QSWidget::set_scale(int scale)
+{
+ if (m_bitmap.is_null())
+ return;
+
+ if (m_scale == scale) {
+ update();
+ return;
+ }
+
+ if (scale < 10)
+ scale = 10;
+ if (scale > 1000)
+ scale = 1000;
+
+ if (scale == 100)
+ m_pan_origin = { 0, 0 };
+
+ m_scale = scale;
+ float scale_factor = (float)m_scale / 100.0f;
+
+ Gfx::IntSize new_size;
+ new_size.set_width(m_bitmap->width() * scale_factor);
+ new_size.set_height(m_bitmap->height() * scale_factor);
+ m_bitmap_rect.set_size(new_size);
+
+ if (on_scale_change)
+ on_scale_change(m_scale, m_bitmap_rect);
+
+ relayout();
+}
+
+void QSWidget::relayout()
+{
+ if (m_bitmap.is_null())
+ return;
+
+ float scale_factor = (float)m_scale / 100.0f;
+ Gfx::IntSize new_size = m_bitmap_rect.size();
+
+ Gfx::IntPoint new_location;
+ new_location.set_x((width() / 2) - (new_size.width() / 2) - (m_pan_origin.x() * scale_factor));
+ new_location.set_y((height() / 2) - (new_size.height() / 2) - (m_pan_origin.y() * scale_factor));
+ m_bitmap_rect.set_location(new_location);
+
+ update();
+}
+
+void QSWidget::resize_event(GUI::ResizeEvent& event)
+{
+ relayout();
+ GUI::Widget::resize_event(event);
+}
+
+void QSWidget::doubleclick_event(GUI::MouseEvent&)
+{
+ on_doubleclick();
+}
+
+void QSWidget::paint_event(GUI::PaintEvent& event)
+{
+ Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.add_clip_rect(frame_inner_rect());
+
+ Gfx::StylePainter::paint_transparency_grid(painter, frame_inner_rect(), palette());
+
+ if (!m_bitmap.is_null())
+ painter.draw_scaled_bitmap(m_bitmap_rect, *m_bitmap, m_bitmap->rect());
+}
+
+void QSWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ if (event.button() != GUI::MouseButton::Left)
+ return;
+ m_click_position = event.position();
+ m_saved_pan_origin = m_pan_origin;
+}
+
+void QSWidget::mouseup_event([[maybe_unused]] GUI::MouseEvent& event) { }
+
+void QSWidget::mousemove_event(GUI::MouseEvent& event)
+{
+ if (!(event.buttons() & GUI::MouseButton::Left))
+ return;
+
+ auto delta = event.position() - m_click_position;
+ float scale_factor = (float)m_scale / 100.0f;
+ m_pan_origin = m_saved_pan_origin.translated(
+ -delta.x() / scale_factor,
+ -delta.y() / scale_factor);
+
+ relayout();
+}
+
+void QSWidget::mousewheel_event(GUI::MouseEvent& event)
+{
+ int new_scale = m_scale - event.wheel_delta() * 10;
+ if (new_scale < 10)
+ new_scale = 10;
+ if (new_scale > 1000)
+ new_scale = 1000;
+
+ if (new_scale == m_scale) {
+ return;
+ }
+
+ auto old_scale_factor = (float)m_scale / 100.0f;
+ auto new_scale_factor = (float)new_scale / 100.0f;
+
+ auto focus_point = Gfx::FloatPoint(
+ m_pan_origin.x() - ((float)event.x() - (float)width() / 2.0) / old_scale_factor,
+ m_pan_origin.y() - ((float)event.y() - (float)height() / 2.0) / old_scale_factor);
+
+ m_pan_origin = Gfx::FloatPoint(
+ focus_point.x() - new_scale_factor / old_scale_factor * (focus_point.x() - m_pan_origin.x()),
+ focus_point.y() - new_scale_factor / old_scale_factor * (focus_point.y() - m_pan_origin.y()));
+
+ set_scale(new_scale);
+}
+
+void QSWidget::load_from_file(const String& path)
+{
+ auto bitmap = Gfx::Bitmap::load_from_file(path);
+ if (!bitmap) {
+ GUI::MessageBox::show(window(), String::formatted("Failed to open {}", path), "Cannot open image", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_path = path;
+ m_bitmap = bitmap;
+ m_scale = -1;
+ set_scale(100);
+}
+
+void QSWidget::drop_event(GUI::DropEvent& event)
+{
+ event.accept();
+ if (on_drop)
+ on_drop(event);
+}
+
+void QSWidget::resize_window()
+{
+ if (window()->is_fullscreen())
+ return;
+
+ if (!m_bitmap)
+ return;
+
+ auto new_size = m_bitmap->size();
+
+ if (new_size.width() < 300)
+ new_size.set_width(300);
+ if (new_size.height() < 200)
+ new_size.set_height(200);
+
+ new_size.set_height(new_size.height() + m_toolbar_height);
+ window()->resize(new_size);
+}
diff --git a/Userland/Applications/QuickShow/QSWidget.h b/Userland/Applications/QuickShow/QSWidget.h
new file mode 100644
index 0000000000..047241e25e
--- /dev/null
+++ b/Userland/Applications/QuickShow/QSWidget.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Point.h>
+
+class QSLabel;
+
+class QSWidget final : public GUI::Frame {
+ C_OBJECT(QSWidget)
+public:
+ enum Directions {
+ First,
+ Back,
+ Forward,
+ Last
+ };
+
+ virtual ~QSWidget() override;
+
+ const Gfx::Bitmap* bitmap() const { return m_bitmap.ptr(); }
+ const String& path() const { return m_path; }
+ void set_scale(int);
+ int scale() { return m_scale; }
+ void set_toolbar_height(int height) { m_toolbar_height = height; }
+ int toolbar_height() { return m_toolbar_height; }
+
+ void clear();
+ void flip(Gfx::Orientation);
+ void rotate(Gfx::RotationDirection);
+ void navigate(Directions);
+ void load_from_file(const String&);
+
+ Function<void(int, Gfx::IntRect)> on_scale_change;
+ Function<void()> on_doubleclick;
+ Function<void(const GUI::DropEvent&)> on_drop;
+
+private:
+ QSWidget();
+ virtual void doubleclick_event(GUI::MouseEvent&) override;
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mousewheel_event(GUI::MouseEvent&) override;
+ virtual void drop_event(GUI::DropEvent&) override;
+
+ void relayout();
+ void resize_window();
+
+ String m_path;
+ RefPtr<Gfx::Bitmap> m_bitmap;
+ int m_toolbar_height { 28 };
+
+ Gfx::IntRect m_bitmap_rect;
+ int m_scale { -1 };
+ Gfx::FloatPoint m_pan_origin;
+
+ Gfx::IntPoint m_click_position;
+ Gfx::FloatPoint m_saved_pan_origin;
+ Vector<String> m_files_in_same_dir;
+};
diff --git a/Userland/Applications/QuickShow/main.cpp b/Userland/Applications/QuickShow/main.cpp
new file mode 100644
index 0000000000..2319c24a9a
--- /dev/null
+++ b/Userland/Applications/QuickShow/main.cpp
@@ -0,0 +1,309 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "QSWidget.h"
+#include <AK/URL.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/MimeData.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/Desktop.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Palette.h>
+#include <LibGfx/Rect.h>
+#include <serenity.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <string.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept cpath rpath wpath unix cpath fattr proc exec thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept cpath rpath wpath proc exec thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("filetype-image");
+
+ const char* path = nullptr;
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(path, "The image file to be displayed.", "file", Core::ArgsParser::Required::No);
+ args_parser.parse(argc, argv);
+
+ auto window = GUI::Window::construct();
+ window->set_double_buffering_enabled(true);
+ window->resize(300, 200);
+ window->set_icon(app_icon.bitmap_for_size(16));
+ window->set_title("QuickShow");
+
+ auto& root_widget = window->set_main_widget<GUI::Widget>();
+ root_widget.set_fill_with_background_color(true);
+ root_widget.set_layout<GUI::VerticalBoxLayout>();
+ root_widget.layout()->set_spacing(2);
+
+ auto& toolbar_container = root_widget.add<GUI::ToolBarContainer>();
+ auto& main_toolbar = toolbar_container.add<GUI::ToolBar>();
+
+ auto& widget = root_widget.add<QSWidget>();
+ widget.on_scale_change = [&](int scale, Gfx::IntRect rect) {
+ if (!widget.bitmap()) {
+ window->set_title("QuickShow");
+ return;
+ }
+
+ window->set_title(String::formatted("{} {} {}% - QuickShow", widget.path(), widget.bitmap()->size().to_string(), scale));
+
+ if (window->is_fullscreen())
+ return;
+
+ if (window->is_maximized())
+ return;
+
+ auto w = max(window->width(), rect.width() + 4);
+ auto h = max(window->height(), rect.height() + widget.toolbar_height() + 6);
+ window->resize(w, h);
+ };
+ widget.on_drop = [&](auto& event) {
+ window->move_to_front();
+
+ if (event.mime_data().has_urls()) {
+ auto urls = event.mime_data().urls();
+
+ if (!urls.is_empty()) {
+ auto url = urls.first();
+ widget.load_from_file(url.path());
+ }
+
+ pid_t child;
+ for (size_t i = 1; i < urls.size(); ++i) {
+ const char* argv[] = { "/bin/QuickShow", urls[i].path().characters(), nullptr };
+ if ((errno = posix_spawn(&child, "/bin/QuickShow", nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ } else {
+ if (disown(child) < 0)
+ perror("disown");
+ }
+ }
+ }
+ };
+ widget.on_doubleclick = [&] {
+ window->set_fullscreen(!window->is_fullscreen());
+ toolbar_container.set_visible(!window->is_fullscreen());
+ };
+
+ // Actions
+ auto open_action = GUI::CommonActions::make_open_action(
+ [&](auto&) {
+ Optional<String> path = GUI::FilePicker::get_open_filepath(window, "Open image...");
+ if (path.has_value()) {
+ widget.load_from_file(path.value());
+ }
+ });
+
+ auto delete_action = GUI::CommonActions::make_delete_action(
+ [&](auto&) {
+ auto path = widget.path();
+ if (path.is_empty())
+ return;
+
+ auto msgbox_result = GUI::MessageBox::show(window,
+ String::formatted("Really delete {}?", path),
+ "Confirm deletion",
+ GUI::MessageBox::Type::Warning,
+ GUI::MessageBox::InputType::OKCancel);
+
+ if (msgbox_result == GUI::MessageBox::ExecCancel)
+ return;
+
+ auto unlink_result = unlink(widget.path().characters());
+ dbgln("unlink_result::{}", unlink_result);
+
+ if (unlink_result < 0) {
+ int saved_errno = errno;
+ GUI::MessageBox::show(window,
+ String::formatted("unlink({}) failed: {}", path, strerror(saved_errno)),
+ "Delete failed",
+ GUI::MessageBox::Type::Error);
+
+ return;
+ }
+
+ widget.clear();
+ });
+
+ auto quit_action = GUI::CommonActions::make_quit_action(
+ [&](auto&) {
+ app->quit();
+ });
+
+ auto rotate_left_action = GUI::Action::create("Rotate Left", { Mod_None, Key_L },
+ [&](auto&) {
+ widget.rotate(Gfx::RotationDirection::Left);
+ });
+
+ auto rotate_right_action = GUI::Action::create("Rotate Right", { Mod_None, Key_R },
+ [&](auto&) {
+ widget.rotate(Gfx::RotationDirection::Right);
+ });
+
+ auto vertical_flip_action = GUI::Action::create("Vertical Flip", { Mod_None, Key_V },
+ [&](auto&) {
+ widget.flip(Gfx::Orientation::Vertical);
+ });
+
+ auto horizontal_flip_action = GUI::Action::create("Horizontal Flip", { Mod_None, Key_H },
+ [&](auto&) {
+ widget.flip(Gfx::Orientation::Horizontal);
+ });
+
+ auto desktop_wallpaper_action = GUI::Action::create("Set as desktop wallpaper",
+ [&](auto&) {
+ GUI::Desktop::the().set_wallpaper(widget.path());
+ });
+
+ auto go_first_action = GUI::Action::create("First", { Mod_None, Key_Home }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-first.png"),
+ [&](auto&) {
+ widget.navigate(QSWidget::Directions::First);
+ });
+
+ auto go_back_action = GUI::Action::create("Back", { Mod_None, Key_Left }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"),
+ [&](auto&) {
+ widget.navigate(QSWidget::Directions::Back);
+ });
+
+ auto go_forward_action = GUI::Action::create("Forward", { Mod_None, Key_Right }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"),
+ [&](auto&) {
+ widget.navigate(QSWidget::Directions::Forward);
+ });
+
+ auto go_last_action = GUI::Action::create("Last", { Mod_None, Key_End }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-last.png"),
+ [&](auto&) {
+ widget.navigate(QSWidget::Directions::Last);
+ });
+
+ auto full_sceen_action = GUI::CommonActions::make_fullscreen_action(
+ [&](auto&) {
+ widget.on_doubleclick();
+ });
+
+ auto zoom_in_action = GUI::Action::create("Zoom In", { Mod_None, Key_Plus }, Gfx::Bitmap::load_from_file("/res/icons/16x16/zoom-in.png"),
+ [&](auto&) {
+ widget.set_scale(widget.scale() + 10);
+ });
+
+ auto zoom_reset_action = GUI::Action::create("Zoom 100%", { Mod_None, Key_0 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/zoom-reset.png"),
+ [&](auto&) {
+ widget.set_scale(100);
+ });
+
+ auto zoom_out_action = GUI::Action::create("Zoom Out", { Mod_None, Key_Minus }, Gfx::Bitmap::load_from_file("/res/icons/16x16/zoom-out.png"),
+ [&](auto&) {
+ widget.set_scale(widget.scale() - 10);
+ });
+
+ auto hide_show_toolbar_action = GUI::Action::create("Hide/Show Toolbar", { Mod_Ctrl, Key_T },
+ [&](auto&) {
+ toolbar_container.set_visible(!toolbar_container.is_visible());
+ });
+
+ auto copy_action = GUI::CommonActions::make_copy_action([&](auto&) {
+ if (widget.bitmap())
+ GUI::Clipboard::the().set_bitmap(*widget.bitmap());
+ });
+
+ main_toolbar.add_action(open_action);
+ main_toolbar.add_action(delete_action);
+ main_toolbar.add_separator();
+ main_toolbar.add_action(go_first_action);
+ main_toolbar.add_action(go_back_action);
+ main_toolbar.add_action(go_forward_action);
+ main_toolbar.add_action(go_last_action);
+ main_toolbar.add_separator();
+ main_toolbar.add_action(zoom_in_action);
+ main_toolbar.add_action(zoom_reset_action);
+ main_toolbar.add_action(zoom_out_action);
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("QuickShow");
+ app_menu.add_action(open_action);
+ app_menu.add_action(delete_action);
+ app_menu.add_separator();
+ app_menu.add_action(quit_action);
+
+ auto& image_menu = menubar->add_menu("Image");
+ image_menu.add_action(rotate_left_action);
+ image_menu.add_action(rotate_right_action);
+ image_menu.add_action(vertical_flip_action);
+ image_menu.add_action(horizontal_flip_action);
+ image_menu.add_separator();
+ image_menu.add_action(desktop_wallpaper_action);
+
+ auto& navigate_menu = menubar->add_menu("Navigate");
+ navigate_menu.add_action(go_first_action);
+ navigate_menu.add_action(go_back_action);
+ navigate_menu.add_action(go_forward_action);
+ navigate_menu.add_action(go_last_action);
+
+ auto& view_menu = menubar->add_menu("View");
+ view_menu.add_action(full_sceen_action);
+ view_menu.add_separator();
+ view_menu.add_action(zoom_in_action);
+ view_menu.add_action(zoom_reset_action);
+ view_menu.add_action(zoom_out_action);
+ view_menu.add_separator();
+ view_menu.add_action(hide_show_toolbar_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("QuickShow", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ if (path != nullptr) {
+ widget.load_from_file(path);
+ }
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/SoundPlayer/CMakeLists.txt b/Userland/Applications/SoundPlayer/CMakeLists.txt
new file mode 100644
index 0000000000..99e1da4653
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES
+ main.cpp
+ PlaybackManager.cpp
+ SampleWidget.cpp
+ SoundPlayerWidget.cpp
+)
+
+serenity_app(SoundPlayer ICON app-sound-player)
+target_link_libraries(SoundPlayer LibAudio LibGUI)
diff --git a/Userland/Applications/SoundPlayer/PlaybackManager.cpp b/Userland/Applications/SoundPlayer/PlaybackManager.cpp
new file mode 100644
index 0000000000..1421e75215
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/PlaybackManager.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PlaybackManager.h"
+
+PlaybackManager::PlaybackManager(NonnullRefPtr<Audio::ClientConnection> connection)
+ : m_connection(connection)
+{
+ m_timer = Core::Timer::construct(100, [&]() {
+ if (!m_loader)
+ return;
+ next_buffer();
+ });
+ m_timer->stop();
+}
+
+PlaybackManager::~PlaybackManager()
+{
+}
+
+void PlaybackManager::set_loader(NonnullRefPtr<Audio::Loader>&& loader)
+{
+ stop();
+ m_loader = loader;
+ if (m_loader) {
+ m_total_length = m_loader->total_samples() / static_cast<float>(m_loader->sample_rate());
+ m_timer->start();
+ load_next_buffer();
+ } else {
+ m_timer->stop();
+ }
+}
+
+void PlaybackManager::stop()
+{
+ set_paused(true);
+ m_connection->clear_buffer(true);
+ m_buffers.clear();
+ m_last_seek = 0;
+ m_next_buffer = nullptr;
+ m_current_buffer = nullptr;
+ m_next_ptr = 0;
+
+ if (m_loader)
+ m_loader->reset();
+}
+
+void PlaybackManager::play()
+{
+ set_paused(false);
+}
+
+void PlaybackManager::loop(bool loop)
+{
+ m_loop = loop;
+}
+
+void PlaybackManager::seek(const int position)
+{
+ if (!m_loader)
+ return;
+
+ m_last_seek = position;
+ bool paused_state = m_paused;
+ set_paused(true);
+
+ m_connection->clear_buffer(true);
+ m_next_buffer = nullptr;
+ m_current_buffer = nullptr;
+ m_next_ptr = 0;
+ m_buffers.clear();
+ m_loader->seek(position);
+
+ if (!paused_state)
+ set_paused(false);
+}
+
+void PlaybackManager::pause()
+{
+ set_paused(true);
+}
+
+void PlaybackManager::remove_dead_buffers()
+{
+ int id = m_connection->get_playing_buffer();
+ int current_id = -1;
+ if (m_current_buffer)
+ current_id = m_current_buffer->shbuf_id();
+
+ if (id >= 0 && id != current_id) {
+ while (!m_buffers.is_empty()) {
+ --m_next_ptr;
+ auto buffer = m_buffers.take_first();
+
+ if (buffer->shbuf_id() == id) {
+ m_current_buffer = buffer;
+ break;
+ }
+ }
+ }
+}
+
+void PlaybackManager::load_next_buffer()
+{
+ if (m_buffers.size() < 10) {
+ for (int i = 0; i < 20 && m_loader->loaded_samples() < m_loader->total_samples(); i++) {
+ auto buffer = m_loader->get_more_samples(PLAYBACK_MANAGER_BUFFER_SIZE);
+ if (buffer)
+ m_buffers.append(buffer);
+ }
+ }
+
+ if (m_next_ptr < m_buffers.size()) {
+ m_next_buffer = m_buffers.at(m_next_ptr++);
+ } else {
+ m_next_buffer = nullptr;
+ }
+}
+
+void PlaybackManager::set_paused(bool paused)
+{
+ if (!m_next_buffer && m_loader)
+ load_next_buffer();
+
+ m_paused = paused;
+ m_connection->set_paused(paused);
+}
+
+bool PlaybackManager::toggle_pause()
+{
+ if (m_paused) {
+ play();
+ } else {
+ pause();
+ }
+ return m_paused;
+}
+
+void PlaybackManager::next_buffer()
+{
+ if (on_update)
+ on_update();
+
+ if (m_paused)
+ return;
+
+ remove_dead_buffers();
+ if (!m_next_buffer) {
+ if (!m_connection->get_remaining_samples() && !m_paused) {
+ dbgln("Exhausted samples :^)");
+ if (m_loop)
+ seek(0);
+ else
+ stop();
+ }
+
+ return;
+ }
+
+ bool enqueued = m_connection->try_enqueue(*m_next_buffer);
+ if (!enqueued)
+ return;
+
+ load_next_buffer();
+}
diff --git a/Userland/Applications/SoundPlayer/PlaybackManager.h b/Userland/Applications/SoundPlayer/PlaybackManager.h
new file mode 100644
index 0000000000..35be78fe9d
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/PlaybackManager.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+#include <LibAudio/Buffer.h>
+#include <LibAudio/ClientConnection.h>
+#include <LibAudio/Loader.h>
+#include <LibCore/Timer.h>
+
+#define PLAYBACK_MANAGER_BUFFER_SIZE 64 * KiB
+#define PLAYBACK_MANAGER_RATE 44100
+
+class PlaybackManager final {
+public:
+ PlaybackManager(NonnullRefPtr<Audio::ClientConnection>);
+ ~PlaybackManager();
+
+ void play();
+ void stop();
+ void pause();
+ void seek(const int position);
+ void loop(bool);
+ bool toggle_pause();
+ void set_loader(NonnullRefPtr<Audio::Loader>&&);
+
+ int last_seek() const { return m_last_seek; }
+ bool is_paused() const { return m_paused; }
+ float total_length() const { return m_total_length; }
+ RefPtr<Audio::Buffer> current_buffer() const { return m_current_buffer; }
+
+ NonnullRefPtr<Audio::ClientConnection> connection() const { return m_connection; }
+
+ Function<void()> on_update;
+
+private:
+ void next_buffer();
+ void set_paused(bool);
+ void load_next_buffer();
+ void remove_dead_buffers();
+
+ bool m_paused { true };
+ bool m_loop = { false };
+ size_t m_next_ptr { 0 };
+ size_t m_last_seek { 0 };
+ float m_total_length { 0 };
+ RefPtr<Audio::Loader> m_loader { nullptr };
+ NonnullRefPtr<Audio::ClientConnection> m_connection;
+ RefPtr<Audio::Buffer> m_next_buffer;
+ RefPtr<Audio::Buffer> m_current_buffer;
+ Vector<RefPtr<Audio::Buffer>> m_buffers;
+ RefPtr<Core::Timer> m_timer;
+};
diff --git a/Userland/Applications/SoundPlayer/SampleWidget.cpp b/Userland/Applications/SoundPlayer/SampleWidget.cpp
new file mode 100644
index 0000000000..b3884a175a
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/SampleWidget.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SampleWidget.h"
+#include <LibAudio/Buffer.h>
+#include <LibGUI/Painter.h>
+#include <math.h>
+
+SampleWidget::SampleWidget()
+{
+}
+
+SampleWidget::~SampleWidget()
+{
+}
+
+void SampleWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+ GUI::Painter painter(*this);
+
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect(frame_inner_rect(), Color::Black);
+
+ float sample_max = 0;
+ int count = 0;
+ int x_offset = frame_inner_rect().x();
+ int x = x_offset;
+ int y_offset = frame_inner_rect().center().y();
+
+ if (m_buffer) {
+ int samples_per_pixel = m_buffer->sample_count() / frame_inner_rect().width();
+ for (int sample_index = 0; sample_index < m_buffer->sample_count() && (x - x_offset) < frame_inner_rect().width(); ++sample_index) {
+ float sample = fabsf((float)m_buffer->samples()[sample_index].left);
+
+ sample_max = max(sample, sample_max);
+ ++count;
+
+ if (count >= samples_per_pixel) {
+ Gfx::IntPoint min_point = { x, y_offset + static_cast<int>(-sample_max * frame_inner_rect().height() / 2) };
+ Gfx::IntPoint max_point = { x++, y_offset + static_cast<int>(sample_max * frame_inner_rect().height() / 2) };
+ painter.draw_line(min_point, max_point, Color::Green);
+
+ count = 0;
+ sample_max = 0;
+ }
+ }
+ } else {
+ painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green);
+ }
+}
+
+void SampleWidget::set_buffer(Audio::Buffer* buffer)
+{
+ if (m_buffer == buffer)
+ return;
+ m_buffer = buffer;
+ update();
+}
diff --git a/Userland/Applications/SoundPlayer/SampleWidget.h b/Userland/Applications/SoundPlayer/SampleWidget.h
new file mode 100644
index 0000000000..7da9c20b38
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/SampleWidget.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+namespace Audio {
+class Buffer;
+}
+
+class SampleWidget final : public GUI::Frame {
+ C_OBJECT(SampleWidget)
+public:
+ virtual ~SampleWidget() override;
+
+ void set_buffer(Audio::Buffer*);
+
+private:
+ SampleWidget();
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ RefPtr<Audio::Buffer> m_buffer;
+};
diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp b/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp
new file mode 100644
index 0000000000..f2066aca89
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/SoundPlayerWidget.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SoundPlayerWidget.h"
+#include <AK/StringBuilder.h>
+#include <LibCore/MimeData.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <math.h>
+
+SoundPlayerWidget::SoundPlayerWidget(GUI::Window& window, NonnullRefPtr<Audio::ClientConnection> connection)
+ : m_window(window)
+ , m_connection(connection)
+ , m_manager(connection)
+{
+ set_fill_with_background_color(true);
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 2, 2, 2, 2 });
+
+ auto& status_widget = add<GUI::Widget>();
+ status_widget.set_fill_with_background_color(true);
+ status_widget.set_layout<GUI::HorizontalBoxLayout>();
+
+ m_elapsed = status_widget.add<GUI::Label>();
+ m_elapsed->set_frame_shape(Gfx::FrameShape::Container);
+ m_elapsed->set_frame_shadow(Gfx::FrameShadow::Sunken);
+ m_elapsed->set_frame_thickness(2);
+ m_elapsed->set_fixed_width(80);
+
+ auto& sample_widget_container = status_widget.add<GUI::Widget>();
+ sample_widget_container.set_layout<GUI::HorizontalBoxLayout>();
+
+ m_sample_widget = sample_widget_container.add<SampleWidget>();
+
+ m_remaining = status_widget.add<GUI::Label>();
+ m_remaining->set_frame_shape(Gfx::FrameShape::Container);
+ m_remaining->set_frame_shadow(Gfx::FrameShadow::Sunken);
+ m_remaining->set_frame_thickness(2);
+ m_remaining->set_fixed_width(80);
+
+ m_slider = add<Slider>(Orientation::Horizontal);
+ m_slider->set_min(0);
+ m_slider->set_enabled(false);
+ m_slider->on_knob_released = [&](int value) { m_manager.seek(denormalize_rate(value)); };
+
+ auto& control_widget = add<GUI::Widget>();
+ control_widget.set_fill_with_background_color(true);
+ control_widget.set_layout<GUI::HorizontalBoxLayout>();
+ control_widget.set_fixed_height(30);
+ control_widget.layout()->set_margins({ 10, 2, 10, 2 });
+ control_widget.layout()->set_spacing(10);
+
+ m_play = control_widget.add<GUI::Button>();
+ m_play->set_icon(*m_pause_icon);
+ m_play->set_enabled(false);
+ m_play->on_click = [this](auto) {
+ m_play->set_icon(m_manager.toggle_pause() ? *m_play_icon : *m_pause_icon);
+ };
+
+ m_stop = control_widget.add<GUI::Button>();
+ m_stop->set_enabled(false);
+ m_stop->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/stop.png"));
+ m_stop->on_click = [this](auto) { m_manager.stop(); };
+
+ m_status = add<GUI::Label>();
+ m_status->set_frame_shape(Gfx::FrameShape::Box);
+ m_status->set_frame_shadow(Gfx::FrameShadow::Raised);
+ m_status->set_frame_thickness(4);
+ m_status->set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ m_status->set_fixed_height(18);
+ m_status->set_text("No file open!");
+
+ update_position(0);
+
+ m_manager.on_update = [&]() { update_ui(); };
+}
+
+SoundPlayerWidget::~SoundPlayerWidget()
+{
+}
+
+SoundPlayerWidget::Slider::~Slider()
+{
+}
+
+void SoundPlayerWidget::hide_scope(bool hide)
+{
+ m_sample_widget->set_visible(!hide);
+}
+
+void SoundPlayerWidget::open_file(String path)
+{
+ NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
+ if (loader->has_error() || !loader->sample_rate()) {
+ const String error_string = loader->error_string();
+ GUI::MessageBox::show(window(),
+ String::formatted("Failed to load audio file: {} ({})", path, error_string.is_null() ? "Unknown error" : error_string),
+ "Filetype error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_sample_ratio = PLAYBACK_MANAGER_RATE / static_cast<float>(loader->sample_rate());
+
+ m_slider->set_max(normalize_rate(static_cast<int>(loader->total_samples())));
+ m_slider->set_enabled(true);
+ m_play->set_enabled(true);
+ m_stop->set_enabled(true);
+
+ m_window.set_title(String::formatted("{} - SoundPlayer", loader->file()->filename()));
+ m_status->set_text(String::formatted(
+ "Sample rate {}Hz, {} channel(s), {} bits per sample",
+ loader->sample_rate(),
+ loader->num_channels(),
+ loader->bits_per_sample()));
+
+ m_manager.set_loader(move(loader));
+ update_position(0);
+}
+
+void SoundPlayerWidget::drop_event(GUI::DropEvent& event)
+{
+ event.accept();
+ window()->move_to_front();
+
+ if (event.mime_data().has_urls()) {
+ auto urls = event.mime_data().urls();
+ if (urls.is_empty())
+ return;
+ open_file(urls.first().path());
+ }
+}
+
+int SoundPlayerWidget::normalize_rate(int rate) const
+{
+ return static_cast<int>(rate * m_sample_ratio);
+}
+
+int SoundPlayerWidget::denormalize_rate(int rate) const
+{
+ return static_cast<int>(rate / m_sample_ratio);
+}
+
+void SoundPlayerWidget::update_ui()
+{
+ m_sample_widget->set_buffer(m_manager.current_buffer());
+ m_play->set_icon(m_manager.is_paused() ? *m_play_icon : *m_pause_icon);
+ update_position(m_manager.connection()->get_played_samples());
+}
+
+void SoundPlayerWidget::update_position(const int position)
+{
+ int total_norm_samples = position + normalize_rate(m_manager.last_seek());
+ float seconds = (total_norm_samples / static_cast<float>(PLAYBACK_MANAGER_RATE));
+ float remaining_seconds = m_manager.total_length() - seconds;
+
+ m_elapsed->set_text(String::formatted(
+ "Elapsed:\n{}:{:02}.{:02}",
+ static_cast<int>(seconds / 60),
+ static_cast<int>(seconds) % 60,
+ static_cast<int>(seconds * 100) % 100));
+
+ m_remaining->set_text(String::formatted(
+ "Remaining:\n{}:{:02}.{:02}",
+ static_cast<int>(remaining_seconds / 60),
+ static_cast<int>(remaining_seconds) % 60,
+ static_cast<int>(remaining_seconds * 100) % 100));
+
+ m_slider->set_value(total_norm_samples);
+}
diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidget.h b/Userland/Applications/SoundPlayer/SoundPlayerWidget.h
new file mode 100644
index 0000000000..ab7bc60aea
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/SoundPlayerWidget.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "PlaybackManager.h"
+#include "SampleWidget.h"
+#include <LibGUI/Button.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Slider.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+
+class SoundPlayerWidget final : public GUI::Widget {
+ C_OBJECT(SoundPlayerWidget)
+public:
+ virtual ~SoundPlayerWidget() override;
+ void open_file(String path);
+ void hide_scope(bool);
+ PlaybackManager& manager() { return m_manager; }
+
+private:
+ explicit SoundPlayerWidget(GUI::Window&, NonnullRefPtr<Audio::ClientConnection>);
+
+ virtual void drop_event(GUI::DropEvent&) override;
+
+ void update_position(const int position);
+ void update_ui();
+ int normalize_rate(int) const;
+ int denormalize_rate(int) const;
+
+ class Slider final : public GUI::Slider {
+ C_OBJECT(Slider)
+ public:
+ virtual ~Slider() override;
+ Function<void(int)> on_knob_released;
+ void set_value(int value)
+ {
+ if (!knob_dragging())
+ GUI::Slider::set_value(value);
+ }
+
+ protected:
+ Slider(Orientation orientation)
+ : GUI::Slider(orientation)
+ {
+ }
+
+ virtual void mouseup_event(GUI::MouseEvent& event) override
+ {
+ if (on_knob_released && is_enabled())
+ on_knob_released(value());
+
+ GUI::Slider::mouseup_event(event);
+ }
+ };
+
+ GUI::Window& m_window;
+ NonnullRefPtr<Audio::ClientConnection> m_connection;
+ PlaybackManager m_manager;
+ float m_sample_ratio { 1.0 };
+ RefPtr<GUI::Label> m_status;
+ RefPtr<GUI::Label> m_elapsed;
+ RefPtr<GUI::Label> m_remaining;
+ RefPtr<Slider> m_slider;
+ RefPtr<SampleWidget> m_sample_widget;
+ RefPtr<Gfx::Bitmap> m_play_icon { Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png") };
+ RefPtr<Gfx::Bitmap> m_pause_icon { Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png") };
+ RefPtr<GUI::Button> m_play;
+ RefPtr<GUI::Button> m_stop;
+};
diff --git a/Userland/Applications/SoundPlayer/main.cpp b/Userland/Applications/SoundPlayer/main.cpp
new file mode 100644
index 0000000000..47dbd555c8
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/main.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SoundPlayerWidget.h"
+#include <LibAudio/ClientConnection.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/CharacterBitmap.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer accept rpath thread unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer accept rpath thread unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto audio_client = Audio::ClientConnection::construct();
+ audio_client->handshake();
+
+ if (pledge("stdio shared_buffer accept rpath thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-sound-player");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Sound Player");
+ window->set_resizable(false);
+ window->resize(350, 140);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Sound Player");
+ auto& player = window->set_main_widget<SoundPlayerWidget>(window, audio_client);
+
+ if (argc > 1) {
+ String path = argv[1];
+ player.open_file(path);
+ player.manager().play();
+ }
+
+ auto hide_scope = GUI::Action::create_checkable("Hide scope", { Mod_Ctrl, Key_H }, [&](auto& action) {
+ player.hide_scope(action.is_checked());
+ });
+
+ app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
+ Optional<String> path = GUI::FilePicker::get_open_filepath(window, "Open sound file...");
+ if (path.has_value()) {
+ player.open_file(path.value());
+ }
+ }));
+ app_menu.add_action(move(hide_scope));
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
+ app->quit();
+ }));
+
+ auto& playback_menu = menubar->add_menu("Playback");
+
+ auto loop = GUI::Action::create_checkable("Loop", { Mod_Ctrl, Key_R }, [&](auto& action) {
+ player.manager().loop(action.is_checked());
+ });
+
+ playback_menu.add_action(move(loop));
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Sound Player", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+ return app->exec();
+}
diff --git a/Userland/Applications/SpaceAnalyzer/CMakeLists.txt b/Userland/Applications/SpaceAnalyzer/CMakeLists.txt
new file mode 100644
index 0000000000..23cc18f13e
--- /dev/null
+++ b/Userland/Applications/SpaceAnalyzer/CMakeLists.txt
@@ -0,0 +1,10 @@
+compile_gml(SpaceAnalyzer.gml SpaceAnalyzerGML.h space_analyzer_gml)
+
+set(SOURCES
+ main.cpp
+ TreeMapWidget.cpp
+ SpaceAnalyzerGML.h
+)
+
+serenity_app(SpaceAnalyzer ICON app-space-analyzer)
+target_link_libraries(SpaceAnalyzer LibGfx LibGUI)
diff --git a/Userland/Applications/SpaceAnalyzer/SpaceAnalyzer.gml b/Userland/Applications/SpaceAnalyzer/SpaceAnalyzer.gml
new file mode 100644
index 0000000000..a4a3363c6a
--- /dev/null
+++ b/Userland/Applications/SpaceAnalyzer/SpaceAnalyzer.gml
@@ -0,0 +1,20 @@
+@GUI::Widget {
+ layout: @GUI::VerticalBoxLayout {
+ spacing: 0
+ }
+
+ @GUI::ToolBarContainer {
+ @GUI::BreadcrumbBar {
+ fixed_height: 25
+ name: "breadcrumb_bar"
+ }
+ }
+
+ @SpaceAnalyzer::TreeMapWidget {
+ name: "tree_map"
+ }
+
+ @GUI::StatusBar {
+ name: "status_bar"
+ }
+}
diff --git a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp
new file mode 100644
index 0000000000..97d9b58a9e
--- /dev/null
+++ b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TreeMapWidget.h"
+#include <AK/NumberFormat.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/WindowServerConnection.h>
+#include <LibGfx/Font.h>
+#include <WindowServer/WindowManager.h>
+
+namespace SpaceAnalyzer {
+
+REGISTER_WIDGET(SpaceAnalyzer, TreeMapWidget)
+
+TreeMapWidget::TreeMapWidget()
+ : m_viewpoint(0)
+{
+}
+
+TreeMapWidget::~TreeMapWidget()
+{
+}
+
+static const Color colors[] = {
+ Color(253, 231, 37),
+ Color(148, 216, 64),
+ Color(60, 188, 117),
+ Color(31, 150, 139),
+ Color(45, 112, 142),
+ Color(63, 71, 136),
+ Color(85, 121, 104),
+};
+
+static float get_normalized_aspect_ratio(float a, float b)
+{
+ if (a < b) {
+ return a / b;
+ } else {
+ return b / a;
+ }
+}
+
+static bool node_is_leaf(const TreeMapNode& node)
+{
+ return node.num_children() == 0;
+}
+
+bool TreeMapWidget::rect_can_contain_label(const Gfx::IntRect& rect) const
+{
+ return rect.height() > font().presentation_size() && rect.width() > 20;
+}
+
+bool TreeMapWidget::rect_can_contain_children(const Gfx::IntRect& rect) const
+{
+ return rect.height() > 10 && rect.width() > 10;
+}
+
+Gfx::IntRect TreeMapWidget::inner_rect_for_frame(const Gfx::IntRect& rect) const
+{
+ const int margin = 5;
+ Gfx::IntRect tmp_rect = rect;
+ tmp_rect.shrink(2, 2); // border
+ tmp_rect.shrink(2, 2); // shading
+ if (rect_can_contain_label(rect)) {
+ tmp_rect.set_y(tmp_rect.y() + font().presentation_size() + margin);
+ tmp_rect.set_height(tmp_rect.height() - (font().presentation_size() + margin * 2));
+ tmp_rect.set_x(tmp_rect.x() + margin);
+ tmp_rect.set_width(tmp_rect.width() - margin * 2);
+ }
+ return tmp_rect;
+}
+
+void TreeMapWidget::paint_cell_frame(GUI::Painter& painter, const TreeMapNode& node, const Gfx::IntRect& cell_rect, int depth, bool fill_frame) const
+{
+ const Gfx::IntRect border_rect = cell_rect.shrunken(2, 2);
+ const Gfx::IntRect outer_rect = border_rect.shrunken(2, 2);
+ const Gfx::IntRect inner_rect = inner_rect_for_frame(cell_rect);
+
+ painter.clear_clip_rect();
+ painter.add_clip_rect(cell_rect);
+ Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))];
+ if (m_selected_node_cache == &node) {
+ color = color.darkened(0.8f);
+ }
+
+ // Draw borders.
+ painter.draw_rect(cell_rect, Color::Black, false);
+ painter.draw_line(border_rect.bottom_left(), border_rect.top_left(), color.lightened());
+ painter.draw_line(border_rect.top_left(), border_rect.top_right(), color.lightened());
+ painter.draw_line(border_rect.top_right(), border_rect.bottom_right(), color.darkened());
+ painter.draw_line(border_rect.bottom_left(), border_rect.bottom_right(), color.darkened());
+
+ // Paint the background.
+ if (fill_frame) {
+ painter.fill_rect(outer_rect, color);
+ } else {
+ for (auto& shard : outer_rect.shatter(inner_rect)) {
+ painter.fill_rect(shard, color);
+ }
+ }
+
+ // Paint text.
+ if (rect_can_contain_label(outer_rect)) {
+ Gfx::IntRect text_rect = outer_rect;
+ text_rect.move_by(2, 2);
+ painter.draw_text(text_rect, node.name(), font(), Gfx::TextAlignment::TopLeft, Color::Black);
+ if (node_is_leaf(node)) {
+ text_rect.move_by(0, font().presentation_size() + 1);
+ painter.draw_text(text_rect, human_readable_size(node.area()), font(), Gfx::TextAlignment::TopLeft, Color::Black);
+ }
+ }
+}
+
+template<typename Function>
+void TreeMapWidget::lay_out_children(const TreeMapNode& node, const Gfx::IntRect& rect, int depth, Function callback)
+{
+ if (node.num_children() == 0) {
+ return;
+ }
+
+ // Check if the children are sorted yet, if not do that now.
+ for (size_t k = 0; k < node.num_children() - 1; k++) {
+ if (node.child_at(k).area() < node.child_at(k + 1).area()) {
+ node.sort_children_by_area();
+ break;
+ }
+ }
+
+ int total_area = node.area();
+ Gfx::IntRect canvas = rect;
+ bool remaining_nodes_are_too_small = false;
+ for (size_t i = 0; !remaining_nodes_are_too_small && i < node.num_children(); i++) {
+ const int i_node_area = node.child_at(i).area();
+ if (i_node_area == 0)
+ break;
+
+ const int long_side_size = max(canvas.width(), canvas.height());
+ const int short_side_size = min(canvas.width(), canvas.height());
+
+ int row_or_column_size = (long long int)long_side_size * i_node_area / total_area;
+ int node_area_sum = i_node_area;
+ size_t k = i + 1;
+
+ // Try to add nodes to this row or column so long as the worst aspect ratio of
+ // the new set of nodes is better than the worst aspect ratio of the current set.
+ {
+ float best_worst_aspect_ratio_so_far = get_normalized_aspect_ratio(row_or_column_size, short_side_size);
+ for (; k < node.num_children(); k++) {
+ // Do a preliminary calculation of the worst aspect ratio of the nodes at index i and k
+ // if that aspect ratio is better than the 'best_worst_aspect_ratio_so_far' we keep it,
+ // otherwise it is discarded.
+ int k_node_area = node.child_at(k).area();
+ if (k_node_area == 0) {
+ break;
+ }
+ int new_node_area_sum = node_area_sum + k_node_area;
+ int new_row_or_column_size = (long long int)long_side_size * new_node_area_sum / total_area;
+ int i_node_size = (long long int)short_side_size * i_node_area / new_node_area_sum;
+ int k_node_size = (long long int)short_side_size * k_node_area / new_node_area_sum;
+ float i_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, i_node_size);
+ float k_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, k_node_size);
+ float new_worst_aspect_ratio = min(i_node_aspect_ratio, k_node_aspect_ratio);
+ if (new_worst_aspect_ratio < best_worst_aspect_ratio_so_far) {
+ break;
+ }
+ best_worst_aspect_ratio_so_far = new_worst_aspect_ratio;
+ node_area_sum = new_node_area_sum;
+ row_or_column_size = new_row_or_column_size;
+ }
+ }
+
+ // Paint the elements from 'i' up to and including 'k-1'.
+ {
+ const int fixed_side_size = row_or_column_size;
+ int placement_area = node_area_sum;
+ int main_dim = short_side_size;
+
+ // Lay out nodes in a row or column.
+ Orientation orientation = canvas.width() > canvas.height() ? Orientation::Horizontal : Orientation::Vertical;
+ Gfx::IntRect layout_rect = canvas;
+ layout_rect.set_primary_size_for_orientation(orientation, fixed_side_size);
+ for (size_t q = i; q < k; q++) {
+ auto& child = node.child_at(q);
+ int node_size = (long long int)main_dim * child.area() / placement_area;
+ Gfx::IntRect cell_rect = layout_rect;
+ cell_rect.set_secondary_size_for_orientation(orientation, node_size);
+ Gfx::IntRect inner_rect = inner_rect_for_frame(cell_rect);
+ bool is_visual_leaf = child.num_children() == 0 || !rect_can_contain_children(inner_rect);
+ callback(child, q, cell_rect, depth, is_visual_leaf ? IsVisualLeaf::Yes : IsVisualLeaf::No, IsRemainder::No);
+ if (cell_rect.width() * cell_rect.height() < 16) {
+ remaining_nodes_are_too_small = true;
+ } else {
+ lay_out_children(child, inner_rect, depth + 1, callback);
+ }
+ layout_rect.set_secondary_offset_for_orientation(orientation, layout_rect.secondary_offset_for_orientation(orientation) + node_size);
+ main_dim -= node_size;
+ placement_area -= child.area();
+ }
+ canvas.set_primary_offset_for_orientation(orientation, canvas.primary_offset_for_orientation(orientation) + fixed_side_size);
+ canvas.set_primary_size_for_orientation(orientation, canvas.primary_size_for_orientation(orientation) - fixed_side_size);
+ }
+
+ // Consume nodes that were added to this row or column.
+ i = k - 1;
+ total_area -= node_area_sum;
+ }
+
+ // If not the entire canvas was filled with nodes, fill the remaining area with a dither pattern.
+ if (!canvas.is_empty()) {
+ callback(node, 0, canvas, depth, IsVisualLeaf::No, IsRemainder::Yes);
+ }
+}
+
+const TreeMapNode* TreeMapWidget::path_node(size_t n) const
+{
+ if (!m_tree.ptr())
+ return nullptr;
+ const TreeMapNode* iter = &m_tree->root();
+ size_t path_index = 0;
+ while (iter && path_index < m_path.size() && path_index < n) {
+ size_t child_index = m_path[path_index];
+ if (child_index >= iter->num_children()) {
+ return nullptr;
+ }
+ iter = &iter->child_at(child_index);
+ path_index++;
+ }
+ return iter;
+}
+
+void TreeMapWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+ GUI::Painter painter(*this);
+
+ m_selected_node_cache = path_node(m_path.size());
+
+ const TreeMapNode* node = path_node(m_viewpoint);
+ if (!node) {
+ painter.fill_rect(frame_inner_rect(), Color::MidGray);
+ } else if (node_is_leaf(*node)) {
+ paint_cell_frame(painter, *node, frame_inner_rect(), m_viewpoint - 1, true);
+ } else {
+ lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](const TreeMapNode& node, int, const Gfx::IntRect& rect, int depth, IsVisualLeaf visual_leaf, IsRemainder remainder) {
+ if (remainder == IsRemainder::No) {
+ bool fill = visual_leaf == IsVisualLeaf::Yes ? true : false;
+ paint_cell_frame(painter, node, rect, depth, fill);
+ } else {
+ Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))];
+ painter.clear_clip_rect();
+ painter.add_clip_rect(rect);
+ painter.draw_rect(rect, Color::Black);
+ painter.fill_rect_with_dither_pattern(rect.shrunken(2, 2), color, Color::Black);
+ }
+ });
+ }
+}
+
+Vector<int> TreeMapWidget::path_to_position(const Gfx::IntPoint& position)
+{
+ const TreeMapNode* node = path_node(m_viewpoint);
+ if (!node) {
+ return {};
+ }
+ Vector<int> path;
+ lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](const TreeMapNode&, int index, const Gfx::IntRect& rect, int, IsVisualLeaf, IsRemainder is_remainder) {
+ if (is_remainder == IsRemainder::No && rect.contains(position)) {
+ path.append(index);
+ }
+ });
+ return path;
+}
+
+void TreeMapWidget::mousedown_event(GUI::MouseEvent& event)
+{
+ const TreeMapNode* node = path_node(m_viewpoint);
+ if (node && !node_is_leaf(*node)) {
+ Vector<int> path = path_to_position(event.position());
+ if (!path.is_empty()) {
+ m_path.shrink(m_viewpoint);
+ m_path.append(path);
+ if (on_path_change) {
+ on_path_change();
+ }
+ update();
+ }
+ }
+}
+
+void TreeMapWidget::doubleclick_event(GUI::MouseEvent& event)
+{
+ const TreeMapNode* node = path_node(m_viewpoint);
+ if (node && !node_is_leaf(*node)) {
+ Vector<int> path = path_to_position(event.position());
+ m_path.shrink(m_viewpoint);
+ m_path.append(path);
+ m_viewpoint = m_path.size();
+ if (on_path_change) {
+ on_path_change();
+ }
+ update();
+ }
+}
+
+void TreeMapWidget::mousewheel_event(GUI::MouseEvent& event)
+{
+ int delta = event.wheel_delta();
+ // FIXME: The wheel_delta is premultiplied in the window server, we actually want a raw value here.
+ int step_size = GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::GetScrollStepSize>()->step_size();
+ if (delta > 0) {
+ size_t step_back = delta / step_size;
+ if (step_back > m_viewpoint)
+ step_back = m_viewpoint;
+ set_viewpoint(m_viewpoint - step_back);
+ } else {
+ size_t step_up = (-delta) / step_size;
+ set_viewpoint(m_viewpoint + step_up);
+ }
+}
+
+void TreeMapWidget::set_tree(RefPtr<TreeMap> tree)
+{
+ m_tree = tree;
+ m_path.clear();
+ m_viewpoint = 0;
+ if (on_path_change) {
+ on_path_change();
+ }
+ update();
+}
+
+void TreeMapWidget::set_viewpoint(size_t viewpoint)
+{
+ if (viewpoint > m_path.size())
+ viewpoint = m_path.size();
+ m_viewpoint = viewpoint;
+ if (on_path_change) {
+ on_path_change();
+ }
+ update();
+}
+
+size_t TreeMapWidget::path_size() const
+{
+ return m_path.size() + 1;
+}
+
+size_t TreeMapWidget::viewpoint() const
+{
+ return m_viewpoint;
+}
+
+}
diff --git a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h
new file mode 100644
index 0000000000..8b4e1e9269
--- /dev/null
+++ b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGfx/Rect.h>
+
+namespace SpaceAnalyzer {
+
+struct TreeMapNode {
+ virtual String name() const = 0;
+ virtual int64_t area() const = 0;
+ virtual size_t num_children() const = 0;
+ virtual const TreeMapNode& child_at(size_t i) const = 0;
+ virtual void sort_children_by_area() const = 0;
+};
+
+struct TreeMap : public RefCounted<TreeMap> {
+ virtual ~TreeMap() { }
+ virtual const TreeMapNode& root() const = 0;
+};
+
+class TreeMapWidget final : public GUI::Frame {
+ C_OBJECT(TreeMapWidget)
+
+public:
+ virtual ~TreeMapWidget() override;
+ Function<void()> on_path_change;
+ size_t path_size() const;
+ const TreeMapNode* path_node(size_t n) const;
+ size_t viewpoint() const;
+ void set_viewpoint(size_t);
+ void set_tree(RefPtr<TreeMap> tree);
+
+private:
+ TreeMapWidget();
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void mousedown_event(GUI::MouseEvent&) override;
+ virtual void doubleclick_event(GUI::MouseEvent&) override;
+ virtual void mousewheel_event(GUI::MouseEvent&) override;
+
+ bool rect_can_contain_children(const Gfx::IntRect& rect) const;
+ bool rect_can_contain_label(const Gfx::IntRect& rect) const;
+ Gfx::IntRect inner_rect_for_frame(const Gfx::IntRect& rect) const;
+
+ enum class IsVisualLeaf {
+ Yes,
+ No
+ };
+ enum class IsRemainder {
+ Yes,
+ No
+ };
+
+ template<typename Function>
+ void lay_out_children(const TreeMapNode&, const Gfx::IntRect&, int depth, Function);
+ void paint_cell_frame(GUI::Painter&, const TreeMapNode&, const Gfx::IntRect&, int depth, bool fill) const;
+ Vector<int> path_to_position(const Gfx::IntPoint&);
+
+ RefPtr<TreeMap> m_tree;
+ Vector<int> m_path;
+ size_t m_viewpoint;
+ const void* m_selected_node_cache;
+};
+
+}
diff --git a/Userland/Applications/SpaceAnalyzer/main.cpp b/Userland/Applications/SpaceAnalyzer/main.cpp
new file mode 100644
index 0000000000..2ce061f786
--- /dev/null
+++ b/Userland/Applications/SpaceAnalyzer/main.cpp
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "TreeMapWidget.h"
+#include <AK/Queue.h>
+#include <AK/QuickSort.h>
+#include <AK/RefCounted.h>
+#include <Applications/SpaceAnalyzer/SpaceAnalyzerGML.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <LibGUI/AboutDialog.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BreadcrumbBar.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/StatusBar.h>
+#include <sys/stat.h>
+
+static const char* APP_NAME = "Space Analyzer";
+
+struct TreeNode : public SpaceAnalyzer::TreeMapNode {
+ TreeNode(String name)
+ : m_name(move(name)) {};
+
+ virtual String name() const { return m_name; }
+ virtual int64_t area() const { return m_area; }
+ virtual size_t num_children() const
+ {
+ if (m_children) {
+ return m_children->size();
+ }
+ return 0;
+ }
+ virtual const TreeNode& child_at(size_t i) const { return m_children->at(i); }
+ virtual void sort_children_by_area() const
+ {
+ if (m_children) {
+ Vector<TreeNode>* children = const_cast<Vector<TreeNode>*>(m_children.ptr());
+ quick_sort(*children, [](auto& a, auto& b) { return b.m_area < a.m_area; });
+ }
+ }
+
+ String m_name;
+ int64_t m_area { 0 };
+ OwnPtr<Vector<TreeNode>> m_children;
+};
+
+struct Tree : public SpaceAnalyzer::TreeMap {
+ Tree(String root_name)
+ : m_root(move(root_name)) {};
+ virtual ~Tree() {};
+ TreeNode m_root;
+ virtual const SpaceAnalyzer::TreeMapNode& root() const override
+ {
+ return m_root;
+ };
+};
+
+struct MountInfo {
+ String mount_point;
+ String source;
+};
+
+static void fill_mounts(Vector<MountInfo>& output)
+{
+ // Output info about currently mounted filesystems.
+ auto df = Core::File::construct("/proc/df");
+ if (!df->open(Core::IODevice::ReadOnly)) {
+ fprintf(stderr, "Failed to open /proc/df: %s\n", df->error_string());
+ return;
+ }
+
+ auto content = df->read_all();
+ auto json = JsonValue::from_string(content);
+ ASSERT(json.has_value());
+
+ json.value().as_array().for_each([&output](auto& value) {
+ auto filesystem_object = value.as_object();
+ MountInfo mount_info;
+ mount_info.mount_point = filesystem_object.get("mount_point").to_string();
+ mount_info.source = filesystem_object.get("source").as_string_or("none");
+ output.append(mount_info);
+ });
+}
+
+static MountInfo* find_mount_for_path(String path, Vector<MountInfo>& mounts)
+{
+ MountInfo* result = nullptr;
+ size_t length = 0;
+ for (auto& mount_info : mounts) {
+ String& mount_point = mount_info.mount_point;
+ if (path.starts_with(mount_point)) {
+ if (!result || mount_point.length() > length) {
+ result = &mount_info;
+ length = mount_point.length();
+ }
+ }
+ }
+ return result;
+}
+
+static long long int update_totals(TreeNode& node)
+{
+ long long int result = 0;
+ if (node.m_children) {
+ for (auto& child : *node.m_children) {
+ result += update_totals(child);
+ }
+ node.m_area = result;
+ } else {
+ result = node.m_area;
+ }
+ return result;
+}
+
+struct QueueEntry {
+ QueueEntry(String path, TreeNode* node)
+ : path(move(path))
+ , node(node) {};
+ String path;
+ TreeNode* node { nullptr };
+};
+
+static void populate_filesize_tree(TreeNode& root, Vector<MountInfo>& mounts, HashMap<int, int>& error_accumulator)
+{
+ ASSERT(!root.m_name.ends_with("/"));
+
+ Queue<QueueEntry> queue;
+ queue.enqueue(QueueEntry(root.m_name, &root));
+
+ StringBuilder builder = StringBuilder();
+ builder.append(root.m_name);
+ builder.append("/");
+ MountInfo* root_mount_info = find_mount_for_path(builder.to_string(), mounts);
+ if (!root_mount_info) {
+ return;
+ }
+ while (!queue.is_empty()) {
+ QueueEntry queue_entry = queue.dequeue();
+
+ builder.clear();
+ builder.append(queue_entry.path);
+ builder.append("/");
+
+ MountInfo* mount_info = find_mount_for_path(builder.to_string(), mounts);
+ if (!mount_info || (mount_info != root_mount_info && mount_info->source != root_mount_info->source)) {
+ continue;
+ }
+
+ Core::DirIterator dir_iterator(builder.to_string(), Core::DirIterator::SkipParentAndBaseDir);
+ if (dir_iterator.has_error()) {
+ int error_sum = error_accumulator.get(dir_iterator.error()).value_or(0);
+ error_accumulator.set(dir_iterator.error(), error_sum + 1);
+ } else {
+ queue_entry.node->m_children = make<Vector<TreeNode>>();
+ while (dir_iterator.has_next()) {
+ queue_entry.node->m_children->append(TreeNode(dir_iterator.next_path()));
+ }
+ for (auto& child : *queue_entry.node->m_children) {
+ String& name = child.m_name;
+ int name_len = name.length();
+ builder.append(name);
+ struct stat st;
+ int stat_result = lstat(builder.to_string().characters(), &st);
+ if (stat_result < 0) {
+ int error_sum = error_accumulator.get(errno).value_or(0);
+ error_accumulator.set(errno, error_sum + 1);
+ } else {
+ if (S_ISDIR(st.st_mode)) {
+ queue.enqueue(QueueEntry(builder.to_string(), &child));
+ } else {
+ child.m_area = st.st_size;
+ }
+ }
+ builder.trim(name_len);
+ }
+ }
+ }
+
+ update_totals(root);
+}
+
+static void analyze(RefPtr<Tree> tree, SpaceAnalyzer::TreeMapWidget& treemapwidget, GUI::StatusBar& statusbar)
+{
+ // Build an in-memory tree mirroring the filesystem and for each node
+ // calculate the sum of the file size for all its descendants.
+ TreeNode* root = &tree->m_root;
+ Vector<MountInfo> mounts;
+ fill_mounts(mounts);
+ HashMap<int, int> error_accumulator;
+ populate_filesize_tree(*root, mounts, error_accumulator);
+
+ // Display an error summary in the statusbar.
+ if (!error_accumulator.is_empty()) {
+ StringBuilder builder;
+ bool first = true;
+ builder.append("Some directories were not analyzed: ");
+ for (auto& key : error_accumulator.keys()) {
+ if (!first) {
+ builder.append(", ");
+ }
+ builder.append(strerror(key));
+ builder.append(" (");
+ int value = error_accumulator.get(key).value();
+ builder.append(String::number(value));
+ if (value == 1) {
+ builder.append(" time");
+ } else {
+ builder.append(" times");
+ }
+ builder.append(")");
+ first = false;
+ }
+ statusbar.set_text(builder.to_string());
+ } else {
+ statusbar.set_text("No errors");
+ }
+ treemapwidget.set_tree(tree);
+}
+
+int main(int argc, char* argv[])
+{
+ auto app = GUI::Application::construct(argc, argv);
+
+ RefPtr<Tree> tree = adopt(*new Tree(""));
+
+ // Configure application window.
+ auto app_icon = GUI::Icon::default_icon("app-space-analyzer");
+ auto window = GUI::Window::construct();
+ window->set_title(APP_NAME);
+ window->resize(640, 480);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ // Load widgets.
+ auto& mainwidget = window->set_main_widget<GUI::Widget>();
+ mainwidget.load_from_gml(space_analyzer_gml);
+ auto& breadcrumbbar = *mainwidget.find_descendant_of_type_named<GUI::BreadcrumbBar>("breadcrumb_bar");
+ auto& treemapwidget = *mainwidget.find_descendant_of_type_named<SpaceAnalyzer::TreeMapWidget>("tree_map");
+ auto& statusbar = *mainwidget.find_descendant_of_type_named<GUI::StatusBar>("status_bar");
+
+ // Configure the menubar.
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu(APP_NAME);
+ app_menu.add_action(GUI::Action::create("Analyze", [&](auto&) {
+ analyze(tree, treemapwidget, statusbar);
+ }));
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
+ app->quit();
+ }));
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action(APP_NAME, app_icon, window));
+ app->set_menubar(move(menubar));
+
+ // Configure event handlers.
+ breadcrumbbar.on_segment_click = [&](size_t index) {
+ ASSERT(index < treemapwidget.path_size());
+ treemapwidget.set_viewpoint(index);
+ };
+ treemapwidget.on_path_change = [&]() {
+ breadcrumbbar.clear_segments();
+ for (size_t k = 0; k < treemapwidget.path_size(); k++) {
+ if (k == 0) {
+ breadcrumbbar.append_segment("/");
+ } else {
+ const SpaceAnalyzer::TreeMapNode* node = treemapwidget.path_node(k);
+ breadcrumbbar.append_segment(node->name());
+ }
+ }
+ breadcrumbbar.set_selected_segment(treemapwidget.viewpoint());
+ };
+
+ // At startup automatically do an analysis of root.
+ analyze(tree, treemapwidget, statusbar);
+
+ window->show();
+ return app->exec();
+}
diff --git a/Userland/Applications/Spreadsheet/CMakeLists.txt b/Userland/Applications/Spreadsheet/CMakeLists.txt
new file mode 100644
index 0000000000..f41922655a
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CMakeLists.txt
@@ -0,0 +1,28 @@
+compile_gml(CondFormatting.gml CondFormattingGML.h cond_fmt_gml)
+compile_gml(CondView.gml CondFormattingViewGML.h cond_fmt_view_gml)
+
+set(SOURCES
+ Cell.cpp
+ CellSyntaxHighlighter.cpp
+ CellType/Date.cpp
+ CellType/Format.cpp
+ CellType/Identity.cpp
+ CellType/Numeric.cpp
+ CellType/String.cpp
+ CellType/Type.cpp
+ CellTypeDialog.cpp
+ CondFormattingGML.h
+ CondFormattingViewGML.h
+ HelpWindow.cpp
+ JSIntegration.cpp
+ Readers/XSV.cpp
+ Spreadsheet.cpp
+ SpreadsheetModel.cpp
+ SpreadsheetView.cpp
+ SpreadsheetWidget.cpp
+ Workbook.cpp
+ main.cpp
+)
+
+serenity_app(Spreadsheet ICON app-spreadsheet)
+target_link_libraries(Spreadsheet LibGUI LibJS LibWeb)
diff --git a/Userland/Applications/Spreadsheet/Cell.cpp b/Userland/Applications/Spreadsheet/Cell.cpp
new file mode 100644
index 0000000000..d2f511753f
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Cell.cpp
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Cell.h"
+#include "Spreadsheet.h"
+#include <AK/StringBuilder.h>
+#include <AK/TemporaryChange.h>
+
+namespace Spreadsheet {
+
+void Cell::set_data(String new_data)
+{
+ if (m_data == new_data)
+ return;
+
+ if (new_data.starts_with("=")) {
+ new_data = new_data.substring(1, new_data.length() - 1);
+ m_kind = Formula;
+ } else {
+ m_kind = LiteralString;
+ }
+
+ m_data = move(new_data);
+ m_dirty = true;
+ m_evaluated_externally = false;
+}
+
+void Cell::set_data(JS::Value new_data)
+{
+ m_dirty = true;
+ m_evaluated_externally = true;
+
+ StringBuilder builder;
+
+ builder.append(new_data.to_string_without_side_effects());
+ m_data = builder.build();
+
+ m_evaluated_data = move(new_data);
+}
+
+void Cell::set_type(const CellType* type)
+{
+ m_type = type;
+}
+
+void Cell::set_type(const StringView& name)
+{
+ auto* cell_type = CellType::get_by_name(name);
+ if (cell_type) {
+ return set_type(cell_type);
+ }
+
+ ASSERT_NOT_REACHED();
+}
+
+void Cell::set_type_metadata(CellTypeMetadata&& metadata)
+{
+ m_type_metadata = move(metadata);
+}
+
+const CellType& Cell::type() const
+{
+ if (m_type)
+ return *m_type;
+
+ if (m_kind == LiteralString) {
+ if (m_data.to_int().has_value())
+ return *CellType::get_by_name("Numeric");
+ }
+
+ return *CellType::get_by_name("Identity");
+}
+
+String Cell::typed_display() const
+{
+ return type().display(const_cast<Cell&>(*this), m_type_metadata);
+}
+
+JS::Value Cell::typed_js_data() const
+{
+ return type().js_value(const_cast<Cell&>(*this), m_type_metadata);
+}
+
+void Cell::update_data(Badge<Sheet>)
+{
+ TemporaryChange cell_change { m_sheet->current_evaluated_cell(), this };
+ if (!m_dirty)
+ return;
+
+ m_js_exception = {};
+
+ if (m_dirty) {
+ m_dirty = false;
+ if (m_kind == Formula) {
+ if (!m_evaluated_externally) {
+ auto [value, exception] = m_sheet->evaluate(m_data, this);
+ m_evaluated_data = value;
+ m_js_exception = move(exception);
+ }
+ }
+
+ for (auto& ref : m_referencing_cells) {
+ if (ref) {
+ ref->m_dirty = true;
+ ref->update();
+ }
+ }
+ }
+
+ m_evaluated_formats.background_color.clear();
+ m_evaluated_formats.foreground_color.clear();
+ if (!m_js_exception) {
+ StringBuilder builder;
+ for (auto& fmt : m_conditional_formats) {
+ if (!fmt.condition.is_empty()) {
+ builder.clear();
+ builder.append("return (");
+ builder.append(fmt.condition);
+ builder.append(')');
+ auto [value, exception] = m_sheet->evaluate(builder.string_view(), this);
+ if (exception) {
+ m_js_exception = move(exception);
+ } else {
+ if (value.to_boolean()) {
+ if (fmt.background_color.has_value())
+ m_evaluated_formats.background_color = fmt.background_color;
+ if (fmt.foreground_color.has_value())
+ m_evaluated_formats.foreground_color = fmt.foreground_color;
+ }
+ }
+ }
+ }
+ }
+}
+
+void Cell::update()
+{
+ m_sheet->update(*this);
+}
+
+JS::Value Cell::js_data()
+{
+ if (m_dirty)
+ update();
+
+ if (m_kind == Formula)
+ return m_evaluated_data;
+
+ return JS::js_string(m_sheet->interpreter().heap(), m_data);
+}
+
+String Cell::source() const
+{
+ StringBuilder builder;
+ if (m_kind == Formula)
+ builder.append('=');
+ builder.append(m_data);
+ return builder.to_string();
+}
+
+// FIXME: Find a better way to figure out dependencies
+void Cell::reference_from(Cell* other)
+{
+ if (!other || other == this)
+ return;
+
+ if (!m_referencing_cells.find_if([other](const auto& ptr) { return ptr.ptr() == other; }).is_end())
+ return;
+
+ m_referencing_cells.append(other->make_weak_ptr());
+}
+
+void Cell::copy_from(const Cell& other)
+{
+ m_dirty = true;
+ m_evaluated_externally = other.m_evaluated_externally;
+ m_data = other.m_data;
+ m_evaluated_data = other.m_evaluated_data;
+ m_kind = other.m_kind;
+ m_type = other.m_type;
+ m_type_metadata = other.m_type_metadata;
+ m_conditional_formats = other.m_conditional_formats;
+ m_evaluated_formats = other.m_evaluated_formats;
+ if (!other.m_js_exception)
+ m_js_exception = other.m_js_exception;
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/Cell.h b/Userland/Applications/Spreadsheet/Cell.h
new file mode 100644
index 0000000000..fef9031d5e
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Cell.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "CellType/Type.h"
+#include "ConditionalFormatting.h"
+#include "Forward.h"
+#include "JSIntegration.h"
+#include "Position.h"
+#include <AK/String.h>
+#include <AK/Types.h>
+#include <AK/WeakPtr.h>
+
+namespace Spreadsheet {
+
+struct Cell : public Weakable<Cell> {
+ enum Kind {
+ LiteralString,
+ Formula,
+ };
+
+ Cell(String data, Position position, WeakPtr<Sheet> sheet)
+ : m_dirty(false)
+ , m_data(move(data))
+ , m_kind(LiteralString)
+ , m_sheet(sheet)
+ , m_position(move(position))
+ {
+ }
+
+ Cell(String source, JS::Value&& cell_value, Position position, WeakPtr<Sheet> sheet)
+ : m_dirty(false)
+ , m_data(move(source))
+ , m_evaluated_data(move(cell_value))
+ , m_kind(Formula)
+ , m_sheet(sheet)
+ , m_position(move(position))
+ {
+ }
+
+ void reference_from(Cell*);
+
+ void set_data(String new_data);
+ void set_data(JS::Value new_data);
+ bool dirty() const { return m_dirty; }
+ void clear_dirty() { m_dirty = false; }
+
+ void set_exception(JS::Exception* exc) { m_js_exception = exc; }
+ JS::Exception* exception() const { return m_js_exception; }
+
+ const String& data() const { return m_data; }
+ const JS::Value& evaluated_data() const { return m_evaluated_data; }
+ Kind kind() const { return m_kind; }
+ const Vector<WeakPtr<Cell>>& referencing_cells() const { return m_referencing_cells; }
+
+ void set_type(const StringView& name);
+ void set_type(const CellType*);
+ void set_type_metadata(CellTypeMetadata&&);
+
+ const Position& position() const { return m_position; }
+ void set_position(Position position, Badge<Sheet>)
+ {
+ if (position != m_position) {
+ m_dirty = true;
+ m_position = move(position);
+ }
+ }
+
+ const Format& evaluated_formats() const { return m_evaluated_formats; }
+ Format& evaluated_formats() { return m_evaluated_formats; }
+ const Vector<ConditionalFormat>& conditional_formats() const { return m_conditional_formats; }
+ void set_conditional_formats(Vector<ConditionalFormat>&& fmts)
+ {
+ m_dirty = true;
+ m_conditional_formats = move(fmts);
+ }
+
+ String typed_display() const;
+ JS::Value typed_js_data() const;
+
+ const CellType& type() const;
+ const CellTypeMetadata& type_metadata() const { return m_type_metadata; }
+ CellTypeMetadata& type_metadata() { return m_type_metadata; }
+
+ String source() const;
+
+ JS::Value js_data();
+
+ void update();
+ void update_data(Badge<Sheet>);
+
+ const Sheet& sheet() const { return *m_sheet; }
+ Sheet& sheet() { return *m_sheet; }
+
+ void copy_from(const Cell&);
+
+private:
+ bool m_dirty { false };
+ bool m_evaluated_externally { false };
+ String m_data;
+ JS::Value m_evaluated_data;
+ JS::Exception* m_js_exception { nullptr };
+ Kind m_kind { LiteralString };
+ WeakPtr<Sheet> m_sheet;
+ Vector<WeakPtr<Cell>> m_referencing_cells;
+ const CellType* m_type { nullptr };
+ CellTypeMetadata m_type_metadata;
+ Position m_position;
+
+ Vector<ConditionalFormat> m_conditional_formats;
+ Format m_evaluated_formats;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp b/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp
new file mode 100644
index 0000000000..c26d2aa238
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CellSyntaxHighlighter.h"
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGfx/Palette.h>
+#include <LibJS/Lexer.h>
+
+namespace Spreadsheet {
+
+void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette)
+{
+ ASSERT(m_editor);
+ auto text = m_editor->text();
+ m_editor->document().spans().clear();
+ if (!text.starts_with('=')) {
+ m_editor->update();
+ return;
+ }
+
+ JSSyntaxHighlighter::rehighlight(palette);
+
+ // Highlight the '='
+ m_editor->document().spans().empend(
+ GUI::TextRange { { 0, 0 }, { 0, 1 } },
+ Gfx::TextAttributes {
+ palette.syntax_keyword(),
+ Optional<Color> {},
+ false,
+ false,
+ },
+ nullptr,
+ false);
+
+ if (m_cell && m_cell->exception()) {
+ auto range = m_cell->exception()->source_ranges().first();
+ GUI::TextRange text_range { { range.start.line - 1, range.start.column }, { range.end.line - 1, range.end.column - 1 } };
+ m_editor->document().spans().prepend(
+ GUI::TextDocumentSpan {
+ text_range,
+ Gfx::TextAttributes {
+ Color::Black,
+ Color::Red,
+ false,
+ false,
+ },
+ nullptr,
+ false });
+ }
+ m_editor->update();
+}
+
+CellSyntaxHighlighter::~CellSyntaxHighlighter()
+{
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h b/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h
new file mode 100644
index 0000000000..f02f7e2bbb
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellSyntaxHighlighter.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Cell.h"
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/SyntaxHighlighter.h>
+
+namespace Spreadsheet {
+
+class CellSyntaxHighlighter final : public GUI::JSSyntaxHighlighter {
+public:
+ CellSyntaxHighlighter() { }
+ virtual ~CellSyntaxHighlighter() override;
+
+ virtual void rehighlight(Gfx::Palette) override;
+ void set_cell(const Cell* cell) { m_cell = cell; }
+
+private:
+ const Cell* m_cell { nullptr };
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Date.cpp b/Userland/Applications/Spreadsheet/CellType/Date.cpp
new file mode 100644
index 0000000000..005620006f
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Date.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Date.h"
+#include "../Cell.h"
+#include "../Spreadsheet.h"
+#include <AK/ScopeGuard.h>
+#include <LibCore/DateTime.h>
+
+namespace Spreadsheet {
+
+DateCell::DateCell()
+ : CellType("Date")
+{
+}
+
+DateCell::~DateCell()
+{
+}
+
+String DateCell::display(Cell& cell, const CellTypeMetadata& metadata) const
+{
+ ScopeGuard propagate_exception { [&cell] {
+ if (auto exc = cell.sheet().interpreter().exception()) {
+ cell.sheet().interpreter().vm().clear_exception();
+ cell.set_exception(exc);
+ }
+ } };
+ auto timestamp = js_value(cell, metadata);
+ auto string = Core::DateTime::from_timestamp(timestamp.to_i32(cell.sheet().global_object())).to_string(metadata.format.is_empty() ? "%Y-%m-%d %H:%M:%S" : metadata.format.characters());
+
+ if (metadata.length >= 0)
+ return string.substring(0, metadata.length);
+
+ return string;
+}
+
+JS::Value DateCell::js_value(Cell& cell, const CellTypeMetadata&) const
+{
+ auto js_data = cell.js_data();
+ auto value = js_data.to_double(cell.sheet().global_object());
+ return JS::Value(value / 1000); // Turn it to seconds
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Date.h b/Userland/Applications/Spreadsheet/CellType/Date.h
new file mode 100644
index 0000000000..3221fad0f1
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Date.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Type.h"
+
+namespace Spreadsheet {
+
+class DateCell : public CellType {
+
+public:
+ DateCell();
+ virtual ~DateCell() override;
+ virtual String display(Cell&, const CellTypeMetadata&) const override;
+ virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Format.cpp b/Userland/Applications/Spreadsheet/CellType/Format.cpp
new file mode 100644
index 0000000000..d7c590a550
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Format.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Format.h"
+#include <AK/PrintfImplementation.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+
+namespace Spreadsheet {
+
+template<typename T, typename V>
+struct SingleEntryListNext {
+ ALWAYS_INLINE T operator()(V value) const
+ {
+ return (T)value;
+ }
+};
+
+template<typename PutChFunc, typename ArgumentListRefT, template<typename T, typename U = ArgumentListRefT> typename NextArgument>
+struct PrintfImpl : public PrintfImplementation::PrintfImpl<PutChFunc, ArgumentListRefT, NextArgument> {
+ ALWAYS_INLINE PrintfImpl(PutChFunc& putch, char*& bufptr, const int& nwritten)
+ : PrintfImplementation::PrintfImpl<PutChFunc, ArgumentListRefT, NextArgument>(putch, bufptr, nwritten)
+ {
+ }
+
+ // Disallow pointer formats.
+ ALWAYS_INLINE int format_n(const PrintfImplementation::ModifierState&, ArgumentListRefT&) const
+ {
+ return 0;
+ }
+ ALWAYS_INLINE int format_s(const PrintfImplementation::ModifierState&, ArgumentListRefT&) const
+ {
+ return 0;
+ }
+};
+
+String format_double(const char* format, double value)
+{
+ StringBuilder builder;
+ auto putch = [&](auto, auto ch) { builder.append(ch); };
+ printf_internal<decltype(putch), PrintfImpl, double, SingleEntryListNext>(putch, nullptr, format, value);
+
+ return builder.build();
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Format.h b/Userland/Applications/Spreadsheet/CellType/Format.h
new file mode 100644
index 0000000000..98f3cdc741
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Format.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Forward.h>
+
+namespace Spreadsheet {
+
+String format_double(const char* format, double value);
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Identity.cpp b/Userland/Applications/Spreadsheet/CellType/Identity.cpp
new file mode 100644
index 0000000000..eed7cb190c
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Identity.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Identity.h"
+#include "../Cell.h"
+#include "../Spreadsheet.h"
+
+namespace Spreadsheet {
+
+IdentityCell::IdentityCell()
+ : CellType("Identity")
+{
+}
+
+IdentityCell::~IdentityCell()
+{
+}
+
+String IdentityCell::display(Cell& cell, const CellTypeMetadata&) const
+{
+ return cell.js_data().to_string_without_side_effects();
+}
+
+JS::Value IdentityCell::js_value(Cell& cell, const CellTypeMetadata&) const
+{
+ return cell.js_data();
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Identity.h b/Userland/Applications/Spreadsheet/CellType/Identity.h
new file mode 100644
index 0000000000..09089153ec
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Identity.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Type.h"
+
+namespace Spreadsheet {
+
+class IdentityCell : public CellType {
+
+public:
+ IdentityCell();
+ virtual ~IdentityCell() override;
+ virtual String display(Cell&, const CellTypeMetadata&) const override;
+ virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Numeric.cpp b/Userland/Applications/Spreadsheet/CellType/Numeric.cpp
new file mode 100644
index 0000000000..d58d48cd85
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Numeric.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Numeric.h"
+#include "../Cell.h"
+#include "../Spreadsheet.h"
+#include "Format.h"
+#include <AK/ScopeGuard.h>
+
+namespace Spreadsheet {
+
+NumericCell::NumericCell()
+ : CellType("Numeric")
+{
+}
+
+NumericCell::~NumericCell()
+{
+}
+
+String NumericCell::display(Cell& cell, const CellTypeMetadata& metadata) const
+{
+ ScopeGuard propagate_exception { [&cell] {
+ if (auto exc = cell.sheet().interpreter().exception()) {
+ cell.sheet().interpreter().vm().clear_exception();
+ cell.set_exception(exc);
+ }
+ } };
+ auto value = js_value(cell, metadata);
+ String string;
+ if (metadata.format.is_empty())
+ string = value.to_string_without_side_effects();
+ else
+ string = format_double(metadata.format.characters(), value.to_double(cell.sheet().global_object()));
+
+ if (metadata.length >= 0)
+ return string.substring(0, metadata.length);
+
+ return string;
+}
+
+JS::Value NumericCell::js_value(Cell& cell, const CellTypeMetadata&) const
+{
+ ScopeGuard propagate_exception { [&cell] {
+ if (auto exc = cell.sheet().interpreter().exception()) {
+ cell.sheet().interpreter().vm().clear_exception();
+ cell.set_exception(exc);
+ }
+ } };
+ return cell.js_data().to_number(cell.sheet().global_object());
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Numeric.h b/Userland/Applications/Spreadsheet/CellType/Numeric.h
new file mode 100644
index 0000000000..e0cd4a295a
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Numeric.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Type.h"
+
+namespace Spreadsheet {
+
+class NumericCell : public CellType {
+
+public:
+ NumericCell();
+ virtual ~NumericCell() override;
+ virtual String display(Cell&, const CellTypeMetadata&) const override;
+ virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/String.cpp b/Userland/Applications/Spreadsheet/CellType/String.cpp
new file mode 100644
index 0000000000..ccda9947fe
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/String.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "String.h"
+#include "../Cell.h"
+#include "../Spreadsheet.h"
+
+namespace Spreadsheet {
+
+StringCell::StringCell()
+ : CellType("String")
+{
+}
+
+StringCell::~StringCell()
+{
+}
+
+String StringCell::display(Cell& cell, const CellTypeMetadata& metadata) const
+{
+ auto string = cell.js_data().to_string_without_side_effects();
+ if (metadata.length >= 0)
+ return string.substring(0, metadata.length);
+
+ return string;
+}
+
+JS::Value StringCell::js_value(Cell& cell, const CellTypeMetadata& metadata) const
+{
+ auto string = display(cell, metadata);
+ return JS::js_string(cell.sheet().interpreter().heap(), string);
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/String.h b/Userland/Applications/Spreadsheet/CellType/String.h
new file mode 100644
index 0000000000..e4ba28eb24
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/String.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Type.h"
+
+namespace Spreadsheet {
+
+class StringCell : public CellType {
+
+public:
+ StringCell();
+ virtual ~StringCell() override;
+ virtual String display(Cell&, const CellTypeMetadata&) const override;
+ virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const override;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Type.cpp b/Userland/Applications/Spreadsheet/CellType/Type.cpp
new file mode 100644
index 0000000000..cd64e8f239
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Type.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Type.h"
+#include "Date.h"
+#include "Identity.h"
+#include "Numeric.h"
+#include "String.h"
+#include <AK/HashMap.h>
+#include <AK/OwnPtr.h>
+
+static HashMap<String, Spreadsheet::CellType*> s_cell_types;
+static Spreadsheet::StringCell s_string_cell;
+static Spreadsheet::NumericCell s_numeric_cell;
+static Spreadsheet::IdentityCell s_identity_cell;
+static Spreadsheet::DateCell s_date_cell;
+
+namespace Spreadsheet {
+
+const CellType* CellType::get_by_name(const StringView& name)
+{
+ return s_cell_types.get(name).value_or(nullptr);
+}
+
+Vector<StringView> CellType::names()
+{
+ Vector<StringView> names;
+ for (auto& it : s_cell_types)
+ names.append(it.key);
+ return names;
+}
+
+CellType::CellType(const StringView& name)
+ : m_name(name)
+{
+ ASSERT(!s_cell_types.contains(name));
+ s_cell_types.set(name, this);
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellType/Type.h b/Userland/Applications/Spreadsheet/CellType/Type.h
new file mode 100644
index 0000000000..0145a8fa8d
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellType/Type.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "../ConditionalFormatting.h"
+#include "../Forward.h"
+#include <AK/Forward.h>
+#include <AK/String.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/TextAlignment.h>
+#include <LibJS/Forward.h>
+
+namespace Spreadsheet {
+
+struct CellTypeMetadata {
+ int length { -1 };
+ String format;
+ Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterRight };
+ Format static_format;
+};
+
+class CellType {
+public:
+ static const CellType* get_by_name(const StringView&);
+ static Vector<StringView> names();
+
+ virtual String display(Cell&, const CellTypeMetadata&) const = 0;
+ virtual JS::Value js_value(Cell&, const CellTypeMetadata&) const = 0;
+ virtual ~CellType() { }
+
+ const String& name() const { return m_name; }
+
+protected:
+ CellType(const StringView& name);
+
+private:
+ String m_name;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellTypeDialog.cpp b/Userland/Applications/Spreadsheet/CellTypeDialog.cpp
new file mode 100644
index 0000000000..b94feab5e5
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellTypeDialog.cpp
@@ -0,0 +1,483 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "CellTypeDialog.h"
+#include "Cell.h"
+#include "Spreadsheet.h"
+#include <AK/StringBuilder.h>
+#include <Applications/Spreadsheet/CondFormattingGML.h>
+#include <Applications/Spreadsheet/CondFormattingViewGML.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/ColorInput.h>
+#include <LibGUI/ComboBox.h>
+#include <LibGUI/ItemListModel.h>
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/ListView.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/FontDatabase.h>
+
+REGISTER_WIDGET(Spreadsheet, ConditionsView);
+
+namespace Spreadsheet {
+
+CellTypeDialog::CellTypeDialog(const Vector<Position>& positions, Sheet& sheet, GUI::Window* parent)
+ : GUI::Dialog(parent)
+{
+ ASSERT(!positions.is_empty());
+
+ StringBuilder builder;
+
+ if (positions.size() == 1)
+ builder.appendff("Format cell {}{}", positions.first().column, positions.first().row);
+ else
+ builder.appendff("Format {} cells", positions.size());
+
+ set_title(builder.string_view());
+ set_icon(parent->icon());
+ resize(285, 360);
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ main_widget.set_fill_with_background_color(true);
+
+ auto& tab_widget = main_widget.add<GUI::TabWidget>();
+ setup_tabs(tab_widget, positions, sheet);
+
+ auto& buttonbox = main_widget.add<GUI::Widget>();
+ buttonbox.set_shrink_to_fit(true);
+ auto& button_layout = buttonbox.set_layout<GUI::HorizontalBoxLayout>();
+ button_layout.set_spacing(10);
+ button_layout.add_spacer();
+ auto& ok_button = buttonbox.add<GUI::Button>("OK");
+ ok_button.set_fixed_width(80);
+ ok_button.on_click = [&](auto) { done(ExecOK); };
+}
+
+const Vector<String> g_horizontal_alignments { "Left", "Center", "Right" };
+const Vector<String> g_vertical_alignments { "Top", "Center", "Bottom" };
+Vector<String> g_types;
+
+constexpr static CellTypeDialog::VerticalAlignment vertical_alignment_from(Gfx::TextAlignment alignment)
+{
+ switch (alignment) {
+ case Gfx::TextAlignment::CenterRight:
+ case Gfx::TextAlignment::CenterLeft:
+ case Gfx::TextAlignment::Center:
+ return CellTypeDialog::VerticalAlignment::Center;
+
+ case Gfx::TextAlignment::TopRight:
+ case Gfx::TextAlignment::TopLeft:
+ return CellTypeDialog::VerticalAlignment::Top;
+
+ case Gfx::TextAlignment::BottomRight:
+ return CellTypeDialog::VerticalAlignment::Bottom;
+ }
+
+ return CellTypeDialog::VerticalAlignment::Center;
+}
+
+constexpr static CellTypeDialog::HorizontalAlignment horizontal_alignment_from(Gfx::TextAlignment alignment)
+{
+ switch (alignment) {
+ case Gfx::TextAlignment::Center:
+ return CellTypeDialog::HorizontalAlignment::Center;
+
+ case Gfx::TextAlignment::CenterRight:
+ case Gfx::TextAlignment::TopRight:
+ case Gfx::TextAlignment::BottomRight:
+ return CellTypeDialog::HorizontalAlignment::Right;
+
+ case Gfx::TextAlignment::TopLeft:
+ case Gfx::TextAlignment::CenterLeft:
+ return CellTypeDialog::HorizontalAlignment::Left;
+ }
+
+ return CellTypeDialog::HorizontalAlignment::Right;
+}
+
+void CellTypeDialog::setup_tabs(GUI::TabWidget& tabs, const Vector<Position>& positions, Sheet& sheet)
+{
+ g_types.clear();
+ for (auto& type_name : CellType::names())
+ g_types.append(type_name);
+
+ Vector<Cell*> cells;
+ for (auto& position : positions) {
+ if (auto cell = sheet.at(position))
+ cells.append(cell);
+ }
+
+ if (cells.size() == 1) {
+ auto& cell = *cells.first();
+ m_format = cell.type_metadata().format;
+ m_length = cell.type_metadata().length;
+ m_type = &cell.type();
+ m_vertical_alignment = vertical_alignment_from(cell.type_metadata().alignment);
+ m_horizontal_alignment = horizontal_alignment_from(cell.type_metadata().alignment);
+ m_static_format = cell.type_metadata().static_format;
+ m_conditional_formats = cell.conditional_formats();
+ }
+
+ auto& type_tab = tabs.add_tab<GUI::Widget>("Type");
+ type_tab.set_layout<GUI::HorizontalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ {
+ auto& left_side = type_tab.add<GUI::Widget>();
+ left_side.set_layout<GUI::VerticalBoxLayout>();
+ auto& right_side = type_tab.add<GUI::Widget>();
+ right_side.set_layout<GUI::VerticalBoxLayout>();
+ right_side.set_fixed_width(170);
+
+ auto& type_list = left_side.add<GUI::ListView>();
+ type_list.set_model(*GUI::ItemListModel<String>::create(g_types));
+ type_list.set_should_hide_unnecessary_scrollbars(true);
+ type_list.on_selection = [&](auto& index) {
+ if (!index.is_valid()) {
+ m_type = nullptr;
+ return;
+ }
+
+ m_type = CellType::get_by_name(g_types.at(index.row()));
+ };
+
+ {
+ auto& checkbox = right_side.add<GUI::CheckBox>("Override max length");
+ auto& spinbox = right_side.add<GUI::SpinBox>();
+ checkbox.set_checked(m_length != -1);
+ spinbox.set_min(0);
+ spinbox.set_enabled(m_length != -1);
+ if (m_length > -1)
+ spinbox.set_value(m_length);
+
+ checkbox.on_checked = [&](auto checked) {
+ spinbox.set_enabled(checked);
+ if (!checked) {
+ m_length = -1;
+ spinbox.set_value(0);
+ }
+ };
+ spinbox.on_change = [&](auto value) {
+ m_length = value;
+ };
+ }
+ {
+ auto& checkbox = right_side.add<GUI::CheckBox>("Override display format");
+ auto& editor = right_side.add<GUI::TextEditor>();
+ checkbox.set_checked(!m_format.is_empty());
+ editor.set_should_hide_unnecessary_scrollbars(true);
+ editor.set_enabled(!m_format.is_empty());
+ editor.set_text(m_format);
+
+ checkbox.on_checked = [&](auto checked) {
+ editor.set_enabled(checked);
+ if (!checked)
+ m_format = String::empty();
+ editor.set_text(m_format);
+ };
+ editor.on_change = [&] {
+ m_format = editor.text();
+ };
+ }
+ }
+
+ auto& alignment_tab = tabs.add_tab<GUI::Widget>("Alignment");
+ alignment_tab.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ {
+ // FIXME: Frame?
+ // Horizontal alignment
+ {
+ auto& horizontal_alignment_selection_container = alignment_tab.add<GUI::Widget>();
+ horizontal_alignment_selection_container.set_layout<GUI::HorizontalBoxLayout>();
+ horizontal_alignment_selection_container.layout()->set_margins({ 0, 4, 0, 0 });
+ horizontal_alignment_selection_container.set_fixed_height(22);
+
+ auto& horizontal_alignment_label = horizontal_alignment_selection_container.add<GUI::Label>();
+ horizontal_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ horizontal_alignment_label.set_text("Horizontal text alignment");
+
+ auto& horizontal_combobox = alignment_tab.add<GUI::ComboBox>();
+ horizontal_combobox.set_only_allow_values_from_model(true);
+ horizontal_combobox.set_model(*GUI::ItemListModel<String>::create(g_horizontal_alignments));
+ horizontal_combobox.set_selected_index((int)m_horizontal_alignment);
+ horizontal_combobox.on_change = [&](auto&, const GUI::ModelIndex& index) {
+ switch (index.row()) {
+ case 0:
+ m_horizontal_alignment = HorizontalAlignment::Left;
+ break;
+ case 1:
+ m_horizontal_alignment = HorizontalAlignment::Center;
+ break;
+ case 2:
+ m_horizontal_alignment = HorizontalAlignment::Right;
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ };
+ }
+
+ // Vertical alignment
+ {
+ auto& vertical_alignment_container = alignment_tab.add<GUI::Widget>();
+ vertical_alignment_container.set_layout<GUI::HorizontalBoxLayout>();
+ vertical_alignment_container.layout()->set_margins({ 0, 4, 0, 0 });
+ vertical_alignment_container.set_fixed_height(22);
+
+ auto& vertical_alignment_label = vertical_alignment_container.add<GUI::Label>();
+ vertical_alignment_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ vertical_alignment_label.set_text("Vertical text alignment");
+
+ auto& vertical_combobox = alignment_tab.add<GUI::ComboBox>();
+ vertical_combobox.set_only_allow_values_from_model(true);
+ vertical_combobox.set_model(*GUI::ItemListModel<String>::create(g_vertical_alignments));
+ vertical_combobox.set_selected_index((int)m_vertical_alignment);
+ vertical_combobox.on_change = [&](auto&, const GUI::ModelIndex& index) {
+ switch (index.row()) {
+ case 0:
+ m_vertical_alignment = VerticalAlignment::Top;
+ break;
+ case 1:
+ m_vertical_alignment = VerticalAlignment::Center;
+ break;
+ case 2:
+ m_vertical_alignment = VerticalAlignment::Bottom;
+ break;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ };
+ }
+ }
+
+ auto& colors_tab = tabs.add_tab<GUI::Widget>("Color");
+ colors_tab.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ {
+ // Static formatting
+ {
+ auto& static_formatting_container = colors_tab.add<GUI::Widget>();
+ static_formatting_container.set_layout<GUI::VerticalBoxLayout>();
+ static_formatting_container.set_shrink_to_fit(true);
+
+ // Foreground
+ {
+ // FIXME: Somehow allow unsetting these.
+ auto& foreground_container = static_formatting_container.add<GUI::Widget>();
+ foreground_container.set_layout<GUI::HorizontalBoxLayout>();
+ foreground_container.layout()->set_margins({ 0, 4, 0, 0 });
+ foreground_container.set_fixed_height(22);
+
+ auto& foreground_label = foreground_container.add<GUI::Label>();
+ foreground_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ foreground_label.set_text("Static foreground color");
+
+ auto& foreground_selector = foreground_container.add<GUI::ColorInput>();
+ if (m_static_format.foreground_color.has_value())
+ foreground_selector.set_color(m_static_format.foreground_color.value());
+ foreground_selector.on_change = [&]() {
+ m_static_format.foreground_color = foreground_selector.color();
+ };
+ }
+
+ // Background
+ {
+ // FIXME: Somehow allow unsetting these.
+ auto& background_container = static_formatting_container.add<GUI::Widget>();
+ background_container.set_layout<GUI::HorizontalBoxLayout>();
+ background_container.layout()->set_margins({ 0, 4, 0, 0 });
+ background_container.set_fixed_height(22);
+
+ auto& background_label = background_container.add<GUI::Label>();
+ background_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ background_label.set_text("Static background color");
+
+ auto& background_selector = background_container.add<GUI::ColorInput>();
+ if (m_static_format.background_color.has_value())
+ background_selector.set_color(m_static_format.background_color.value());
+ background_selector.on_change = [&]() {
+ m_static_format.background_color = background_selector.color();
+ };
+ }
+ }
+ }
+
+ auto& conditional_fmt_tab = tabs.add_tab<GUI::Widget>("Conditional format");
+ conditional_fmt_tab.load_from_gml(cond_fmt_gml);
+ {
+ auto& view = *conditional_fmt_tab.find_descendant_of_type_named<Spreadsheet::ConditionsView>("conditions_view");
+ view.set_formats(&m_conditional_formats);
+
+ auto& add_button = *conditional_fmt_tab.find_descendant_of_type_named<GUI::Button>("add_button");
+ add_button.on_click = [&](auto) {
+ view.add_format();
+ };
+
+ // FIXME: Disable this when empty.
+ auto& remove_button = *conditional_fmt_tab.find_descendant_of_type_named<GUI::Button>("remove_button");
+ remove_button.on_click = [&](auto) {
+ view.remove_top();
+ };
+ }
+}
+
+CellTypeMetadata CellTypeDialog::metadata() const
+{
+ CellTypeMetadata metadata;
+ metadata.format = m_format;
+ metadata.length = m_length;
+ metadata.static_format = m_static_format;
+
+ switch (m_vertical_alignment) {
+ case VerticalAlignment::Top:
+ switch (m_horizontal_alignment) {
+ case HorizontalAlignment::Left:
+ metadata.alignment = Gfx::TextAlignment::TopLeft;
+ break;
+ case HorizontalAlignment::Center:
+ metadata.alignment = Gfx::TextAlignment::Center; // TopCenter?
+ break;
+ case HorizontalAlignment::Right:
+ metadata.alignment = Gfx::TextAlignment::TopRight;
+ break;
+ }
+ break;
+ case VerticalAlignment::Center:
+ switch (m_horizontal_alignment) {
+ case HorizontalAlignment::Left:
+ metadata.alignment = Gfx::TextAlignment::CenterLeft;
+ break;
+ case HorizontalAlignment::Center:
+ metadata.alignment = Gfx::TextAlignment::Center;
+ break;
+ case HorizontalAlignment::Right:
+ metadata.alignment = Gfx::TextAlignment::CenterRight;
+ break;
+ }
+ break;
+ case VerticalAlignment::Bottom:
+ switch (m_horizontal_alignment) {
+ case HorizontalAlignment::Left:
+ metadata.alignment = Gfx::TextAlignment::CenterLeft; // BottomLeft?
+ break;
+ case HorizontalAlignment::Center:
+ metadata.alignment = Gfx::TextAlignment::Center;
+ break;
+ case HorizontalAlignment::Right:
+ metadata.alignment = Gfx::TextAlignment::BottomRight;
+ break;
+ }
+ break;
+ }
+
+ return metadata;
+}
+
+ConditionView::ConditionView(ConditionalFormat& fmt)
+ : m_format(fmt)
+{
+ load_from_gml(cond_fmt_view_gml);
+
+ auto& fg_input = *find_descendant_of_type_named<GUI::ColorInput>("foreground_input");
+ auto& bg_input = *find_descendant_of_type_named<GUI::ColorInput>("background_input");
+ auto& formula_editor = *find_descendant_of_type_named<GUI::TextEditor>("formula_editor");
+
+ if (m_format.foreground_color.has_value())
+ fg_input.set_color(m_format.foreground_color.value());
+
+ if (m_format.background_color.has_value())
+ bg_input.set_color(m_format.background_color.value());
+
+ formula_editor.set_text(m_format.condition);
+
+ // FIXME: Allow unsetting these.
+ fg_input.on_change = [&] {
+ m_format.foreground_color = fg_input.color();
+ };
+
+ bg_input.on_change = [&] {
+ m_format.background_color = bg_input.color();
+ };
+
+ formula_editor.set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
+ formula_editor.set_should_hide_unnecessary_scrollbars(true);
+ formula_editor.set_font(&Gfx::FontDatabase::default_fixed_width_font());
+ formula_editor.on_change = [&] {
+ m_format.condition = formula_editor.text();
+ };
+}
+
+ConditionView::~ConditionView()
+{
+}
+
+ConditionsView::ConditionsView()
+{
+ set_layout<GUI::VerticalBoxLayout>().set_spacing(2);
+}
+
+void ConditionsView::set_formats(Vector<ConditionalFormat>* formats)
+{
+ ASSERT(!m_formats);
+
+ m_formats = formats;
+
+ for (auto& entry : *m_formats)
+ m_widgets.append(add<ConditionView>(entry));
+}
+
+void ConditionsView::add_format()
+{
+ ASSERT(m_formats);
+
+ m_formats->empend();
+ auto& last = m_formats->last();
+
+ m_widgets.append(add<ConditionView>(last));
+
+ update();
+}
+
+void ConditionsView::remove_top()
+{
+ ASSERT(m_formats);
+
+ if (m_formats->is_empty())
+ return;
+
+ m_formats->take_last();
+ m_widgets.take_last()->remove_from_parent();
+ update();
+}
+
+ConditionsView::~ConditionsView()
+{
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/CellTypeDialog.h b/Userland/Applications/Spreadsheet/CellTypeDialog.h
new file mode 100644
index 0000000000..ede19ebd51
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CellTypeDialog.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "CellType/Type.h"
+#include "ConditionalFormatting.h"
+#include "Forward.h"
+#include <LibGUI/Dialog.h>
+
+namespace Spreadsheet {
+
+class CellTypeDialog : public GUI::Dialog {
+ C_OBJECT(CellTypeDialog);
+
+public:
+ CellTypeMetadata metadata() const;
+ const CellType* type() const { return m_type; }
+ Vector<ConditionalFormat> conditional_formats() { return m_conditional_formats; }
+
+ enum class HorizontalAlignment : int {
+ Left = 0,
+ Center,
+ Right,
+ };
+ enum class VerticalAlignment : int {
+ Top = 0,
+ Center,
+ Bottom,
+ };
+
+private:
+ CellTypeDialog(const Vector<Position>&, Sheet&, GUI::Window* parent = nullptr);
+ void setup_tabs(GUI::TabWidget&, const Vector<Position>&, Sheet&);
+
+ const CellType* m_type { nullptr };
+
+ int m_length { -1 };
+ String m_format;
+ HorizontalAlignment m_horizontal_alignment { HorizontalAlignment::Right };
+ VerticalAlignment m_vertical_alignment { VerticalAlignment::Center };
+ Format m_static_format;
+ Vector<ConditionalFormat> m_conditional_formats;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/CondFormatting.gml b/Userland/Applications/Spreadsheet/CondFormatting.gml
new file mode 100644
index 0000000000..437b5a1c3d
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CondFormatting.gml
@@ -0,0 +1,40 @@
+@GUI::Widget {
+ name: "main"
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [4, 4, 4, 4]
+ spacing: 4
+ }
+
+ @Spreadsheet::ConditionsView {
+ name: "conditions_view"
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+
+ layout: @GUI::HorizontalBoxLayout {
+ spacing: 10
+ }
+
+ @GUI::Widget {
+ }
+
+ @GUI::Button {
+ name: "add_button"
+ text: "Add"
+ fixed_width: 70
+ }
+
+ @GUI::Button {
+ name: "remove_button"
+ text: "Remove"
+ fixed_width: 70
+ }
+
+ @GUI::Widget {
+ }
+
+ }
+}
diff --git a/Userland/Applications/Spreadsheet/CondView.gml b/Userland/Applications/Spreadsheet/CondView.gml
new file mode 100644
index 0000000000..d2161da89f
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/CondView.gml
@@ -0,0 +1,54 @@
+@GUI::Widget {
+ layout: @GUI::VerticalBoxLayout {
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "if..."
+ fixed_width: 40
+ }
+
+ @GUI::TextEditor {
+ name: "formula_editor"
+ fixed_height: 25
+ tooltip: "Use 'value' to refer to the current cell's value"
+ }
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Foreground..."
+ fixed_width: 150
+ }
+
+ @GUI::ColorInput {
+ name: "foreground_input"
+ }
+ }
+
+ @GUI::Widget {
+ shrink_to_fit: true
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Label {
+ text: "Background..."
+ fixed_width: 150
+ }
+
+ @GUI::ColorInput {
+ name: "background_input"
+ }
+ }
+}
diff --git a/Userland/Applications/Spreadsheet/ConditionalFormatting.h b/Userland/Applications/Spreadsheet/ConditionalFormatting.h
new file mode 100644
index 0000000000..8e32bc8046
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/ConditionalFormatting.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Forward.h"
+#include <AK/String.h>
+#include <LibGUI/ScrollableWidget.h>
+#include <LibGfx/Color.h>
+
+namespace Spreadsheet {
+
+struct Format {
+ Optional<Color> foreground_color;
+ Optional<Color> background_color;
+};
+
+struct ConditionalFormat : public Format {
+ String condition;
+};
+
+class ConditionView : public GUI::Widget {
+ C_OBJECT(ConditionView)
+public:
+ virtual ~ConditionView() override;
+
+private:
+ ConditionView(ConditionalFormat&);
+
+ ConditionalFormat& m_format;
+};
+
+class ConditionsView : public GUI::Widget {
+ C_OBJECT(ConditionsView)
+public:
+ virtual ~ConditionsView() override;
+
+ void set_formats(Vector<ConditionalFormat>*);
+
+ void add_format();
+ void remove_top();
+
+private:
+ ConditionsView();
+
+ Vector<ConditionalFormat>* m_formats { nullptr };
+ NonnullRefPtrVector<GUI::Widget> m_widgets;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Forward.h b/Userland/Applications/Spreadsheet/Forward.h
new file mode 100644
index 0000000000..183c684972
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Forward.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+namespace Spreadsheet {
+
+class ConditionView;
+class Sheet;
+class SheetGlobalObject;
+class Workbook;
+class WorkbookObject;
+struct Cell;
+struct ConditionalFormat;
+struct Format;
+struct Position;
+
+}
diff --git a/Userland/Applications/Spreadsheet/HelpWindow.cpp b/Userland/Applications/Spreadsheet/HelpWindow.cpp
new file mode 100644
index 0000000000..eecd0bbb45
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/HelpWindow.cpp
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HelpWindow.h"
+#include "SpreadsheetWidget.h"
+#include <AK/LexicalPath.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Frame.h>
+#include <LibGUI/ListView.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Splitter.h>
+#include <LibMarkdown/Document.h>
+#include <LibWeb/Layout/Node.h>
+#include <LibWeb/OutOfProcessWebView.h>
+
+namespace Spreadsheet {
+
+class HelpListModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<HelpListModel> create() { return adopt(*new HelpListModel); }
+
+ virtual ~HelpListModel() override { }
+
+ virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_keys.size(); }
+ virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
+ virtual void update() override { }
+
+ virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role = GUI::ModelRole::Display) const override
+ {
+ if (role == GUI::ModelRole::Display) {
+ return key(index);
+ }
+
+ return {};
+ }
+
+ String key(const GUI::ModelIndex& index) const { return m_keys[index.row()]; }
+
+ void set_from(const JsonObject& object)
+ {
+ m_keys.clear();
+ object.for_each_member([this](auto& name, auto&) {
+ m_keys.append(name);
+ });
+ did_update();
+ }
+
+private:
+ HelpListModel()
+ {
+ }
+
+ Vector<String> m_keys;
+};
+
+RefPtr<HelpWindow> HelpWindow::s_the { nullptr };
+
+HelpWindow::HelpWindow(GUI::Window* parent)
+ : GUI::Window(parent)
+{
+ resize(530, 365);
+ set_title("Spreadsheet Functions Help");
+ set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"));
+
+ auto& widget = set_main_widget<GUI::Widget>();
+ widget.set_layout<GUI::VerticalBoxLayout>();
+ widget.set_fill_with_background_color(true);
+
+ auto& splitter = widget.add<GUI::HorizontalSplitter>();
+ auto& left_frame = splitter.add<GUI::Frame>();
+ left_frame.set_layout<GUI::VerticalBoxLayout>();
+ left_frame.set_fixed_width(100);
+ m_listview = left_frame.add<GUI::ListView>();
+ m_listview->set_activates_on_selection(true);
+ m_listview->set_model(HelpListModel::create());
+
+ m_webview = splitter.add<Web::OutOfProcessWebView>();
+ m_webview->on_link_click = [this](auto& url, auto&, auto&&) {
+ ASSERT(url.protocol() == "spreadsheet");
+ if (url.host() == "example") {
+ auto entry = LexicalPath(url.path()).basename();
+ auto doc_option = m_docs.get(entry);
+ if (!doc_option.is_object()) {
+ GUI::MessageBox::show_error(this, String::formatted("No documentation entry found for '{}'", url.path()));
+ return;
+ }
+ auto& doc = doc_option.as_object();
+ const auto& name = url.fragment();
+
+ auto example_data_value = doc.get_or("example_data", JsonObject {});
+ if (!example_data_value.is_object()) {
+ GUI::MessageBox::show_error(this, String::formatted("No example data found for '{}'", url.path()));
+ return;
+ }
+
+ auto& example_data = example_data_value.as_object();
+ auto value = example_data.get(name);
+ if (!value.is_object()) {
+ GUI::MessageBox::show_error(this, String::formatted("Example '{}' not found for '{}'", name, url.path()));
+ return;
+ }
+
+ auto window = GUI::Window::construct(this);
+ window->resize(size());
+ window->set_icon(icon());
+ window->set_title(String::formatted("Spreadsheet Help - Example {} for {}", name, entry));
+ window->on_close = [window = window.ptr()] { window->remove_from_parent(); };
+
+ auto& widget = window->set_main_widget<SpreadsheetWidget>(NonnullRefPtrVector<Sheet> {}, false);
+ auto sheet = Sheet::from_json(value.as_object(), widget.workbook());
+ if (!sheet) {
+ GUI::MessageBox::show_error(this, String::formatted("Corrupted example '{}' in '{}'", name, url.path()));
+ return;
+ }
+
+ widget.add_sheet(sheet.release_nonnull());
+ window->show();
+ } else if (url.host() == "doc") {
+ auto entry = LexicalPath(url.path()).basename();
+ m_webview->load(URL::create_with_data("text/html", render(entry)));
+ } else {
+ dbgln("Invalid spreadsheet action domain '{}'", url.host());
+ }
+ };
+
+ m_listview->on_activation = [this](auto& index) {
+ if (!m_webview)
+ return;
+
+ auto key = static_cast<HelpListModel*>(m_listview->model())->key(index);
+ m_webview->load(URL::create_with_data("text/html", render(key)));
+ };
+}
+
+String HelpWindow::render(const StringView& key)
+{
+ auto doc_option = m_docs.get(key);
+ ASSERT(doc_option.is_object());
+
+ auto& doc = doc_option.as_object();
+
+ auto name = doc.get("name").to_string();
+ auto argc = doc.get("argc").to_u32(0);
+ auto argnames_value = doc.get("argnames");
+ ASSERT(argnames_value.is_array());
+ auto& argnames = argnames_value.as_array();
+
+ auto docstring = doc.get("doc").to_string();
+ auto examples_value = doc.get_or("examples", JsonObject {});
+ ASSERT(examples_value.is_object());
+ auto& examples = examples_value.as_object();
+
+ StringBuilder markdown_builder;
+
+ markdown_builder.append("# NAME\n`");
+ markdown_builder.append(name);
+ markdown_builder.append("`\n\n");
+
+ markdown_builder.append("# ARGUMENTS\n");
+ if (argc > 0)
+ markdown_builder.appendff("{} required argument(s):\n", argc);
+ else
+ markdown_builder.appendf("No required arguments.\n");
+
+ for (size_t i = 0; i < argc; ++i)
+ markdown_builder.appendff("- `{}`\n", argnames.at(i).to_string());
+
+ if (argc > 0)
+ markdown_builder.append("\n");
+
+ if ((size_t)argnames.size() > argc) {
+ auto opt_count = argnames.size() - argc;
+ markdown_builder.appendff("{} optional argument(s):\n", opt_count);
+ for (size_t i = argc; i < (size_t)argnames.size(); ++i)
+ markdown_builder.appendff("- `{}`\n", argnames.at(i).to_string());
+ markdown_builder.append("\n");
+ }
+
+ markdown_builder.append("# DESCRIPTION\n");
+ markdown_builder.append(docstring);
+ markdown_builder.append("\n\n");
+
+ if (!examples.is_empty()) {
+ markdown_builder.append("# EXAMPLES\n");
+ examples.for_each_member([&](auto& text, auto& description_value) {
+ dbgln("- {}\n\n```js\n{}\n```\n", description_value.to_string(), text);
+ markdown_builder.appendff("- {}\n\n```js\n{}\n```\n", description_value.to_string(), text);
+ });
+ }
+
+ auto document = Markdown::Document::parse(markdown_builder.string_view());
+ return document->render_to_html();
+}
+
+void HelpWindow::set_docs(JsonObject&& docs)
+{
+ m_docs = move(docs);
+ static_cast<HelpListModel*>(m_listview->model())->set_from(m_docs);
+ m_listview->update();
+}
+
+HelpWindow::~HelpWindow()
+{
+}
+}
diff --git a/Userland/Applications/Spreadsheet/HelpWindow.h b/Userland/Applications/Spreadsheet/HelpWindow.h
new file mode 100644
index 0000000000..1c02d90eed
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/HelpWindow.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/JsonObject.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibWeb/OutOfProcessWebView.h>
+
+namespace Spreadsheet {
+
+class HelpWindow : public GUI::Window {
+ C_OBJECT(HelpWindow);
+
+public:
+ static NonnullRefPtr<HelpWindow> the(GUI::Window* window)
+ {
+ if (s_the)
+ return *s_the;
+
+ return *(s_the = adopt(*new HelpWindow(window)));
+ }
+
+ virtual ~HelpWindow() override;
+
+ void set_docs(JsonObject&& docs);
+
+private:
+ static RefPtr<HelpWindow> s_the;
+ String render(const StringView& key);
+ HelpWindow(GUI::Window* parent = nullptr);
+
+ JsonObject m_docs;
+ RefPtr<Web::OutOfProcessWebView> m_webview;
+ RefPtr<GUI::ListView> m_listview;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/JSIntegration.cpp b/Userland/Applications/Spreadsheet/JSIntegration.cpp
new file mode 100644
index 0000000000..12a249593e
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/JSIntegration.cpp
@@ -0,0 +1,444 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "JSIntegration.h"
+#include "Spreadsheet.h"
+#include "Workbook.h"
+#include <LibJS/Lexer.h>
+#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <LibJS/Runtime/Object.h>
+#include <LibJS/Runtime/Value.h>
+
+namespace Spreadsheet {
+
+Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source)
+{
+ JS::Lexer lexer { source };
+ // Track <identifier> <OpenParen>'s, and how many complete expressions are inside the parenthesised expression.
+ Vector<size_t> state;
+ StringView last_name;
+ Vector<StringView> names;
+ size_t open_parens_since_last_commit = 0;
+ size_t open_curlies_and_brackets_since_last_commit = 0;
+ bool previous_was_identifier = false;
+ auto token = lexer.next();
+ while (token.type() != JS::TokenType::Eof) {
+ switch (token.type()) {
+ case JS::TokenType::Identifier:
+ previous_was_identifier = true;
+ last_name = token.value();
+ break;
+ case JS::TokenType::ParenOpen:
+ if (!previous_was_identifier) {
+ open_parens_since_last_commit++;
+ break;
+ }
+ previous_was_identifier = false;
+ state.append(0);
+ names.append(last_name);
+ break;
+ case JS::TokenType::ParenClose:
+ previous_was_identifier = false;
+ if (open_parens_since_last_commit == 0) {
+ state.take_last();
+ names.take_last();
+ break;
+ }
+ --open_parens_since_last_commit;
+ break;
+ case JS::TokenType::Comma:
+ previous_was_identifier = false;
+ if (open_parens_since_last_commit == 0 && open_curlies_and_brackets_since_last_commit == 0) {
+ state.last()++;
+ break;
+ }
+ break;
+ case JS::TokenType::BracketOpen:
+ previous_was_identifier = false;
+ open_curlies_and_brackets_since_last_commit++;
+ break;
+ case JS::TokenType::BracketClose:
+ previous_was_identifier = false;
+ if (open_curlies_and_brackets_since_last_commit > 0)
+ open_curlies_and_brackets_since_last_commit--;
+ break;
+ case JS::TokenType::CurlyOpen:
+ previous_was_identifier = false;
+ open_curlies_and_brackets_since_last_commit++;
+ break;
+ case JS::TokenType::CurlyClose:
+ previous_was_identifier = false;
+ if (open_curlies_and_brackets_since_last_commit > 0)
+ open_curlies_and_brackets_since_last_commit--;
+ break;
+ default:
+ previous_was_identifier = false;
+ break;
+ }
+
+ token = lexer.next();
+ }
+ if (!names.is_empty() && !state.is_empty())
+ return FunctionAndArgumentIndex { names.last(), state.last() };
+ return {};
+}
+
+SheetGlobalObject::SheetGlobalObject(Sheet& sheet)
+ : m_sheet(sheet)
+{
+}
+
+SheetGlobalObject::~SheetGlobalObject()
+{
+}
+
+JS::Value SheetGlobalObject::get(const JS::PropertyName& name, JS::Value receiver) const
+{
+ if (name.is_string()) {
+ if (name.as_string() == "value") {
+ if (auto cell = m_sheet.current_evaluated_cell())
+ return cell->js_data();
+
+ return JS::js_undefined();
+ }
+ if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+ auto& cell = m_sheet.ensure(pos.value());
+ cell.reference_from(m_sheet.current_evaluated_cell());
+ return cell.typed_js_data();
+ }
+ }
+
+ return GlobalObject::get(name, receiver);
+}
+
+bool SheetGlobalObject::put(const JS::PropertyName& name, JS::Value value, JS::Value receiver)
+{
+ if (name.is_string()) {
+ if (auto pos = Sheet::parse_cell_name(name.as_string()); pos.has_value()) {
+ auto& cell = m_sheet.ensure(pos.value());
+ if (auto current = m_sheet.current_evaluated_cell())
+ current->reference_from(&cell);
+
+ cell.set_data(value); // FIXME: This produces un-savable state!
+ return true;
+ }
+ }
+
+ return GlobalObject::put(name, value, receiver);
+}
+
+void SheetGlobalObject::initialize()
+{
+ GlobalObject::initialize();
+ define_native_function("get_real_cell_contents", get_real_cell_contents, 1);
+ define_native_function("set_real_cell_contents", set_real_cell_contents, 2);
+ define_native_function("parse_cell_name", parse_cell_name, 1);
+ define_native_function("current_cell_position", current_cell_position, 0);
+ define_native_function("column_arithmetic", column_arithmetic, 2);
+ define_native_function("column_index", column_index, 1);
+}
+
+void SheetGlobalObject::visit_edges(Visitor& visitor)
+{
+ GlobalObject::visit_edges(visitor);
+ for (auto& it : m_sheet.cells()) {
+ if (it.value->exception())
+ visitor.visit(it.value->exception());
+ visitor.visit(it.value->evaluated_data());
+ }
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
+{
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return JS::js_null();
+
+ if (StringView("SheetGlobalObject") != this_object->class_name()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
+ return {};
+ }
+
+ auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+
+ if (vm.argument_count() != 1) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to get_real_cell_contents()");
+ return {};
+ }
+
+ auto name_value = vm.argument(0);
+ if (!name_value.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected a String argument to get_real_cell_contents()");
+ return {};
+ }
+ auto position = Sheet::parse_cell_name(name_value.as_string().string());
+ if (!position.has_value()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Invalid cell name");
+ return {};
+ }
+
+ const auto* cell = sheet_object->m_sheet.at(position.value());
+ if (!cell)
+ return JS::js_undefined();
+
+ if (cell->kind() == Spreadsheet::Cell::Kind::Formula)
+ return JS::js_string(vm.heap(), String::formatted("={}", cell->data()));
+
+ return JS::js_string(vm.heap(), cell->data());
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::set_real_cell_contents)
+{
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return JS::js_null();
+
+ if (StringView("SheetGlobalObject") != this_object->class_name()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
+ return {};
+ }
+
+ auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+
+ if (vm.argument_count() != 2) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly two arguments to set_real_cell_contents()");
+ return {};
+ }
+
+ auto name_value = vm.argument(0);
+ if (!name_value.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected the first argument of set_real_cell_contents() to be a String");
+ return {};
+ }
+ auto position = Sheet::parse_cell_name(name_value.as_string().string());
+ if (!position.has_value()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Invalid cell name");
+ return {};
+ }
+
+ auto new_contents_value = vm.argument(1);
+ if (!new_contents_value.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected the second argument of set_real_cell_contents() to be a String");
+ return {};
+ }
+
+ auto& cell = sheet_object->m_sheet.ensure(position.value());
+ auto& new_contents = new_contents_value.as_string().string();
+ cell.set_data(new_contents);
+ return JS::js_null();
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name)
+{
+ if (vm.argument_count() != 1) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to parse_cell_name()");
+ return {};
+ }
+ auto name_value = vm.argument(0);
+ if (!name_value.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected a String argument to parse_cell_name()");
+ return {};
+ }
+ auto position = Sheet::parse_cell_name(name_value.as_string().string());
+ if (!position.has_value())
+ return JS::js_undefined();
+
+ auto object = JS::Object::create_empty(global_object);
+ object->put("column", JS::js_string(vm, position.value().column));
+ object->put("row", JS::Value((unsigned)position.value().row));
+
+ return object;
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::current_cell_position)
+{
+ if (vm.argument_count() != 0) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected no arguments to current_cell_position()");
+ return {};
+ }
+
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return JS::js_null();
+
+ if (StringView("SheetGlobalObject") != this_object->class_name()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
+ return {};
+ }
+
+ auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+ auto* current_cell = sheet_object->m_sheet.current_evaluated_cell();
+ if (!current_cell)
+ return JS::js_null();
+
+ auto position = current_cell->position();
+
+ auto object = JS::Object::create_empty(global_object);
+ object->put("column", JS::js_string(vm, position.column));
+ object->put("row", JS::Value((unsigned)position.row));
+
+ return object;
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_index)
+{
+ if (vm.argument_count() != 1) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to column_index()");
+ return {};
+ }
+
+ auto column_name = vm.argument(0);
+ if (!column_name.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
+ return {};
+ }
+
+ auto& column_name_str = column_name.as_string().string();
+
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return JS::js_null();
+
+ if (StringView("SheetGlobalObject") != this_object->class_name()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
+ return {};
+ }
+
+ auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+ auto& sheet = sheet_object->m_sheet;
+ auto column_index = sheet.column_index(column_name_str);
+ if (!column_index.has_value()) {
+ vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
+ return {};
+ }
+
+ return JS::Value((i32)column_index.value());
+}
+
+JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_arithmetic)
+{
+ if (vm.argument_count() != 2) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly two arguments to column_arithmetic()");
+ return {};
+ }
+
+ auto column_name = vm.argument(0);
+ if (!column_name.is_string()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "String");
+ return {};
+ }
+
+ auto& column_name_str = column_name.as_string().string();
+
+ auto offset = vm.argument(1).to_number(global_object);
+ if (!offset.is_number())
+ return {};
+
+ auto offset_number = offset.as_i32();
+
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return JS::js_null();
+
+ if (StringView("SheetGlobalObject") != this_object->class_name()) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "SheetGlobalObject");
+ return {};
+ }
+
+ auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
+ auto& sheet = sheet_object->m_sheet;
+ auto new_column = sheet.column_arithmetic(column_name_str, offset_number);
+ if (!new_column.has_value()) {
+ vm.throw_exception(global_object, JS::TypeError::create(global_object, String::formatted("'{}' is not a valid column", column_name_str)));
+ return {};
+ }
+
+ return JS::js_string(vm, new_column.release_value());
+}
+
+WorkbookObject::WorkbookObject(Workbook& workbook)
+ : JS::Object(*JS::Object::create_empty(workbook.global_object()))
+ , m_workbook(workbook)
+{
+}
+
+WorkbookObject::~WorkbookObject()
+{
+}
+
+void WorkbookObject::initialize(JS::GlobalObject& global_object)
+{
+ Object::initialize(global_object);
+ define_native_function("sheet", sheet, 1);
+}
+
+void WorkbookObject::visit_edges(Visitor& visitor)
+{
+ Base::visit_edges(visitor);
+ for (auto& sheet : m_workbook.sheets())
+ visitor.visit(&sheet.global_object());
+}
+
+JS_DEFINE_NATIVE_FUNCTION(WorkbookObject::sheet)
+{
+ if (vm.argument_count() != 1) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected exactly one argument to sheet()");
+ return {};
+ }
+ auto name_value = vm.argument(0);
+ if (!name_value.is_string() && !name_value.is_number()) {
+ vm.throw_exception<JS::TypeError>(global_object, "Expected a String or Number argument to sheet()");
+ return {};
+ }
+
+ auto* this_object = vm.this_value(global_object).to_object(global_object);
+ if (!this_object)
+ return {};
+
+ if (!is<WorkbookObject>(this_object)) {
+ vm.throw_exception<JS::TypeError>(global_object, JS::ErrorType::NotA, "WorkbookObject");
+ return {};
+ }
+
+ auto& workbook = static_cast<WorkbookObject*>(this_object)->m_workbook;
+
+ if (name_value.is_string()) {
+ auto& name = name_value.as_string().string();
+ for (auto& sheet : workbook.sheets()) {
+ if (sheet.name() == name)
+ return JS::Value(&sheet.global_object());
+ }
+ } else {
+ auto index = name_value.as_size_t();
+ if (index < workbook.sheets().size())
+ return JS::Value(&workbook.sheets()[index].global_object());
+ }
+
+ return JS::js_undefined();
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/JSIntegration.h b/Userland/Applications/Spreadsheet/JSIntegration.h
new file mode 100644
index 0000000000..88f46d929d
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/JSIntegration.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Forward.h"
+#include <LibJS/Forward.h>
+#include <LibJS/Runtime/GlobalObject.h>
+
+namespace Spreadsheet {
+
+struct FunctionAndArgumentIndex {
+ String function_name;
+ size_t argument_index { 0 };
+};
+Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source);
+
+class SheetGlobalObject final : public JS::GlobalObject {
+ JS_OBJECT(SheetGlobalObject, JS::GlobalObject);
+
+public:
+ SheetGlobalObject(Sheet&);
+
+ virtual ~SheetGlobalObject() override;
+
+ virtual JS::Value get(const JS::PropertyName&, JS::Value receiver = {}) const override;
+ virtual bool put(const JS::PropertyName&, JS::Value value, JS::Value receiver = {}) override;
+ virtual void initialize() override;
+
+ JS_DECLARE_NATIVE_FUNCTION(get_real_cell_contents);
+ JS_DECLARE_NATIVE_FUNCTION(set_real_cell_contents);
+ JS_DECLARE_NATIVE_FUNCTION(parse_cell_name);
+ JS_DECLARE_NATIVE_FUNCTION(current_cell_position);
+ JS_DECLARE_NATIVE_FUNCTION(column_index);
+ JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
+
+private:
+ virtual void visit_edges(Visitor&) override;
+ Sheet& m_sheet;
+};
+
+class WorkbookObject final : public JS::Object {
+ JS_OBJECT(WorkbookObject, JS::Object);
+
+public:
+ WorkbookObject(Workbook&);
+
+ virtual ~WorkbookObject() override;
+
+ virtual void initialize(JS::GlobalObject&) override;
+
+ JS_DECLARE_NATIVE_FUNCTION(sheet);
+
+private:
+ virtual void visit_edges(Visitor&) override;
+ Workbook& m_workbook;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Position.h b/Userland/Applications/Spreadsheet/Position.h
new file mode 100644
index 0000000000..af7d7f70a9
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Position.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Types.h>
+#include <AK/URL.h>
+
+namespace Spreadsheet {
+
+struct Position {
+ String column;
+ size_t row { 0 };
+
+ bool operator==(const Position& other) const
+ {
+ return row == other.row && column == other.column;
+ }
+
+ bool operator!=(const Position& other) const
+ {
+ return !(other == *this);
+ }
+
+ URL to_url() const
+ {
+ URL url;
+ url.set_protocol("spreadsheet");
+ url.set_host("cell");
+ url.set_path(String::formatted("/{}", getpid()));
+ url.set_fragment(String::formatted("{}{}", column, row));
+ return url;
+ }
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Readers/CSV.h b/Userland/Applications/Spreadsheet/Readers/CSV.h
new file mode 100644
index 0000000000..866ae67141
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Readers/CSV.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "XSV.h"
+#include <AK/Forward.h>
+#include <AK/StringView.h>
+
+namespace Reader {
+
+class CSV : public XSV {
+public:
+ CSV(StringView source, ParserBehaviour behaviours = default_behaviours())
+ : XSV(source, { ",", "\"", ParserTraits::Repeat }, behaviours)
+ {
+ }
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp b/Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp
new file mode 100644
index 0000000000..b80093d556
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Readers/Test/TestXSV.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/TestSuite.h>
+
+#include "../CSV.h"
+#include "../XSV.h"
+#include <LibCore/File.h>
+
+TEST_CASE(should_parse_valid_data)
+{
+ {
+ auto data = R"~~~(Foo, Bar, Baz
+ 1, 2, 3
+ 4, 5, 6
+ """x", y"z, 9)~~~";
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
+ EXPECT(!csv.has_error());
+
+ EXPECT_EQ(csv[0]["Foo"], "1");
+ EXPECT_EQ(csv[2]["Foo"], "\"x");
+ EXPECT_EQ(csv[2]["Bar"], "y\"z");
+ }
+
+ {
+ auto data = R"~~~(Foo, Bar, Baz
+ 1 , 2, 3
+ 4, "5 " , 6
+ """x", y"z, 9 )~~~";
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces | Reader::ParserBehaviour::TrimTrailingFieldSpaces };
+ EXPECT(!csv.has_error());
+
+ EXPECT_EQ(csv[0]["Foo"], "1");
+ EXPECT_EQ(csv[1]["Bar"], "5 ");
+ EXPECT_EQ(csv[2]["Foo"], "\"x");
+ EXPECT_EQ(csv[2]["Baz"], "9");
+ }
+}
+
+TEST_CASE(should_fail_nicely)
+{
+ {
+ auto data = R"~~~(Foo, Bar, Baz
+ x, y)~~~";
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
+ EXPECT(csv.has_error());
+ EXPECT_EQ(csv.error(), Reader::ReadError::NonConformingColumnCount);
+ }
+
+ {
+ auto data = R"~~~(Foo, Bar, Baz
+ x, y, "z)~~~";
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
+ EXPECT(csv.has_error());
+ EXPECT_EQ(csv.error(), Reader::ReadError::QuoteFailure);
+ }
+}
+
+TEST_CASE(should_iterate_rows)
+{
+ auto data = R"~~~(Foo, Bar, Baz
+ 1, 2, 3
+ 4, 5, 6
+ """x", y"z, 9)~~~";
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders | Reader::ParserBehaviour::TrimLeadingFieldSpaces };
+ EXPECT(!csv.has_error());
+
+ bool ran = false;
+ for (auto row : csv)
+ ran = !row[0].is_empty();
+
+ EXPECT(ran);
+}
+
+BENCHMARK_CASE(fairly_big_data)
+{
+ auto file_or_error = Core::File::open(__FILE__ ".data", Core::IODevice::OpenMode::ReadOnly);
+ EXPECT_EQ_FORCE(file_or_error.is_error(), false);
+
+ auto data = file_or_error.value()->read_all();
+ auto csv = Reader::CSV { data, Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders };
+
+ EXPECT(!csv.has_error());
+ EXPECT_EQ(csv.size(), 100000u);
+}
+
+TEST_MAIN(XSV)
diff --git a/Userland/Applications/Spreadsheet/Readers/XSV.cpp b/Userland/Applications/Spreadsheet/Readers/XSV.cpp
new file mode 100644
index 0000000000..99a61b0abc
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Readers/XSV.cpp
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "XSV.h"
+#include <AK/StringBuilder.h>
+
+namespace Reader {
+
+ParserBehaviour operator&(ParserBehaviour left, ParserBehaviour right)
+{
+ return static_cast<ParserBehaviour>(static_cast<u32>(left) & static_cast<u32>(right));
+}
+
+ParserBehaviour operator|(ParserBehaviour left, ParserBehaviour right)
+{
+ return static_cast<ParserBehaviour>(static_cast<u32>(left) | static_cast<u32>(right));
+}
+
+void XSV::set_error(ReadError error)
+{
+ if (m_error == ReadError::None)
+ m_error = error;
+}
+
+Vector<String> XSV::headers() const
+{
+ Vector<String> headers;
+ for (auto& field : m_names)
+ headers.append(field.is_string_view ? field.as_string_view : field.as_string.view());
+
+ return headers;
+}
+
+void XSV::parse()
+{
+ if ((m_behaviours & ParserBehaviour::ReadHeaders) != ParserBehaviour::None)
+ read_headers();
+
+ while (!has_error() && !m_lexer.is_eof())
+ m_rows.append(read_row());
+
+ if (!m_lexer.is_eof())
+ set_error(ReadError::DataPastLogicalEnd);
+}
+
+void XSV::read_headers()
+{
+ if (!m_names.is_empty()) {
+ set_error(ReadError::InternalError);
+ m_names.clear();
+ }
+
+ m_names = read_row(true);
+}
+
+Vector<XSV::Field> XSV::read_row(bool header_row)
+{
+ Vector<Field> row;
+ bool first = true;
+ while (!(m_lexer.is_eof() || m_lexer.next_is('\n') || m_lexer.next_is("\r\n")) && (first || m_lexer.consume_specific(m_traits.separator))) {
+ first = false;
+ row.append(read_one_field());
+ }
+
+ if (!m_lexer.is_eof()) {
+ auto crlf_ok = m_lexer.consume_specific("\r\n");
+ if (!crlf_ok) {
+ auto lf_ok = m_lexer.consume_specific('\n');
+ if (!lf_ok)
+ set_error(ReadError::DataPastLogicalEnd);
+ }
+ }
+
+ if (!header_row && (m_behaviours & ParserBehaviour::ReadHeaders) != ParserBehaviour::None && row.size() != m_names.size())
+ set_error(ReadError::NonConformingColumnCount);
+
+ return row;
+}
+
+XSV::Field XSV::read_one_field()
+{
+ if ((m_behaviours & ParserBehaviour::TrimLeadingFieldSpaces) != ParserBehaviour::None)
+ m_lexer.consume_while(is_any_of(" \t\v"));
+
+ bool is_quoted = false;
+ Field field;
+ if (m_lexer.next_is(m_traits.quote.view())) {
+ is_quoted = true;
+ field = read_one_quoted_field();
+ } else {
+ field = read_one_unquoted_field();
+ }
+
+ if ((m_behaviours & ParserBehaviour::TrimTrailingFieldSpaces) != ParserBehaviour::None) {
+ m_lexer.consume_while(is_any_of(" \t\v"));
+
+ if (!is_quoted) {
+ // Also have to trim trailing spaces from unquoted fields.
+ StringView view;
+ if (field.is_string_view)
+ view = field.as_string_view;
+ else
+ view = field.as_string;
+
+ if (!view.is_empty()) {
+ ssize_t i = view.length() - 1;
+ for (; i >= 0; --i) {
+ if (!view.substring_view(i, 1).is_one_of(" ", "\t", "\v"))
+ break;
+ }
+ view = view.substring_view(0, i + 1);
+ }
+
+ if (field.is_string_view)
+ field.as_string_view = view;
+ else
+ field.as_string = field.as_string.substring(0, view.length());
+ }
+ }
+
+ return field;
+}
+
+XSV::Field XSV::read_one_quoted_field()
+{
+ if (!m_lexer.consume_specific(m_traits.quote))
+ set_error(ReadError::InternalError);
+
+ size_t start = m_lexer.tell(), end = start;
+ bool is_copy = false;
+ StringBuilder builder;
+ auto allow_newlines = (m_behaviours & ParserBehaviour::AllowNewlinesInFields) != ParserBehaviour::None;
+
+ for (; !m_lexer.is_eof();) {
+ char ch;
+ switch (m_traits.quote_escape) {
+ case ParserTraits::Backslash:
+ if (m_lexer.consume_specific('\\') && m_lexer.consume_specific(m_traits.quote)) {
+ // If there is an escaped quote, we have no choice but to make a copy.
+ if (!is_copy) {
+ is_copy = true;
+ builder.append(m_source.substring_view(start, end - start));
+ }
+ builder.append(m_traits.quote);
+ end = m_lexer.tell();
+ continue;
+ }
+ break;
+ case ParserTraits::Repeat:
+ if (m_lexer.consume_specific(m_traits.quote)) {
+ if (m_lexer.consume_specific(m_traits.quote)) {
+ // If there is an escaped quote, we have no choice but to make a copy.
+ if (!is_copy) {
+ is_copy = true;
+ builder.append(m_source.substring_view(start, end - start));
+ }
+ builder.append(m_traits.quote);
+ end = m_lexer.tell();
+ continue;
+ }
+ for (size_t i = 0; i < m_traits.quote.length(); ++i)
+ m_lexer.retreat();
+ goto end;
+ }
+ break;
+ }
+
+ if (m_lexer.next_is(m_traits.quote.view()))
+ goto end;
+
+ if (!allow_newlines) {
+ if (m_lexer.next_is('\n') || m_lexer.next_is("\r\n"))
+ goto end;
+ }
+
+ ch = m_lexer.consume();
+ if (is_copy)
+ builder.append(ch);
+ end = m_lexer.tell();
+ continue;
+
+ end:
+ break;
+ }
+
+ if (!m_lexer.consume_specific(m_traits.quote))
+ set_error(ReadError::QuoteFailure);
+
+ if (is_copy)
+ return { {}, builder.to_string(), false };
+
+ return { m_source.substring_view(start, end - start), {}, true };
+}
+
+XSV::Field XSV::read_one_unquoted_field()
+{
+ size_t start = m_lexer.tell(), end = start;
+ bool allow_quote_in_field = (m_behaviours & ParserBehaviour::QuoteOnlyInFieldStart) != ParserBehaviour::None;
+
+ for (; !m_lexer.is_eof();) {
+ if (m_lexer.next_is(m_traits.separator.view()))
+ break;
+
+ if (m_lexer.next_is("\r\n") || m_lexer.next_is("\n"))
+ break;
+
+ if (m_lexer.consume_specific(m_traits.quote)) {
+ if (!allow_quote_in_field)
+ set_error(ReadError::QuoteFailure);
+ end = m_lexer.tell();
+ continue;
+ }
+
+ m_lexer.consume();
+ end = m_lexer.tell();
+ }
+
+ return { m_source.substring_view(start, end - start), {}, true };
+}
+
+StringView XSV::Row::operator[](StringView name) const
+{
+ ASSERT(!m_xsv.m_names.is_empty());
+ auto it = m_xsv.m_names.find_if([&](const auto& entry) { return name == entry; });
+ ASSERT(!it.is_end());
+
+ return (*this)[it.index()];
+}
+
+StringView XSV::Row::operator[](size_t column) const
+{
+ auto& field = m_xsv.m_rows[m_index][column];
+ if (field.is_string_view)
+ return field.as_string_view;
+ return field.as_string;
+}
+
+const XSV::Row XSV::operator[](size_t index) const
+{
+ return const_cast<XSV&>(*this)[index];
+}
+
+XSV::Row XSV::operator[](size_t index)
+{
+ ASSERT(m_rows.size() > index);
+ return Row { *this, index };
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/Readers/XSV.h b/Userland/Applications/Spreadsheet/Readers/XSV.h
new file mode 100644
index 0000000000..0b32ca767d
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Readers/XSV.h
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/GenericLexer.h>
+#include <AK/String.h>
+#include <AK/StringView.h>
+#include <AK/Types.h>
+#include <AK/Vector.h>
+
+namespace Reader {
+
+enum class ParserBehaviour : u32 {
+ None = 0,
+ ReadHeaders = 1,
+ AllowNewlinesInFields = ReadHeaders << 1,
+ TrimLeadingFieldSpaces = ReadHeaders << 2,
+ TrimTrailingFieldSpaces = ReadHeaders << 3,
+ QuoteOnlyInFieldStart = ReadHeaders << 4,
+};
+
+ParserBehaviour operator&(ParserBehaviour left, ParserBehaviour right);
+ParserBehaviour operator|(ParserBehaviour left, ParserBehaviour right);
+
+struct ParserTraits {
+ String separator;
+ String quote { "\"" };
+ enum {
+ Repeat,
+ Backslash,
+ } quote_escape { Repeat };
+};
+
+#define ENUMERATE_READ_ERRORS() \
+ E(None, "No errors") \
+ E(NonConformingColumnCount, "Header count does not match given column count") \
+ E(QuoteFailure, "Quoting failure") \
+ E(InternalError, "Internal error") \
+ E(DataPastLogicalEnd, "Exrta data past the logical end of the rows")
+
+enum class ReadError {
+#define E(name, _) name,
+ ENUMERATE_READ_ERRORS()
+#undef E
+};
+
+inline constexpr ParserBehaviour default_behaviours()
+{
+ return ParserBehaviour::QuoteOnlyInFieldStart;
+}
+
+class XSV {
+public:
+ XSV(StringView source, const ParserTraits& traits, ParserBehaviour behaviours = default_behaviours())
+ : m_source(source)
+ , m_lexer(m_source)
+ , m_traits(traits)
+ , m_behaviours(behaviours)
+ {
+ parse();
+ }
+
+ virtual ~XSV() { }
+
+ bool has_error() const { return m_error != ReadError::None; }
+ ReadError error() const { return m_error; }
+ String error_string() const
+ {
+ switch (m_error) {
+#define E(x, y) \
+ case ReadError::x: \
+ return y;
+
+ ENUMERATE_READ_ERRORS();
+#undef E
+ }
+ ASSERT_NOT_REACHED();
+ }
+
+ size_t size() const { return m_rows.size(); }
+ Vector<String> headers() const;
+
+ class Row {
+ public:
+ explicit Row(XSV& xsv, size_t index)
+ : m_xsv(xsv)
+ , m_index(index)
+ {
+ }
+
+ StringView operator[](StringView name) const;
+ StringView operator[](size_t column) const;
+
+ size_t index() const { return m_index; }
+
+ // FIXME: Implement begin() and end(), keeping `Field' out of the API.
+
+ private:
+ XSV& m_xsv;
+ size_t m_index { 0 };
+ };
+
+ template<bool const_>
+ class RowIterator {
+ public:
+ explicit RowIterator(const XSV& xsv, size_t init_index = 0) requires(const_)
+ : m_xsv(const_cast<XSV&>(xsv))
+ , m_index(init_index)
+ {
+ }
+
+ explicit RowIterator(XSV& xsv, size_t init_index = 0) requires(!const_)
+ : m_xsv(xsv)
+ , m_index(init_index)
+ {
+ }
+
+ Row operator*() const { return Row { m_xsv, m_index }; }
+ Row operator*() requires(!const_) { return Row { m_xsv, m_index }; }
+
+ RowIterator& operator++()
+ {
+ ++m_index;
+ return *this;
+ }
+
+ bool is_end() const { return m_index == m_xsv.m_rows.size(); }
+ bool operator==(const RowIterator& other) const
+ {
+ return m_index == other.m_index && &m_xsv == &other.m_xsv;
+ }
+ bool operator==(const RowIterator<!const_>& other) const
+ {
+ return m_index == other.m_index && &m_xsv == &other.m_xsv;
+ }
+
+ private:
+ XSV& m_xsv;
+ size_t m_index { 0 };
+ };
+
+ const Row operator[](size_t index) const;
+ Row operator[](size_t index);
+
+ auto begin() { return RowIterator<false>(*this); }
+ auto end() { return RowIterator<false>(*this, m_rows.size()); }
+
+ auto begin() const { return RowIterator<true>(*this); }
+ auto end() const { return RowIterator<true>(*this, m_rows.size()); }
+
+ using ConstIterator = RowIterator<true>;
+ using Iterator = RowIterator<false>;
+
+private:
+ struct Field {
+ StringView as_string_view;
+ String as_string; // This member only used if the parser couldn't use the original source verbatim.
+ bool is_string_view { true };
+
+ bool operator==(StringView other) const
+ {
+ if (is_string_view)
+ return other == as_string_view;
+ return as_string == other;
+ }
+ };
+ void set_error(ReadError error);
+ void parse();
+ void read_headers();
+ Vector<Field> read_row(bool header_row = false);
+ Field read_one_field();
+ Field read_one_quoted_field();
+ Field read_one_unquoted_field();
+
+ StringView m_source;
+ GenericLexer m_lexer;
+ const ParserTraits& m_traits;
+ ParserBehaviour m_behaviours;
+ Vector<Field> m_names;
+ Vector<Vector<Field>> m_rows;
+ ReadError m_error { ReadError::None };
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Spreadsheet.cpp b/Userland/Applications/Spreadsheet/Spreadsheet.cpp
new file mode 100644
index 0000000000..0d20b22fdd
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Spreadsheet.cpp
@@ -0,0 +1,734 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Spreadsheet.h"
+#include "JSIntegration.h"
+#include "Workbook.h"
+#include <AK/ByteBuffer.h>
+#include <AK/GenericLexer.h>
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonParser.h>
+#include <AK/ScopeGuard.h>
+#include <AK/TemporaryChange.h>
+#include <AK/URL.h>
+#include <LibCore/File.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/Function.h>
+#include <ctype.h>
+
+//#define COPY_DEBUG
+
+namespace Spreadsheet {
+
+Sheet::Sheet(const StringView& name, Workbook& workbook)
+ : Sheet(workbook)
+{
+ m_name = name;
+
+ for (size_t i = 0; i < default_row_count; ++i)
+ add_row();
+
+ for (size_t i = 0; i < default_column_count; ++i)
+ add_column();
+}
+
+Sheet::Sheet(Workbook& workbook)
+ : m_workbook(workbook)
+{
+ JS::DeferGC defer_gc(m_workbook.interpreter().heap());
+ m_global_object = m_workbook.interpreter().heap().allocate_without_global_object<SheetGlobalObject>(*this);
+ global_object().initialize();
+ global_object().put("workbook", m_workbook.workbook_object());
+ global_object().put("thisSheet", &global_object()); // Self-reference is unfortunate, but required.
+
+ // Sadly, these have to be evaluated once per sheet.
+ auto file_or_error = Core::File::open("/res/js/Spreadsheet/runtime.js", Core::IODevice::OpenMode::ReadOnly);
+ if (!file_or_error.is_error()) {
+ auto buffer = file_or_error.value()->read_all();
+ JS::Parser parser { JS::Lexer(buffer) };
+ if (parser.has_errors()) {
+ warnln("Spreadsheet: Failed to parse runtime code");
+ parser.print_errors();
+ } else {
+ interpreter().run(global_object(), parser.parse_program());
+ if (auto exc = interpreter().exception()) {
+ warnln("Spreadsheet: Failed to run runtime code: ");
+ for (auto& t : exc->trace())
+ warnln("{}", t);
+ interpreter().vm().clear_exception();
+ }
+ }
+ }
+}
+
+Sheet::~Sheet()
+{
+}
+
+JS::Interpreter& Sheet::interpreter() const
+{
+ return m_workbook.interpreter();
+}
+
+size_t Sheet::add_row()
+{
+ return m_rows++;
+}
+
+static String convert_to_string(size_t value, unsigned base = 26, StringView map = {})
+{
+ if (map.is_null())
+ map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ ASSERT(base >= 2 && base <= map.length());
+
+ // The '8 bits per byte' assumption may need to go?
+ Array<char, round_up_to_power_of_two(sizeof(size_t) * 8 + 1, 2)> buffer;
+ size_t i = 0;
+ do {
+ buffer[i++] = map[value % base];
+ value /= base;
+ } while (value > 0);
+
+ // NOTE: Weird as this may seem, the thing that comes after 'A' is 'AA', which as a number would be '00'
+ // to make this work, only the most significant digit has to be in a range of (1..25) as opposed to (0..25),
+ // but only if it's not the only digit in the string.
+ if (i > 1)
+ --buffer[i - 1];
+
+ for (size_t j = 0; j < i / 2; ++j)
+ swap(buffer[j], buffer[i - j - 1]);
+
+ return String { ReadonlyBytes(buffer.data(), i) };
+}
+
+static size_t convert_from_string(StringView str, unsigned base = 26, StringView map = {})
+{
+ if (map.is_null())
+ map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ ASSERT(base >= 2 && base <= map.length());
+
+ size_t value = 0;
+ for (size_t i = str.length(); i > 0; --i) {
+ auto digit_value = map.find_first_of(str[i - 1]).value_or(0);
+ // NOTE: Refer to the note in `convert_to_string()'.
+ if (i == str.length() && str.length() > 1)
+ ++digit_value;
+ value = value * base + digit_value;
+ }
+
+ return value;
+}
+
+String Sheet::add_column()
+{
+ auto next_column = convert_to_string(m_columns.size());
+ m_columns.append(next_column);
+ return next_column;
+}
+
+void Sheet::update()
+{
+ if (m_should_ignore_updates) {
+ m_update_requested = true;
+ return;
+ }
+ m_visited_cells_in_update.clear();
+ Vector<Cell*> cells_copy;
+
+ // Grab a copy as updates might insert cells into the table.
+ for (auto& it : m_cells) {
+ if (it.value->dirty()) {
+ cells_copy.append(it.value);
+ m_workbook.set_dirty(true);
+ }
+ }
+
+ for (auto& cell : cells_copy)
+ update(*cell);
+
+ m_visited_cells_in_update.clear();
+}
+
+void Sheet::update(Cell& cell)
+{
+ if (m_should_ignore_updates) {
+ m_update_requested = true;
+ return;
+ }
+ if (cell.dirty()) {
+ if (has_been_visited(&cell)) {
+ // This may be part of an cyclic reference chain,
+ // so just ignore it.
+ cell.clear_dirty();
+ return;
+ }
+ m_visited_cells_in_update.set(&cell);
+ cell.update_data({});
+ }
+}
+
+Sheet::ValueAndException Sheet::evaluate(const StringView& source, Cell* on_behalf_of)
+{
+ TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of };
+ ScopeGuard clear_exception { [&] { interpreter().vm().clear_exception(); } };
+
+ auto parser = JS::Parser(JS::Lexer(source));
+ if (parser.has_errors() || interpreter().exception())
+ return { JS::js_undefined(), interpreter().exception() };
+
+ auto program = parser.parse_program();
+ interpreter().run(global_object(), program);
+ if (interpreter().exception()) {
+ auto exc = interpreter().exception();
+ return { JS::js_undefined(), exc };
+ }
+
+ auto value = interpreter().vm().last_value();
+ if (value.is_empty())
+ return { JS::js_undefined(), {} };
+ return { value, {} };
+}
+
+Cell* Sheet::at(const StringView& name)
+{
+ auto pos = parse_cell_name(name);
+ if (pos.has_value())
+ return at(pos.value());
+
+ return nullptr;
+}
+
+Cell* Sheet::at(const Position& position)
+{
+ auto it = m_cells.find(position);
+
+ if (it == m_cells.end())
+ return nullptr;
+
+ return it->value;
+}
+
+Optional<Position> Sheet::parse_cell_name(const StringView& name)
+{
+ GenericLexer lexer(name);
+ auto col = lexer.consume_while(isalpha);
+ auto row = lexer.consume_while(isdigit);
+
+ if (!lexer.is_eof() || row.is_empty() || col.is_empty())
+ return {};
+
+ return Position { col, row.to_uint().value() };
+}
+
+Optional<size_t> Sheet::column_index(const StringView& column_name) const
+{
+ auto index = convert_from_string(column_name);
+ if (m_columns.size() <= index || m_columns[index] != column_name)
+ return {};
+
+ return index;
+}
+
+Optional<String> Sheet::column_arithmetic(const StringView& column_name, int offset)
+{
+ auto maybe_index = column_index(column_name);
+ if (!maybe_index.has_value())
+ return {};
+
+ if (offset < 0 && maybe_index.value() < (size_t)(0 - offset))
+ return m_columns.first();
+
+ auto index = maybe_index.value() + offset;
+ if (m_columns.size() > index)
+ return m_columns[index];
+
+ for (size_t i = m_columns.size(); i <= index; ++i)
+ add_column();
+
+ return m_columns.last();
+}
+
+Cell* Sheet::from_url(const URL& url)
+{
+ auto maybe_position = position_from_url(url);
+ if (!maybe_position.has_value())
+ return nullptr;
+
+ return at(maybe_position.value());
+}
+
+Optional<Position> Sheet::position_from_url(const URL& url) const
+{
+ if (!url.is_valid()) {
+ dbgln("Invalid url: {}", url.to_string());
+ return {};
+ }
+
+ if (url.protocol() != "spreadsheet" || url.host() != "cell") {
+ dbgln("Bad url: {}", url.to_string());
+ return {};
+ }
+
+ // FIXME: Figure out a way to do this cross-process.
+ ASSERT(url.path() == String::formatted("/{}", getpid()));
+
+ return parse_cell_name(url.fragment());
+}
+
+Position Sheet::offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const
+{
+ auto offset_column_it = m_columns.find(offset.column);
+ auto offset_base_column_it = m_columns.find(offset_base.column);
+ auto base_column_it = m_columns.find(base.column);
+
+ if (offset_column_it.is_end()) {
+ dbgln("Column '{}' does not exist!", offset.column);
+ return base;
+ }
+ if (offset_base_column_it.is_end()) {
+ dbgln("Column '{}' does not exist!", offset.column);
+ return base;
+ }
+ if (base_column_it.is_end()) {
+ dbgln("Column '{}' does not exist!", offset.column);
+ return offset;
+ }
+
+ auto new_column = column(offset_column_it.index() + base_column_it.index() - offset_base_column_it.index());
+ auto new_row = offset.row + base.row - offset_base.row;
+
+ return { move(new_column), new_row };
+}
+
+void Sheet::copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to)
+{
+ auto copy_to = [&](auto& source_position, Position target_position) {
+ auto& target_cell = ensure(target_position);
+ auto* source_cell = at(source_position);
+
+ if (!source_cell) {
+ target_cell.set_data("");
+ return;
+ }
+
+ target_cell.copy_from(*source_cell);
+ };
+
+ if (from.size() == to.size()) {
+ auto from_it = from.begin();
+ // FIXME: Ordering.
+ for (auto& position : to)
+ copy_to(*from_it++, position);
+
+ return;
+ }
+
+ if (to.size() == 1) {
+ // Resolve each index as relative to the first index offset from the selection.
+ auto& target = to.first();
+
+ for (auto& position : from) {
+#ifdef COPY_DEBUG
+ dbg() << "Paste from '" << position.to_url() << "' to '" << target.to_url() << "'";
+#endif
+ copy_to(position, resolve_relative_to.has_value() ? offset_relative_to(target, position, resolve_relative_to.value()) : target);
+ }
+
+ return;
+ }
+
+ if (from.size() == 1) {
+ // Fill the target selection with the single cell.
+ auto& source = from.first();
+ for (auto& position : to) {
+#ifdef COPY_DEBUG
+ dbg() << "Paste from '" << source.to_url() << "' to '" << position.to_url() << "'";
+#endif
+ copy_to(source, resolve_relative_to.has_value() ? offset_relative_to(position, source, resolve_relative_to.value()) : position);
+ }
+ return;
+ }
+
+ // Just disallow misaligned copies.
+ dbgln("Cannot copy {} cells to {} cells", from.size(), to.size());
+}
+
+RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook)
+{
+ auto sheet = adopt(*new Sheet(workbook));
+ auto rows = object.get("rows").to_u32(default_row_count);
+ auto columns = object.get("columns");
+ auto name = object.get("name").as_string_or("Sheet");
+
+ sheet->set_name(name);
+
+ for (size_t i = 0; i < max(rows, (unsigned)Sheet::default_row_count); ++i)
+ sheet->add_row();
+
+ // FIXME: Better error checking.
+ if (columns.is_array()) {
+ columns.as_array().for_each([&](auto& value) {
+ sheet->m_columns.append(value.as_string());
+ return IterationDecision::Continue;
+ });
+ }
+
+ if (sheet->m_columns.size() < default_column_count && sheet->columns_are_standard()) {
+ for (size_t i = sheet->m_columns.size(); i < default_column_count; ++i)
+ sheet->add_column();
+ }
+
+ auto cells = object.get("cells").as_object();
+ auto json = sheet->interpreter().global_object().get("JSON");
+ auto& parse_function = json.as_object().get("parse").as_function();
+
+ auto read_format = [](auto& format, const auto& obj) {
+ if (auto value = obj.get("foreground_color"); value.is_string())
+ format.foreground_color = Color::from_string(value.as_string());
+ if (auto value = obj.get("background_color"); value.is_string())
+ format.background_color = Color::from_string(value.as_string());
+ };
+
+ cells.for_each_member([&](auto& name, JsonValue& value) {
+ auto position_option = parse_cell_name(name);
+ if (!position_option.has_value())
+ return IterationDecision::Continue;
+
+ auto position = position_option.value();
+ auto& obj = value.as_object();
+ auto kind = obj.get("kind").as_string_or("LiteralString") == "LiteralString" ? Cell::LiteralString : Cell::Formula;
+
+ OwnPtr<Cell> cell;
+ switch (kind) {
+ case Cell::LiteralString:
+ cell = make<Cell>(obj.get("value").to_string(), position, *sheet);
+ break;
+ case Cell::Formula: {
+ auto& interpreter = sheet->interpreter();
+ auto value = interpreter.vm().call(parse_function, json, JS::js_string(interpreter.heap(), obj.get("value").as_string()));
+ cell = make<Cell>(obj.get("source").to_string(), move(value), position, *sheet);
+ break;
+ }
+ }
+
+ auto type_name = obj.get_or("type", "Numeric").to_string();
+ cell->set_type(type_name);
+
+ auto type_meta = obj.get("type_metadata");
+ if (type_meta.is_object()) {
+ auto& meta_obj = type_meta.as_object();
+ auto meta = cell->type_metadata();
+ if (auto value = meta_obj.get("length"); value.is_number())
+ meta.length = value.to_i32();
+ if (auto value = meta_obj.get("format"); value.is_string())
+ meta.format = value.as_string();
+ read_format(meta.static_format, meta_obj);
+
+ cell->set_type_metadata(move(meta));
+ }
+
+ auto conditional_formats = obj.get("conditional_formats");
+ auto cformats = cell->conditional_formats();
+ if (conditional_formats.is_array()) {
+ conditional_formats.as_array().for_each([&](const auto& fmt_val) {
+ if (!fmt_val.is_object())
+ return IterationDecision::Continue;
+
+ auto& fmt_obj = fmt_val.as_object();
+ auto fmt_cond = fmt_obj.get("condition").to_string();
+ if (fmt_cond.is_empty())
+ return IterationDecision::Continue;
+
+ ConditionalFormat fmt;
+ fmt.condition = move(fmt_cond);
+ read_format(fmt, fmt_obj);
+ cformats.append(move(fmt));
+
+ return IterationDecision::Continue;
+ });
+ cell->set_conditional_formats(move(cformats));
+ }
+
+ auto evaluated_format = obj.get("evaluated_formats");
+ if (evaluated_format.is_object()) {
+ auto& evaluated_format_obj = evaluated_format.as_object();
+ auto& evaluated_fmts = cell->evaluated_formats();
+
+ read_format(evaluated_fmts, evaluated_format_obj);
+ }
+
+ sheet->m_cells.set(position, cell.release_nonnull());
+ return IterationDecision::Continue;
+ });
+
+ return sheet;
+}
+
+Position Sheet::written_data_bounds() const
+{
+ Position bound;
+ for (auto& entry : m_cells) {
+ if (entry.key.row >= bound.row)
+ bound.row = entry.key.row;
+ if (entry.key.column >= bound.column)
+ bound.column = entry.key.column;
+ }
+
+ return bound;
+}
+
+/// The sheet is allowed to have nonstandard column names
+/// this checks whether all existing columns are 'standard'
+/// (i.e. as generated by 'convert_to_string()'
+bool Sheet::columns_are_standard() const
+{
+ for (size_t i = 0; i < m_columns.size(); ++i) {
+ if (m_columns[i] != convert_to_string(i))
+ return false;
+ }
+
+ return true;
+}
+
+JsonObject Sheet::to_json() const
+{
+ JsonObject object;
+ object.set("name", m_name);
+
+ auto save_format = [](const auto& format, auto& obj) {
+ if (format.foreground_color.has_value())
+ obj.set("foreground_color", format.foreground_color.value().to_string());
+ if (format.background_color.has_value())
+ obj.set("background_color", format.background_color.value().to_string());
+ };
+
+ auto bottom_right = written_data_bounds();
+
+ if (!columns_are_standard()) {
+ auto columns = JsonArray();
+ for (auto& column : m_columns)
+ columns.append(column);
+ object.set("columns", move(columns));
+ }
+ object.set("rows", bottom_right.row + 1);
+
+ JsonObject cells;
+ for (auto& it : m_cells) {
+ StringBuilder builder;
+ builder.append(it.key.column);
+ builder.appendff("{}", it.key.row);
+ auto key = builder.to_string();
+
+ JsonObject data;
+ data.set("kind", it.value->kind() == Cell::Kind::Formula ? "Formula" : "LiteralString");
+ if (it.value->kind() == Cell::Formula) {
+ data.set("source", it.value->data());
+ auto json = interpreter().global_object().get("JSON");
+ auto stringified = interpreter().vm().call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data());
+ data.set("value", stringified.to_string_without_side_effects());
+ } else {
+ data.set("value", it.value->data());
+ }
+
+ // Set type & meta
+ auto& type = it.value->type();
+ auto& meta = it.value->type_metadata();
+ data.set("type", type.name());
+
+ JsonObject metadata_object;
+ metadata_object.set("length", meta.length);
+ metadata_object.set("format", meta.format);
+#if 0
+ metadata_object.set("alignment", alignment_to_string(meta.alignment));
+#endif
+ save_format(meta.static_format, metadata_object);
+
+ data.set("type_metadata", move(metadata_object));
+
+ // Set conditional formats
+ JsonArray conditional_formats;
+ for (auto& fmt : it.value->conditional_formats()) {
+ JsonObject fmt_object;
+ fmt_object.set("condition", fmt.condition);
+ save_format(fmt, fmt_object);
+
+ conditional_formats.append(move(fmt_object));
+ }
+
+ data.set("conditional_formats", move(conditional_formats));
+
+ auto& evaluated_formats = it.value->evaluated_formats();
+ JsonObject evaluated_formats_obj;
+
+ save_format(evaluated_formats, evaluated_formats_obj);
+ data.set("evaluated_formats", move(evaluated_formats_obj));
+
+ cells.set(key, move(data));
+ }
+ object.set("cells", move(cells));
+
+ return object;
+}
+
+Vector<Vector<String>> Sheet::to_xsv() const
+{
+ Vector<Vector<String>> data;
+
+ auto bottom_right = written_data_bounds();
+
+ // First row = headers.
+ size_t column_count = m_columns.size();
+ if (columns_are_standard()) {
+ column_count = convert_from_string(bottom_right.column) + 1;
+ Vector<String> cols;
+ for (size_t i = 0; i < column_count; ++i)
+ cols.append(m_columns[i]);
+ data.append(move(cols));
+ } else {
+ data.append(m_columns);
+ }
+
+ for (size_t i = 0; i <= bottom_right.row; ++i) {
+ Vector<String> row;
+ row.resize(column_count);
+ for (size_t j = 0; j < column_count; ++j) {
+ auto cell = at({ m_columns[j], i });
+ if (cell)
+ row[j] = cell->typed_display();
+ }
+
+ data.append(move(row));
+ }
+
+ return data;
+}
+
+RefPtr<Sheet> Sheet::from_xsv(const Reader::XSV& xsv, Workbook& workbook)
+{
+ auto cols = xsv.headers();
+ auto rows = xsv.size();
+
+ auto sheet = adopt(*new Sheet(workbook));
+ sheet->m_columns = cols;
+ for (size_t i = 0; i < max(rows, Sheet::default_row_count); ++i)
+ sheet->add_row();
+ if (sheet->columns_are_standard()) {
+ for (size_t i = sheet->m_columns.size(); i < Sheet::default_column_count; ++i)
+ sheet->add_column();
+ }
+
+ for (auto row : xsv) {
+ for (size_t i = 0; i < cols.size(); ++i) {
+ auto str = row[i];
+ if (str.is_empty())
+ continue;
+ Position position { cols[i], row.index() };
+ auto cell = make<Cell>(str, position, *sheet);
+ sheet->m_cells.set(position, move(cell));
+ }
+ }
+
+ return sheet;
+}
+
+JsonObject Sheet::gather_documentation() const
+{
+ JsonObject object;
+ const JS::PropertyName doc_name { "__documentation" };
+
+ auto add_docs_from = [&](auto& it, auto& global_object) {
+ auto value = global_object.get(it.key);
+ if (!value.is_function() && !value.is_object())
+ return;
+
+ auto& value_object = value.is_object() ? value.as_object() : value.as_function();
+ if (!value_object.has_own_property(doc_name))
+ return;
+
+ dbgln("Found '{}'", it.key.to_display_string());
+ auto doc = value_object.get(doc_name);
+ if (!doc.is_string())
+ return;
+
+ JsonParser parser(doc.to_string_without_side_effects());
+ auto doc_object = parser.parse();
+
+ if (doc_object.has_value())
+ object.set(it.key.to_display_string(), doc_object.value());
+ else
+ dbgln("Sheet::gather_documentation(): Failed to parse the documentation for '{}'!", it.key.to_display_string());
+ };
+
+ for (auto& it : interpreter().global_object().shape().property_table())
+ add_docs_from(it, interpreter().global_object());
+
+ for (auto& it : global_object().shape().property_table())
+ add_docs_from(it, global_object());
+
+ m_cached_documentation = move(object);
+ return m_cached_documentation.value();
+}
+
+String Sheet::generate_inline_documentation_for(StringView function, size_t argument_index)
+{
+ if (!m_cached_documentation.has_value())
+ gather_documentation();
+
+ auto& docs = m_cached_documentation.value();
+ auto entry = docs.get(function);
+ if (entry.is_null() || !entry.is_object())
+ return String::formatted("{}(...???{})", function, argument_index);
+
+ auto& entry_object = entry.as_object();
+ size_t argc = entry_object.get("argc").to_int(0);
+ auto argnames_value = entry_object.get("argnames");
+ if (!argnames_value.is_array())
+ return String::formatted("{}(...{}???{})", function, argc, argument_index);
+ auto& argnames = argnames_value.as_array();
+ StringBuilder builder;
+ builder.appendff("{}(", function);
+ for (size_t i = 0; i < (size_t)argnames.size(); ++i) {
+ if (i != 0 && i < (size_t)argnames.size())
+ builder.append(", ");
+ if (i == argument_index)
+ builder.append('<');
+ else if (i >= argc)
+ builder.append('[');
+ builder.append(argnames[i].to_string());
+ if (i == argument_index)
+ builder.append('>');
+ else if (i >= argc)
+ builder.append(']');
+ }
+
+ builder.append(')');
+ return builder.build();
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/Spreadsheet.h b/Userland/Applications/Spreadsheet/Spreadsheet.h
new file mode 100644
index 0000000000..bbef6c8116
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Spreadsheet.h
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Cell.h"
+#include "Forward.h"
+#include "Readers/XSV.h"
+#include <AK/HashMap.h>
+#include <AK/HashTable.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Traits.h>
+#include <AK/Types.h>
+#include <AK/WeakPtr.h>
+#include <AK/Weakable.h>
+#include <LibCore/Object.h>
+#include <LibJS/Interpreter.h>
+
+namespace Spreadsheet {
+
+class Sheet : public Core::Object {
+ C_OBJECT(Sheet);
+
+public:
+ constexpr static size_t default_row_count = 100;
+ constexpr static size_t default_column_count = 26;
+
+ ~Sheet();
+
+ static Optional<Position> parse_cell_name(const StringView&);
+ Optional<size_t> column_index(const StringView& column_name) const;
+ Optional<String> column_arithmetic(const StringView& column_name, int offset);
+
+ Cell* from_url(const URL&);
+ const Cell* from_url(const URL& url) const { return const_cast<Sheet*>(this)->from_url(url); }
+ Optional<Position> position_from_url(const URL& url) const;
+
+ /// Resolve 'offset' to an absolute position assuming 'base' is at 'offset_base'.
+ /// Effectively, "Walk the distance between 'offset' and 'offset_base' away from 'base'".
+ Position offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const;
+
+ JsonObject to_json() const;
+ static RefPtr<Sheet> from_json(const JsonObject&, Workbook&);
+
+ Vector<Vector<String>> to_xsv() const;
+ static RefPtr<Sheet> from_xsv(const Reader::XSV&, Workbook&);
+
+ const String& name() const { return m_name; }
+ void set_name(const StringView& name) { m_name = name; }
+
+ JsonObject gather_documentation() const;
+
+ const HashTable<Position>& selected_cells() const { return m_selected_cells; }
+ HashTable<Position>& selected_cells() { return m_selected_cells; }
+ const HashMap<Position, NonnullOwnPtr<Cell>>& cells() const { return m_cells; }
+ HashMap<Position, NonnullOwnPtr<Cell>>& cells() { return m_cells; }
+
+ Cell* at(const Position& position);
+ const Cell* at(const Position& position) const { return const_cast<Sheet*>(this)->at(position); }
+
+ const Cell* at(const StringView& name) const { return const_cast<Sheet*>(this)->at(name); }
+ Cell* at(const StringView&);
+
+ const Cell& ensure(const Position& position) const { return const_cast<Sheet*>(this)->ensure(position); }
+ Cell& ensure(const Position& position)
+ {
+ if (auto cell = at(position))
+ return *cell;
+
+ m_cells.set(position, make<Cell>(String::empty(), position, *this));
+ return *at(position);
+ }
+
+ size_t add_row();
+ String add_column();
+
+ size_t row_count() const { return m_rows; }
+ size_t column_count() const { return m_columns.size(); }
+ const Vector<String>& columns() const { return m_columns; }
+ const String& column(size_t index)
+ {
+ for (size_t i = column_count(); i < index; ++i)
+ add_column();
+
+ ASSERT(column_count() > index);
+ return m_columns[index];
+ }
+ const String& column(size_t index) const
+ {
+ ASSERT(column_count() > index);
+ return m_columns[index];
+ }
+
+ void update();
+ void update(Cell&);
+ void disable_updates() { m_should_ignore_updates = true; }
+ void enable_updates()
+ {
+ m_should_ignore_updates = false;
+ if (m_update_requested) {
+ m_update_requested = false;
+ update();
+ }
+ }
+
+ struct ValueAndException {
+ JS::Value value;
+ JS::Exception* exception { nullptr };
+ };
+ ValueAndException evaluate(const StringView&, Cell* = nullptr);
+ JS::Interpreter& interpreter() const;
+ SheetGlobalObject& global_object() const { return *m_global_object; }
+
+ Cell*& current_evaluated_cell() { return m_current_cell_being_evaluated; }
+ bool has_been_visited(Cell* cell) const { return m_visited_cells_in_update.contains(cell); }
+
+ const Workbook& workbook() const { return m_workbook; }
+
+ void copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to = {});
+
+ /// Gives the bottom-right corner of the smallest bounding box containing all the written data.
+ Position written_data_bounds() const;
+
+ bool columns_are_standard() const;
+
+ String generate_inline_documentation_for(StringView function, size_t argument_index);
+
+private:
+ explicit Sheet(Workbook&);
+ explicit Sheet(const StringView& name, Workbook&);
+
+ String m_name;
+ Vector<String> m_columns;
+ size_t m_rows { 0 };
+ HashMap<Position, NonnullOwnPtr<Cell>> m_cells;
+ HashTable<Position> m_selected_cells;
+
+ Workbook& m_workbook;
+ mutable SheetGlobalObject* m_global_object;
+
+ Cell* m_current_cell_being_evaluated { nullptr };
+
+ HashTable<Cell*> m_visited_cells_in_update;
+ bool m_should_ignore_updates { false };
+ bool m_update_requested { false };
+ mutable Optional<JsonObject> m_cached_documentation;
+};
+
+}
+
+namespace AK {
+
+template<>
+struct Traits<Spreadsheet::Position> : public GenericTraits<Spreadsheet::Position> {
+ static constexpr bool is_trivial() { return false; }
+ static unsigned hash(const Spreadsheet::Position& p)
+ {
+ return pair_int_hash(
+ string_hash(p.column.characters(), p.column.length()),
+ u64_hash(p.row));
+ }
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp b/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp
new file mode 100644
index 0000000000..70193e4b63
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetModel.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SpreadsheetModel.h"
+#include "ConditionalFormatting.h"
+#include <AK/URL.h>
+#include <LibGUI/AbstractView.h>
+#include <LibJS/Runtime/Error.h>
+#include <LibJS/Runtime/Object.h>
+
+namespace Spreadsheet {
+
+SheetModel::~SheetModel()
+{
+}
+
+GUI::Variant SheetModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ if (!index.is_valid())
+ return {};
+
+ if (role == GUI::ModelRole::Display) {
+ const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
+ if (!cell)
+ return String::empty();
+
+ if (cell->kind() == Spreadsheet::Cell::Formula) {
+ if (auto exception = cell->exception()) {
+ StringBuilder builder;
+ builder.append("Error: ");
+ auto value = exception->value();
+ if (value.is_object()) {
+ auto& object = value.as_object();
+ if (is<JS::Error>(object)) {
+ auto error = object.get("message").to_string_without_side_effects();
+ builder.append(error);
+ return builder.to_string();
+ }
+ }
+ auto error = value.to_string(cell->sheet().global_object());
+ // This is annoying, but whatever.
+ cell->sheet().interpreter().vm().clear_exception();
+
+ builder.append(error);
+ return builder.to_string();
+ }
+ }
+
+ return cell->typed_display();
+ }
+
+ if (role == GUI::ModelRole::MimeData)
+ return Position { m_sheet->column(index.column()), (size_t)index.row() }.to_url().to_string();
+
+ if (role == GUI::ModelRole::TextAlignment) {
+ const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
+ if (!cell)
+ return {};
+
+ return cell->type_metadata().alignment;
+ }
+
+ if (role == GUI::ModelRole::ForegroundColor) {
+ const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
+ if (!cell)
+ return {};
+
+ if (cell->kind() == Spreadsheet::Cell::Formula) {
+ if (cell->exception())
+ return Color(Color::Red);
+ }
+
+ if (cell->evaluated_formats().foreground_color.has_value())
+ return cell->evaluated_formats().foreground_color.value();
+
+ if (auto color = cell->type_metadata().static_format.foreground_color; color.has_value())
+ return color.value();
+
+ return {};
+ }
+
+ if (role == GUI::ModelRole::BackgroundColor) {
+ const auto* cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
+ if (!cell)
+ return {};
+
+ if (cell->evaluated_formats().background_color.has_value())
+ return cell->evaluated_formats().background_color.value();
+
+ if (auto color = cell->type_metadata().static_format.background_color; color.has_value())
+ return color.value();
+
+ return {};
+ }
+
+ return {};
+}
+
+RefPtr<Core::MimeData> SheetModel::mime_data(const GUI::ModelSelection& selection) const
+{
+ auto mime_data = GUI::Model::mime_data(selection);
+
+ bool first = true;
+ const GUI::ModelIndex* cursor = nullptr;
+ const_cast<SheetModel*>(this)->for_each_view([&](const GUI::AbstractView& view) {
+ if (!first)
+ return;
+ cursor = &view.cursor_index();
+ first = false;
+ });
+
+ ASSERT(cursor);
+
+ Position cursor_position { m_sheet->column(cursor->column()), (size_t)cursor->row() };
+ auto new_data = String::formatted("{}\n{}",
+ cursor_position.to_url().to_string(),
+ StringView(mime_data->data("text/x-spreadsheet-data")));
+ mime_data->set_data("text/x-spreadsheet-data", new_data.to_byte_buffer());
+
+ return mime_data;
+}
+
+String SheetModel::column_name(int index) const
+{
+ if (index < 0)
+ return {};
+
+ return m_sheet->column(index);
+}
+
+bool SheetModel::is_editable(const GUI::ModelIndex& index) const
+{
+ if (!index.is_valid())
+ return false;
+
+ return true;
+}
+
+void SheetModel::set_data(const GUI::ModelIndex& index, const GUI::Variant& value)
+{
+ if (!index.is_valid())
+ return;
+
+ auto& cell = m_sheet->ensure({ m_sheet->column(index.column()), (size_t)index.row() });
+ cell.set_data(value.to_string());
+ update();
+}
+
+void SheetModel::update()
+{
+ m_sheet->update();
+ did_update(UpdateFlag::DontInvalidateIndexes);
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetModel.h b/Userland/Applications/Spreadsheet/SpreadsheetModel.h
new file mode 100644
index 0000000000..14527d4c37
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetModel.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Spreadsheet.h"
+#include <LibGUI/Model.h>
+
+namespace Spreadsheet {
+
+class SheetModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<SheetModel> create(Sheet& sheet) { return adopt(*new SheetModel(sheet)); }
+ virtual ~SheetModel() override;
+
+ virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->row_count(); }
+ virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_sheet->column_count(); }
+ virtual String column_name(int) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual RefPtr<Core::MimeData> mime_data(const GUI::ModelSelection&) const override;
+ virtual bool is_editable(const GUI::ModelIndex&) const override;
+ virtual void set_data(const GUI::ModelIndex&, const GUI::Variant&) override;
+ virtual void update() override;
+ virtual bool is_column_sortable(int) const override { return false; }
+ virtual StringView drag_data_type() const override { return "text/x-spreadsheet-data"; }
+ Sheet& sheet() { return *m_sheet; }
+
+private:
+ explicit SheetModel(Sheet& sheet)
+ : m_sheet(sheet)
+ {
+ }
+
+ NonnullRefPtr<Sheet> m_sheet;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetView.cpp b/Userland/Applications/Spreadsheet/SpreadsheetView.cpp
new file mode 100644
index 0000000000..9762f77578
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetView.cpp
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SpreadsheetView.h"
+#include "CellTypeDialog.h"
+#include "SpreadsheetModel.h"
+#include <AK/ScopeGuard.h>
+#include <AK/URL.h>
+#include <LibCore/MimeData.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/HeaderView.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/ModelEditingDelegate.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/ScrollBar.h>
+#include <LibGUI/TableView.h>
+#include <LibGfx/Palette.h>
+
+namespace Spreadsheet {
+
+SpreadsheetView::~SpreadsheetView()
+{
+}
+
+void SpreadsheetView::EditingDelegate::set_value(const GUI::Variant& value)
+{
+ if (value.as_string().is_null()) {
+ StringModelEditingDelegate::set_value("");
+ commit();
+ return;
+ }
+
+ if (m_has_set_initial_value)
+ return StringModelEditingDelegate::set_value(value);
+
+ m_has_set_initial_value = true;
+ const auto option = m_sheet.at({ m_sheet.column(index().column()), (size_t)index().row() });
+ if (option)
+ return StringModelEditingDelegate::set_value(option->source());
+
+ StringModelEditingDelegate::set_value("");
+}
+
+void InfinitelyScrollableTableView::did_scroll()
+{
+ TableView::did_scroll();
+ auto& vscrollbar = vertical_scrollbar();
+ auto& hscrollbar = horizontal_scrollbar();
+ if (vscrollbar.is_visible() && vscrollbar.value() == vscrollbar.max()) {
+ if (on_reaching_vertical_end)
+ on_reaching_vertical_end();
+ }
+ if (hscrollbar.is_visible() && hscrollbar.value() == hscrollbar.max()) {
+ if (on_reaching_horizontal_end)
+ on_reaching_horizontal_end();
+ }
+}
+
+void InfinitelyScrollableTableView::mousemove_event(GUI::MouseEvent& event)
+{
+ if (auto model = this->model()) {
+ auto index = index_at_event_position(event.position());
+ if (!index.is_valid())
+ return TableView::mousemove_event(event);
+
+ auto& sheet = static_cast<SheetModel&>(*model).sheet();
+ sheet.disable_updates();
+ ScopeGuard sheet_update_enabler { [&] { sheet.enable_updates(); } };
+
+ auto holding_left_button = !!(event.buttons() & GUI::MouseButton::Left);
+ auto rect = content_rect(index);
+ auto distance = rect.center().absolute_relative_distance_to(event.position());
+ if (distance.x() >= rect.width() / 2 - 5 && distance.y() >= rect.height() / 2 - 5) {
+ set_override_cursor(Gfx::StandardCursor::Crosshair);
+ m_should_intercept_drag = false;
+ if (holding_left_button) {
+ m_has_committed_to_dragging = true;
+ // Force a drag to happen by moving the mousedown position to the center of the cell.
+ m_left_mousedown_position = rect.center();
+ }
+ } else if (!m_should_intercept_drag) {
+ set_override_cursor(Gfx::StandardCursor::Arrow);
+ if (!holding_left_button) {
+ m_starting_selection_index = index;
+ } else {
+ m_should_intercept_drag = true;
+ m_might_drag = false;
+ }
+ }
+
+ if (holding_left_button && m_should_intercept_drag && !m_has_committed_to_dragging) {
+ if (!m_starting_selection_index.is_valid())
+ m_starting_selection_index = index;
+
+ Vector<GUI::ModelIndex> new_selection;
+ for (auto i = min(m_starting_selection_index.row(), index.row()), imax = max(m_starting_selection_index.row(), index.row()); i <= imax; ++i) {
+ for (auto j = min(m_starting_selection_index.column(), index.column()), jmax = max(m_starting_selection_index.column(), index.column()); j <= jmax; ++j) {
+ auto index = model->index(i, j);
+ if (index.is_valid())
+ new_selection.append(move(index));
+ }
+ }
+
+ if (!event.ctrl())
+ selection().clear();
+ selection().add_all(new_selection);
+ }
+ }
+
+ TableView::mousemove_event(event);
+}
+
+void InfinitelyScrollableTableView::mouseup_event(GUI::MouseEvent& event)
+{
+ m_should_intercept_drag = false;
+ m_has_committed_to_dragging = false;
+ TableView::mouseup_event(event);
+}
+
+void SpreadsheetView::update_with_model()
+{
+ m_table_view->model()->update();
+ m_table_view->update();
+}
+
+SpreadsheetView::SpreadsheetView(Sheet& sheet)
+ : m_sheet(sheet)
+{
+ set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
+ m_table_view = add<InfinitelyScrollableTableView>();
+ m_table_view->set_grid_style(GUI::TableView::GridStyle::Both);
+ m_table_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectItems);
+ m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed | GUI::AbstractView::AnyKeyPressed | GUI::AbstractView::DoubleClicked);
+ m_table_view->set_tab_key_navigation_enabled(true);
+ m_table_view->row_header().set_visible(true);
+ m_table_view->set_model(SheetModel::create(*m_sheet));
+ m_table_view->on_reaching_vertical_end = [&]() {
+ for (size_t i = 0; i < 100; ++i) {
+ auto index = m_sheet->add_row();
+ m_table_view->set_column_painting_delegate(index, make<TableCellPainter>(*m_table_view));
+ };
+ update_with_model();
+ };
+ m_table_view->on_reaching_horizontal_end = [&]() {
+ for (size_t i = 0; i < 10; ++i) {
+ m_sheet->add_column();
+ auto last_column_index = m_sheet->column_count() - 1;
+ m_table_view->set_column_width(last_column_index, 50);
+ m_table_view->set_column_header_alignment(last_column_index, Gfx::TextAlignment::Center);
+ }
+ update_with_model();
+ };
+
+ set_focus_proxy(m_table_view);
+
+ // FIXME: This is dumb.
+ for (size_t i = 0; i < m_sheet->column_count(); ++i) {
+ m_table_view->set_column_painting_delegate(i, make<TableCellPainter>(*m_table_view));
+ m_table_view->set_column_width(i, 50);
+ m_table_view->set_column_header_alignment(i, Gfx::TextAlignment::Center);
+ }
+
+ m_table_view->set_alternating_row_colors(false);
+ m_table_view->set_highlight_selected_rows(false);
+ m_table_view->set_editable(true);
+ m_table_view->aid_create_editing_delegate = [this](auto&) {
+ auto delegate = make<EditingDelegate>(*m_sheet);
+ delegate->on_cursor_key_pressed = [this](auto& event) {
+ m_table_view->stop_editing();
+ m_table_view->event(event);
+ };
+ return delegate;
+ };
+
+ m_table_view->on_selection_change = [&] {
+ m_sheet->selected_cells().clear();
+ for (auto& index : m_table_view->selection().indexes()) {
+ Position position { m_sheet->column(index.column()), (size_t)index.row() };
+ m_sheet->selected_cells().set(position);
+ }
+
+ if (m_table_view->selection().is_empty() && on_selection_dropped)
+ return on_selection_dropped();
+
+ Vector<Position> selected_positions;
+ selected_positions.ensure_capacity(m_table_view->selection().size());
+ for (auto& selection : m_table_view->selection().indexes())
+ selected_positions.empend(m_sheet->column(selection.column()), (size_t)selection.row());
+
+ if (on_selection_changed) {
+ on_selection_changed(move(selected_positions));
+ update_with_model();
+ };
+ };
+
+ m_table_view->on_activation = [this](auto&) {
+ m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set);
+ };
+
+ m_table_view->on_context_menu_request = [&](const GUI::ModelIndex&, const GUI::ContextMenuEvent& event) {
+ // NOTE: We ignore the specific cell for now.
+ m_cell_range_context_menu->popup(event.screen_position());
+ };
+
+ m_cell_range_context_menu = GUI::Menu::construct();
+ m_cell_range_context_menu->add_action(GUI::Action::create("Type and Formatting...", [this](auto&) {
+ Vector<Position> positions;
+ for (auto& index : m_table_view->selection().indexes()) {
+ Position position { m_sheet->column(index.column()), (size_t)index.row() };
+ positions.append(move(position));
+ }
+
+ if (positions.is_empty()) {
+ auto& index = m_table_view->cursor_index();
+ Position position { m_sheet->column(index.column()), (size_t)index.row() };
+ positions.append(move(position));
+ }
+
+ auto dialog = CellTypeDialog::construct(positions, *m_sheet, window());
+ if (dialog->exec() == GUI::Dialog::ExecOK) {
+ for (auto& position : positions) {
+ auto& cell = m_sheet->ensure(position);
+ cell.set_type(dialog->type());
+ cell.set_type_metadata(dialog->metadata());
+ cell.set_conditional_formats(dialog->conditional_formats());
+ }
+
+ m_table_view->update();
+ }
+ }));
+
+ m_table_view->on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
+ if (!index.is_valid())
+ return;
+
+ ScopeGuard update_after_drop { [this] { update(); } };
+
+ if (event.mime_data().has_format("text/x-spreadsheet-data")) {
+ auto data = event.mime_data().data("text/x-spreadsheet-data");
+ StringView urls { data.data(), data.size() };
+ Vector<Position> source_positions, target_positions;
+
+ for (auto& line : urls.lines(false)) {
+ auto position = m_sheet->position_from_url(line);
+ if (position.has_value())
+ source_positions.append(position.release_value());
+ }
+
+ // Drop always has a single target.
+ Position target { m_sheet->column(index.column()), (size_t)index.row() };
+ target_positions.append(move(target));
+
+ if (source_positions.is_empty())
+ return;
+
+ auto first_position = source_positions.take_first();
+ m_sheet->copy_cells(move(source_positions), move(target_positions), first_position);
+
+ return;
+ }
+
+ if (event.mime_data().has_text()) {
+ auto* target_cell = m_sheet->at({ m_sheet->column(index.column()), (size_t)index.row() });
+ ASSERT(target_cell);
+
+ target_cell->set_data(event.text());
+ return;
+ }
+ };
+}
+
+void SpreadsheetView::hide_event(GUI::HideEvent&)
+{
+ if (on_selection_dropped)
+ on_selection_dropped();
+}
+
+void SpreadsheetView::show_event(GUI::ShowEvent&)
+{
+ if (on_selection_changed && !m_table_view->selection().is_empty()) {
+ Vector<Position> selected_positions;
+ selected_positions.ensure_capacity(m_table_view->selection().size());
+ for (auto& selection : m_table_view->selection().indexes())
+ selected_positions.empend(m_sheet->column(selection.column()), (size_t)selection.row());
+
+ on_selection_changed(move(selected_positions));
+ }
+}
+
+void SpreadsheetView::TableCellPainter::paint(GUI::Painter& painter, const Gfx::IntRect& rect, const Gfx::Palette& palette, const GUI::ModelIndex& index)
+{
+ // Draw a border.
+ // Undo the horizontal padding done by the table view...
+ auto cell_rect = rect.inflated(m_table_view.horizontal_padding() * 2, 0);
+
+ if (auto bg = index.data(GUI::ModelRole::BackgroundColor); bg.is_color())
+ painter.fill_rect(cell_rect, bg.as_color());
+
+ if (m_table_view.selection().contains(index)) {
+ Color fill_color = palette.selection();
+ fill_color.set_alpha(80);
+ painter.fill_rect(cell_rect, fill_color);
+ }
+
+ auto text_color = index.data(GUI::ModelRole::ForegroundColor).to_color(palette.color(m_table_view.foreground_role()));
+ auto data = index.data();
+ auto text_alignment = index.data(GUI::ModelRole::TextAlignment).to_text_alignment(Gfx::TextAlignment::CenterRight);
+ painter.draw_text(rect, data.to_string(), m_table_view.font_for_index(index), text_alignment, text_color, Gfx::TextElision::Right);
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetView.h b/Userland/Applications/Spreadsheet/SpreadsheetView.h
new file mode 100644
index 0000000000..bc07d31bc3
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetView.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Spreadsheet.h"
+#include <LibGUI/AbstractTableView.h>
+#include <LibGUI/ModelEditingDelegate.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/Widget.h>
+#include <string.h>
+
+namespace Spreadsheet {
+
+class CellEditor final : public GUI::TextEditor {
+ C_OBJECT(CellEditor);
+
+public:
+ virtual ~CellEditor() { }
+
+ Function<void(GUI::KeyEvent&)> on_cursor_key_pressed;
+
+private:
+ CellEditor()
+ : TextEditor(TextEditor::Type::SingleLine)
+ {
+ }
+
+ static bool is_navigation(const GUI::KeyEvent& event)
+ {
+ if (event.modifiers() == KeyModifier::Mod_Shift && event.key() == KeyCode::Key_Tab)
+ return true;
+
+ if (event.modifiers())
+ return false;
+
+ switch (event.key()) {
+ case KeyCode::Key_Tab:
+ case KeyCode::Key_Left:
+ case KeyCode::Key_Right:
+ case KeyCode::Key_Up:
+ case KeyCode::Key_Down:
+ case KeyCode::Key_Return:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ virtual void keydown_event(GUI::KeyEvent& event) override
+ {
+ if (is_navigation(event))
+ on_cursor_key_pressed(event);
+ else
+ TextEditor::keydown_event(event);
+ }
+};
+
+class InfinitelyScrollableTableView : public GUI::TableView {
+ C_OBJECT(InfinitelyScrollableTableView)
+public:
+ Function<void()> on_reaching_vertical_end;
+ Function<void()> on_reaching_horizontal_end;
+
+private:
+ virtual void did_scroll() override;
+ virtual void mousemove_event(GUI::MouseEvent&) override;
+ virtual void mouseup_event(GUI::MouseEvent&) override;
+
+ bool m_should_intercept_drag { false };
+ bool m_has_committed_to_dragging { false };
+ GUI::ModelIndex m_starting_selection_index;
+};
+
+class SpreadsheetView final : public GUI::Widget {
+ C_OBJECT(SpreadsheetView);
+
+public:
+ ~SpreadsheetView();
+
+ const Sheet& sheet() const { return *m_sheet; }
+ Sheet& sheet() { return *m_sheet; }
+
+ const GUI::ModelIndex* cursor() const
+ {
+ return &m_table_view->cursor_index();
+ }
+
+ Function<void(Vector<Position>&&)> on_selection_changed;
+ Function<void()> on_selection_dropped;
+
+private:
+ virtual void hide_event(GUI::HideEvent&) override;
+ virtual void show_event(GUI::ShowEvent&) override;
+
+ void update_with_model();
+
+ SpreadsheetView(Sheet&);
+
+ class EditingDelegate final : public GUI::StringModelEditingDelegate {
+ public:
+ EditingDelegate(const Sheet& sheet)
+ : m_sheet(sheet)
+ {
+ }
+ virtual void set_value(const GUI::Variant& value) override;
+
+ virtual RefPtr<Widget> create_widget() override
+ {
+ auto textbox = CellEditor::construct();
+ textbox->on_escape_pressed = [this] {
+ rollback();
+ };
+ textbox->on_cursor_key_pressed = [this](auto& event) {
+ commit();
+ on_cursor_key_pressed(event);
+ };
+ return textbox;
+ }
+
+ Function<void(GUI::KeyEvent&)> on_cursor_key_pressed;
+
+ private:
+ bool m_has_set_initial_value { false };
+ const Sheet& m_sheet;
+ };
+
+ class TableCellPainter final : public GUI::TableCellPaintingDelegate {
+ public:
+ TableCellPainter(const GUI::TableView& view)
+ : m_table_view(view)
+ {
+ }
+ void paint(GUI::Painter&, const Gfx::IntRect&, const Gfx::Palette&, const GUI::ModelIndex&) override;
+
+ private:
+ const GUI::TableView& m_table_view;
+ };
+
+ NonnullRefPtr<Sheet> m_sheet;
+ RefPtr<InfinitelyScrollableTableView> m_table_view;
+ RefPtr<GUI::Menu> m_cell_range_context_menu;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp b/Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp
new file mode 100644
index 0000000000..65e48e3f12
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetWidget.cpp
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SpreadsheetWidget.h"
+#include "CellSyntaxHighlighter.h"
+#include "HelpWindow.h"
+#include "LibGUI/InputBox.h"
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGfx/FontDatabase.h>
+#include <string.h>
+
+namespace Spreadsheet {
+
+SpreadsheetWidget::SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets, bool should_add_sheet_if_empty)
+ : m_workbook(make<Workbook>(move(sheets)))
+{
+ set_fill_with_background_color(true);
+ set_layout<GUI::VerticalBoxLayout>().set_margins({ 2, 2, 2, 2 });
+ auto& container = add<GUI::VerticalSplitter>();
+
+ auto& top_bar = container.add<GUI::Frame>();
+ top_bar.set_layout<GUI::HorizontalBoxLayout>().set_spacing(1);
+ top_bar.set_fixed_height(26);
+ auto& current_cell_label = top_bar.add<GUI::Label>("");
+ current_cell_label.set_fixed_width(50);
+
+ auto& help_button = top_bar.add<GUI::Button>("🛈");
+ help_button.set_fixed_size(20, 20);
+ help_button.on_click = [&](auto) {
+ auto docs = m_selected_view->sheet().gather_documentation();
+ auto help_window = HelpWindow::the(window());
+ help_window->set_docs(move(docs));
+ help_window->show();
+ };
+
+ auto& cell_value_editor = top_bar.add<GUI::TextEditor>(GUI::TextEditor::Type::SingleLine);
+ cell_value_editor.set_font(Gfx::FontDatabase::default_fixed_width_font());
+ cell_value_editor.set_scrollbars_enabled(false);
+
+ cell_value_editor.set_syntax_highlighter(make<CellSyntaxHighlighter>());
+ cell_value_editor.set_enabled(false);
+ current_cell_label.set_enabled(false);
+
+ m_tab_widget = container.add<GUI::TabWidget>();
+ m_tab_widget->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
+
+ m_cell_value_editor = cell_value_editor;
+ m_current_cell_label = current_cell_label;
+ m_inline_documentation_window = GUI::Window::construct(window());
+ m_inline_documentation_window->set_rect(m_cell_value_editor->rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
+ m_inline_documentation_window->set_window_type(GUI::WindowType::Tooltip);
+ m_inline_documentation_window->set_resizable(false);
+ auto& inline_widget = m_inline_documentation_window->set_main_widget<GUI::Frame>();
+ inline_widget.set_fill_with_background_color(true);
+ inline_widget.set_layout<GUI::VerticalBoxLayout>().set_margins({ 4, 4, 4, 4 });
+ inline_widget.set_frame_shape(Gfx::FrameShape::Box);
+ m_inline_documentation_label = inline_widget.add<GUI::Label>();
+ m_inline_documentation_label->set_fill_with_background_color(true);
+ m_inline_documentation_label->set_autosize(false);
+ m_inline_documentation_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
+
+ if (!m_workbook->has_sheets() && should_add_sheet_if_empty)
+ m_workbook->add_sheet("Sheet 1");
+
+ m_tab_context_menu = GUI::Menu::construct();
+ auto rename_action = GUI::Action::create("Rename...", [this](auto&) {
+ ASSERT(m_tab_context_menu_sheet_view);
+
+ auto& sheet = m_tab_context_menu_sheet_view->sheet();
+ String new_name;
+ if (GUI::InputBox::show(new_name, window(), String::formatted("New name for '{}'", sheet.name()), "Rename sheet") == GUI::Dialog::ExecOK) {
+ sheet.set_name(new_name);
+ sheet.update();
+ m_tab_widget->set_tab_title(static_cast<GUI::Widget&>(*m_tab_context_menu_sheet_view), new_name);
+ }
+ });
+ m_tab_context_menu->add_action(rename_action);
+ m_tab_context_menu->add_action(GUI::Action::create("Add new sheet...", [this](auto&) {
+ String name;
+ if (GUI::InputBox::show(name, window(), "Name for new sheet", "Create sheet") == GUI::Dialog::ExecOK) {
+ NonnullRefPtrVector<Sheet> new_sheets;
+ new_sheets.append(m_workbook->add_sheet(name));
+ setup_tabs(move(new_sheets));
+ }
+ }));
+
+ setup_tabs(m_workbook->sheets());
+}
+
+void SpreadsheetWidget::resize_event(GUI::ResizeEvent& event)
+{
+ GUI::Widget::resize_event(event);
+ if (m_inline_documentation_window && m_cell_value_editor && window())
+ m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
+}
+
+void SpreadsheetWidget::setup_tabs(NonnullRefPtrVector<Sheet> new_sheets)
+{
+ RefPtr<GUI::Widget> first_tab_widget;
+ for (auto& sheet : new_sheets) {
+ auto& tab = m_tab_widget->add_tab<SpreadsheetView>(sheet.name(), sheet);
+ if (!first_tab_widget)
+ first_tab_widget = &tab;
+ }
+
+ auto change = [&](auto& selected_widget) {
+ if (m_selected_view) {
+ m_selected_view->on_selection_changed = nullptr;
+ m_selected_view->on_selection_dropped = nullptr;
+ };
+ m_selected_view = &static_cast<SpreadsheetView&>(selected_widget);
+ m_selected_view->on_selection_changed = [&](Vector<Position>&& selection) {
+ if (selection.is_empty()) {
+ m_current_cell_label->set_enabled(false);
+ m_current_cell_label->set_text({});
+ m_cell_value_editor->on_change = nullptr;
+ m_cell_value_editor->on_focusin = nullptr;
+ m_cell_value_editor->on_focusout = nullptr;
+ m_cell_value_editor->set_text("");
+ m_cell_value_editor->set_enabled(false);
+ return;
+ }
+
+ if (selection.size() == 1) {
+ auto& position = selection.first();
+ StringBuilder builder;
+ builder.append(position.column);
+ builder.appendff("{}", position.row);
+ m_current_cell_label->set_enabled(true);
+ m_current_cell_label->set_text(builder.string_view());
+
+ auto& cell = m_selected_view->sheet().ensure(position);
+ m_cell_value_editor->on_change = nullptr;
+ m_cell_value_editor->set_text(cell.source());
+ m_cell_value_editor->on_change = [&] {
+ auto text = m_cell_value_editor->text();
+ // FIXME: Lines?
+ auto offset = m_cell_value_editor->cursor().column();
+ try_generate_tip_for_input_expression(text, offset);
+ cell.set_data(move(text));
+ m_selected_view->sheet().update();
+ update();
+ };
+ m_cell_value_editor->set_enabled(true);
+ static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(&cell);
+ return;
+ }
+
+ // There are many cells selected, change all of them.
+ StringBuilder builder;
+ builder.appendff("<{}>", selection.size());
+ m_current_cell_label->set_enabled(true);
+ m_current_cell_label->set_text(builder.string_view());
+
+ Vector<Cell*> cells;
+ for (auto& position : selection)
+ cells.append(&m_selected_view->sheet().ensure(position));
+
+ auto first_cell = cells.first();
+ m_cell_value_editor->on_change = nullptr;
+ m_cell_value_editor->set_text("");
+ m_should_change_selected_cells = false;
+ m_cell_value_editor->on_focusin = [this] { m_should_change_selected_cells = true; };
+ m_cell_value_editor->on_focusout = [this] { m_should_change_selected_cells = false; };
+ m_cell_value_editor->on_change = [cells = move(cells), this] {
+ if (m_should_change_selected_cells) {
+ auto text = m_cell_value_editor->text();
+ // FIXME: Lines?
+ auto offset = m_cell_value_editor->cursor().column();
+ try_generate_tip_for_input_expression(text, offset);
+ for (auto* cell : cells)
+ cell->set_data(text);
+ m_selected_view->sheet().update();
+ update();
+ }
+ };
+ m_cell_value_editor->set_enabled(true);
+ static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(first_cell);
+ };
+ m_selected_view->on_selection_dropped = [&]() {
+ m_cell_value_editor->set_enabled(false);
+ static_cast<CellSyntaxHighlighter*>(const_cast<GUI::SyntaxHighlighter*>(m_cell_value_editor->syntax_highlighter()))->set_cell(nullptr);
+ m_cell_value_editor->set_text("");
+ m_current_cell_label->set_enabled(false);
+ m_current_cell_label->set_text("");
+ };
+ };
+
+ if (first_tab_widget)
+ change(*first_tab_widget);
+
+ m_tab_widget->on_change = [change = move(change)](auto& selected_widget) {
+ change(selected_widget);
+ };
+
+ m_tab_widget->on_context_menu_request = [&](auto& widget, auto& event) {
+ m_tab_context_menu_sheet_view = widget;
+ m_tab_context_menu->popup(event.screen_position());
+ };
+}
+
+void SpreadsheetWidget::try_generate_tip_for_input_expression(StringView source, size_t cursor_offset)
+{
+ m_inline_documentation_window->set_rect(m_cell_value_editor->screen_relative_rect().translated(0, m_cell_value_editor->height() + 7).inflated(6, 6));
+ if (!m_selected_view || !source.starts_with('=')) {
+ m_inline_documentation_window->hide();
+ return;
+ }
+ auto maybe_function_and_argument = get_function_and_argument_index(source.substring_view(0, cursor_offset));
+ if (!maybe_function_and_argument.has_value()) {
+ m_inline_documentation_window->hide();
+ return;
+ }
+
+ auto& [name, index] = maybe_function_and_argument.value();
+ auto& sheet = m_selected_view->sheet();
+ auto text = sheet.generate_inline_documentation_for(name, index);
+ if (text.is_empty()) {
+ m_inline_documentation_window->hide();
+ } else {
+ m_inline_documentation_label->set_text(move(text));
+ m_inline_documentation_window->show();
+ }
+}
+
+void SpreadsheetWidget::save(const StringView& filename)
+{
+ auto result = m_workbook->save(filename);
+ if (result.is_error())
+ GUI::MessageBox::show_error(window(), result.error());
+}
+
+void SpreadsheetWidget::load(const StringView& filename)
+{
+ auto result = m_workbook->load(filename);
+ if (result.is_error()) {
+ GUI::MessageBox::show_error(window(), result.error());
+ return;
+ }
+
+ m_tab_widget->on_change = nullptr;
+ m_cell_value_editor->on_change = nullptr;
+ m_current_cell_label->set_text("");
+ m_should_change_selected_cells = false;
+ while (auto* widget = m_tab_widget->active_widget()) {
+ m_tab_widget->remove_tab(*widget);
+ }
+
+ setup_tabs(m_workbook->sheets());
+}
+
+bool SpreadsheetWidget::request_close()
+{
+ if (!m_workbook->dirty())
+ return true;
+
+ auto result = GUI::MessageBox::show(window(), "The spreadsheet has been modified. Would you like to save?", "Unsaved changes", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
+
+ if (result == GUI::MessageBox::ExecYes) {
+ if (current_filename().is_empty()) {
+ String name = "workbook";
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), name, "sheets");
+ if (!save_path.has_value())
+ return false;
+
+ save(save_path.value());
+ } else {
+ save(current_filename());
+ }
+ return true;
+ }
+
+ if (result == GUI::MessageBox::ExecNo)
+ return true;
+
+ return false;
+}
+
+void SpreadsheetWidget::add_sheet()
+{
+ StringBuilder name;
+ name.append("Sheet");
+ name.appendff(" {}", m_workbook->sheets().size() + 1);
+
+ NonnullRefPtrVector<Sheet> new_sheets;
+ new_sheets.append(m_workbook->add_sheet(name.string_view()));
+ setup_tabs(move(new_sheets));
+}
+
+void SpreadsheetWidget::add_sheet(NonnullRefPtr<Sheet>&& sheet)
+{
+ ASSERT(m_workbook == &sheet->workbook());
+
+ NonnullRefPtrVector<Sheet> new_sheets;
+ new_sheets.append(move(sheet));
+ m_workbook->sheets().append(new_sheets);
+ setup_tabs(new_sheets);
+}
+
+void SpreadsheetWidget::set_filename(const String& filename)
+{
+ if (m_workbook->set_filename(filename)) {
+ StringBuilder builder;
+ builder.append("Spreadsheet - ");
+ builder.append(current_filename());
+
+ window()->set_title(builder.string_view());
+ window()->update();
+ }
+}
+
+SpreadsheetWidget::~SpreadsheetWidget()
+{
+}
+}
diff --git a/Userland/Applications/Spreadsheet/SpreadsheetWidget.h b/Userland/Applications/Spreadsheet/SpreadsheetWidget.h
new file mode 100644
index 0000000000..b29fc557d6
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/SpreadsheetWidget.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "SpreadsheetView.h"
+#include "Workbook.h"
+#include <AK/NonnullRefPtrVector.h>
+#include <LibGUI/Widget.h>
+
+namespace Spreadsheet {
+
+class SpreadsheetWidget final : public GUI::Widget {
+ C_OBJECT(SpreadsheetWidget);
+
+public:
+ ~SpreadsheetWidget();
+
+ void save(const StringView& filename);
+ void load(const StringView& filename);
+ bool request_close();
+ void add_sheet();
+ void add_sheet(NonnullRefPtr<Sheet>&&);
+
+ const String& current_filename() const { return m_workbook->current_filename(); }
+ const Sheet& current_worksheet() const { return m_selected_view->sheet(); }
+ Sheet& current_worksheet() { return m_selected_view->sheet(); }
+ void set_filename(const String& filename);
+
+ Workbook& workbook() { return *m_workbook; }
+ const Workbook& workbook() const { return *m_workbook; }
+
+ const GUI::ModelIndex* current_selection_cursor() const
+ {
+ if (!m_selected_view)
+ return nullptr;
+
+ return m_selected_view->cursor();
+ }
+
+private:
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ explicit SpreadsheetWidget(NonnullRefPtrVector<Sheet>&& sheets = {}, bool should_add_sheet_if_empty = true);
+
+ void setup_tabs(NonnullRefPtrVector<Sheet> new_sheets);
+
+ void try_generate_tip_for_input_expression(StringView source, size_t offset);
+
+ SpreadsheetView* m_selected_view { nullptr };
+ RefPtr<GUI::Label> m_current_cell_label;
+ RefPtr<GUI::TextEditor> m_cell_value_editor;
+ RefPtr<GUI::Window> m_inline_documentation_window;
+ RefPtr<GUI::Label> m_inline_documentation_label;
+ RefPtr<GUI::TabWidget> m_tab_widget;
+ RefPtr<GUI::Menu> m_tab_context_menu;
+ RefPtr<SpreadsheetView> m_tab_context_menu_sheet_view;
+ bool m_should_change_selected_cells { false };
+
+ OwnPtr<Workbook> m_workbook;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Workbook.cpp b/Userland/Applications/Spreadsheet/Workbook.cpp
new file mode 100644
index 0000000000..db5bf16cb5
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Workbook.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Workbook.h"
+#include "JSIntegration.h"
+#include "Readers/CSV.h"
+#include "Writers/CSV.h"
+#include <AK/ByteBuffer.h>
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonObjectSerializer.h>
+#include <AK/JsonParser.h>
+#include <AK/Stream.h>
+#include <LibCore/File.h>
+#include <LibCore/FileStream.h>
+#include <LibCore/MimeData.h>
+#include <LibJS/Parser.h>
+#include <LibJS/Runtime/GlobalObject.h>
+#include <string.h>
+
+namespace Spreadsheet {
+
+static JS::VM& global_vm()
+{
+ static RefPtr<JS::VM> vm;
+ if (!vm)
+ vm = JS::VM::create();
+ return *vm;
+}
+
+Workbook::Workbook(NonnullRefPtrVector<Sheet>&& sheets)
+ : m_sheets(move(sheets))
+ , m_interpreter(JS::Interpreter::create<JS::GlobalObject>(global_vm()))
+ , m_interpreter_scope(JS::VM::InterpreterExecutionScope(interpreter()))
+{
+ m_workbook_object = interpreter().heap().allocate<WorkbookObject>(global_object(), *this);
+ global_object().put("workbook", workbook_object());
+}
+
+bool Workbook::set_filename(const String& filename)
+{
+ if (m_current_filename == filename)
+ return false;
+
+ m_current_filename = filename;
+ return true;
+}
+
+Result<bool, String> Workbook::load(const StringView& filename)
+{
+ auto file_or_error = Core::File::open(filename, Core::IODevice::OpenMode::ReadOnly);
+ if (file_or_error.is_error()) {
+ StringBuilder sb;
+ sb.append("Failed to open ");
+ sb.append(filename);
+ sb.append(" for reading. Error: ");
+ sb.append(file_or_error.error());
+
+ return sb.to_string();
+ }
+
+ auto mime = Core::guess_mime_type_based_on_filename(filename);
+
+ if (mime == "text/csv") {
+ // FIXME: Prompt the user for settings.
+ NonnullRefPtrVector<Sheet> sheets;
+
+ auto sheet = Sheet::from_xsv(Reader::CSV(file_or_error.value()->read_all(), Reader::default_behaviours() | Reader::ParserBehaviour::ReadHeaders), *this);
+ if (sheet)
+ sheets.append(sheet.release_nonnull());
+
+ m_sheets.clear();
+ m_sheets = move(sheets);
+ } else {
+ // Assume JSON.
+ auto json_value_option = JsonParser(file_or_error.value()->read_all()).parse();
+ if (!json_value_option.has_value()) {
+ StringBuilder sb;
+ sb.append("Failed to parse ");
+ sb.append(filename);
+
+ return sb.to_string();
+ }
+
+ auto& json_value = json_value_option.value();
+ if (!json_value.is_array()) {
+ StringBuilder sb;
+ sb.append("Did not find a spreadsheet in ");
+ sb.append(filename);
+
+ return sb.to_string();
+ }
+
+ NonnullRefPtrVector<Sheet> sheets;
+
+ auto& json_array = json_value.as_array();
+ json_array.for_each([&](auto& sheet_json) {
+ if (!sheet_json.is_object())
+ return IterationDecision::Continue;
+
+ auto sheet = Sheet::from_json(sheet_json.as_object(), *this);
+ if (!sheet)
+ return IterationDecision::Continue;
+
+ sheets.append(sheet.release_nonnull());
+
+ return IterationDecision::Continue;
+ });
+
+ m_sheets.clear();
+ m_sheets = move(sheets);
+ }
+
+ set_filename(filename);
+
+ return true;
+}
+
+Result<bool, String> Workbook::save(const StringView& filename)
+{
+ auto mime = Core::guess_mime_type_based_on_filename(filename);
+ auto file = Core::File::construct(filename);
+ file->open(Core::IODevice::WriteOnly);
+ if (!file->is_open()) {
+ StringBuilder sb;
+ sb.append("Failed to open ");
+ sb.append(filename);
+ sb.append(" for write. Error: ");
+ sb.append(file->error_string());
+
+ return sb.to_string();
+ }
+
+ if (mime == "text/csv") {
+ // FIXME: Prompt the user for settings and which sheet to export.
+ Core::OutputFileStream stream { file };
+ auto data = m_sheets[0].to_xsv();
+ auto header_string = data.take_first();
+ Vector<StringView> headers;
+ for (auto& str : header_string)
+ headers.append(str);
+ Writer::CSV csv { stream, data, headers };
+ if (csv.has_error())
+ return String::formatted("Unable to save file, CSV writer error: {}", csv.error_string());
+ } else {
+ JsonArray array;
+ for (auto& sheet : m_sheets)
+ array.append(sheet.to_json());
+
+ auto file_content = array.to_string();
+ bool result = file->write(file_content);
+ if (!result) {
+ int error_number = errno;
+ StringBuilder sb;
+ sb.append("Unable to save file. Error: ");
+ sb.append(strerror(error_number));
+
+ return sb.to_string();
+ }
+ }
+
+ set_filename(filename);
+ set_dirty(false);
+ return true;
+}
+
+}
diff --git a/Userland/Applications/Spreadsheet/Workbook.h b/Userland/Applications/Spreadsheet/Workbook.h
new file mode 100644
index 0000000000..63c016d501
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Workbook.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Forward.h"
+#include "Spreadsheet.h"
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/Result.h>
+
+namespace Spreadsheet {
+
+class Workbook {
+public:
+ Workbook(NonnullRefPtrVector<Sheet>&& sheets);
+
+ Result<bool, String> save(const StringView& filename);
+ Result<bool, String> load(const StringView& filename);
+
+ const String& current_filename() const { return m_current_filename; }
+ bool set_filename(const String& filename);
+ bool dirty() { return m_dirty; }
+ void set_dirty(bool dirty) { m_dirty = dirty; }
+
+ bool has_sheets() const { return !m_sheets.is_empty(); }
+
+ const NonnullRefPtrVector<Sheet>& sheets() const { return m_sheets; }
+ NonnullRefPtrVector<Sheet> sheets() { return m_sheets; }
+
+ Sheet& add_sheet(const StringView& name)
+ {
+ auto sheet = Sheet::construct(name, *this);
+ m_sheets.append(sheet);
+ return *sheet;
+ }
+
+ JS::Interpreter& interpreter() { return *m_interpreter; }
+ const JS::Interpreter& interpreter() const { return *m_interpreter; }
+
+ JS::GlobalObject& global_object() { return m_interpreter->global_object(); }
+ const JS::GlobalObject& global_object() const { return m_interpreter->global_object(); }
+
+ WorkbookObject* workbook_object() { return m_workbook_object; }
+
+private:
+ NonnullRefPtrVector<Sheet> m_sheets;
+ NonnullOwnPtr<JS::Interpreter> m_interpreter;
+ JS::VM::InterpreterExecutionScope m_interpreter_scope;
+ WorkbookObject* m_workbook_object { nullptr };
+
+ String m_current_filename;
+ bool m_dirty { false };
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Writers/CSV.h b/Userland/Applications/Spreadsheet/Writers/CSV.h
new file mode 100644
index 0000000000..49940fbdf1
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Writers/CSV.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "XSV.h"
+#include <AK/Forward.h>
+#include <AK/StringView.h>
+
+namespace Writer {
+
+template<typename ContainerType>
+class CSV : public XSV<ContainerType> {
+public:
+ CSV(OutputStream& output, const ContainerType& data, const Vector<StringView>& headers = {}, WriterBehaviour behaviours = default_behaviours())
+ : XSV<ContainerType>(output, data, { ",", "\"", WriterTraits::Repeat }, headers, behaviours)
+ {
+ }
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/Writers/Test/TestXSVWriter.cpp b/Userland/Applications/Spreadsheet/Writers/Test/TestXSVWriter.cpp
new file mode 100644
index 0000000000..4971658431
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Writers/Test/TestXSVWriter.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/TestSuite.h>
+
+#include "../CSV.h"
+#include "../XSV.h"
+#include <AK/MemoryStream.h>
+
+TEST_CASE(can_write)
+{
+ Vector<Vector<int>> data = {
+ { 1, 2, 3 },
+ { 4, 5, 6 },
+ { 7, 8, 9 },
+ };
+
+ auto buffer = ByteBuffer::create_uninitialized(1024);
+ OutputMemoryStream stream { buffer };
+
+ Writer::CSV csv(stream, data);
+
+ auto expected_output = R"~(1,2,3
+4,5,6
+7,8,9
+)~";
+
+ EXPECT_EQ(StringView { stream.bytes() }, expected_output);
+}
+
+TEST_CASE(can_write_with_header)
+{
+ Vector<Vector<int>> data = {
+ { 1, 2, 3 },
+ { 4, 5, 6 },
+ { 7, 8, 9 },
+ };
+
+ auto buffer = ByteBuffer::create_uninitialized(1024);
+ OutputMemoryStream stream { buffer };
+
+ Writer::CSV csv(stream, data, { "A", "B\"", "C" });
+
+ auto expected_output = R"~(A,"B""",C
+1,2,3
+4,5,6
+7,8,9
+)~";
+
+ EXPECT_EQ(StringView { stream.bytes() }, expected_output);
+}
+
+TEST_CASE(can_write_with_different_behaviours)
+{
+ Vector<Vector<String>> data = {
+ { "Well", "Hello\"", "Friends" },
+ { "We\"ll", "Hello,", " Friends" },
+ };
+
+ auto buffer = ByteBuffer::create_uninitialized(1024);
+ OutputMemoryStream stream { buffer };
+
+ Writer::CSV csv(stream, data, { "A", "B\"", "C" }, Writer::WriterBehaviour::QuoteOnlyInFieldStart | Writer::WriterBehaviour::WriteHeaders);
+
+ auto expected_output = R"~(A,B",C
+Well,Hello",Friends
+We"ll,"Hello,", Friends
+)~";
+
+ EXPECT_EQ(StringView { stream.bytes() }, expected_output);
+}
+
+TEST_MAIN(XSV)
diff --git a/Userland/Applications/Spreadsheet/Writers/XSV.h b/Userland/Applications/Spreadsheet/Writers/XSV.h
new file mode 100644
index 0000000000..7a065f87d1
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/Writers/XSV.h
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/GenericLexer.h>
+#include <AK/OwnPtr.h>
+#include <AK/Stream.h>
+#include <AK/String.h>
+#include <AK/StringView.h>
+#include <AK/Types.h>
+#include <AK/Vector.h>
+
+namespace Writer {
+
+enum class WriterBehaviour : u32 {
+ None = 0,
+ WriteHeaders = 1,
+ AllowNewlinesInFields = WriteHeaders << 1,
+ QuoteOnlyInFieldStart = WriteHeaders << 2,
+ QuoteAll = WriteHeaders << 3,
+};
+
+inline WriterBehaviour operator&(WriterBehaviour left, WriterBehaviour right)
+{
+ return static_cast<WriterBehaviour>(static_cast<u32>(left) & static_cast<u32>(right));
+}
+
+inline WriterBehaviour operator|(WriterBehaviour left, WriterBehaviour right)
+{
+ return static_cast<WriterBehaviour>(static_cast<u32>(left) | static_cast<u32>(right));
+}
+
+struct WriterTraits {
+ String separator;
+ String quote { "\"" };
+ enum {
+ Repeat,
+ Backslash,
+ } quote_escape { Repeat };
+};
+
+#define ENUMERATE_WRITE_ERRORS() \
+ E(None, "No errors") \
+ E(NonConformingColumnCount, "Header count does not match given column count") \
+ E(InternalError, "Internal error")
+
+enum class WriteError {
+#define E(name, _) name,
+ ENUMERATE_WRITE_ERRORS()
+#undef E
+};
+
+inline constexpr WriterBehaviour default_behaviours()
+{
+ return WriterBehaviour::None;
+}
+
+template<typename ContainerType>
+class XSV {
+public:
+ XSV(OutputStream& output, const ContainerType& data, const WriterTraits& traits, const Vector<StringView>& headers = {}, WriterBehaviour behaviours = default_behaviours())
+ : m_data(data)
+ , m_traits(traits)
+ , m_behaviours(behaviours)
+ , m_names(headers)
+ , m_output(output)
+ {
+ if (!headers.is_empty())
+ m_behaviours = m_behaviours | WriterBehaviour::WriteHeaders;
+
+ generate();
+ }
+
+ virtual ~XSV() { }
+
+ bool has_error() const { return m_error != WriteError::None; }
+ WriteError error() const { return m_error; }
+ String error_string() const
+ {
+ switch (m_error) {
+#define E(x, y) \
+ case WriteError::x: \
+ return y;
+
+ ENUMERATE_WRITE_ERRORS();
+#undef E
+ }
+ ASSERT_NOT_REACHED();
+ }
+
+private:
+ void set_error(WriteError error)
+ {
+ if (m_error == WriteError::None)
+ m_error = error;
+ }
+
+ void generate()
+ {
+ auto with_headers = (m_behaviours & WriterBehaviour::WriteHeaders) != WriterBehaviour::None;
+ if (with_headers) {
+ write_row(m_names);
+ if (m_output.write({ "\n", 1 }) != 1)
+ set_error(WriteError::InternalError);
+ }
+
+ for (auto&& row : m_data) {
+ if (with_headers) {
+ if (row.size() != m_names.size())
+ set_error(WriteError::NonConformingColumnCount);
+ }
+
+ write_row(row);
+ if (m_output.write({ "\n", 1 }) != 1)
+ set_error(WriteError::InternalError);
+ }
+ }
+
+ template<typename T>
+ void write_row(T&& row)
+ {
+ bool first = true;
+ for (auto&& entry : row) {
+ if (!first) {
+ if (m_output.write(m_traits.separator.bytes()) != m_traits.separator.length())
+ set_error(WriteError::InternalError);
+ }
+ first = false;
+ write_entry(entry);
+ }
+ }
+
+ template<typename T>
+ void write_entry(T&& entry)
+ {
+ auto string = String::formatted("{}", FormatIfSupported(entry));
+
+ auto safe_to_write_normally = !string.contains("\n") && !string.contains(m_traits.separator);
+ if (safe_to_write_normally) {
+ if ((m_behaviours & WriterBehaviour::QuoteOnlyInFieldStart) == WriterBehaviour::None)
+ safe_to_write_normally = !string.contains(m_traits.quote);
+ else
+ safe_to_write_normally = !string.starts_with(m_traits.quote);
+ }
+ if (safe_to_write_normally) {
+ if (m_output.write(string.bytes()) != string.length())
+ set_error(WriteError::InternalError);
+ return;
+ }
+
+ if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
+ set_error(WriteError::InternalError);
+
+ GenericLexer lexer(string);
+ while (!lexer.is_eof()) {
+ if (lexer.consume_specific(m_traits.quote)) {
+ switch (m_traits.quote_escape) {
+ case WriterTraits::Repeat:
+ if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
+ set_error(WriteError::InternalError);
+ if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
+ set_error(WriteError::InternalError);
+ break;
+ case WriterTraits::Backslash:
+ if (m_output.write({ "\\", 1 }) != 1)
+ set_error(WriteError::InternalError);
+ if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
+ set_error(WriteError::InternalError);
+ break;
+ }
+ continue;
+ }
+
+ auto ch = lexer.consume();
+ if (m_output.write({ &ch, 1 }) != 1)
+ set_error(WriteError::InternalError);
+ }
+
+ if (m_output.write(m_traits.quote.bytes()) != m_traits.quote.length())
+ set_error(WriteError::InternalError);
+ }
+
+ const ContainerType& m_data;
+ const WriterTraits& m_traits;
+ WriterBehaviour m_behaviours;
+ const Vector<StringView>& m_names;
+ WriteError m_error { WriteError::None };
+ OutputStream& m_output;
+};
+
+}
diff --git a/Userland/Applications/Spreadsheet/main.cpp b/Userland/Applications/Spreadsheet/main.cpp
new file mode 100644
index 0000000000..e13a89cdac
--- /dev/null
+++ b/Userland/Applications/Spreadsheet/main.cpp
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HelpWindow.h"
+#include "Spreadsheet.h"
+#include "SpreadsheetWidget.h"
+#include <AK/ScopeGuard.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/File.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Clipboard.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Forward.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Window.h>
+
+int main(int argc, char* argv[])
+{
+ if (pledge("stdio shared_buffer accept rpath unix cpath wpath fattr thread", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* filename = nullptr;
+
+ Core::ArgsParser args_parser;
+ args_parser.add_positional_argument(filename, "File to read from", "file", Core::ArgsParser::Required::No);
+
+ args_parser.parse(argc, argv);
+
+ if (filename) {
+ if (!Core::File::exists(filename) || Core::File::is_directory(filename)) {
+ warnln("File does not exist or is a directory: {}", filename);
+ return 1;
+ }
+ }
+
+ if (unveil("/tmp/portal/webcontent", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(Core::StandardPaths::home_directory().characters(), "rwc") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-spreadsheet");
+ auto window = GUI::Window::construct();
+ window->set_title("Spreadsheet");
+ window->resize(640, 480);
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto& spreadsheet_widget = window->set_main_widget<Spreadsheet::SpreadsheetWidget>(NonnullRefPtrVector<Spreadsheet::Sheet> {}, filename == nullptr);
+
+ if (filename)
+ spreadsheet_widget.load(filename);
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Spreadsheet");
+
+ app_menu.add_action(GUI::Action::create("Add New Sheet", Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"), [&](auto&) {
+ spreadsheet_widget.add_sheet();
+ }));
+
+ app_menu.add_separator();
+
+ app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
+ if (!spreadsheet_widget.request_close())
+ return;
+ app->quit(0);
+ }));
+
+ window->on_close_request = [&]() -> GUI::Window::CloseRequestDecision {
+ if (spreadsheet_widget.request_close())
+ return GUI::Window::CloseRequestDecision::Close;
+ return GUI::Window::CloseRequestDecision::StayOpen;
+ };
+
+ auto& file_menu = menubar->add_menu("File");
+ file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
+ Optional<String> load_path = GUI::FilePicker::get_open_filepath(window);
+ if (!load_path.has_value())
+ return;
+
+ spreadsheet_widget.load(load_path.value());
+ }));
+
+ file_menu.add_action(GUI::CommonActions::make_save_action([&](auto&) {
+ if (spreadsheet_widget.current_filename().is_empty()) {
+ String name = "workbook";
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, name, "sheets");
+ if (!save_path.has_value())
+ return;
+
+ spreadsheet_widget.save(save_path.value());
+ } else {
+ spreadsheet_widget.save(spreadsheet_widget.current_filename());
+ }
+ }));
+
+ file_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
+ auto current_filename = spreadsheet_widget.current_filename();
+ String name = "workbook";
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, name, "sheets");
+ if (!save_path.has_value())
+ return;
+
+ spreadsheet_widget.save(save_path.value());
+
+ if (!current_filename.is_empty())
+ spreadsheet_widget.set_filename(current_filename);
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ edit_menu.add_action(GUI::CommonActions::make_copy_action([&](auto&) {
+ /// text/x-spreadsheet-data:
+ /// - currently selected cell
+ /// - selected cell+
+ auto& cells = spreadsheet_widget.current_worksheet().selected_cells();
+ ASSERT(!cells.is_empty());
+ StringBuilder text_builder, url_builder;
+ bool first = true;
+ auto cursor = spreadsheet_widget.current_selection_cursor();
+ if (cursor) {
+ Spreadsheet::Position position { spreadsheet_widget.current_worksheet().column(cursor->column()), (size_t)cursor->row() };
+ url_builder.append(position.to_url().to_string());
+ url_builder.append('\n');
+ }
+
+ for (auto& cell : cells) {
+ if (first && !cursor) {
+ url_builder.append(cell.to_url().to_string());
+ url_builder.append('\n');
+ }
+
+ url_builder.append(cell.to_url().to_string());
+ url_builder.append('\n');
+
+ auto cell_data = spreadsheet_widget.current_worksheet().at(cell);
+ if (!first)
+ text_builder.append('\t');
+ if (cell_data)
+ text_builder.append(cell_data->data());
+ first = false;
+ }
+ HashMap<String, String> metadata;
+ metadata.set("text/x-spreadsheet-data", url_builder.to_string());
+
+ GUI::Clipboard::the().set_data(text_builder.string_view().bytes(), "text/plain", move(metadata));
+ },
+ window));
+ edit_menu.add_action(GUI::CommonActions::make_paste_action([&](auto&) {
+ ScopeGuard update_after_paste { [&] { spreadsheet_widget.update(); } };
+
+ auto& cells = spreadsheet_widget.current_worksheet().selected_cells();
+ ASSERT(!cells.is_empty());
+ const auto& data = GUI::Clipboard::the().data_and_type();
+ if (auto spreadsheet_data = data.metadata.get("text/x-spreadsheet-data"); spreadsheet_data.has_value()) {
+ Vector<Spreadsheet::Position> source_positions, target_positions;
+ auto& sheet = spreadsheet_widget.current_worksheet();
+
+ for (auto& line : spreadsheet_data.value().split_view('\n')) {
+ dbgln("Paste line '{}'", line);
+ auto position = sheet.position_from_url(line);
+ if (position.has_value())
+ source_positions.append(position.release_value());
+ }
+
+ for (auto& position : spreadsheet_widget.current_worksheet().selected_cells())
+ target_positions.append(position);
+
+ if (source_positions.is_empty())
+ return;
+
+ auto first_position = source_positions.take_first();
+ sheet.copy_cells(move(source_positions), move(target_positions), first_position);
+ } else {
+ for (auto& cell : spreadsheet_widget.current_worksheet().selected_cells())
+ spreadsheet_widget.current_worksheet().ensure(cell).set_data(StringView { data.data.data(), data.data.size() });
+ spreadsheet_widget.update();
+ }
+ },
+ window));
+
+ auto& help_menu = menubar->add_menu("Help");
+
+ help_menu.add_action(GUI::Action::create(
+ "Functions Help", [&](auto&) {
+ auto docs = spreadsheet_widget.current_worksheet().gather_documentation();
+ auto help_window = Spreadsheet::HelpWindow::the(window);
+ help_window->set_docs(move(docs));
+ help_window->show();
+ },
+ window));
+
+ app_menu.add_separator();
+
+ help_menu.add_action(GUI::CommonActions::make_about_action("Spreadsheet", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Userland/Applications/SystemMonitor/CMakeLists.txt b/Userland/Applications/SystemMonitor/CMakeLists.txt
new file mode 100644
index 0000000000..ee2e4d2323
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/CMakeLists.txt
@@ -0,0 +1,16 @@
+set(SOURCES
+ DevicesModel.cpp
+ GraphWidget.cpp
+ InterruptsWidget.cpp
+ main.cpp
+ MemoryStatsWidget.cpp
+ NetworkStatisticsWidget.cpp
+ ProcessFileDescriptorMapWidget.cpp
+ ProcessMemoryMapWidget.cpp
+ ProcessModel.cpp
+ ProcessUnveiledPathsWidget.cpp
+ ThreadStackWidget.cpp
+)
+
+serenity_app(SystemMonitor ICON app-system-monitor)
+target_link_libraries(SystemMonitor LibGUI LibPCIDB)
diff --git a/Userland/Applications/SystemMonitor/DevicesModel.cpp b/Userland/Applications/SystemMonitor/DevicesModel.cpp
new file mode 100644
index 0000000000..f0692cdd24
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/DevicesModel.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DevicesModel.h"
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <sys/stat.h>
+
+NonnullRefPtr<DevicesModel> DevicesModel::create()
+{
+ return adopt(*new DevicesModel);
+}
+
+DevicesModel::DevicesModel()
+{
+}
+
+DevicesModel::~DevicesModel()
+{
+}
+
+int DevicesModel::row_count(const GUI::ModelIndex&) const
+{
+ return m_devices.size();
+}
+
+int DevicesModel::column_count(const GUI::ModelIndex&) const
+{
+ return Column::__Count;
+}
+
+String DevicesModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Device:
+ return "Device";
+ case Column::Major:
+ return "Major";
+ case Column::Minor:
+ return "Minor";
+ case Column::ClassName:
+ return "Class";
+ case Column::Type:
+ return "Type";
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+GUI::Variant DevicesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ ASSERT(is_valid(index));
+
+ if (role == GUI::ModelRole::TextAlignment) {
+ switch (index.column()) {
+ case Column::Device:
+ return Gfx::TextAlignment::CenterLeft;
+ case Column::Major:
+ return Gfx::TextAlignment::CenterRight;
+ case Column::Minor:
+ return Gfx::TextAlignment::CenterRight;
+ case Column::ClassName:
+ return Gfx::TextAlignment::CenterLeft;
+ case Column::Type:
+ return Gfx::TextAlignment::CenterLeft;
+ }
+ return {};
+ }
+
+ if (role == GUI::ModelRole::Sort) {
+ const DeviceInfo& device = m_devices[index.row()];
+ switch (index.column()) {
+ case Column::Device:
+ return device.path;
+ case Column::Major:
+ return device.major;
+ case Column::Minor:
+ return device.minor;
+ case Column::ClassName:
+ return device.class_name;
+ case Column::Type:
+ return device.type;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ if (role == GUI::ModelRole::Display) {
+ const DeviceInfo& device = m_devices[index.row()];
+ switch (index.column()) {
+ case Column::Device:
+ return device.path;
+ case Column::Major:
+ return device.major;
+ case Column::Minor:
+ return device.minor;
+ case Column::ClassName:
+ return device.class_name;
+ case Column::Type:
+ switch (device.type) {
+ case DeviceInfo::Type::Block:
+ return "Block";
+ case DeviceInfo::Type::Character:
+ return "Character";
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ return {};
+}
+
+void DevicesModel::update()
+{
+ auto proc_devices = Core::File::construct("/proc/devices");
+ if (!proc_devices->open(Core::IODevice::OpenMode::ReadOnly))
+ ASSERT_NOT_REACHED();
+
+ auto json = JsonValue::from_string(proc_devices->read_all());
+ ASSERT(json.has_value());
+
+ m_devices.clear();
+ json.value().as_array().for_each([this](auto& value) {
+ JsonObject device = value.as_object();
+ DeviceInfo device_info;
+
+ device_info.major = device.get("major").to_uint();
+ device_info.minor = device.get("minor").to_uint();
+ device_info.class_name = device.get("class_name").to_string();
+
+ String type_str = device.get("type").to_string();
+ if (type_str == "block")
+ device_info.type = DeviceInfo::Type::Block;
+ else if (type_str == "character")
+ device_info.type = DeviceInfo::Type::Character;
+ else
+ ASSERT_NOT_REACHED();
+
+ m_devices.append(move(device_info));
+ });
+
+ auto fill_in_paths_from_dir = [this](const String& dir) {
+ Core::DirIterator dir_iter { dir, Core::DirIterator::Flags::SkipDots };
+ while (dir_iter.has_next()) {
+ auto name = dir_iter.next_path();
+ auto path = String::format("%s/%s", dir.characters(), name.characters());
+ struct stat statbuf;
+ if (lstat(path.characters(), &statbuf) != 0) {
+ ASSERT_NOT_REACHED();
+ }
+ if (!S_ISBLK(statbuf.st_mode) && !S_ISCHR(statbuf.st_mode))
+ continue;
+ unsigned _major = major(statbuf.st_rdev);
+ unsigned _minor = minor(statbuf.st_rdev);
+
+ auto it = m_devices.find_if([_major, _minor](const auto& device_info) {
+ return device_info.major == _major && device_info.minor == _minor;
+ });
+ if (it != m_devices.end()) {
+ (*it).path = move(path);
+ }
+ }
+ };
+
+ fill_in_paths_from_dir("/dev");
+ fill_in_paths_from_dir("/dev/pts");
+
+ did_update();
+}
diff --git a/Userland/Applications/SystemMonitor/DevicesModel.h b/Userland/Applications/SystemMonitor/DevicesModel.h
new file mode 100644
index 0000000000..dd8cacc7e0
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/DevicesModel.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Vector.h>
+#include <LibGUI/Model.h>
+
+class DevicesModel final : public GUI::Model {
+public:
+ enum Column {
+ Device = 0,
+ Major,
+ Minor,
+ ClassName,
+ Type,
+ __Count
+ };
+
+ virtual ~DevicesModel() override;
+ static NonnullRefPtr<DevicesModel> create();
+
+ virtual int row_count(const GUI::ModelIndex&) const override;
+ virtual int column_count(const GUI::ModelIndex&) const override;
+ virtual String column_name(int column) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+
+private:
+ DevicesModel();
+
+ struct DeviceInfo {
+ String path;
+ unsigned major;
+ unsigned minor;
+ String class_name;
+ enum Type {
+ Block,
+ Character
+ };
+ Type type;
+ };
+
+ Vector<DeviceInfo> m_devices;
+};
diff --git a/Userland/Applications/SystemMonitor/GraphWidget.cpp b/Userland/Applications/SystemMonitor/GraphWidget.cpp
new file mode 100644
index 0000000000..5c1d9982d4
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/GraphWidget.cpp
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "GraphWidget.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Path.h>
+
+GraphWidget::GraphWidget()
+{
+}
+
+GraphWidget::~GraphWidget()
+{
+}
+
+void GraphWidget::add_value(Vector<int, 1>&& value)
+{
+ m_values.enqueue(move(value));
+ update();
+}
+
+void GraphWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.add_clip_rect(frame_inner_rect());
+ painter.fill_rect(event.rect(), m_background_color);
+
+ auto inner_rect = frame_inner_rect();
+ float scale = (float)inner_rect.height() / (float)m_max;
+
+ if (!m_values.is_empty()) {
+ // Draw one set of values at a time
+ for (size_t k = 0; k < m_value_format.size(); k++) {
+ const auto& format = m_value_format[k];
+ if (format.line_color == Color::Transparent && format.background_color == Color::Transparent)
+ continue;
+ m_calculated_points.clear_with_capacity();
+ for (size_t i = 0; i < m_values.size(); i++) {
+ int x = inner_rect.right() - (i * 2) + 1;
+ if (x < 0)
+ break;
+ const auto& current_values = m_values.at(m_values.size() - i - 1);
+ if (current_values.size() <= k) {
+ // Don't have a data point
+ m_calculated_points.append({ -1, -1 });
+ continue;
+ }
+ float value = current_values[k];
+ if (m_stack_values) {
+ for (size_t l = k + 1; l < current_values.size(); l++)
+ value += current_values[l];
+ }
+ float scaled_value = value * scale;
+ Gfx::IntPoint current_point { x, inner_rect.bottom() - (int)scaled_value };
+ m_calculated_points.append(current_point);
+ }
+ ASSERT(m_calculated_points.size() <= m_values.size());
+ if (format.background_color != Color::Transparent) {
+ // Fill the background for the area we have values for
+ Gfx::Path path;
+ size_t points_in_path = 0;
+ bool started_path = false;
+ const Gfx::IntPoint* current_point = nullptr;
+ const Gfx::IntPoint* first_point = nullptr;
+ auto check_fill_area = [&]() {
+ if (!started_path)
+ return;
+ if (points_in_path > 1) {
+ ASSERT(current_point);
+ ASSERT(first_point);
+ path.line_to({ current_point->x() - 1, inner_rect.bottom() + 1 });
+ path.line_to({ first_point->x() + 1, inner_rect.bottom() + 1 });
+ path.close();
+ painter.fill_path(path, format.background_color, Gfx::Painter::WindingRule::EvenOdd);
+ } else if (points_in_path == 1 && current_point) {
+ // Can't fill any area, we only have one data point.
+ // Just draw a vertical line as a "fill"...
+ painter.draw_line(*current_point, { current_point->x(), inner_rect.bottom() }, format.background_color);
+ }
+ path = {};
+ points_in_path = 0;
+ first_point = nullptr;
+ started_path = false;
+ };
+ for (size_t i = 0; i < m_calculated_points.size(); i++) {
+ current_point = &m_calculated_points[i];
+ if (current_point->x() < 0) {
+ check_fill_area();
+ continue;
+ }
+ if (!started_path) {
+ path.move_to({ current_point->x() + 1, current_point->y() });
+ points_in_path = 1;
+ first_point = current_point;
+ started_path = true;
+ } else {
+ path.line_to({ current_point->x(), current_point->y() });
+ points_in_path++;
+ }
+ }
+ check_fill_area();
+ }
+ if (format.line_color != Color::Transparent) {
+ // Draw the line for the data points we have
+ const Gfx::IntPoint* previous_point = nullptr;
+ for (size_t i = 0; i < m_calculated_points.size(); i++) {
+ const auto& current_point = m_calculated_points[i];
+ if (current_point.x() < 0) {
+ previous_point = nullptr;
+ continue;
+ }
+ if (previous_point)
+ painter.draw_line(*previous_point, current_point, format.line_color);
+ previous_point = &current_point;
+ }
+ }
+ }
+ }
+
+ if (!m_values.is_empty() && !m_value_format.is_empty()) {
+ const auto& current_values = m_values.last();
+ int y = 0;
+ for (size_t i = 0; i < min(m_value_format.size(), current_values.size()); i++) {
+ const auto& format = m_value_format[i];
+ if (!format.text_formatter)
+ continue;
+ auto constrain_rect = inner_rect.shrunken(8, 8);
+ auto text_rect = constrain_rect.translated(0, y).intersected(constrain_rect);
+ text_rect.set_height(font().glyph_height());
+ auto text = format.text_formatter(current_values[i]);
+ if (format.text_shadow_color != Color::Transparent)
+ painter.draw_text(text_rect.translated(1, 1), text.characters(), Gfx::TextAlignment::CenterRight, format.text_shadow_color);
+ painter.draw_text(text_rect, text.characters(), Gfx::TextAlignment::CenterRight, format.line_color);
+ y += text_rect.height() + 4;
+ }
+ }
+}
diff --git a/Userland/Applications/SystemMonitor/GraphWidget.h b/Userland/Applications/SystemMonitor/GraphWidget.h
new file mode 100644
index 0000000000..fc40f59bbd
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/GraphWidget.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/CircularQueue.h>
+#include <LibGUI/Frame.h>
+
+class GraphWidget final : public GUI::Frame {
+ C_OBJECT(GraphWidget)
+public:
+ virtual ~GraphWidget() override;
+
+ void set_max(int max) { m_max = max; }
+ int max() const { return m_max; }
+
+ void add_value(Vector<int, 1>&&);
+
+ void set_background_color(Color color) { m_background_color = color; }
+
+ struct ValueFormat {
+ Color line_color { Color::Transparent };
+ Color background_color { Color::Transparent };
+ Color text_shadow_color { Color::Transparent };
+ Function<String(int)> text_formatter;
+ };
+ void set_value_format(size_t index, ValueFormat&& format)
+ {
+ if (m_value_format.size() <= index)
+ m_value_format.resize(index + 1);
+ m_value_format[index] = move(format);
+ }
+ void set_stack_values(bool stack_values) { m_stack_values = stack_values; }
+
+private:
+ explicit GraphWidget();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+
+ int m_max { 100 };
+ Vector<ValueFormat, 1> m_value_format;
+ CircularQueue<Vector<int, 1>, 4000> m_values;
+ Color m_background_color { Color::Black };
+ bool m_stack_values { false };
+
+ Vector<Gfx::IntPoint, 1> m_calculated_points;
+};
diff --git a/Userland/Applications/SystemMonitor/InterruptsWidget.cpp b/Userland/Applications/SystemMonitor/InterruptsWidget.cpp
new file mode 100644
index 0000000000..3e56f29ec4
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/InterruptsWidget.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "InterruptsWidget.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/TableView.h>
+
+InterruptsWidget::InterruptsWidget()
+{
+ on_first_show = [this](auto&) {
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+
+ Vector<GUI::JsonArrayModel::FieldSpec> interrupts_field;
+ interrupts_field.empend("interrupt_line", "Line", Gfx::TextAlignment::CenterRight);
+ interrupts_field.empend("purpose", "Purpose", Gfx::TextAlignment::CenterLeft);
+ interrupts_field.empend("controller", "Controller", Gfx::TextAlignment::CenterLeft);
+ interrupts_field.empend("cpu_handler", "CPU Handler", Gfx::TextAlignment::CenterRight);
+ interrupts_field.empend("device_sharing", "# Devices Sharing", Gfx::TextAlignment::CenterRight);
+ interrupts_field.empend("call_count", "Call Count", Gfx::TextAlignment::CenterRight);
+
+ m_interrupt_table_view = add<GUI::TableView>();
+ m_interrupt_model = GUI::JsonArrayModel::create("/proc/interrupts", move(interrupts_field));
+ m_interrupt_table_view->set_model(GUI::SortingProxyModel::create(*m_interrupt_model));
+
+ m_update_timer = add<Core::Timer>(
+ 1000, [this] {
+ update_model();
+ });
+
+ update_model();
+ };
+}
+
+InterruptsWidget::~InterruptsWidget()
+{
+}
+
+void InterruptsWidget::update_model()
+{
+ m_interrupt_table_view->model()->update();
+}
diff --git a/Userland/Applications/SystemMonitor/InterruptsWidget.h b/Userland/Applications/SystemMonitor/InterruptsWidget.h
new file mode 100644
index 0000000000..bc11215217
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/InterruptsWidget.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/Timer.h>
+#include <LibGUI/LazyWidget.h>
+
+class InterruptsWidget final : public GUI::LazyWidget {
+ C_OBJECT(InterruptsWidget)
+public:
+ virtual ~InterruptsWidget() override;
+
+private:
+ InterruptsWidget();
+ void update_model();
+
+ RefPtr<GUI::TableView> m_interrupt_table_view;
+ RefPtr<GUI::JsonArrayModel> m_interrupt_model;
+ RefPtr<Core::Timer> m_update_timer;
+};
diff --git a/Userland/Applications/SystemMonitor/MemoryStatsWidget.cpp b/Userland/Applications/SystemMonitor/MemoryStatsWidget.cpp
new file mode 100644
index 0000000000..6675ea8c4a
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/MemoryStatsWidget.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "MemoryStatsWidget.h"
+#include "GraphWidget.h"
+#include <AK/ByteBuffer.h>
+#include <AK/JsonObject.h>
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibGfx/StylePainter.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+static MemoryStatsWidget* s_the;
+
+MemoryStatsWidget* MemoryStatsWidget::the()
+{
+ return s_the;
+}
+
+MemoryStatsWidget::MemoryStatsWidget(GraphWidget& graph)
+ : m_graph(graph)
+{
+ ASSERT(!s_the);
+ s_the = this;
+
+ set_fixed_height(110);
+
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 0, 8, 0, 0 });
+ layout()->set_spacing(3);
+
+ auto build_widgets_for_label = [this](const String& description) -> RefPtr<GUI::Label> {
+ auto& container = add<GUI::Widget>();
+ container.set_layout<GUI::HorizontalBoxLayout>();
+ container.set_fixed_size(275, 12);
+ auto& description_label = container.add<GUI::Label>(description);
+ description_label.set_font(Gfx::FontDatabase::default_bold_font());
+ description_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ auto& label = container.add<GUI::Label>();
+ label.set_text_alignment(Gfx::TextAlignment::CenterRight);
+ return label;
+ };
+
+ m_user_physical_pages_label = build_widgets_for_label("Physical memory:");
+ m_user_physical_pages_committed_label = build_widgets_for_label("Committed memory:");
+ m_supervisor_physical_pages_label = build_widgets_for_label("Supervisor physical:");
+ m_kmalloc_space_label = build_widgets_for_label("Kernel heap:");
+ m_kmalloc_count_label = build_widgets_for_label("Calls kmalloc:");
+ m_kfree_count_label = build_widgets_for_label("Calls kfree:");
+ m_kmalloc_difference_label = build_widgets_for_label("Difference:");
+
+ refresh();
+}
+
+MemoryStatsWidget::~MemoryStatsWidget()
+{
+}
+
+static inline size_t page_count_to_kb(size_t kb)
+{
+ return (kb * 4096) / 1024;
+}
+
+static inline size_t bytes_to_kb(size_t bytes)
+{
+ return bytes / 1024;
+}
+
+void MemoryStatsWidget::refresh()
+{
+ auto proc_memstat = Core::File::construct("/proc/memstat");
+ if (!proc_memstat->open(Core::IODevice::OpenMode::ReadOnly))
+ ASSERT_NOT_REACHED();
+
+ auto file_contents = proc_memstat->read_all();
+ auto json_result = JsonValue::from_string(file_contents);
+ ASSERT(json_result.has_value());
+ auto json = json_result.value().as_object();
+
+ [[maybe_unused]] unsigned kmalloc_eternal_allocated = json.get("kmalloc_eternal_allocated").to_u32();
+ unsigned kmalloc_allocated = json.get("kmalloc_allocated").to_u32();
+ unsigned kmalloc_available = json.get("kmalloc_available").to_u32();
+ unsigned user_physical_allocated = json.get("user_physical_allocated").to_u32();
+ unsigned user_physical_available = json.get("user_physical_available").to_u32();
+ unsigned user_physical_committed = json.get("user_physical_committed").to_u32();
+ unsigned user_physical_uncommitted = json.get("user_physical_uncommitted").to_u32();
+ unsigned super_physical_alloc = json.get("super_physical_allocated").to_u32();
+ unsigned super_physical_free = json.get("super_physical_available").to_u32();
+ unsigned kmalloc_call_count = json.get("kmalloc_call_count").to_u32();
+ unsigned kfree_call_count = json.get("kfree_call_count").to_u32();
+
+ size_t kmalloc_bytes_total = kmalloc_allocated + kmalloc_available;
+ size_t user_physical_pages_total = user_physical_allocated + user_physical_available;
+ size_t supervisor_pages_total = super_physical_alloc + super_physical_free;
+
+ size_t physical_pages_total = user_physical_pages_total + supervisor_pages_total;
+ size_t physical_pages_in_use = user_physical_allocated + super_physical_alloc;
+ size_t total_userphysical_and_swappable_pages = user_physical_allocated + user_physical_committed + user_physical_uncommitted;
+
+ m_kmalloc_space_label->set_text(String::formatted("{}K/{}K", bytes_to_kb(kmalloc_allocated), bytes_to_kb(kmalloc_bytes_total)));
+ m_user_physical_pages_label->set_text(String::formatted("{}K/{}K", page_count_to_kb(physical_pages_in_use), page_count_to_kb(physical_pages_total)));
+ m_user_physical_pages_committed_label->set_text(String::formatted("{}K", page_count_to_kb(user_physical_committed)));
+ m_supervisor_physical_pages_label->set_text(String::formatted("{}K/{}K", page_count_to_kb(super_physical_alloc), page_count_to_kb(supervisor_pages_total)));
+ m_kmalloc_count_label->set_text(String::formatted("{}", kmalloc_call_count));
+ m_kfree_count_label->set_text(String::formatted("{}", kfree_call_count));
+ m_kmalloc_difference_label->set_text(String::formatted("{:+}", kmalloc_call_count - kfree_call_count));
+
+ m_graph.set_max(page_count_to_kb(total_userphysical_and_swappable_pages) + bytes_to_kb(kmalloc_bytes_total));
+ m_graph.add_value({ (int)page_count_to_kb(user_physical_committed), (int)page_count_to_kb(user_physical_allocated), (int)bytes_to_kb(kmalloc_bytes_total) });
+}
diff --git a/Userland/Applications/SystemMonitor/MemoryStatsWidget.h b/Userland/Applications/SystemMonitor/MemoryStatsWidget.h
new file mode 100644
index 0000000000..e915730257
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/MemoryStatsWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+class GraphWidget;
+
+class MemoryStatsWidget final : public GUI::Widget {
+ C_OBJECT(MemoryStatsWidget)
+public:
+ static MemoryStatsWidget* the();
+
+ virtual ~MemoryStatsWidget() override;
+
+ void refresh();
+
+private:
+ MemoryStatsWidget(GraphWidget& graph);
+
+ GraphWidget& m_graph;
+ RefPtr<GUI::Label> m_user_physical_pages_label;
+ RefPtr<GUI::Label> m_user_physical_pages_committed_label;
+ RefPtr<GUI::Label> m_supervisor_physical_pages_label;
+ RefPtr<GUI::Label> m_kmalloc_space_label;
+ RefPtr<GUI::Label> m_kmalloc_count_label;
+ RefPtr<GUI::Label> m_kfree_count_label;
+ RefPtr<GUI::Label> m_kmalloc_difference_label;
+};
diff --git a/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.cpp b/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.cpp
new file mode 100644
index 0000000000..7ee2450ee1
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "NetworkStatisticsWidget.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/TableView.h>
+
+NetworkStatisticsWidget::NetworkStatisticsWidget()
+{
+ on_first_show = [this](auto&) {
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+ set_fill_with_background_color(true);
+
+ auto& adapters_group_box = add<GUI::GroupBox>("Adapters");
+ adapters_group_box.set_layout<GUI::VerticalBoxLayout>();
+ adapters_group_box.layout()->set_margins({ 6, 16, 6, 6 });
+ adapters_group_box.set_fixed_height(120);
+
+ m_adapter_table_view = adapters_group_box.add<GUI::TableView>();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> net_adapters_fields;
+ net_adapters_fields.empend("name", "Name", Gfx::TextAlignment::CenterLeft);
+ net_adapters_fields.empend("class_name", "Class", Gfx::TextAlignment::CenterLeft);
+ net_adapters_fields.empend("mac_address", "MAC", Gfx::TextAlignment::CenterLeft);
+ net_adapters_fields.empend("ipv4_address", "IPv4", Gfx::TextAlignment::CenterLeft);
+ net_adapters_fields.empend("packets_in", "Pkt In", Gfx::TextAlignment::CenterRight);
+ net_adapters_fields.empend("packets_out", "Pkt Out", Gfx::TextAlignment::CenterRight);
+ net_adapters_fields.empend("bytes_in", "Bytes In", Gfx::TextAlignment::CenterRight);
+ net_adapters_fields.empend("bytes_out", "Bytes Out", Gfx::TextAlignment::CenterRight);
+ m_adapter_model = GUI::JsonArrayModel::create("/proc/net/adapters", move(net_adapters_fields));
+ m_adapter_table_view->set_model(GUI::SortingProxyModel::create(*m_adapter_model));
+
+ auto& sockets_group_box = add<GUI::GroupBox>("Sockets");
+ sockets_group_box.set_layout<GUI::VerticalBoxLayout>();
+ sockets_group_box.layout()->set_margins({ 6, 16, 6, 6 });
+
+ m_socket_table_view = sockets_group_box.add<GUI::TableView>();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> net_tcp_fields;
+ net_tcp_fields.empend("peer_address", "Peer", Gfx::TextAlignment::CenterLeft);
+ net_tcp_fields.empend("peer_port", "Port", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("local_address", "Local", Gfx::TextAlignment::CenterLeft);
+ net_tcp_fields.empend("local_port", "Port", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("state", "State", Gfx::TextAlignment::CenterLeft);
+ net_tcp_fields.empend("ack_number", "Ack#", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("sequence_number", "Seq#", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("packets_in", "Pkt In", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("packets_out", "Pkt Out", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("bytes_in", "Bytes In", Gfx::TextAlignment::CenterRight);
+ net_tcp_fields.empend("bytes_out", "Bytes Out", Gfx::TextAlignment::CenterRight);
+ m_socket_model = GUI::JsonArrayModel::create("/proc/net/tcp", move(net_tcp_fields));
+ m_socket_table_view->set_model(GUI::SortingProxyModel::create(*m_socket_model));
+
+ m_update_timer = add<Core::Timer>(
+ 1000, [this] {
+ update_models();
+ });
+
+ update_models();
+ };
+}
+
+NetworkStatisticsWidget::~NetworkStatisticsWidget()
+{
+}
+
+void NetworkStatisticsWidget::update_models()
+{
+ m_adapter_table_view->model()->update();
+ m_socket_table_view->model()->update();
+}
diff --git a/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.h b/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.h
new file mode 100644
index 0000000000..d49a9641c8
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/NetworkStatisticsWidget.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibCore/Timer.h>
+#include <LibGUI/LazyWidget.h>
+
+class NetworkStatisticsWidget final : public GUI::LazyWidget {
+ C_OBJECT(NetworkStatisticsWidget)
+public:
+ virtual ~NetworkStatisticsWidget() override;
+
+private:
+ NetworkStatisticsWidget();
+ void update_models();
+
+ RefPtr<GUI::TableView> m_adapter_table_view;
+ RefPtr<GUI::TableView> m_socket_table_view;
+ RefPtr<GUI::JsonArrayModel> m_adapter_model;
+ RefPtr<GUI::JsonArrayModel> m_socket_model;
+ RefPtr<Core::Timer> m_update_timer;
+};
diff --git a/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp b/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp
new file mode 100644
index 0000000000..48dfae098d
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProcessFileDescriptorMapWidget.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/TableView.h>
+
+ProcessFileDescriptorMapWidget::ProcessFileDescriptorMapWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+ m_table_view = add<GUI::TableView>();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> pid_fds_fields;
+ pid_fds_fields.empend("fd", "FD", Gfx::TextAlignment::CenterRight);
+ pid_fds_fields.empend("class", "Class", Gfx::TextAlignment::CenterLeft);
+ pid_fds_fields.empend("offset", "Offset", Gfx::TextAlignment::CenterRight);
+ pid_fds_fields.empend("absolute_path", "Path", Gfx::TextAlignment::CenterLeft);
+ pid_fds_fields.empend("Access", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ return object.get("seekable").to_bool() ? "Seekable" : "Sequential";
+ });
+ pid_fds_fields.empend("Blocking", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ return object.get("blocking").to_bool() ? "Blocking" : "Nonblocking";
+ });
+ pid_fds_fields.empend("On exec", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ return object.get("cloexec").to_bool() ? "Close" : "Keep";
+ });
+ pid_fds_fields.empend("Can read", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ return object.get("can_read").to_bool() ? "Yes" : "No";
+ });
+ pid_fds_fields.empend("Can write", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ return object.get("can_write").to_bool() ? "Yes" : "No";
+ });
+
+ m_model = GUI::JsonArrayModel::create({}, move(pid_fds_fields));
+ m_table_view->set_model(GUI::SortingProxyModel::create(*m_model));
+}
+
+ProcessFileDescriptorMapWidget::~ProcessFileDescriptorMapWidget()
+{
+}
+
+void ProcessFileDescriptorMapWidget::set_pid(pid_t pid)
+{
+ if (m_pid == pid)
+ return;
+ m_pid = pid;
+ m_model->set_json_path(String::formatted("/proc/{}/fds", m_pid));
+}
diff --git a/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h b/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h
new file mode 100644
index 0000000000..2c45577525
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+class ProcessFileDescriptorMapWidget final : public GUI::Widget {
+ C_OBJECT(ProcessFileDescriptorMapWidget);
+
+public:
+ virtual ~ProcessFileDescriptorMapWidget() override;
+
+ void set_pid(pid_t);
+
+private:
+ ProcessFileDescriptorMapWidget();
+
+ RefPtr<GUI::TableView> m_table_view;
+ RefPtr<GUI::JsonArrayModel> m_model;
+ pid_t m_pid { -1 };
+};
diff --git a/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp b/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp
new file mode 100644
index 0000000000..d36a7f57ab
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProcessMemoryMapWidget.h"
+#include <LibCore/Timer.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/TableView.h>
+#include <LibGfx/Palette.h>
+
+class PagemapPaintingDelegate final : public GUI::TableCellPaintingDelegate {
+public:
+ virtual ~PagemapPaintingDelegate() override { }
+
+ virtual void paint(GUI::Painter& painter, const Gfx::IntRect& a_rect, const Gfx::Palette&, const GUI::ModelIndex& index) override
+ {
+ auto rect = a_rect.shrunken(2, 2);
+ auto pagemap = index.data(GUI::ModelRole::Custom).to_string();
+
+ float scale_factor = (float)pagemap.length() / (float)rect.width();
+
+ for (int i = 0; i < rect.width(); ++i) {
+ int x = rect.x() + i;
+ char c = pagemap[(float)i * scale_factor];
+ Color color;
+ if (c == 'N') // Null (no page at all, typically an inode-backed page that hasn't been paged in.)
+ color = Color::White;
+ else if (c == 'Z') // Zero (globally shared zero page, typically an untouched anonymous page.)
+ color = Color::from_rgb(0xc0c0ff);
+ else if (c == 'P') // Physical (a resident page)
+ color = Color::Black;
+ else
+ ASSERT_NOT_REACHED();
+
+ painter.draw_line({ x, rect.top() }, { x, rect.bottom() }, color);
+ }
+
+ painter.draw_rect(rect, Color::Black);
+ }
+};
+
+ProcessMemoryMapWidget::ProcessMemoryMapWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+ m_table_view = add<GUI::TableView>();
+ Vector<GUI::JsonArrayModel::FieldSpec> pid_vm_fields;
+ pid_vm_fields.empend(
+ "Address", Gfx::TextAlignment::CenterLeft,
+ [](auto& object) { return String::formatted("{:#x}", object.get("address").to_u32()); },
+ [](auto& object) { return object.get("address").to_u32(); });
+ pid_vm_fields.empend("size", "Size", Gfx::TextAlignment::CenterRight);
+ pid_vm_fields.empend("amount_resident", "Resident", Gfx::TextAlignment::CenterRight);
+ pid_vm_fields.empend("amount_dirty", "Dirty", Gfx::TextAlignment::CenterRight);
+ pid_vm_fields.empend("Access", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ StringBuilder builder;
+ if (!object.get("user_accessible").to_bool())
+ builder.append('K');
+ if (object.get("readable").to_bool())
+ builder.append('R');
+ if (object.get("writable").to_bool())
+ builder.append('W');
+ if (object.get("executable").to_bool())
+ builder.append('X');
+ if (object.get("shared").to_bool())
+ builder.append('S');
+ if (object.get("stack").to_bool())
+ builder.append('T');
+ return builder.to_string();
+ });
+ pid_vm_fields.empend("vmobject", "VMObject type", Gfx::TextAlignment::CenterLeft);
+ pid_vm_fields.empend("Purgeable", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ if (object.get("volatile").to_bool())
+ return "Volatile";
+ return "Non-volatile";
+ });
+ pid_vm_fields.empend(
+ "Page map", Gfx::TextAlignment::CenterLeft,
+ [](auto&) {
+ return GUI::Variant();
+ },
+ [](auto&) {
+ return GUI::Variant(0);
+ },
+ [](const JsonObject& object) {
+ auto pagemap = object.get("pagemap").as_string_or({});
+ return pagemap;
+ });
+ pid_vm_fields.empend("cow_pages", "# CoW", Gfx::TextAlignment::CenterRight);
+ pid_vm_fields.empend("name", "Name", Gfx::TextAlignment::CenterLeft);
+ m_json_model = GUI::JsonArrayModel::create({}, move(pid_vm_fields));
+ m_table_view->set_model(GUI::SortingProxyModel::create(*m_json_model));
+
+ m_table_view->set_column_painting_delegate(7, make<PagemapPaintingDelegate>());
+
+ m_table_view->set_key_column_and_sort_order(0, GUI::SortOrder::Ascending);
+ m_timer = add<Core::Timer>(1000, [this] { refresh(); });
+}
+
+ProcessMemoryMapWidget::~ProcessMemoryMapWidget()
+{
+}
+
+void ProcessMemoryMapWidget::set_pid(pid_t pid)
+{
+ if (m_pid == pid)
+ return;
+ m_pid = pid;
+ m_json_model->set_json_path(String::formatted("/proc/{}/vm", pid));
+}
+
+void ProcessMemoryMapWidget::refresh()
+{
+ if (m_pid != -1)
+ m_json_model->update();
+}
diff --git a/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.h b/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.h
new file mode 100644
index 0000000000..71a554fe51
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessMemoryMapWidget.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+class ProcessMemoryMapWidget final : public GUI::Widget {
+ C_OBJECT(ProcessMemoryMapWidget);
+
+public:
+ virtual ~ProcessMemoryMapWidget() override;
+
+ void set_pid(pid_t);
+ void refresh();
+
+private:
+ ProcessMemoryMapWidget();
+ RefPtr<GUI::TableView> m_table_view;
+ RefPtr<GUI::JsonArrayModel> m_json_model;
+ pid_t m_pid { -1 };
+ RefPtr<Core::Timer> m_timer;
+};
diff --git a/Userland/Applications/SystemMonitor/ProcessModel.cpp b/Userland/Applications/SystemMonitor/ProcessModel.cpp
new file mode 100644
index 0000000000..c12a5503ab
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessModel.cpp
@@ -0,0 +1,454 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProcessModel.h"
+#include "GraphWidget.h"
+#include <AK/JsonArray.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <AK/SharedBuffer.h>
+#include <LibCore/File.h>
+#include <LibCore/ProcessStatisticsReader.h>
+#include <LibGUI/FileIconProvider.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+static ProcessModel* s_the;
+
+ProcessModel& ProcessModel::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+ProcessModel::ProcessModel()
+{
+ ASSERT(!s_the);
+ s_the = this;
+ m_generic_process_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/gear.png");
+ m_high_priority_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/highpriority.png");
+ m_low_priority_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/lowpriority.png");
+ m_normal_priority_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/normalpriority.png");
+
+ auto file = Core::File::construct("/proc/cpuinfo");
+ if (file->open(Core::IODevice::ReadOnly)) {
+ auto json = JsonValue::from_string({ file->read_all() });
+ auto cpuinfo_array = json.value().as_array();
+ cpuinfo_array.for_each([&](auto& value) {
+ auto& cpu_object = value.as_object();
+ auto cpu_id = cpu_object.get("processor").as_u32();
+ m_cpus.append(make<CpuInfo>(cpu_id));
+ });
+ }
+
+ if (m_cpus.is_empty())
+ m_cpus.append(make<CpuInfo>(0));
+}
+
+ProcessModel::~ProcessModel()
+{
+}
+
+int ProcessModel::row_count(const GUI::ModelIndex&) const
+{
+ return m_pids.size();
+}
+
+int ProcessModel::column_count(const GUI::ModelIndex&) const
+{
+ return Column::__Count;
+}
+
+String ProcessModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Icon:
+ return "";
+ case Column::PID:
+ return "PID";
+ case Column::TID:
+ return "TID";
+ case Column::PPID:
+ return "PPID";
+ case Column::PGID:
+ return "PGID";
+ case Column::SID:
+ return "SID";
+ case Column::State:
+ return "State";
+ case Column::User:
+ return "User";
+ case Column::Priority:
+ return "Pr";
+ case Column::EffectivePriority:
+ return "EPr";
+ case Column::Virtual:
+ return "Virtual";
+ case Column::Physical:
+ return "Physical";
+ case Column::DirtyPrivate:
+ return "DirtyP";
+ case Column::CleanInode:
+ return "CleanI";
+ case Column::PurgeableVolatile:
+ return "Purg:V";
+ case Column::PurgeableNonvolatile:
+ return "Purg:N";
+ case Column::CPU:
+ return "CPU";
+ case Column::Processor:
+ return "Processor";
+ case Column::Name:
+ return "Name";
+ case Column::Syscalls:
+ return "Syscalls";
+ case Column::InodeFaults:
+ return "F:Inode";
+ case Column::ZeroFaults:
+ return "F:Zero";
+ case Column::CowFaults:
+ return "F:CoW";
+ case Column::IPv4SocketReadBytes:
+ return "IPv4 In";
+ case Column::IPv4SocketWriteBytes:
+ return "IPv4 Out";
+ case Column::UnixSocketReadBytes:
+ return "Unix In";
+ case Column::UnixSocketWriteBytes:
+ return "Unix Out";
+ case Column::FileReadBytes:
+ return "File In";
+ case Column::FileWriteBytes:
+ return "File Out";
+ case Column::Pledge:
+ return "Pledge";
+ case Column::Veil:
+ return "Veil";
+ default:
+ ASSERT_NOT_REACHED();
+ }
+}
+
+static String pretty_byte_size(size_t size)
+{
+ return String::formatted("{}K", size / 1024);
+}
+
+GUI::Variant ProcessModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ ASSERT(is_valid(index));
+
+ if (role == GUI::ModelRole::TextAlignment) {
+ switch (index.column()) {
+ case Column::Icon:
+ case Column::Name:
+ case Column::State:
+ case Column::User:
+ case Column::Pledge:
+ case Column::Veil:
+ return Gfx::TextAlignment::CenterLeft;
+ case Column::PID:
+ case Column::TID:
+ case Column::PPID:
+ case Column::PGID:
+ case Column::SID:
+ case Column::Priority:
+ case Column::EffectivePriority:
+ case Column::Virtual:
+ case Column::Physical:
+ case Column::DirtyPrivate:
+ case Column::CleanInode:
+ case Column::PurgeableVolatile:
+ case Column::PurgeableNonvolatile:
+ case Column::CPU:
+ case Column::Processor:
+ case Column::Syscalls:
+ case Column::InodeFaults:
+ case Column::ZeroFaults:
+ case Column::CowFaults:
+ case Column::FileReadBytes:
+ case Column::FileWriteBytes:
+ case Column::UnixSocketReadBytes:
+ case Column::UnixSocketWriteBytes:
+ case Column::IPv4SocketReadBytes:
+ case Column::IPv4SocketWriteBytes:
+ return Gfx::TextAlignment::CenterRight;
+ default:
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ auto it = m_threads.find(m_pids[index.row()]);
+ auto& thread = *(*it).value;
+
+ if (role == GUI::ModelRole::Sort) {
+ switch (index.column()) {
+ case Column::Icon:
+ return 0;
+ case Column::PID:
+ return thread.current_state.pid;
+ case Column::TID:
+ return thread.current_state.tid;
+ case Column::PPID:
+ return thread.current_state.ppid;
+ case Column::PGID:
+ return thread.current_state.pgid;
+ case Column::SID:
+ return thread.current_state.sid;
+ case Column::State:
+ return thread.current_state.state;
+ case Column::User:
+ return thread.current_state.user;
+ case Column::Priority:
+ return thread.current_state.priority;
+ case Column::EffectivePriority:
+ return thread.current_state.effective_priority;
+ case Column::Virtual:
+ return (int)thread.current_state.amount_virtual;
+ case Column::Physical:
+ return (int)thread.current_state.amount_resident;
+ case Column::DirtyPrivate:
+ return (int)thread.current_state.amount_dirty_private;
+ case Column::CleanInode:
+ return (int)thread.current_state.amount_clean_inode;
+ case Column::PurgeableVolatile:
+ return (int)thread.current_state.amount_purgeable_volatile;
+ case Column::PurgeableNonvolatile:
+ return (int)thread.current_state.amount_purgeable_nonvolatile;
+ case Column::CPU:
+ return thread.current_state.cpu_percent;
+ case Column::Processor:
+ return thread.current_state.cpu;
+ case Column::Name:
+ return thread.current_state.name;
+ case Column::Syscalls:
+ return thread.current_state.syscall_count;
+ case Column::InodeFaults:
+ return thread.current_state.inode_faults;
+ case Column::ZeroFaults:
+ return thread.current_state.zero_faults;
+ case Column::CowFaults:
+ return thread.current_state.cow_faults;
+ case Column::IPv4SocketReadBytes:
+ return thread.current_state.ipv4_socket_read_bytes;
+ case Column::IPv4SocketWriteBytes:
+ return thread.current_state.ipv4_socket_write_bytes;
+ case Column::UnixSocketReadBytes:
+ return thread.current_state.unix_socket_read_bytes;
+ case Column::UnixSocketWriteBytes:
+ return thread.current_state.unix_socket_write_bytes;
+ case Column::FileReadBytes:
+ return thread.current_state.file_read_bytes;
+ case Column::FileWriteBytes:
+ return thread.current_state.file_write_bytes;
+ case Column::Pledge:
+ return thread.current_state.pledge;
+ case Column::Veil:
+ return thread.current_state.veil;
+ }
+ ASSERT_NOT_REACHED();
+ return {};
+ }
+
+ if (role == GUI::ModelRole::Display) {
+ switch (index.column()) {
+ case Column::Icon: {
+ auto icon = GUI::FileIconProvider::icon_for_executable(thread.current_state.executable);
+ if (auto* bitmap = icon.bitmap_for_size(16))
+ return *bitmap;
+ return *m_generic_process_icon;
+ }
+ case Column::PID:
+ return thread.current_state.pid;
+ case Column::TID:
+ return thread.current_state.tid;
+ case Column::PPID:
+ return thread.current_state.ppid;
+ case Column::PGID:
+ return thread.current_state.pgid;
+ case Column::SID:
+ return thread.current_state.sid;
+ case Column::State:
+ return thread.current_state.state;
+ case Column::User:
+ return thread.current_state.user;
+ case Column::Priority:
+ return thread.current_state.priority;
+ case Column::EffectivePriority:
+ return thread.current_state.effective_priority;
+ case Column::Virtual:
+ return pretty_byte_size(thread.current_state.amount_virtual);
+ case Column::Physical:
+ return pretty_byte_size(thread.current_state.amount_resident);
+ case Column::DirtyPrivate:
+ return pretty_byte_size(thread.current_state.amount_dirty_private);
+ case Column::CleanInode:
+ return pretty_byte_size(thread.current_state.amount_clean_inode);
+ case Column::PurgeableVolatile:
+ return pretty_byte_size(thread.current_state.amount_purgeable_volatile);
+ case Column::PurgeableNonvolatile:
+ return pretty_byte_size(thread.current_state.amount_purgeable_nonvolatile);
+ case Column::CPU:
+ return thread.current_state.cpu_percent;
+ case Column::Processor:
+ return thread.current_state.cpu;
+ case Column::Name:
+ return thread.current_state.name;
+ case Column::Syscalls:
+ return thread.current_state.syscall_count;
+ case Column::InodeFaults:
+ return thread.current_state.inode_faults;
+ case Column::ZeroFaults:
+ return thread.current_state.zero_faults;
+ case Column::CowFaults:
+ return thread.current_state.cow_faults;
+ case Column::IPv4SocketReadBytes:
+ return thread.current_state.ipv4_socket_read_bytes;
+ case Column::IPv4SocketWriteBytes:
+ return thread.current_state.ipv4_socket_write_bytes;
+ case Column::UnixSocketReadBytes:
+ return thread.current_state.unix_socket_read_bytes;
+ case Column::UnixSocketWriteBytes:
+ return thread.current_state.unix_socket_write_bytes;
+ case Column::FileReadBytes:
+ return thread.current_state.file_read_bytes;
+ case Column::FileWriteBytes:
+ return thread.current_state.file_write_bytes;
+ case Column::Pledge:
+ return thread.current_state.pledge;
+ case Column::Veil:
+ return thread.current_state.veil;
+ }
+ }
+
+ return {};
+}
+
+void ProcessModel::update()
+{
+ auto previous_pid_count = m_pids.size();
+ auto all_processes = Core::ProcessStatisticsReader::get_all(m_proc_all);
+
+ u64 last_sum_ticks_scheduled = 0, last_sum_ticks_scheduled_kernel = 0;
+ for (auto& it : m_threads) {
+ auto& current_state = it.value->current_state;
+ last_sum_ticks_scheduled += current_state.ticks_user + current_state.ticks_kernel;
+ last_sum_ticks_scheduled_kernel += current_state.ticks_kernel;
+ }
+
+ HashTable<PidAndTid> live_pids;
+ u64 sum_ticks_scheduled = 0, sum_ticks_scheduled_kernel = 0;
+ if (all_processes.has_value()) {
+ for (auto& it : all_processes.value()) {
+ for (auto& thread : it.value.threads) {
+ ThreadState state;
+ state.pid = it.value.pid;
+ state.user = it.value.username;
+ state.pledge = it.value.pledge;
+ state.veil = it.value.veil;
+ state.syscall_count = thread.syscall_count;
+ state.inode_faults = thread.inode_faults;
+ state.zero_faults = thread.zero_faults;
+ state.cow_faults = thread.cow_faults;
+ state.unix_socket_read_bytes = thread.unix_socket_read_bytes;
+ state.unix_socket_write_bytes = thread.unix_socket_write_bytes;
+ state.ipv4_socket_read_bytes = thread.ipv4_socket_read_bytes;
+ state.ipv4_socket_write_bytes = thread.ipv4_socket_write_bytes;
+ state.file_read_bytes = thread.file_read_bytes;
+ state.file_write_bytes = thread.file_write_bytes;
+ state.amount_virtual = it.value.amount_virtual;
+ state.amount_resident = it.value.amount_resident;
+ state.amount_dirty_private = it.value.amount_dirty_private;
+ state.amount_clean_inode = it.value.amount_clean_inode;
+ state.amount_purgeable_volatile = it.value.amount_purgeable_volatile;
+ state.amount_purgeable_nonvolatile = it.value.amount_purgeable_nonvolatile;
+
+ state.name = thread.name;
+ state.executable = it.value.executable;
+
+ state.ppid = it.value.ppid;
+ state.tid = thread.tid;
+ state.pgid = it.value.pgid;
+ state.sid = it.value.sid;
+ state.times_scheduled = thread.times_scheduled;
+ state.ticks_user = thread.ticks_user;
+ state.ticks_kernel = thread.ticks_kernel;
+ state.cpu = thread.cpu;
+ state.cpu_percent = 0;
+ state.priority = thread.priority;
+ state.effective_priority = thread.effective_priority;
+ state.state = thread.state;
+ sum_ticks_scheduled += thread.ticks_user + thread.ticks_kernel;
+ sum_ticks_scheduled_kernel += thread.ticks_kernel;
+ {
+ auto pit = m_threads.find({ it.value.pid, thread.tid });
+ if (pit == m_threads.end())
+ m_threads.set({ it.value.pid, thread.tid }, make<Thread>());
+ }
+ auto pit = m_threads.find({ it.value.pid, thread.tid });
+ ASSERT(pit != m_threads.end());
+ (*pit).value->previous_state = (*pit).value->current_state;
+ (*pit).value->current_state = state;
+
+ live_pids.set({ it.value.pid, thread.tid });
+ }
+ }
+ }
+
+ m_pids.clear();
+ for (auto& c : m_cpus) {
+ c.total_cpu_percent = 0.0;
+ c.total_cpu_percent_kernel = 0.0;
+ }
+ Vector<PidAndTid, 16> pids_to_remove;
+ for (auto& it : m_threads) {
+ if (!live_pids.contains(it.key)) {
+ pids_to_remove.append(it.key);
+ continue;
+ }
+ auto& process = *it.value;
+ u32 ticks_scheduled_diff = (process.current_state.ticks_user + process.current_state.ticks_kernel)
+ - (process.previous_state.ticks_user + process.previous_state.ticks_kernel);
+ u32 ticks_scheduled_diff_kernel = process.current_state.ticks_kernel - process.previous_state.ticks_kernel;
+ process.current_state.cpu_percent = ((float)ticks_scheduled_diff * 100) / (float)(sum_ticks_scheduled - last_sum_ticks_scheduled);
+ process.current_state.cpu_percent_kernel = ((float)ticks_scheduled_diff_kernel * 100) / (float)(sum_ticks_scheduled - last_sum_ticks_scheduled);
+ if (it.key.pid != 0) {
+ auto& cpu_info = m_cpus[process.current_state.cpu];
+ cpu_info.total_cpu_percent += process.current_state.cpu_percent;
+ cpu_info.total_cpu_percent_kernel += process.current_state.cpu_percent_kernel;
+ m_pids.append(it.key);
+ }
+ }
+ for (auto pid : pids_to_remove)
+ m_threads.remove(pid);
+
+ if (on_cpu_info_change)
+ on_cpu_info_change(m_cpus);
+
+ // FIXME: This is a rather hackish way of invalidating indexes.
+ // It would be good if GUI::Model had a way to orchestrate removal/insertion while preserving indexes.
+ did_update(previous_pid_count == m_pids.size() ? GUI::Model::UpdateFlag::DontInvalidateIndexes : GUI::Model::UpdateFlag::InvalidateAllIndexes);
+}
diff --git a/Userland/Applications/SystemMonitor/ProcessModel.h b/Userland/Applications/SystemMonitor/ProcessModel.h
new file mode 100644
index 0000000000..11f39cac76
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessModel.h
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashMap.h>
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/String.h>
+#include <AK/Vector.h>
+#include <LibGUI/Model.h>
+#include <unistd.h>
+
+class GraphWidget;
+
+struct PidAndTid {
+ bool operator==(const PidAndTid& other) const
+ {
+ return pid == other.pid && tid == other.tid;
+ }
+ pid_t pid;
+ int tid;
+};
+
+class ProcessModel final : public GUI::Model {
+public:
+ enum Column {
+ Icon = 0,
+ Name,
+ CPU,
+ Processor,
+ State,
+ Priority,
+ EffectivePriority,
+ User,
+ PID,
+ TID,
+ PPID,
+ PGID,
+ SID,
+ Virtual,
+ Physical,
+ DirtyPrivate,
+ CleanInode,
+ PurgeableVolatile,
+ PurgeableNonvolatile,
+ Veil,
+ Pledge,
+ Syscalls,
+ InodeFaults,
+ ZeroFaults,
+ CowFaults,
+ FileReadBytes,
+ FileWriteBytes,
+ UnixSocketReadBytes,
+ UnixSocketWriteBytes,
+ IPv4SocketReadBytes,
+ IPv4SocketWriteBytes,
+ __Count
+ };
+
+ static ProcessModel& the();
+
+ static NonnullRefPtr<ProcessModel> create() { return adopt(*new ProcessModel); }
+ virtual ~ProcessModel() override;
+
+ virtual int row_count(const GUI::ModelIndex&) const override;
+ virtual int column_count(const GUI::ModelIndex&) const override;
+ virtual String column_name(int column) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+
+ struct CpuInfo {
+ u32 id;
+ float total_cpu_percent { 0.0 };
+ float total_cpu_percent_kernel { 0.0 };
+
+ CpuInfo(u32 id)
+ : id(id)
+ {
+ }
+ };
+
+ Function<void(const NonnullOwnPtrVector<CpuInfo>&)> on_cpu_info_change;
+
+ const NonnullOwnPtrVector<CpuInfo>& cpus() const { return m_cpus; }
+
+private:
+ ProcessModel();
+
+ struct ThreadState {
+ pid_t tid;
+ pid_t pid;
+ pid_t ppid;
+ pid_t pgid;
+ pid_t sid;
+ unsigned times_scheduled;
+ unsigned ticks_user;
+ unsigned ticks_kernel;
+ String executable;
+ String name;
+ String state;
+ String user;
+ String pledge;
+ String veil;
+ u32 cpu;
+ u32 priority;
+ u32 effective_priority;
+ size_t amount_virtual;
+ size_t amount_resident;
+ size_t amount_dirty_private;
+ size_t amount_clean_inode;
+ size_t amount_purgeable_volatile;
+ size_t amount_purgeable_nonvolatile;
+ unsigned syscall_count;
+ unsigned inode_faults;
+ unsigned zero_faults;
+ unsigned cow_faults;
+ unsigned unix_socket_read_bytes;
+ unsigned unix_socket_write_bytes;
+ unsigned ipv4_socket_read_bytes;
+ unsigned ipv4_socket_write_bytes;
+ unsigned file_read_bytes;
+ unsigned file_write_bytes;
+ float cpu_percent;
+ float cpu_percent_kernel;
+ };
+
+ struct Thread {
+ ThreadState current_state;
+ ThreadState previous_state;
+ };
+
+ HashMap<uid_t, String> m_usernames;
+ HashMap<PidAndTid, NonnullOwnPtr<Thread>> m_threads;
+ NonnullOwnPtrVector<CpuInfo> m_cpus;
+ Vector<PidAndTid> m_pids;
+ RefPtr<Gfx::Bitmap> m_generic_process_icon;
+ RefPtr<Gfx::Bitmap> m_high_priority_icon;
+ RefPtr<Gfx::Bitmap> m_low_priority_icon;
+ RefPtr<Gfx::Bitmap> m_normal_priority_icon;
+ RefPtr<Core::File> m_proc_all;
+};
+
+namespace AK {
+template<>
+struct Traits<PidAndTid> : public GenericTraits<PidAndTid> {
+ static unsigned hash(const PidAndTid& value) { return pair_int_hash(value.pid, value.tid); }
+};
+}
diff --git a/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.cpp b/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.cpp
new file mode 100644
index 0000000000..9e2d22443c
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProcessUnveiledPathsWidget.h"
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/TableView.h>
+
+ProcessUnveiledPathsWidget::ProcessUnveiledPathsWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+ m_table_view = add<GUI::TableView>();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> pid_unveil_fields;
+ pid_unveil_fields.empend("path", "Path", Gfx::TextAlignment::CenterLeft);
+ pid_unveil_fields.empend("permissions", "Permissions", Gfx::TextAlignment::CenterLeft);
+
+ m_model = GUI::JsonArrayModel::create({}, move(pid_unveil_fields));
+ m_table_view->set_model(GUI::SortingProxyModel::create(*m_model));
+}
+
+ProcessUnveiledPathsWidget::~ProcessUnveiledPathsWidget()
+{
+}
+
+void ProcessUnveiledPathsWidget::set_pid(pid_t pid)
+{
+ if (m_pid == pid)
+ return;
+ m_pid = pid;
+ m_model->set_json_path(String::formatted("/proc/{}/unveil", m_pid));
+}
diff --git a/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.h b/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.h
new file mode 100644
index 0000000000..d1eac898b2
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ProcessUnveiledPathsWidget.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Widget.h>
+
+class ProcessUnveiledPathsWidget final : public GUI::Widget {
+ C_OBJECT(ProcessUnveiledPathsWidget);
+
+public:
+ virtual ~ProcessUnveiledPathsWidget() override;
+
+ void set_pid(pid_t);
+
+private:
+ ProcessUnveiledPathsWidget();
+
+ RefPtr<GUI::TableView> m_table_view;
+ RefPtr<GUI::JsonArrayModel> m_model;
+ pid_t m_pid { -1 };
+};
diff --git a/Userland/Applications/SystemMonitor/ThreadStackWidget.cpp b/Userland/Applications/SystemMonitor/ThreadStackWidget.cpp
new file mode 100644
index 0000000000..01e37791a0
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ThreadStackWidget.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ThreadStackWidget.h"
+#include <AK/ByteBuffer.h>
+#include <LibCore/File.h>
+#include <LibCore/Timer.h>
+#include <LibGUI/BoxLayout.h>
+
+ThreadStackWidget::ThreadStackWidget()
+{
+ set_layout<GUI::VerticalBoxLayout>();
+ layout()->set_margins({ 4, 4, 4, 4 });
+ m_stack_editor = add<GUI::TextEditor>();
+ m_stack_editor->set_mode(GUI::TextEditor::ReadOnly);
+
+ m_timer = add<Core::Timer>(1000, [this] { refresh(); });
+}
+
+ThreadStackWidget::~ThreadStackWidget()
+{
+}
+
+void ThreadStackWidget::set_ids(pid_t pid, pid_t tid)
+{
+ if (m_pid == pid && m_tid == tid)
+ return;
+ m_pid = pid;
+ m_tid = tid;
+ refresh();
+}
+
+void ThreadStackWidget::refresh()
+{
+ auto file = Core::File::construct(String::formatted("/proc/{}/stacks/{}", m_pid, m_tid));
+ if (!file->open(Core::IODevice::ReadOnly)) {
+ m_stack_editor->set_text(String::formatted("Unable to open {}", file->filename()));
+ return;
+ }
+
+ auto new_text = file->read_all();
+ if (m_stack_editor->text() != new_text) {
+ m_stack_editor->set_text(new_text);
+ }
+}
diff --git a/Userland/Applications/SystemMonitor/ThreadStackWidget.h b/Userland/Applications/SystemMonitor/ThreadStackWidget.h
new file mode 100644
index 0000000000..5a10e93b0a
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/ThreadStackWidget.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Widget.h>
+
+class ThreadStackWidget final : public GUI::Widget {
+ C_OBJECT(ThreadStackWidget)
+public:
+ virtual ~ThreadStackWidget() override;
+
+ void set_ids(pid_t pid, pid_t tid);
+ void refresh();
+
+private:
+ ThreadStackWidget();
+
+ pid_t m_pid { -1 };
+ pid_t m_tid { -1 };
+ RefPtr<GUI::TextEditor> m_stack_editor;
+ RefPtr<Core::Timer> m_timer;
+};
diff --git a/Userland/Applications/SystemMonitor/main.cpp b/Userland/Applications/SystemMonitor/main.cpp
new file mode 100644
index 0000000000..bdf88e44d1
--- /dev/null
+++ b/Userland/Applications/SystemMonitor/main.cpp
@@ -0,0 +1,670 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DevicesModel.h"
+#include "GraphWidget.h"
+#include "InterruptsWidget.h"
+#include "MemoryStatsWidget.h"
+#include "NetworkStatisticsWidget.h"
+#include "ProcessFileDescriptorMapWidget.h"
+#include "ProcessMemoryMapWidget.h"
+#include "ProcessModel.h"
+#include "ProcessUnveiledPathsWidget.h"
+#include "ThreadStackWidget.h"
+#include <AK/NumberFormat.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/Timer.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/JsonArrayModel.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/LazyWidget.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/SortingProxyModel.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/TabWidget.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Palette.h>
+#include <LibPCIDB/Database.h>
+#include <serenity.h>
+#include <signal.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static NonnullRefPtr<GUI::Widget> build_file_systems_tab();
+static NonnullRefPtr<GUI::Widget> build_pci_devices_tab();
+static NonnullRefPtr<GUI::Widget> build_devices_tab();
+static NonnullRefPtr<GUI::Widget> build_graphs_tab();
+static NonnullRefPtr<GUI::Widget> build_processors_tab();
+
+class UnavailableProcessWidget final : public GUI::Frame {
+ C_OBJECT(UnavailableProcessWidget)
+public:
+ virtual ~UnavailableProcessWidget() override { }
+
+ const String& text() const { return m_text; }
+ void set_text(String text) { m_text = move(text); }
+
+private:
+ UnavailableProcessWidget(String text)
+ : m_text(move(text))
+ {
+ }
+
+ virtual void paint_event(GUI::PaintEvent& event) override
+ {
+ Frame::paint_event(event);
+ if (text().is_empty())
+ return;
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.draw_text(frame_inner_rect(), text(), Gfx::TextAlignment::Center, palette().window_text(), Gfx::TextElision::Right);
+ }
+
+ String m_text;
+};
+
+static bool can_access_pid(pid_t pid)
+{
+ auto path = String::formatted("/proc/{}", pid);
+ return access(path.characters(), X_OK) == 0;
+}
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio proc shared_buffer accept rpath exec unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio proc shared_buffer accept rpath exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/etc/passwd", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/proc", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/dev", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/Profiler", "rx") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/Inspector", "rx") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ const char* args_tab = "processes";
+ Core::ArgsParser parser;
+ parser.add_option(args_tab, "Tab, one of 'processes', 'graphs', 'fs', 'pci', 'devices', 'network', 'processors' or 'interrupts'", "open-tab", 't', "tab");
+ parser.parse(argc, argv);
+ StringView args_tab_view = args_tab;
+
+ auto app_icon = GUI::Icon::default_icon("app-system-monitor");
+
+ auto window = GUI::Window::construct();
+ window->set_title("System Monitor");
+ window->resize(680, 400);
+
+ auto& keeper = window->set_main_widget<GUI::Widget>();
+ keeper.set_layout<GUI::VerticalBoxLayout>();
+ keeper.set_fill_with_background_color(true);
+ keeper.layout()->set_margins({ 2, 2, 2, 2 });
+
+ auto& tabwidget = keeper.add<GUI::TabWidget>();
+
+ auto process_container_splitter = GUI::VerticalSplitter::construct();
+ tabwidget.add_widget("Processes", process_container_splitter);
+ process_container_splitter->layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& process_table_container = process_container_splitter->add<GUI::Widget>();
+
+ auto graphs_widget = build_graphs_tab();
+ tabwidget.add_widget("Graphs", graphs_widget);
+
+ auto file_systems_widget = build_file_systems_tab();
+ tabwidget.add_widget("File systems", file_systems_widget);
+
+ auto pci_devices_widget = build_pci_devices_tab();
+ tabwidget.add_widget("PCI devices", pci_devices_widget);
+
+ auto devices_widget = build_devices_tab();
+ tabwidget.add_widget("Devices", devices_widget);
+
+ auto network_stats_widget = NetworkStatisticsWidget::construct();
+ tabwidget.add_widget("Network", network_stats_widget);
+
+ auto processors_widget = build_processors_tab();
+ tabwidget.add_widget("Processors", processors_widget);
+
+ auto interrupts_widget = InterruptsWidget::construct();
+ tabwidget.add_widget("Interrupts", interrupts_widget);
+
+ process_table_container.set_layout<GUI::VerticalBoxLayout>();
+ process_table_container.layout()->set_spacing(0);
+
+ auto& process_table_view = process_table_container.add<GUI::TableView>();
+ process_table_view.set_column_headers_visible(true);
+ process_table_view.set_model(GUI::SortingProxyModel::create(ProcessModel::create()));
+ process_table_view.set_key_column_and_sort_order(ProcessModel::Column::CPU, GUI::SortOrder::Descending);
+ process_table_view.model()->update();
+
+ auto& refresh_timer = window->add<Core::Timer>(
+ 3000, [&] {
+ process_table_view.model()->update();
+ if (auto* memory_stats_widget = MemoryStatsWidget::the())
+ memory_stats_widget->refresh();
+ });
+
+ auto selected_id = [&](ProcessModel::Column column) -> pid_t {
+ if (process_table_view.selection().is_empty())
+ return -1;
+ auto pid_index = process_table_view.model()->index(process_table_view.selection().first().row(), column);
+ return pid_index.data().to_i32();
+ };
+
+ auto kill_action = GUI::Action::create("Kill process", { Mod_Ctrl, Key_K }, Gfx::Bitmap::load_from_file("/res/icons/16x16/kill.png"), [&](const GUI::Action&) {
+ pid_t pid = selected_id(ProcessModel::Column::PID);
+ if (pid != -1)
+ kill(pid, SIGKILL);
+ });
+
+ auto stop_action = GUI::Action::create("Stop process", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/stop-hand.png"), [&](const GUI::Action&) {
+ pid_t pid = selected_id(ProcessModel::Column::PID);
+ if (pid != -1)
+ kill(pid, SIGSTOP);
+ });
+
+ auto continue_action = GUI::Action::create("Continue process", { Mod_Ctrl, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/continue.png"), [&](const GUI::Action&) {
+ pid_t pid = selected_id(ProcessModel::Column::PID);
+ if (pid != -1)
+ kill(pid, SIGCONT);
+ });
+
+ auto profile_action = GUI::Action::create("Profile process", { Mod_Ctrl, Key_P },
+ Gfx::Bitmap::load_from_file("/res/icons/16x16/app-profiler.png"), [&](auto&) {
+ pid_t pid = selected_id(ProcessModel::Column::PID);
+ if (pid != -1) {
+ auto pid_string = String::format("%d", pid);
+ pid_t child;
+ const char* argv[] = { "/bin/Profiler", "--pid", pid_string.characters(), nullptr };
+ if ((errno = posix_spawn(&child, "/bin/Profiler", nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ } else {
+ if (disown(child) < 0)
+ perror("disown");
+ }
+ }
+ });
+
+ auto inspect_action = GUI::Action::create("Inspect process", { Mod_Ctrl, Key_I },
+ Gfx::Bitmap::load_from_file("/res/icons/16x16/app-inspector.png"), [&](auto&) {
+ pid_t pid = selected_id(ProcessModel::Column::PID);
+ if (pid != -1) {
+ auto pid_string = String::format("%d", pid);
+ pid_t child;
+ const char* argv[] = { "/bin/Inspector", pid_string.characters(), nullptr };
+ if ((errno = posix_spawn(&child, "/bin/Inspector", nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ } else {
+ if (disown(child) < 0)
+ perror("disown");
+ }
+ }
+ });
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("System Monitor");
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ GUI::Application::the()->quit();
+ return;
+ }));
+
+ auto& process_menu = menubar->add_menu("Process");
+ process_menu.add_action(kill_action);
+ process_menu.add_action(stop_action);
+ process_menu.add_action(continue_action);
+ process_menu.add_separator();
+ process_menu.add_action(profile_action);
+ process_menu.add_action(inspect_action);
+
+ auto process_context_menu = GUI::Menu::construct();
+ process_context_menu->add_action(kill_action);
+ process_context_menu->add_action(stop_action);
+ process_context_menu->add_action(continue_action);
+ process_context_menu->add_separator();
+ process_context_menu->add_action(profile_action);
+ process_context_menu->add_action(inspect_action);
+ process_table_view.on_context_menu_request = [&]([[maybe_unused]] const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
+ process_context_menu->popup(event.screen_position());
+ };
+
+ auto& frequency_menu = menubar->add_menu("Frequency");
+ GUI::ActionGroup frequency_action_group;
+ frequency_action_group.set_exclusive(true);
+
+ auto make_frequency_action = [&](auto& title, int interval, bool checked = false) {
+ auto action = GUI::Action::create_checkable(title, [&refresh_timer, interval](auto&) {
+ refresh_timer.restart(interval);
+ });
+ action->set_checked(checked);
+ frequency_action_group.add_action(*action);
+ frequency_menu.add_action(*action);
+ };
+
+ make_frequency_action("1 sec", 1000);
+ make_frequency_action("3 sec", 3000, true);
+ make_frequency_action("5 sec", 5000);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("System Monitor", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ auto& process_tab_unused_widget = process_container_splitter->add<UnavailableProcessWidget>("No process selected");
+ process_tab_unused_widget.set_visible(true);
+
+ auto& process_tab_widget = process_container_splitter->add<GUI::TabWidget>();
+ process_tab_widget.set_tab_position(GUI::TabWidget::TabPosition::Bottom);
+ process_tab_widget.set_visible(false);
+
+ auto& memory_map_widget = process_tab_widget.add_tab<ProcessMemoryMapWidget>("Memory map");
+ auto& open_files_widget = process_tab_widget.add_tab<ProcessFileDescriptorMapWidget>("Open files");
+ auto& unveiled_paths_widget = process_tab_widget.add_tab<ProcessUnveiledPathsWidget>("Unveiled paths");
+ auto& stack_widget = process_tab_widget.add_tab<ThreadStackWidget>("Stack");
+
+ process_table_view.on_selection = [&](auto&) {
+ auto pid = selected_id(ProcessModel::Column::PID);
+ auto tid = selected_id(ProcessModel::Column::TID);
+ if (!can_access_pid(pid)) {
+ process_tab_widget.set_visible(false);
+ process_tab_unused_widget.set_text("Process cannot be accessed");
+ process_tab_unused_widget.set_visible(true);
+ return;
+ }
+
+ process_tab_widget.set_visible(true);
+ process_tab_unused_widget.set_visible(false);
+ open_files_widget.set_pid(pid);
+ stack_widget.set_ids(pid, tid);
+ memory_map_widget.set_pid(pid);
+ unveiled_paths_widget.set_pid(pid);
+ };
+
+ window->show();
+
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ if (args_tab_view == "processes")
+ tabwidget.set_active_widget(process_container_splitter);
+ else if (args_tab_view == "graphs")
+ tabwidget.set_active_widget(graphs_widget);
+ else if (args_tab_view == "fs")
+ tabwidget.set_active_widget(file_systems_widget);
+ else if (args_tab_view == "pci")
+ tabwidget.set_active_widget(pci_devices_widget);
+ else if (args_tab_view == "devices")
+ tabwidget.set_active_widget(devices_widget);
+ else if (args_tab_view == "network")
+ tabwidget.set_active_widget(network_stats_widget);
+ else if (args_tab_view == "processors")
+ tabwidget.set_active_widget(processors_widget);
+ else if (args_tab_view == "interrupts")
+ tabwidget.set_active_widget(interrupts_widget);
+
+ return app->exec();
+}
+
+class ProgressBarPaintingDelegate final : public GUI::TableCellPaintingDelegate {
+public:
+ virtual ~ProgressBarPaintingDelegate() override { }
+
+ virtual void paint(GUI::Painter& painter, const Gfx::IntRect& a_rect, const Palette& palette, const GUI::ModelIndex& index) override
+ {
+ auto rect = a_rect.shrunken(2, 2);
+ auto percentage = index.data(GUI::ModelRole::Custom).to_i32();
+
+ auto data = index.data();
+ String text;
+ if (data.is_string())
+ text = data.as_string();
+ Gfx::StylePainter::paint_progress_bar(painter, rect, palette, 0, 100, percentage, text);
+ painter.draw_rect(rect, Color::Black);
+ }
+};
+
+NonnullRefPtr<GUI::Widget> build_file_systems_tab()
+{
+ auto fs_widget = GUI::LazyWidget::construct();
+
+ fs_widget->on_first_show = [](GUI::LazyWidget& self) {
+ self.set_layout<GUI::VerticalBoxLayout>();
+ self.layout()->set_margins({ 4, 4, 4, 4 });
+ auto& fs_table_view = self.add<GUI::TableView>();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> df_fields;
+ df_fields.empend("mount_point", "Mount point", Gfx::TextAlignment::CenterLeft);
+ df_fields.empend("class_name", "Class", Gfx::TextAlignment::CenterLeft);
+ df_fields.empend("source", "Source", Gfx::TextAlignment::CenterLeft);
+ df_fields.empend(
+ "Size", Gfx::TextAlignment::CenterRight,
+ [](const JsonObject& object) {
+ StringBuilder size_builder;
+ size_builder.append(" ");
+ size_builder.append(human_readable_size(object.get("total_block_count").to_u32() * object.get("block_size").to_u32()));
+ size_builder.append(" ");
+ return size_builder.to_string();
+ },
+ [](const JsonObject& object) {
+ return object.get("total_block_count").to_u32() * object.get("block_size").to_u32();
+ },
+ [](const JsonObject& object) {
+ auto total_blocks = object.get("total_block_count").to_u32();
+ if (total_blocks == 0)
+ return 0;
+ auto free_blocks = object.get("free_block_count").to_u32();
+ auto used_blocks = total_blocks - free_blocks;
+ int percentage = (int)((float)used_blocks / (float)total_blocks * 100.0f);
+ return percentage;
+ });
+ df_fields.empend(
+ "Used", Gfx::TextAlignment::CenterRight,
+ [](const JsonObject& object) {
+ auto total_blocks = object.get("total_block_count").to_u32();
+ auto free_blocks = object.get("free_block_count").to_u32();
+ auto used_blocks = total_blocks - free_blocks;
+ return human_readable_size(used_blocks * object.get("block_size").to_u32()); },
+ [](const JsonObject& object) {
+ auto total_blocks = object.get("total_block_count").to_u32();
+ auto free_blocks = object.get("free_block_count").to_u32();
+ auto used_blocks = total_blocks - free_blocks;
+ return used_blocks * object.get("block_size").to_u32();
+ });
+ df_fields.empend(
+ "Available", Gfx::TextAlignment::CenterRight,
+ [](const JsonObject& object) {
+ return human_readable_size(object.get("free_block_count").to_u32() * object.get("block_size").to_u32());
+ },
+ [](const JsonObject& object) {
+ return object.get("free_block_count").to_u32() * object.get("block_size").to_u32();
+ });
+ df_fields.empend("Access", Gfx::TextAlignment::CenterLeft, [](const JsonObject& object) {
+ bool readonly = object.get("readonly").to_bool();
+ int mount_flags = object.get("mount_flags").to_int();
+ return readonly || (mount_flags & MS_RDONLY) ? "Read-only" : "Read/Write";
+ });
+ df_fields.empend("Mount flags", Gfx::TextAlignment::CenterLeft, [](const JsonObject& object) {
+ int mount_flags = object.get("mount_flags").to_int();
+ StringBuilder builder;
+ bool first = true;
+ auto check = [&](int flag, const char* name) {
+ if (!(mount_flags & flag))
+ return;
+ if (!first)
+ builder.append(',');
+ builder.append(name);
+ first = false;
+ };
+ check(MS_NODEV, "nodev");
+ check(MS_NOEXEC, "noexec");
+ check(MS_NOSUID, "nosuid");
+ check(MS_BIND, "bind");
+ check(MS_RDONLY, "ro");
+ if (builder.string_view().is_empty())
+ return String("defaults");
+ return builder.to_string();
+ });
+ df_fields.empend("free_block_count", "Free blocks", Gfx::TextAlignment::CenterRight);
+ df_fields.empend("total_block_count", "Total blocks", Gfx::TextAlignment::CenterRight);
+ df_fields.empend("free_inode_count", "Free inodes", Gfx::TextAlignment::CenterRight);
+ df_fields.empend("total_inode_count", "Total inodes", Gfx::TextAlignment::CenterRight);
+ df_fields.empend("block_size", "Block size", Gfx::TextAlignment::CenterRight);
+ fs_table_view.set_model(GUI::SortingProxyModel::create(GUI::JsonArrayModel::create("/proc/df", move(df_fields))));
+
+ fs_table_view.set_column_painting_delegate(3, make<ProgressBarPaintingDelegate>());
+
+ fs_table_view.model()->update();
+ };
+ return fs_widget;
+}
+
+NonnullRefPtr<GUI::Widget> build_pci_devices_tab()
+{
+ auto pci_widget = GUI::LazyWidget::construct();
+
+ pci_widget->on_first_show = [](GUI::LazyWidget& self) {
+ self.set_layout<GUI::VerticalBoxLayout>();
+ self.layout()->set_margins({ 4, 4, 4, 4 });
+ auto& pci_table_view = self.add<GUI::TableView>();
+
+ auto db = PCIDB::Database::open();
+
+ Vector<GUI::JsonArrayModel::FieldSpec> pci_fields;
+ pci_fields.empend(
+ "Address", Gfx::TextAlignment::CenterLeft,
+ [](const JsonObject& object) {
+ auto seg = object.get("seg").to_u32();
+ auto bus = object.get("bus").to_u32();
+ auto slot = object.get("slot").to_u32();
+ auto function = object.get("function").to_u32();
+ return String::formatted("{:04x}:{:02x}:{:02x}.{}", seg, bus, slot, function);
+ });
+ pci_fields.empend(
+ "Class", Gfx::TextAlignment::CenterLeft,
+ [db](const JsonObject& object) {
+ auto class_id = object.get("class").to_u32();
+ String class_name = db->get_class(class_id);
+ return class_name == "" ? String::formatted("{:04x}", class_id) : class_name;
+ });
+ pci_fields.empend(
+ "Vendor", Gfx::TextAlignment::CenterLeft,
+ [db](const JsonObject& object) {
+ auto vendor_id = object.get("vendor_id").to_u32();
+ String vendor_name = db->get_vendor(vendor_id);
+ return vendor_name == "" ? String::formatted("{:02x}", vendor_id) : vendor_name;
+ });
+ pci_fields.empend(
+ "Device", Gfx::TextAlignment::CenterLeft,
+ [db](const JsonObject& object) {
+ auto vendor_id = object.get("vendor_id").to_u32();
+ auto device_id = object.get("device_id").to_u32();
+ String device_name = db->get_device(vendor_id, device_id);
+ return device_name == "" ? String::formatted("{:02x}", device_id) : device_name;
+ });
+ pci_fields.empend(
+ "Revision", Gfx::TextAlignment::CenterRight,
+ [](const JsonObject& object) {
+ auto revision_id = object.get("revision_id").to_u32();
+ return String::formatted("{:02x}", revision_id);
+ });
+
+ pci_table_view.set_model(GUI::SortingProxyModel::create(GUI::JsonArrayModel::create("/proc/pci", move(pci_fields))));
+ pci_table_view.model()->update();
+ };
+
+ return pci_widget;
+}
+
+NonnullRefPtr<GUI::Widget> build_devices_tab()
+{
+ auto devices_widget = GUI::LazyWidget::construct();
+
+ devices_widget->on_first_show = [](GUI::LazyWidget& self) {
+ self.set_layout<GUI::VerticalBoxLayout>();
+ self.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& devices_table_view = self.add<GUI::TableView>();
+ devices_table_view.set_model(GUI::SortingProxyModel::create(DevicesModel::create()));
+ devices_table_view.model()->update();
+ };
+
+ return devices_widget;
+}
+
+NonnullRefPtr<GUI::Widget> build_graphs_tab()
+{
+ auto graphs_container = GUI::LazyWidget::construct();
+
+ graphs_container->on_first_show = [](GUI::LazyWidget& self) {
+ self.set_fill_with_background_color(true);
+ self.set_background_role(ColorRole::Button);
+ self.set_layout<GUI::VerticalBoxLayout>();
+ self.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& cpu_graph_group_box = self.add<GUI::GroupBox>("CPU usage");
+ cpu_graph_group_box.set_layout<GUI::HorizontalBoxLayout>();
+ cpu_graph_group_box.layout()->set_margins({ 6, 16, 6, 6 });
+ cpu_graph_group_box.set_fixed_height(120);
+ Vector<GraphWidget*> cpu_graphs;
+ for (size_t i = 0; i < ProcessModel::the().cpus().size(); i++) {
+ auto& cpu_graph = cpu_graph_group_box.add<GraphWidget>();
+ cpu_graph.set_max(100);
+ cpu_graph.set_background_color(Color::White);
+ cpu_graph.set_value_format(0, {
+ .line_color = Color::Blue,
+ .background_color = Color::from_rgb(0xaaaaff),
+ .text_formatter = [](int value) {
+ return String::formatted("Total: {}%", value);
+ },
+ });
+ cpu_graph.set_value_format(1, {
+ .line_color = Color::Red,
+ .background_color = Color::from_rgb(0xffaaaa),
+ .text_formatter = [](int value) {
+ return String::formatted("Kernel: {}%", value);
+ },
+ });
+ cpu_graphs.append(&cpu_graph);
+ }
+ ProcessModel::the().on_cpu_info_change = [cpu_graphs](const NonnullOwnPtrVector<ProcessModel::CpuInfo>& cpus) {
+ for (size_t i = 0; i < cpus.size(); i++)
+ cpu_graphs[i]->add_value({ (int)cpus[i].total_cpu_percent, (int)cpus[i].total_cpu_percent_kernel });
+ };
+
+ auto& memory_graph_group_box = self.add<GUI::GroupBox>("Memory usage");
+ memory_graph_group_box.set_layout<GUI::VerticalBoxLayout>();
+ memory_graph_group_box.layout()->set_margins({ 6, 16, 6, 6 });
+ memory_graph_group_box.set_fixed_height(120);
+ auto& memory_graph = memory_graph_group_box.add<GraphWidget>();
+ memory_graph.set_background_color(Color::White);
+ memory_graph.set_stack_values(true);
+ memory_graph.set_value_format(0, {
+ .line_color = Color::from_rgb(0x619910),
+ .background_color = Color::from_rgb(0xbbffbb),
+ .text_formatter = [&memory_graph](int value) {
+ return String::formatted("Committed: {} KiB", value);
+ },
+ });
+ memory_graph.set_value_format(1, {
+ .line_color = Color::Blue,
+ .background_color = Color::from_rgb(0xaaaaff),
+ .text_formatter = [&memory_graph](int value) {
+ return String::formatted("Allocated: {} KiB", value);
+ },
+ });
+ memory_graph.set_value_format(2, {
+ .line_color = Color::Red,
+ .background_color = Color::from_rgb(0xffaaaa),
+ .text_formatter = [&memory_graph](int value) {
+ return String::formatted("Kernel heap: {} KiB", value);
+ },
+ });
+
+ self.add<MemoryStatsWidget>(memory_graph);
+ };
+ return graphs_container;
+}
+
+NonnullRefPtr<GUI::Widget> build_processors_tab()
+{
+ auto processors_widget = GUI::LazyWidget::construct();
+
+ processors_widget->on_first_show = [](GUI::LazyWidget& self) {
+ self.set_layout<GUI::VerticalBoxLayout>();
+ self.layout()->set_margins({ 4, 4, 4, 4 });
+
+ Vector<GUI::JsonArrayModel::FieldSpec> processors_field;
+ processors_field.empend("processor", "Processor", Gfx::TextAlignment::CenterRight);
+ processors_field.empend("cpuid", "CPUID", Gfx::TextAlignment::CenterLeft);
+ processors_field.empend("brandstr", "Brand", Gfx::TextAlignment::CenterLeft);
+ processors_field.empend("Features", Gfx::TextAlignment::CenterLeft, [](auto& object) {
+ StringBuilder builder;
+ auto features = object.get("features").as_array();
+ for (auto& feature : features.values()) {
+ builder.append(feature.to_string());
+ builder.append(' ');
+ }
+ return GUI::Variant(builder.to_string());
+ });
+ processors_field.empend("family", "Family", Gfx::TextAlignment::CenterRight);
+ processors_field.empend("model", "Model", Gfx::TextAlignment::CenterRight);
+ processors_field.empend("stepping", "Stepping", Gfx::TextAlignment::CenterRight);
+ processors_field.empend("type", "Type", Gfx::TextAlignment::CenterRight);
+
+ auto& processors_table_view = self.add<GUI::TableView>();
+ processors_table_view.set_model(GUI::JsonArrayModel::create("/proc/cpuinfo", move(processors_field)));
+ processors_table_view.model()->update();
+ };
+
+ return processors_widget;
+}
diff --git a/Userland/Applications/Terminal/CMakeLists.txt b/Userland/Applications/Terminal/CMakeLists.txt
new file mode 100644
index 0000000000..1fb969546e
--- /dev/null
+++ b/Userland/Applications/Terminal/CMakeLists.txt
@@ -0,0 +1,9 @@
+compile_gml(TerminalSettingsWindow.gml TerminalSettingsWindowGML.h terminal_settings_window_gml)
+
+set(SOURCES
+ TerminalSettingsWindowGML.h
+ main.cpp
+)
+
+serenity_app(Terminal ICON app-terminal)
+target_link_libraries(Terminal LibGUI LibVT)
diff --git a/Userland/Applications/Terminal/TerminalSettingsWindow.gml b/Userland/Applications/Terminal/TerminalSettingsWindow.gml
new file mode 100644
index 0000000000..07b8400c56
--- /dev/null
+++ b/Userland/Applications/Terminal/TerminalSettingsWindow.gml
@@ -0,0 +1,63 @@
+@GUI::Widget {
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [4, 4, 4, 4]
+ }
+
+ @GUI::GroupBox {
+ title: "Bell mode"
+ shrink_to_fit: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [6, 16, 6, 6]
+ }
+
+ @GUI::RadioButton {
+ name: "beep_bell_radio"
+ text: "System beep"
+ }
+
+ @GUI::RadioButton {
+ name: "visual_bell_radio"
+ text: "Visual bell"
+ }
+
+ @GUI::RadioButton {
+ name: "no_bell_radio"
+ text: "No bell"
+ }
+ }
+
+ @GUI::GroupBox {
+ title: "Background opacity"
+ shrink_to_fit: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [6, 16, 6, 6]
+ }
+
+ @GUI::OpacitySlider {
+ name: "background_opacity_slider"
+ min: 0
+ max: 255
+ orientation: "Horizontal"
+ }
+ }
+
+ @GUI::GroupBox {
+ title: "Scrollback size (lines)"
+ shrink_to_fit: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [6, 16, 6, 6]
+ }
+
+ @GUI::SpinBox {
+ name: "history_size_spinbox"
+ min: 0
+ max: 40960
+ orientation: "Horizontal"
+ }
+ }
+}
diff --git a/Userland/Applications/Terminal/main.cpp b/Userland/Applications/Terminal/main.cpp
new file mode 100644
index 0000000000..9267b9465f
--- /dev/null
+++ b/Userland/Applications/Terminal/main.cpp
@@ -0,0 +1,525 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/URL.h>
+#include <Applications/Terminal/TerminalSettingsWindowGML.h>
+#include <LibCore/ArgsParser.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/Event.h>
+#include <LibGUI/FontPicker.h>
+#include <LibGUI/GroupBox.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/OpacitySlider.h>
+#include <LibGUI/RadioButton.h>
+#include <LibGUI/SpinBox.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+#include <LibVT/TerminalWidget.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <serenity.h>
+#include <signal.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static void utmp_update(const char* tty, pid_t pid, bool create)
+{
+ if (!tty)
+ return;
+ int utmpupdate_pid = fork();
+ if (utmpupdate_pid < 0) {
+ perror("fork");
+ return;
+ }
+ if (utmpupdate_pid == 0) {
+ // Be careful here! Because fork() only clones one thread it's
+ // possible that we deadlock on anything involving a mutex,
+ // including the heap! So resort to low-level APIs
+ char pid_str[32];
+ snprintf(pid_str, sizeof(pid_str), "%d", pid);
+ execl("/bin/utmpupdate", "/bin/utmpupdate", "-f", "Terminal", "-p", pid_str, (create ? "-c" : "-d"), tty, nullptr);
+ } else {
+ wait_again:
+ int status = 0;
+ if (waitpid(utmpupdate_pid, &status, 0) < 0) {
+ int err = errno;
+ if (err == EINTR)
+ goto wait_again;
+ perror("waitpid");
+ return;
+ }
+ if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
+ dbgln("Terminal: utmpupdate exited with status {}", WEXITSTATUS(status));
+ else if (WIFSIGNALED(status))
+ dbgln("Terminal: utmpupdate exited due to unhandled signal {}", WTERMSIG(status));
+ }
+}
+
+static pid_t run_command(int ptm_fd, String command)
+{
+ pid_t pid = fork();
+ if (pid < 0) {
+ perror("fork");
+ dbgln("run_command: could not fork to run '{}'", command);
+ return pid;
+ }
+
+ if (pid == 0) {
+ const char* tty_name = ptsname(ptm_fd);
+ if (!tty_name) {
+ perror("ptsname");
+ exit(1);
+ }
+ close(ptm_fd);
+ int pts_fd = open(tty_name, O_RDWR);
+ if (pts_fd < 0) {
+ perror("open");
+ exit(1);
+ }
+
+ if (setsid() < 0) {
+ perror("setsid");
+ }
+
+ close(0);
+ close(1);
+ close(2);
+
+ int rc = dup2(pts_fd, 0);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = dup2(pts_fd, 1);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = dup2(pts_fd, 2);
+ if (rc < 0) {
+ perror("dup2");
+ exit(1);
+ }
+ rc = close(pts_fd);
+ if (rc < 0) {
+ perror("close");
+ exit(1);
+ }
+ rc = ioctl(0, TIOCSCTTY);
+ if (rc < 0) {
+ perror("ioctl(TIOCSCTTY)");
+ exit(1);
+ }
+
+ String shell = "/bin/Shell";
+ auto* pw = getpwuid(getuid());
+ if (pw && pw->pw_shell) {
+ shell = pw->pw_shell;
+ }
+ endpwent();
+
+ const char* args[4] = { shell.characters(), nullptr, nullptr, nullptr };
+ if (!command.is_empty()) {
+ args[1] = "-c";
+ args[2] = command.characters();
+ }
+ const char* envs[] = { "PROMPT=\\X\\u@\\h:\\w\\a\\e[33;1m\\h\\e[0m \\e[34;1m\\w\\e[0m \\p ", "TERM=xterm", "PAGER=more", "PATH=/bin:/usr/bin:/usr/local/bin", nullptr };
+ rc = execve(shell.characters(), const_cast<char**>(args), const_cast<char**>(envs));
+ if (rc < 0) {
+ perror("execve");
+ exit(1);
+ }
+ ASSERT_NOT_REACHED();
+ }
+
+ return pid;
+}
+
+static RefPtr<GUI::Window> create_settings_window(TerminalWidget& terminal)
+{
+ auto window = GUI::Window::construct(terminal.window());
+ window->set_title("Terminal settings");
+ window->set_minimizable(false);
+ window->set_resizable(false);
+ window->resize(200, 210);
+ window->set_modal(true);
+
+ auto& settings = window->set_main_widget<GUI::Widget>();
+ settings.load_from_gml(terminal_settings_window_gml);
+
+ auto& beep_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("beep_bell_radio");
+ auto& visual_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("visual_bell_radio");
+ auto& no_bell_radio = *settings.find_descendant_of_type_named<GUI::RadioButton>("no_bell_radio");
+
+ switch (terminal.bell_mode()) {
+ case TerminalWidget::BellMode::Visible:
+ visual_bell_radio.set_checked(true);
+ break;
+ case TerminalWidget::BellMode::AudibleBeep:
+ beep_bell_radio.set_checked(true);
+ break;
+ case TerminalWidget::BellMode::Disabled:
+ no_bell_radio.set_checked(true);
+ break;
+ }
+
+ beep_bell_radio.on_checked = [&terminal](bool) {
+ terminal.set_bell_mode(TerminalWidget::BellMode::AudibleBeep);
+ };
+ visual_bell_radio.on_checked = [&terminal](bool) {
+ terminal.set_bell_mode(TerminalWidget::BellMode::Visible);
+ };
+ no_bell_radio.on_checked = [&terminal](bool) {
+ terminal.set_bell_mode(TerminalWidget::BellMode::Disabled);
+ };
+
+ auto& slider = *settings.find_descendant_of_type_named<GUI::OpacitySlider>("background_opacity_slider");
+ slider.on_change = [&terminal](int value) {
+ terminal.set_opacity(value);
+ };
+ slider.set_value(terminal.opacity());
+
+ auto& history_size_spinbox = *settings.find_descendant_of_type_named<GUI::SpinBox>("history_size_spinbox");
+ history_size_spinbox.set_value(terminal.max_history_size());
+ history_size_spinbox.on_change = [&terminal](int value) {
+ terminal.set_max_history_size(value);
+ };
+
+ return window;
+}
+
+static RefPtr<GUI::Window> create_find_window(TerminalWidget& terminal)
+{
+ auto window = GUI::Window::construct();
+ window->set_title("Find in Terminal");
+ window->set_resizable(false);
+ window->resize(300, 90);
+ window->set_modal(true);
+
+ auto& search = window->set_main_widget<GUI::Widget>();
+ search.set_fill_with_background_color(true);
+ search.set_background_role(ColorRole::Button);
+ search.set_layout<GUI::VerticalBoxLayout>();
+ search.layout()->set_margins({ 4, 4, 4, 4 });
+
+ auto& find = search.add<GUI::Widget>();
+ find.set_layout<GUI::HorizontalBoxLayout>();
+ find.layout()->set_margins({ 4, 4, 4, 4 });
+ find.set_fixed_height(30);
+
+ auto& find_textbox = find.add<GUI::TextBox>();
+ find_textbox.set_fixed_width(230);
+ find_textbox.set_focus(true);
+ if (terminal.has_selection()) {
+ String selected_text = terminal.selected_text();
+ selected_text.replace("\n", " ", true);
+ find_textbox.set_text(selected_text);
+ }
+ auto& find_backwards = find.add<GUI::Button>();
+ find_backwards.set_fixed_width(25);
+ find_backwards.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/upward-triangle.png"));
+ auto& find_forwards = find.add<GUI::Button>();
+ find_forwards.set_fixed_width(25);
+ find_forwards.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/downward-triangle.png"));
+
+ find_textbox.on_return_pressed = [&]() {
+ find_backwards.click();
+ };
+
+ auto& match_case = search.add<GUI::CheckBox>("Case sensitive");
+ auto& wrap_around = search.add<GUI::CheckBox>("Wrap around");
+
+ find_backwards.on_click = [&](auto) {
+ auto needle = find_textbox.text();
+ if (needle.is_empty()) {
+ return;
+ }
+
+ auto found_range = terminal.find_previous(needle, terminal.normalized_selection().start(), match_case.is_checked(), wrap_around.is_checked());
+
+ if (found_range.is_valid()) {
+ terminal.scroll_to_row(found_range.start().row());
+ terminal.set_selection(found_range);
+ }
+ };
+ find_forwards.on_click = [&](auto) {
+ auto needle = find_textbox.text();
+ if (needle.is_empty()) {
+ return;
+ }
+
+ auto found_range = terminal.find_next(needle, terminal.normalized_selection().end(), match_case.is_checked(), wrap_around.is_checked());
+
+ if (found_range.is_valid()) {
+ terminal.scroll_to_row(found_range.start().row());
+ terminal.set_selection(found_range);
+ }
+ };
+
+ return window;
+}
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec unix fattr sigaction", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ struct sigaction act;
+ memset(&act, 0, sizeof(act));
+ act.sa_flags = SA_NOCLDWAIT;
+ act.sa_handler = SIG_IGN;
+ int rc = sigaction(SIGCHLD, &act, nullptr);
+ if (rc < 0) {
+ perror("sigaction");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio tty rpath accept cpath wpath shared_buffer proc exec unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* command_to_execute = nullptr;
+
+ Core::ArgsParser args_parser;
+ args_parser.add_option(command_to_execute, "Execute this command inside the terminal", nullptr, 'e', "command");
+
+ args_parser.parse(argc, argv);
+
+ int ptm_fd = posix_openpt(O_RDWR | O_CLOEXEC);
+ if (ptm_fd < 0) {
+ perror("posix_openpt");
+ return 1;
+ }
+ if (grantpt(ptm_fd) < 0) {
+ perror("grantpt");
+ return 1;
+ }
+ if (unlockpt(ptm_fd) < 0) {
+ perror("unlockpt");
+ return 1;
+ }
+
+ RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal");
+
+ pid_t shell_pid = 0;
+
+ if (command_to_execute)
+ shell_pid = run_command(ptm_fd, command_to_execute);
+ else
+ shell_pid = run_command(ptm_fd, config->read_entry("Startup", "Command", ""));
+
+ auto* pts_name = ptsname(ptm_fd);
+ utmp_update(pts_name, shell_pid, true);
+
+ auto app_icon = GUI::Icon::default_icon("app-terminal");
+
+ auto window = GUI::Window::construct();
+ window->set_title("Terminal");
+ window->set_background_color(Color::Black);
+ window->set_double_buffering_enabled(false);
+
+ auto& terminal = window->set_main_widget<TerminalWidget>(ptm_fd, true, config);
+ terminal.on_command_exit = [&] {
+ app->quit(0);
+ };
+ terminal.on_title_change = [&](auto& title) {
+ window->set_title(title);
+ };
+ terminal.apply_size_increments_to_window(*window);
+ window->show();
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ auto bell = config->read_entry("Window", "Bell", "Visible");
+ if (bell == "AudibleBeep") {
+ terminal.set_bell_mode(TerminalWidget::BellMode::AudibleBeep);
+ } else if (bell == "Disabled") {
+ terminal.set_bell_mode(TerminalWidget::BellMode::Disabled);
+ } else {
+ terminal.set_bell_mode(TerminalWidget::BellMode::Visible);
+ }
+
+ RefPtr<GUI::Window> settings_window;
+ RefPtr<GUI::Window> find_window;
+
+ auto new_opacity = config->read_num_entry("Window", "Opacity", 255);
+ terminal.set_opacity(new_opacity);
+ window->set_has_alpha_channel(new_opacity < 255);
+
+ auto new_scrollback_size = config->read_num_entry("Terminal", "MaxHistorySize", terminal.max_history_size());
+ terminal.set_max_history_size(new_scrollback_size);
+
+ auto open_settings_action = GUI::Action::create("Settings...", Gfx::Bitmap::load_from_file("/res/icons/16x16/gear.png"),
+ [&](const GUI::Action&) {
+ if (!settings_window) {
+ settings_window = create_settings_window(terminal);
+ settings_window->on_close_request = [&] {
+ settings_window->remove_from_parent();
+ settings_window = nullptr;
+ return GUI::Window::CloseRequestDecision::Close;
+ };
+ }
+ if (!settings_window->is_visible()) {
+ settings_window->center_within(*window);
+ settings_window->show();
+ }
+ settings_window->move_to_front();
+ });
+
+ terminal.context_menu().add_separator();
+ auto pick_font_action = GUI::Action::create("Terminal font...", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-font-editor.png"),
+ [&](auto&) {
+ auto picker = GUI::FontPicker::construct(window, &terminal.font(), true);
+ if (picker->exec() == GUI::Dialog::ExecOK) {
+ terminal.set_font_and_resize_to_fit(*picker->font());
+ window->resize(terminal.size());
+ config->write_entry("Text", "Font", picker->font()->qualified_name());
+ config->sync();
+ }
+ });
+
+ terminal.context_menu().add_action(pick_font_action);
+
+ terminal.context_menu().add_separator();
+ terminal.context_menu().add_action(open_settings_action);
+
+ auto menubar = GUI::MenuBar::construct();
+
+ auto& app_menu = menubar->add_menu("Terminal");
+ app_menu.add_action(GUI::Action::create("Open new terminal", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
+ pid_t child;
+ const char* argv[] = { "Terminal", nullptr };
+ if ((errno = posix_spawn(&child, "/bin/Terminal", nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ } else {
+ if (disown(child) < 0)
+ perror("disown");
+ }
+ }));
+
+ app_menu.add_action(open_settings_action);
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
+ dbgln("Terminal: Quit menu activated!");
+ GUI::Application::the()->quit();
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ edit_menu.add_action(terminal.copy_action());
+ edit_menu.add_action(terminal.paste_action());
+ edit_menu.add_separator();
+ edit_menu.add_action(GUI::Action::create("Find...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"),
+ [&](auto&) {
+ if (!find_window) {
+ find_window = create_find_window(terminal);
+ find_window->on_close_request = [&] {
+ find_window = nullptr;
+ return GUI::Window::CloseRequestDecision::Close;
+ };
+ }
+ find_window->show();
+ find_window->move_to_front();
+ }));
+
+ auto& view_menu = menubar->add_menu("View");
+ view_menu.add_action(terminal.clear_including_history_action());
+ view_menu.add_separator();
+ view_menu.add_action(pick_font_action);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) {
+ Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/Terminal.md"), "/bin/Help");
+ }));
+ help_menu.add_action(GUI::CommonActions::make_about_action("Terminal", app_icon, window));
+
+ app->set_menubar(move(menubar));
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/Terminal", "x") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/bin/utmpupdate", "x") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/etc/FileIconProvider.ini", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil("/tmp/portal/launch", "rw") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(config->file_name().characters(), "rwc")) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ config->sync();
+ int result = app->exec();
+ dbgln("Exiting terminal, updating utmp");
+ utmp_update(pts_name, 0, false);
+ return result;
+}
diff --git a/Userland/Applications/TextEditor/CMakeLists.txt b/Userland/Applications/TextEditor/CMakeLists.txt
new file mode 100644
index 0000000000..07eccd212e
--- /dev/null
+++ b/Userland/Applications/TextEditor/CMakeLists.txt
@@ -0,0 +1,10 @@
+compile_gml(TextEditorWindow.gml TextEditorWindowGML.h text_editor_window_gml)
+
+set(SOURCES
+ main.cpp
+ TextEditorWidget.cpp
+ TextEditorWindowGML.h
+)
+
+serenity_app(TextEditor ICON app-text-editor)
+target_link_libraries(TextEditor LibWeb LibMarkdown LibGUI LibShell LibRegex LibDesktop)
diff --git a/Userland/Applications/TextEditor/TextEditorWidget.cpp b/Userland/Applications/TextEditor/TextEditorWidget.cpp
new file mode 100644
index 0000000000..982bcc282c
--- /dev/null
+++ b/Userland/Applications/TextEditor/TextEditorWidget.cpp
@@ -0,0 +1,667 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TextEditorWidget.h"
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <AK/Optional.h>
+#include <AK/StringBuilder.h>
+#include <AK/URL.h>
+#include <Applications/TextEditor/TextEditorWindowGML.h>
+#include <LibCore/File.h>
+#include <LibCore/MimeData.h>
+#include <LibDesktop/Launcher.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CppSyntaxHighlighter.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/FontPicker.h>
+#include <LibGUI/GMLSyntaxHighlighter.h>
+#include <LibGUI/INISyntaxHighlighter.h>
+#include <LibGUI/JSSyntaxHighlighter.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/RegularEditingEngine.h>
+#include <LibGUI/ShellSyntaxHighlighter.h>
+#include <LibGUI/Splitter.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/ToolBar.h>
+#include <LibGUI/ToolBarContainer.h>
+#include <LibGUI/VimEditingEngine.h>
+#include <LibGfx/Font.h>
+#include <LibMarkdown/Document.h>
+#include <LibWeb/OutOfProcessWebView.h>
+#include <string.h>
+
+TextEditorWidget::TextEditorWidget()
+{
+ load_from_gml(text_editor_window_gml);
+
+ auto& toolbar = *find_descendant_of_type_named<GUI::ToolBar>("toolbar");
+
+ m_editor = *find_descendant_of_type_named<GUI::TextEditor>("editor");
+ m_editor->set_ruler_visible(true);
+ m_editor->set_automatic_indentation_enabled(true);
+ m_editor->set_line_wrapping_enabled(true);
+ m_editor->set_editing_engine(make<GUI::RegularEditingEngine>());
+
+ m_editor->on_change = [this] {
+ update_preview();
+
+ // Do not mark as dirty on the first change (When document is first opened.)
+ if (m_document_opening) {
+ m_document_opening = false;
+ return;
+ }
+
+ bool was_dirty = m_document_dirty;
+ m_document_dirty = true;
+ if (!was_dirty)
+ update_title();
+ };
+
+ m_page_view = *find_descendant_of_type_named<Web::OutOfProcessWebView>("webview");
+ m_page_view->on_link_hover = [this](auto& url) {
+ if (url.is_valid())
+ m_statusbar->set_text(url.to_string());
+ else
+ update_statusbar_cursor_position();
+ };
+ m_page_view->on_link_click = [&](auto& url, auto&, unsigned) {
+ if (!Desktop::Launcher::open(url)) {
+ GUI::MessageBox::show(
+ window(),
+ String::formatted("The link to '{}' could not be opened.", url),
+ "Failed to open link",
+ GUI::MessageBox::Type::Error);
+ }
+ };
+
+ m_find_replace_widget = *find_descendant_of_type_named<GUI::Widget>("find_replace_widget");
+
+ m_find_widget = *find_descendant_of_type_named<GUI::Widget>("find_widget");
+
+ m_replace_widget = *find_descendant_of_type_named<GUI::Widget>("replace_widget");
+
+ m_find_textbox = m_find_widget->add<GUI::TextBox>();
+ m_replace_textbox = m_replace_widget->add<GUI::TextBox>();
+
+ m_find_next_action = GUI::Action::create("Find next", { Mod_Ctrl, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find-next.png"), [&](auto&) {
+ auto needle = m_find_textbox->text();
+ if (needle.is_empty()) {
+ dbgln("find_next(\"\")");
+ return;
+ }
+
+ if (m_find_use_regex)
+ m_editor->document().update_regex_matches(needle);
+
+ auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().end(), GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
+ dbgln("find_next('{}') returned {}", needle, found_range);
+ if (found_range.is_valid()) {
+ m_editor->set_selection(found_range);
+ } else {
+ GUI::MessageBox::show(window(),
+ String::formatted("Not found: \"{}\"", needle),
+ "Not found",
+ GUI::MessageBox::Type::Information);
+ }
+ });
+
+ m_find_regex_action = GUI::Action::create("Find regex", { Mod_Ctrl | Mod_Shift, Key_R }, [&](auto&) {
+ m_find_regex_button->set_checked(!m_find_regex_button->is_checked());
+ m_find_use_regex = m_find_regex_button->is_checked();
+ });
+
+ m_find_previous_action = GUI::Action::create("Find previous", { Mod_Ctrl | Mod_Shift, Key_G }, [&](auto&) {
+ auto needle = m_find_textbox->text();
+ if (needle.is_empty()) {
+ dbgln("find_prev(\"\")");
+ return;
+ }
+
+ auto selection_start = m_editor->normalized_selection().start();
+ if (!selection_start.is_valid())
+ selection_start = m_editor->normalized_selection().end();
+
+ if (m_find_use_regex)
+ m_editor->document().update_regex_matches(needle);
+
+ auto found_range = m_editor->document().find_previous(needle, selection_start, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
+
+ dbgln("find_prev(\"{}\") returned {}", needle, found_range);
+ if (found_range.is_valid()) {
+ m_editor->set_selection(found_range);
+ } else {
+ GUI::MessageBox::show(window(),
+ String::formatted("Not found: \"{}\"", needle),
+ "Not found",
+ GUI::MessageBox::Type::Information);
+ }
+ });
+
+ m_replace_next_action = GUI::Action::create("Replace next", { Mod_Ctrl, Key_F1 }, [&](auto&) {
+ auto needle = m_find_textbox->text();
+ auto substitute = m_replace_textbox->text();
+
+ if (needle.is_empty())
+ return;
+
+ auto selection_start = m_editor->normalized_selection().start();
+ if (!selection_start.is_valid())
+ selection_start = m_editor->normalized_selection().start();
+
+ if (m_find_use_regex)
+ m_editor->document().update_regex_matches(needle);
+
+ auto found_range = m_editor->document().find_next(needle, selection_start, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
+
+ if (found_range.is_valid()) {
+ m_editor->set_selection(found_range);
+ m_editor->insert_at_cursor_or_replace_selection(substitute);
+ } else {
+ GUI::MessageBox::show(window(),
+ String::formatted("Not found: \"{}\"", needle),
+ "Not found",
+ GUI::MessageBox::Type::Information);
+ }
+ });
+
+ m_replace_previous_action = GUI::Action::create("Replace previous", { Mod_Ctrl | Mod_Shift, Key_F1 }, [&](auto&) {
+ auto needle = m_find_textbox->text();
+ auto substitute = m_replace_textbox->text();
+ if (needle.is_empty())
+ return;
+
+ auto selection_start = m_editor->normalized_selection().start();
+ if (!selection_start.is_valid())
+ selection_start = m_editor->normalized_selection().start();
+
+ if (m_find_use_regex)
+ m_editor->document().update_regex_matches(needle);
+
+ auto found_range = m_editor->document().find_previous(needle, selection_start);
+
+ if (found_range.is_valid()) {
+ m_editor->set_selection(found_range);
+ m_editor->insert_at_cursor_or_replace_selection(substitute);
+ } else {
+ GUI::MessageBox::show(window(),
+ String::formatted("Not found: \"{}\"", needle),
+ "Not found",
+ GUI::MessageBox::Type::Information);
+ }
+ });
+
+ m_replace_all_action = GUI::Action::create("Replace all", { Mod_Ctrl, Key_F2 }, [&](auto&) {
+ auto needle = m_find_textbox->text();
+ auto substitute = m_replace_textbox->text();
+ if (needle.is_empty())
+ return;
+ if (m_find_use_regex)
+ m_editor->document().update_regex_matches(needle);
+
+ auto found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
+ while (found_range.is_valid()) {
+ m_editor->set_selection(found_range);
+ m_editor->insert_at_cursor_or_replace_selection(substitute);
+ found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::Yes, m_find_use_regex);
+ }
+ });
+
+ m_find_previous_button = *find_descendant_of_type_named<GUI::Button>("find_previous_button");
+ m_find_previous_button->set_action(*m_find_previous_action);
+
+ m_find_next_button = *find_descendant_of_type_named<GUI::Button>("find_next_button");
+ m_find_next_button->set_action(*m_find_next_action);
+
+ m_find_textbox->on_return_pressed = [this] {
+ m_find_next_button->click();
+ };
+
+ m_find_regex_button = m_find_widget->add<GUI::Button>(".*");
+ m_find_regex_button->set_fixed_width(20);
+ m_find_regex_button->set_action(*m_find_regex_action);
+
+ m_find_textbox->on_escape_pressed = [this] {
+ m_find_replace_widget->set_visible(false);
+ m_editor->set_focus(true);
+ };
+
+ m_replace_previous_button = *find_descendant_of_type_named<GUI::Button>("replace_previous_button");
+ m_replace_previous_button->set_action(*m_replace_previous_action);
+
+ m_replace_next_button = *find_descendant_of_type_named<GUI::Button>("replace_next_button");
+ m_replace_next_button->set_action(*m_replace_next_action);
+
+ m_replace_all_button = *find_descendant_of_type_named<GUI::Button>("replace_all_button");
+ m_replace_all_button->set_action(*m_replace_all_action);
+
+ m_replace_textbox->on_return_pressed = [this] {
+ m_replace_next_button->click();
+ };
+
+ m_replace_textbox->on_escape_pressed = [this] {
+ m_find_replace_widget->set_visible(false);
+ m_editor->set_focus(true);
+ };
+
+ m_vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [&](auto& action) {
+ if (action.is_checked())
+ m_editor->set_editing_engine(make<GUI::VimEditingEngine>());
+ else
+ m_editor->set_editing_engine(make<GUI::RegularEditingEngine>());
+ });
+ m_vim_emulation_setting_action->set_checked(false);
+
+ m_find_replace_action = GUI::Action::create("Find/Replace...", { Mod_Ctrl, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) {
+ m_find_replace_widget->set_visible(true);
+ m_find_widget->set_visible(true);
+ m_replace_widget->set_visible(true);
+ m_find_textbox->set_focus(true);
+
+ if (m_editor->has_selection()) {
+ auto selected_text = m_editor->document().text_in_range(m_editor->normalized_selection());
+ m_find_textbox->set_text(selected_text);
+ }
+ m_find_textbox->select_all();
+ });
+
+ m_editor->add_custom_context_menu_action(*m_find_replace_action);
+ m_editor->add_custom_context_menu_action(*m_find_next_action);
+ m_editor->add_custom_context_menu_action(*m_find_previous_action);
+
+ m_statusbar = *find_descendant_of_type_named<GUI::StatusBar>("statusbar");
+
+ m_editor->on_cursor_change = [this] { update_statusbar_cursor_position(); };
+
+ m_new_action = GUI::Action::create("New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
+ if (m_document_dirty) {
+ auto save_document_first_result = GUI::MessageBox::show(window(), "Save changes to current document first?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
+ if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes)
+ m_save_action->activate();
+ if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel)
+ return;
+ }
+
+ m_document_dirty = false;
+ m_editor->set_text(StringView());
+ set_path(LexicalPath());
+ update_title();
+ });
+
+ m_open_action = GUI::CommonActions::make_open_action([this](auto&) {
+ Optional<String> open_path = GUI::FilePicker::get_open_filepath(window());
+
+ if (!open_path.has_value())
+ return;
+
+ if (m_document_dirty) {
+ auto save_document_first_result = GUI::MessageBox::show(window(), "Save changes to current document first?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
+ if (save_document_first_result == GUI::Dialog::ExecResult::ExecYes)
+ m_save_action->activate();
+ if (save_document_first_result == GUI::Dialog::ExecResult::ExecCancel)
+ return;
+ }
+
+ open_sesame(open_path.value());
+ });
+
+ m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
+ Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), m_name.is_null() ? "Untitled" : m_name, m_extension.is_null() ? "txt" : m_extension);
+ if (!save_path.has_value())
+ return;
+
+ if (!m_editor->write_to_file(save_path.value())) {
+ GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_document_dirty = false;
+ set_path(LexicalPath(save_path.value()));
+ dbgln("Wrote document to {}", save_path.value());
+ });
+
+ m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
+ if (!m_path.is_empty()) {
+ if (!m_editor->write_to_file(m_path)) {
+ GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
+ } else {
+ m_document_dirty = false;
+ update_title();
+ }
+ return;
+ }
+
+ m_save_as_action->activate();
+ });
+
+ m_line_wrapping_setting_action = GUI::Action::create_checkable("Line wrapping", [&](auto& action) {
+ m_editor->set_line_wrapping_enabled(action.is_checked());
+ });
+ m_line_wrapping_setting_action->set_checked(m_editor->is_line_wrapping_enabled());
+
+ auto menubar = GUI::MenuBar::construct();
+ auto& app_menu = menubar->add_menu("Text Editor");
+ app_menu.add_action(*m_new_action);
+ app_menu.add_action(*m_open_action);
+ app_menu.add_action(*m_save_action);
+ app_menu.add_action(*m_save_as_action);
+ app_menu.add_separator();
+ app_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) {
+ if (!request_close())
+ return;
+ GUI::Application::the()->quit();
+ }));
+
+ auto& edit_menu = menubar->add_menu("Edit");
+ edit_menu.add_action(m_editor->undo_action());
+ edit_menu.add_action(m_editor->redo_action());
+ edit_menu.add_separator();
+ edit_menu.add_action(m_editor->cut_action());
+ edit_menu.add_action(m_editor->copy_action());
+ edit_menu.add_action(m_editor->paste_action());
+ edit_menu.add_action(m_editor->delete_action());
+ edit_menu.add_separator();
+ edit_menu.add_action(*m_vim_emulation_setting_action);
+ edit_menu.add_separator();
+ edit_menu.add_action(*m_find_replace_action);
+ edit_menu.add_action(*m_find_next_action);
+ edit_menu.add_action(*m_find_regex_action);
+ edit_menu.add_action(*m_find_previous_action);
+ edit_menu.add_action(*m_replace_next_action);
+ edit_menu.add_action(*m_replace_previous_action);
+ edit_menu.add_action(*m_replace_all_action);
+
+ m_no_preview_action = GUI::Action::create_checkable(
+ "No preview", [this](auto&) {
+ set_preview_mode(PreviewMode::None);
+ });
+
+ m_markdown_preview_action = GUI::Action::create_checkable(
+ "Markdown preview", [this](auto&) {
+ set_preview_mode(PreviewMode::Markdown);
+ },
+ this);
+
+ m_html_preview_action = GUI::Action::create_checkable(
+ "HTML preview", [this](auto&) {
+ set_preview_mode(PreviewMode::HTML);
+ },
+ this);
+
+ m_preview_actions.add_action(*m_no_preview_action);
+ m_preview_actions.add_action(*m_markdown_preview_action);
+ m_preview_actions.add_action(*m_html_preview_action);
+ m_preview_actions.set_exclusive(true);
+
+ auto& view_menu = menubar->add_menu("View");
+ view_menu.add_action(GUI::Action::create("Editor font...", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-font-editor.png"),
+ [&](auto&) {
+ auto picker = GUI::FontPicker::construct(window(), &m_editor->font(), true);
+ if (picker->exec() == GUI::Dialog::ExecOK) {
+ dbgln("setting font {}", picker->font()->qualified_name());
+ m_editor->set_font(picker->font());
+ }
+ }));
+
+ view_menu.add_separator();
+ view_menu.add_action(*m_line_wrapping_setting_action);
+ view_menu.add_separator();
+ view_menu.add_action(*m_no_preview_action);
+ view_menu.add_action(*m_markdown_preview_action);
+ view_menu.add_action(*m_html_preview_action);
+ view_menu.add_separator();
+
+ syntax_actions.set_exclusive(true);
+
+ auto& syntax_menu = view_menu.add_submenu("Syntax");
+ m_plain_text_highlight = GUI::Action::create_checkable("Plain text", [&](auto&) {
+ m_editor->set_syntax_highlighter({});
+ m_editor->update();
+ });
+ m_plain_text_highlight->set_checked(true);
+ syntax_actions.add_action(*m_plain_text_highlight);
+ syntax_menu.add_action(*m_plain_text_highlight);
+
+ m_cpp_highlight = GUI::Action::create_checkable("C++", [&](auto&) {
+ m_editor->set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>());
+ m_editor->update();
+ });
+ syntax_actions.add_action(*m_cpp_highlight);
+ syntax_menu.add_action(*m_cpp_highlight);
+
+ m_js_highlight = GUI::Action::create_checkable("JavaScript", [&](auto&) {
+ m_editor->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
+ m_editor->update();
+ });
+ syntax_actions.add_action(*m_js_highlight);
+ syntax_menu.add_action(*m_js_highlight);
+
+ m_gml_highlight = GUI::Action::create_checkable("GML", [&](auto&) {
+ m_editor->set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>());
+ m_editor->update();
+ });
+ syntax_actions.add_action(*m_gml_highlight);
+ syntax_menu.add_action(*m_gml_highlight);
+
+ m_ini_highlight = GUI::Action::create_checkable("INI File", [&](auto&) {
+ m_editor->set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>());
+ m_editor->update();
+ });
+ syntax_actions.add_action(*m_ini_highlight);
+ syntax_menu.add_action(*m_ini_highlight);
+
+ m_shell_highlight = GUI::Action::create_checkable("Shell File", [&](auto&) {
+ m_editor->set_syntax_highlighter(make<GUI::ShellSyntaxHighlighter>());
+ m_editor->update();
+ });
+ syntax_actions.add_action(*m_shell_highlight);
+ syntax_menu.add_action(*m_shell_highlight);
+
+ auto& help_menu = menubar->add_menu("Help");
+ help_menu.add_action(GUI::CommonActions::make_about_action("Text Editor", GUI::Icon::default_icon("app-text-editor"), window()));
+
+ GUI::Application::the()->set_menubar(move(menubar));
+
+ toolbar.add_action(*m_new_action);
+ toolbar.add_action(*m_open_action);
+ toolbar.add_action(*m_save_action);
+
+ toolbar.add_separator();
+
+ toolbar.add_action(m_editor->cut_action());
+ toolbar.add_action(m_editor->copy_action());
+ toolbar.add_action(m_editor->paste_action());
+ toolbar.add_action(m_editor->delete_action());
+
+ toolbar.add_separator();
+
+ toolbar.add_action(m_editor->undo_action());
+ toolbar.add_action(m_editor->redo_action());
+}
+
+TextEditorWidget::~TextEditorWidget()
+{
+}
+
+void TextEditorWidget::set_path(const LexicalPath& lexical_path)
+{
+ m_path = lexical_path.string();
+ m_name = lexical_path.title();
+ m_extension = lexical_path.extension();
+
+ if (m_extension == "c" || m_extension == "cc" || m_extension == "cxx" || m_extension == "cpp" || m_extension == "h") {
+ m_cpp_highlight->activate();
+ } else if (m_extension == "js" || m_extension == "json") {
+ m_js_highlight->activate();
+ } else if (m_extension == "gml") {
+ m_gml_highlight->activate();
+ } else if (m_extension == "ini") {
+ m_ini_highlight->activate();
+ } else {
+ m_plain_text_highlight->activate();
+ }
+
+ if (m_auto_detect_preview_mode) {
+ if (m_extension == "md")
+ set_preview_mode(PreviewMode::Markdown);
+ else if (m_extension == "html")
+ set_preview_mode(PreviewMode::HTML);
+ else
+ set_preview_mode(PreviewMode::None);
+ }
+
+ update_title();
+}
+
+void TextEditorWidget::update_title()
+{
+ StringBuilder builder;
+ if (m_path.is_empty())
+ builder.append("Untitled");
+ else
+ builder.append(m_path);
+ if (m_document_dirty)
+ builder.append(" (*)");
+ builder.append(" - Text Editor");
+ window()->set_title(builder.to_string());
+}
+
+void TextEditorWidget::open_sesame(const String& path)
+{
+ auto file = Core::File::construct(path);
+ if (!file->open(Core::IODevice::ReadOnly) && file->error() != ENOENT) {
+ GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: {}", path, strerror(errno)), "Error", GUI::MessageBox::Type::Error);
+ return;
+ }
+
+ m_editor->set_text(file->read_all());
+ m_document_dirty = false;
+ m_document_opening = true;
+
+ set_path(LexicalPath(path));
+
+ m_editor->set_focus(true);
+}
+
+bool TextEditorWidget::request_close()
+{
+ if (!m_document_dirty)
+ return true;
+ auto result = GUI::MessageBox::show(window(), "The document has been modified. Would you like to save?", "Unsaved changes", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
+
+ if (result == GUI::MessageBox::ExecYes) {
+ m_save_action->activate();
+ return true;
+ }
+
+ if (result == GUI::MessageBox::ExecNo)
+ return true;
+
+ return false;
+}
+
+void TextEditorWidget::drop_event(GUI::DropEvent& event)
+{
+ event.accept();
+ window()->move_to_front();
+
+ if (event.mime_data().has_urls()) {
+ auto urls = event.mime_data().urls();
+ if (urls.is_empty())
+ return;
+ if (urls.size() > 1) {
+ GUI::MessageBox::show(window(), "TextEditor can only open one file at a time!", "One at a time please!", GUI::MessageBox::Type::Error);
+ return;
+ }
+ open_sesame(urls.first().path());
+ }
+}
+
+void TextEditorWidget::set_preview_mode(PreviewMode mode)
+{
+ if (m_preview_mode == mode)
+ return;
+ m_preview_mode = mode;
+
+ if (m_preview_mode == PreviewMode::HTML) {
+ m_html_preview_action->set_checked(true);
+ m_page_view->set_visible(true);
+ update_html_preview();
+ } else if (m_preview_mode == PreviewMode::Markdown) {
+ m_markdown_preview_action->set_checked(true);
+ m_page_view->set_visible(true);
+ update_markdown_preview();
+ } else {
+ m_no_preview_action->set_checked(true);
+ m_page_view->set_visible(false);
+ }
+}
+
+void TextEditorWidget::update_preview()
+{
+ switch (m_preview_mode) {
+ case PreviewMode::Markdown:
+ update_markdown_preview();
+ break;
+ case PreviewMode::HTML:
+ update_html_preview();
+ break;
+ default:
+ break;
+ }
+}
+
+void TextEditorWidget::update_markdown_preview()
+{
+ auto document = Markdown::Document::parse(m_editor->text());
+ if (document) {
+ auto html = document->render_to_html();
+ auto current_scroll_pos = m_page_view->visible_content_rect();
+ m_page_view->load_html(html, URL::create_with_file_protocol(m_path));
+ m_page_view->scroll_into_view(current_scroll_pos, true, true);
+ }
+}
+
+void TextEditorWidget::update_html_preview()
+{
+ auto current_scroll_pos = m_page_view->visible_content_rect();
+ m_page_view->load_html(m_editor->text(), URL::create_with_file_protocol(m_path));
+ m_page_view->scroll_into_view(current_scroll_pos, true, true);
+}
+
+void TextEditorWidget::update_statusbar_cursor_position()
+{
+ StringBuilder builder;
+ builder.appendff("Line: {}, Column: {}", m_editor->cursor().line() + 1, m_editor->cursor().column());
+ m_statusbar->set_text(builder.to_string());
+}
diff --git a/Userland/Applications/TextEditor/TextEditorWidget.h b/Userland/Applications/TextEditor/TextEditorWidget.h
new file mode 100644
index 0000000000..763087b9e4
--- /dev/null
+++ b/Userland/Applications/TextEditor/TextEditorWidget.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/LexicalPath.h>
+#include <LibGUI/ActionGroup.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGUI/Widget.h>
+#include <LibGUI/Window.h>
+#include <LibWeb/Forward.h>
+
+class TextEditorWidget final : public GUI::Widget {
+ C_OBJECT(TextEditorWidget)
+public:
+ virtual ~TextEditorWidget() override;
+ void open_sesame(const String& path);
+ bool request_close();
+
+ GUI::TextEditor& editor() { return *m_editor; }
+
+ enum class PreviewMode {
+ None,
+ Markdown,
+ HTML,
+ };
+
+ void set_preview_mode(PreviewMode);
+ void set_auto_detect_preview_mode(bool value) { m_auto_detect_preview_mode = value; }
+
+ void update_title();
+
+private:
+ TextEditorWidget();
+ void set_path(const LexicalPath& file);
+ void update_preview();
+ void update_markdown_preview();
+ void update_html_preview();
+ void update_statusbar_cursor_position();
+
+ virtual void drop_event(GUI::DropEvent&) override;
+
+ RefPtr<GUI::TextEditor> m_editor;
+ String m_path;
+ String m_name;
+ String m_extension;
+ RefPtr<GUI::Action> m_new_action;
+ RefPtr<GUI::Action> m_open_action;
+ RefPtr<GUI::Action> m_save_action;
+ RefPtr<GUI::Action> m_save_as_action;
+ RefPtr<GUI::Action> m_find_replace_action;
+ RefPtr<GUI::Action> m_line_wrapping_setting_action;
+ RefPtr<GUI::Action> m_vim_emulation_setting_action;
+
+ RefPtr<GUI::Action> m_find_next_action;
+ RefPtr<GUI::Action> m_find_regex_action;
+ RefPtr<GUI::Action> m_find_previous_action;
+ RefPtr<GUI::Action> m_replace_next_action;
+ RefPtr<GUI::Action> m_replace_previous_action;
+ RefPtr<GUI::Action> m_replace_all_action;
+
+ GUI::ActionGroup m_preview_actions;
+ RefPtr<GUI::Action> m_no_preview_action;
+ RefPtr<GUI::Action> m_markdown_preview_action;
+ RefPtr<GUI::Action> m_html_preview_action;
+
+ RefPtr<GUI::StatusBar> m_statusbar;
+
+ RefPtr<GUI::TextBox> m_find_textbox;
+ RefPtr<GUI::TextBox> m_replace_textbox;
+ RefPtr<GUI::Button> m_find_previous_button;
+ RefPtr<GUI::Button> m_find_next_button;
+ RefPtr<GUI::Button> m_find_regex_button;
+ RefPtr<GUI::Button> m_replace_previous_button;
+ RefPtr<GUI::Button> m_replace_next_button;
+ RefPtr<GUI::Button> m_replace_all_button;
+ RefPtr<GUI::Widget> m_find_replace_widget;
+ RefPtr<GUI::Widget> m_find_widget;
+ RefPtr<GUI::Widget> m_replace_widget;
+
+ GUI::ActionGroup syntax_actions;
+ RefPtr<GUI::Action> m_plain_text_highlight;
+ RefPtr<GUI::Action> m_cpp_highlight;
+ RefPtr<GUI::Action> m_js_highlight;
+ RefPtr<GUI::Action> m_gml_highlight;
+ RefPtr<GUI::Action> m_ini_highlight;
+ RefPtr<GUI::Action> m_shell_highlight;
+
+ RefPtr<Web::OutOfProcessWebView> m_page_view;
+
+ bool m_document_dirty { false };
+ bool m_document_opening { false };
+ bool m_auto_detect_preview_mode { false };
+ bool m_find_use_regex { false };
+
+ PreviewMode m_preview_mode { PreviewMode::None };
+};
diff --git a/Userland/Applications/TextEditor/TextEditorWindow.gml b/Userland/Applications/TextEditor/TextEditorWindow.gml
new file mode 100644
index 0000000000..433d01abff
--- /dev/null
+++ b/Userland/Applications/TextEditor/TextEditorWindow.gml
@@ -0,0 +1,88 @@
+@GUI::Widget {
+ name: "main"
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ spacing: 2
+ }
+
+ @GUI::ToolBarContainer {
+ @GUI::ToolBar {
+ name: "toolbar"
+ }
+ }
+
+ @GUI::HorizontalSplitter {
+ @GUI::TextEditor {
+ name: "editor"
+ }
+
+ @Web::OutOfProcessWebView {
+ name: "webview"
+ visible: false
+ }
+ }
+
+ @GUI::Widget {
+ name: "find_replace_widget"
+ visible: false
+ fill_with_background_color: true
+ fixed_height: 48
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [2, 2, 2, 4]
+ }
+
+ @GUI::Widget {
+ name: "find_widget"
+ fill_with_background_color: true
+ fixed_height: 22
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Button {
+ name: "find_previous_button"
+ text: "Find previous"
+ fixed_width: 150
+ }
+
+ @GUI::Button {
+ name: "find_next_button"
+ text: "Find next"
+ fixed_width: 150
+ }
+ }
+
+ @GUI::Widget {
+ name: "replace_widget"
+ fill_with_background_color: true
+ fixed_height: 22
+
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ @GUI::Button {
+ name: "replace_previous_button"
+ text: "Replace previous"
+ fixed_width: 100
+ }
+
+ @GUI::Button {
+ name: "replace_next_button"
+ text: "Replace next"
+ fixed_width: 100
+ }
+
+ @GUI::Button {
+ name: "replace_all_button"
+ text: "Replace all"
+ fixed_width: 100
+ }
+ }
+ }
+
+ @GUI::StatusBar {
+ name: "statusbar"
+ }
+}
diff --git a/Userland/Applications/TextEditor/main.cpp b/Userland/Applications/TextEditor/main.cpp
new file mode 100644
index 0000000000..8e9c8fdfb8
--- /dev/null
+++ b/Userland/Applications/TextEditor/main.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TextEditorWidget.h"
+#include <LibCore/ArgsParser.h>
+#include <LibGfx/Bitmap.h>
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ const char* preview_mode = "auto";
+ const char* file_to_edit = nullptr;
+ Core::ArgsParser parser;
+ parser.add_option(preview_mode, "Preview mode, one of 'none', 'html', 'markdown', 'auto'", "preview-mode", '\0', "mode");
+ parser.add_positional_argument(file_to_edit, "File to edit", "file", Core::ArgsParser::Required::No);
+
+ parser.parse(argc, argv);
+
+ StringView preview_mode_view = preview_mode;
+
+ auto app_icon = GUI::Icon::default_icon("app-text-editor");
+
+ auto window = GUI::Window::construct();
+ window->resize(640, 400);
+
+ auto& text_widget = window->set_main_widget<TextEditorWidget>();
+
+ text_widget.editor().set_focus(true);
+
+ window->on_close_request = [&]() -> GUI::Window::CloseRequestDecision {
+ if (text_widget.request_close())
+ return GUI::Window::CloseRequestDecision::Close;
+ return GUI::Window::CloseRequestDecision::StayOpen;
+ };
+
+ if (preview_mode_view == "auto") {
+ text_widget.set_auto_detect_preview_mode(true);
+ } else if (preview_mode_view == "markdown") {
+ text_widget.set_preview_mode(TextEditorWidget::PreviewMode::Markdown);
+ } else if (preview_mode_view == "html") {
+ text_widget.set_preview_mode(TextEditorWidget::PreviewMode::HTML);
+ } else if (preview_mode_view == "none") {
+ text_widget.set_preview_mode(TextEditorWidget::PreviewMode::None);
+ } else {
+ warnln("Invalid mode '{}'", preview_mode);
+ return 1;
+ }
+
+ if (file_to_edit)
+ text_widget.open_sesame(file_to_edit);
+ else
+ text_widget.update_title();
+
+ window->show();
+ window->set_icon(app_icon.bitmap_for_size(16));
+
+ return app->exec();
+}
diff --git a/Userland/Applications/ThemeEditor/CMakeLists.txt b/Userland/Applications/ThemeEditor/CMakeLists.txt
new file mode 100644
index 0000000000..94ca0e1f3e
--- /dev/null
+++ b/Userland/Applications/ThemeEditor/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES
+ PreviewWidget.cpp
+ main.cpp
+)
+
+serenity_app(ThemeEditor ICON app-theme-editor)
+target_link_libraries(ThemeEditor LibGUI)
diff --git a/Userland/Applications/ThemeEditor/PreviewWidget.cpp b/Userland/Applications/ThemeEditor/PreviewWidget.cpp
new file mode 100644
index 0000000000..a57da75936
--- /dev/null
+++ b/Userland/Applications/ThemeEditor/PreviewWidget.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PreviewWidget.h"
+#include <AK/StringView.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/CheckBox.h>
+#include <LibGUI/Painter.h>
+#include <LibGUI/RadioButton.h>
+#include <LibGUI/StatusBar.h>
+#include <LibGUI/TextEditor.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/WindowTheme.h>
+
+namespace ThemeEditor {
+
+class MiniWidgetGallery final : public GUI::Widget {
+ C_OBJECT(MiniWidgetGallery);
+
+public:
+ void set_preview_palette(const Gfx::Palette& palette)
+ {
+ set_palette(palette);
+ Function<void(GUI::Widget&)> recurse = [&](GUI::Widget& parent_widget) {
+ parent_widget.for_each_child_widget([&](auto& widget) {
+ widget.set_palette(palette);
+ recurse(widget);
+ return IterationDecision::Continue;
+ });
+ };
+ recurse(*this);
+ }
+
+private:
+ MiniWidgetGallery()
+ {
+ set_fill_with_background_color(true);
+ m_button = add<GUI::Button>();
+ m_button->set_text("Button");
+ m_checkbox = add<GUI::CheckBox>();
+ m_checkbox->set_text("Check box");
+ m_radio = add<GUI::RadioButton>();
+ m_radio->set_text("Radio button");
+ m_statusbar = add<GUI::StatusBar>();
+ m_statusbar->set_text("Status bar");
+ m_editor = add<GUI::TextEditor>();
+ m_editor->set_text("Text editor\nwith multiple\nlines.");
+ }
+
+ virtual void resize_event(GUI::ResizeEvent&) override
+ {
+ m_editor->set_relative_rect(10, 70, 200, 140);
+ m_button->set_relative_rect(10, 10, 200, 20);
+ m_checkbox->set_relative_rect(10, 30, 200, 20);
+ m_radio->set_relative_rect(10, 50, 200, 20);
+ m_statusbar->set_relative_rect(0, height() - 16, width(), 16);
+ }
+
+ RefPtr<GUI::TextEditor> m_editor;
+ RefPtr<GUI::Button> m_button;
+ RefPtr<GUI::CheckBox> m_checkbox;
+ RefPtr<GUI::RadioButton> m_radio;
+ RefPtr<GUI::StatusBar> m_statusbar;
+};
+
+PreviewWidget::PreviewWidget(const Gfx::Palette& preview_palette)
+ : m_preview_palette(preview_palette)
+{
+ m_active_window_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window.png");
+ m_inactive_window_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window.png");
+
+ m_close_bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-close.png");
+ m_maximize_bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/upward-triangle.png");
+ m_minimize_bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/downward-triangle.png");
+
+ m_gallery = add<MiniWidgetGallery>();
+ set_greedy_for_hits(true);
+}
+
+PreviewWidget::~PreviewWidget()
+{
+}
+
+void PreviewWidget::set_preview_palette(const Gfx::Palette& palette)
+{
+ m_preview_palette = palette;
+ m_gallery->set_preview_palette(palette);
+ update();
+}
+
+void PreviewWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+ GUI::Painter painter(*this);
+
+ painter.add_clip_rect(event.rect());
+ painter.add_clip_rect(frame_inner_rect());
+
+ painter.fill_rect(frame_inner_rect(), m_preview_palette.desktop_background());
+
+ struct Button {
+ Gfx::IntRect rect;
+ RefPtr<Gfx::Bitmap> bitmap;
+ };
+
+ auto paint_window = [&](auto& title, const Gfx::IntRect& rect, auto state, const Gfx::Bitmap& icon) {
+ int window_button_width = m_preview_palette.window_title_button_width();
+ int window_button_height = m_preview_palette.window_title_button_height();
+ auto title_bar_text_rect = Gfx::WindowTheme::current().title_bar_text_rect(Gfx::WindowTheme::WindowType::Normal, rect, m_preview_palette);
+ int pos = title_bar_text_rect.right() + 1;
+
+ Vector<Button> buttons;
+ buttons.append(Button { {}, m_close_bitmap });
+ buttons.append(Button { {}, m_maximize_bitmap });
+ buttons.append(Button { {}, m_minimize_bitmap });
+
+ for (auto& button : buttons) {
+ pos -= window_button_width;
+ Gfx::IntRect rect { pos, 0, window_button_width, window_button_height };
+ rect.center_vertically_within(title_bar_text_rect);
+ button.rect = rect;
+ }
+
+ auto frame_rect = Gfx::WindowTheme::current().frame_rect_for_window(Gfx::WindowTheme::WindowType::Normal, rect, m_preview_palette);
+ Gfx::PainterStateSaver saver(painter);
+ painter.translate(frame_rect.location());
+ Gfx::WindowTheme::current().paint_normal_frame(painter, state, rect, title, icon, m_preview_palette, buttons.last().rect);
+
+ for (auto& button : buttons) {
+ Gfx::StylePainter::paint_button(painter, button.rect, m_preview_palette, Gfx::ButtonStyle::Normal, false);
+ auto bitmap_rect = button.bitmap->rect();
+ bitmap_rect.center_within(button.rect);
+ painter.blit(bitmap_rect.location(), *button.bitmap, button.bitmap->rect());
+ }
+ };
+
+ Gfx::IntRect active_rect { 0, 0, 320, 240 };
+ active_rect.center_within(frame_inner_rect());
+ Gfx::IntRect inactive_rect = active_rect.translated(-20, -20);
+
+ paint_window("Inactive window", inactive_rect, Gfx::WindowTheme::WindowState::Inactive, *m_active_window_icon);
+ paint_window("Active window", active_rect, Gfx::WindowTheme::WindowState::Active, *m_inactive_window_icon);
+}
+
+void PreviewWidget::resize_event(GUI::ResizeEvent&)
+{
+ Gfx::IntRect gallery_rect { 0, 0, 320, 240 };
+ gallery_rect.center_within(rect());
+ m_gallery->set_relative_rect(gallery_rect);
+}
+
+}
diff --git a/Userland/Applications/ThemeEditor/PreviewWidget.h b/Userland/Applications/ThemeEditor/PreviewWidget.h
new file mode 100644
index 0000000000..41ef25e0a8
--- /dev/null
+++ b/Userland/Applications/ThemeEditor/PreviewWidget.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+#include <LibGfx/Palette.h>
+
+namespace ThemeEditor {
+
+class MiniWidgetGallery;
+
+class PreviewWidget final : public GUI::Frame {
+ C_OBJECT(PreviewWidget);
+
+public:
+ virtual ~PreviewWidget() override;
+
+ const Gfx::Palette& preview_palette() const { return m_preview_palette; }
+ void set_preview_palette(const Gfx::Palette&);
+
+private:
+ explicit PreviewWidget(const Gfx::Palette&);
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ Gfx::Palette m_preview_palette;
+
+ RefPtr<Gfx::Bitmap> m_active_window_icon;
+ RefPtr<Gfx::Bitmap> m_inactive_window_icon;
+
+ RefPtr<MiniWidgetGallery> m_gallery;
+
+ RefPtr<Gfx::Bitmap> m_close_bitmap;
+ RefPtr<Gfx::Bitmap> m_maximize_bitmap;
+ RefPtr<Gfx::Bitmap> m_minimize_bitmap;
+};
+
+}
diff --git a/Userland/Applications/ThemeEditor/main.cpp b/Userland/Applications/ThemeEditor/main.cpp
new file mode 100644
index 0000000000..d4a5936770
--- /dev/null
+++ b/Userland/Applications/ThemeEditor/main.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PreviewWidget.h"
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/ColorInput.h>
+#include <LibGUI/ComboBox.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Window.h>
+
+class ColorRoleModel final : public GUI::Model {
+public:
+ virtual int row_count(const GUI::ModelIndex&) const { return m_color_roles.size(); }
+ virtual int column_count(const GUI::ModelIndex&) const { return 1; }
+ virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role = GUI::ModelRole::Display) const
+ {
+ if (role == GUI::ModelRole::Display)
+ return Gfx::to_string(m_color_roles[(size_t)index.row()]);
+ return {};
+ }
+ virtual void update() { did_update(); }
+
+ explicit ColorRoleModel(const Vector<Gfx::ColorRole>& color_roles)
+ : m_color_roles(color_roles)
+ {
+ }
+
+ Gfx::ColorRole color_role(const GUI::ModelIndex& index) const
+ {
+ return m_color_roles[index.row()];
+ }
+
+ Gfx::ColorRole color_role(size_t index) const
+ {
+ return m_color_roles[index];
+ }
+
+private:
+ const Vector<Gfx::ColorRole>& m_color_roles;
+};
+
+int main(int argc, char** argv)
+{
+
+ if (pledge("stdio thread rpath accept cpath wpath shared_buffer unix fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio thread rpath accept shared_buffer", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ if (unveil(nullptr, nullptr) < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ auto app_icon = GUI::Icon::default_icon("app-theme-editor");
+
+ Gfx::Palette preview_palette = app->palette();
+
+ auto window = GUI::Window::construct();
+ auto& main_widget = window->set_main_widget<GUI::Widget>();
+ main_widget.set_fill_with_background_color(true);
+ main_widget.set_layout<GUI::VerticalBoxLayout>();
+
+ auto& preview_widget = main_widget.add<ThemeEditor::PreviewWidget>(app->palette());
+ preview_widget.set_fixed_size(480, 360);
+
+ auto& horizontal_container = main_widget.add<GUI::Widget>();
+ horizontal_container.set_layout<GUI::HorizontalBoxLayout>();
+ horizontal_container.set_fixed_size(480, 20);
+
+ auto& combo_box = horizontal_container.add<GUI::ComboBox>();
+ auto& color_input = horizontal_container.add<GUI::ColorInput>();
+
+ Vector<Gfx::ColorRole> color_roles;
+#define __ENUMERATE_COLOR_ROLE(role) color_roles.append(Gfx::ColorRole::role);
+ ENUMERATE_COLOR_ROLES(__ENUMERATE_COLOR_ROLE)
+#undef __ENUMERATE_COLOR_ROLE
+
+ combo_box.set_only_allow_values_from_model(true);
+ combo_box.set_model(adopt(*new ColorRoleModel(color_roles)));
+ combo_box.on_change = [&](auto&, auto& index) {
+ auto role = static_cast<const ColorRoleModel*>(index.model())->color_role(index);
+ color_input.set_color(preview_palette.color(role));
+ };
+
+ combo_box.set_selected_index((size_t)Gfx::ColorRole::Window - 1);
+
+ color_input.on_change = [&] {
+ auto role = static_cast<const ColorRoleModel*>(combo_box.model())->color_role(combo_box.selected_index());
+ preview_palette.set_color(role, color_input.color());
+ preview_widget.set_preview_palette(preview_palette);
+ };
+
+ window->resize(480, 500);
+ window->show();
+ window->set_title("Theme Editor");
+ window->set_icon(app_icon.bitmap_for_size(16));
+ return app->exec();
+}
diff --git a/Userland/Applications/Welcome/BackgroundWidget.cpp b/Userland/Applications/Welcome/BackgroundWidget.cpp
new file mode 100644
index 0000000000..c64f2fc93b
--- /dev/null
+++ b/Userland/Applications/Welcome/BackgroundWidget.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BackgroundWidget.h"
+#include <LibGUI/Painter.h>
+#include <LibGfx/Color.h>
+#include <LibGfx/Palette.h>
+
+BackgroundWidget::BackgroundWidget()
+{
+}
+
+BackgroundWidget::~BackgroundWidget()
+{
+}
+
+void BackgroundWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+ painter.fill_rect_with_gradient(event.rect(), Color::from_rgb(0xccdddd), Color::from_rgb(0xdcdcde));
+}
+
+void BackgroundWidget::resize_event(GUI::ResizeEvent& event)
+{
+ GUI::Widget::resize_event(event);
+}
diff --git a/Userland/Applications/Welcome/BackgroundWidget.h b/Userland/Applications/Welcome/BackgroundWidget.h
new file mode 100644
index 0000000000..fea3871edc
--- /dev/null
+++ b/Userland/Applications/Welcome/BackgroundWidget.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Frame.h>
+
+class BackgroundWidget : public GUI::Frame {
+ C_OBJECT(BackgroundWidget)
+public:
+ virtual ~BackgroundWidget() override;
+
+private:
+ BackgroundWidget();
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+};
diff --git a/Userland/Applications/Welcome/CMakeLists.txt b/Userland/Applications/Welcome/CMakeLists.txt
new file mode 100644
index 0000000000..e9d1ef3380
--- /dev/null
+++ b/Userland/Applications/Welcome/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES
+ BackgroundWidget.cpp
+ main.cpp
+ TextWidget.cpp
+ UnuncheckableButton.cpp
+)
+
+serenity_bin(Welcome)
+target_link_libraries(Welcome LibGUI)
diff --git a/Userland/Applications/Welcome/TextWidget.cpp b/Userland/Applications/Welcome/TextWidget.cpp
new file mode 100644
index 0000000000..263d4514f8
--- /dev/null
+++ b/Userland/Applications/Welcome/TextWidget.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "TextWidget.h"
+#include <AK/Optional.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Vector.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+
+TextWidget::TextWidget(const StringView& text)
+ : m_text(text)
+{
+ set_frame_thickness(0);
+ set_frame_shadow(Gfx::FrameShadow::Plain);
+ set_frame_shape(Gfx::FrameShape::NoFrame);
+}
+
+TextWidget::~TextWidget()
+{
+}
+
+void TextWidget::set_text(const StringView& text)
+{
+ if (text == m_text)
+ return;
+ m_text = text;
+ wrap_and_set_height();
+ update();
+}
+
+void TextWidget::paint_event(GUI::PaintEvent& event)
+{
+ GUI::Frame::paint_event(event);
+
+ GUI::Painter painter(*this);
+ painter.add_clip_rect(event.rect());
+
+ int indent = 0;
+ if (frame_thickness() > 0)
+ indent = font().glyph_width('x') / 2;
+
+ for (size_t i = 0; i < m_lines.size(); i++) {
+ auto& line = m_lines[i];
+
+ auto text_rect = frame_inner_rect();
+ text_rect.move_by(indent, i * m_line_height);
+ if (!line.is_empty())
+ text_rect.set_width(text_rect.width() - indent * 2);
+
+ if (is_enabled()) {
+ painter.draw_text(text_rect, line, m_text_alignment, palette().color(foreground_role()), Gfx::TextElision::None);
+ } else {
+ painter.draw_text(text_rect.translated(1, 1), line, font(), text_alignment(), Color::White, Gfx::TextElision::Right);
+ painter.draw_text(text_rect, line, font(), text_alignment(), Color::from_rgb(0x808080), Gfx::TextElision::Right);
+ }
+ }
+}
+
+void TextWidget::resize_event(GUI::ResizeEvent& event)
+{
+ wrap_and_set_height();
+ GUI::Widget::resize_event(event);
+}
+
+void TextWidget::wrap_and_set_height()
+{
+ Vector<String> words;
+ Optional<size_t> start;
+ for (size_t i = 0; i < m_text.length(); i++) {
+ auto ch = m_text[i];
+
+ if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
+ if (start.has_value())
+ words.append(m_text.substring(start.value(), i - start.value()));
+ start.clear();
+ } else if (!start.has_value()) {
+ start = i;
+ }
+ }
+ if (start.has_value())
+ words.append(m_text.substring(start.value(), m_text.length() - start.value()));
+
+ auto rect = frame_inner_rect();
+ if (frame_thickness() > 0)
+ rect.set_width(rect.width() - font().glyph_width('x'));
+
+ StringBuilder builder;
+ Vector<String> lines;
+ int line_width = 0;
+ for (auto& word : words) {
+ int word_width = font().width(word);
+ if (line_width != 0)
+ word_width += font().glyph_width('x');
+
+ if (line_width + word_width > rect.width()) {
+ lines.append(builder.to_string());
+ builder.clear();
+ line_width = 0;
+ }
+
+ if (line_width != 0)
+ builder.append(' ');
+ builder.append(word);
+ line_width += word_width;
+ }
+ auto last_line = builder.to_string();
+ if (!last_line.is_empty()) {
+ lines.append(last_line);
+ }
+
+ m_lines = lines;
+
+ set_fixed_height(m_lines.size() * m_line_height + frame_thickness() * 2);
+}
diff --git a/Userland/Applications/Welcome/TextWidget.h b/Userland/Applications/Welcome/TextWidget.h
new file mode 100644
index 0000000000..c4353096ff
--- /dev/null
+++ b/Userland/Applications/Welcome/TextWidget.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <AK/Vector.h>
+#include <LibGUI/Frame.h>
+#include <LibGfx/TextAlignment.h>
+
+class TextWidget : public GUI::Frame {
+ C_OBJECT(TextWidget);
+
+public:
+ virtual ~TextWidget() override;
+
+ String text() const { return m_text; }
+ void set_text(const StringView&);
+
+ Gfx::TextAlignment text_alignment() const { return m_text_alignment; }
+ void set_text_alignment(Gfx::TextAlignment text_alignment) { m_text_alignment = text_alignment; }
+
+ bool should_wrap() const { return m_should_wrap; }
+ void set_should_wrap(bool should_wrap) { m_should_wrap = should_wrap; }
+
+ int line_height() const { return m_line_height; }
+ void set_line_height(int line_height) { m_line_height = line_height; }
+
+ void wrap_and_set_height();
+
+private:
+ explicit TextWidget(const StringView& text = {});
+
+ virtual void paint_event(GUI::PaintEvent&) override;
+ virtual void resize_event(GUI::ResizeEvent&) override;
+
+ String m_text;
+ Vector<String> m_lines;
+ Gfx::TextAlignment m_text_alignment { Gfx::TextAlignment::Center };
+ bool m_should_wrap { false };
+ int m_line_height { 0 };
+};
diff --git a/Userland/Applications/Welcome/UnuncheckableButton.cpp b/Userland/Applications/Welcome/UnuncheckableButton.cpp
new file mode 100644
index 0000000000..29194fff81
--- /dev/null
+++ b/Userland/Applications/Welcome/UnuncheckableButton.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "UnuncheckableButton.h"
+
+UnuncheckableButton::UnuncheckableButton()
+{
+}
+
+UnuncheckableButton::~UnuncheckableButton()
+{
+}
diff --git a/Userland/Applications/Welcome/UnuncheckableButton.h b/Userland/Applications/Welcome/UnuncheckableButton.h
new file mode 100644
index 0000000000..b91a41ef9c
--- /dev/null
+++ b/Userland/Applications/Welcome/UnuncheckableButton.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Button.h>
+
+class UnuncheckableButton : public GUI::Button {
+ C_OBJECT(UnuncheckableButton)
+public:
+ virtual ~UnuncheckableButton() override;
+
+ virtual bool is_uncheckable() const override { return false; }
+
+private:
+ UnuncheckableButton();
+};
diff --git a/Userland/Applications/Welcome/main.cpp b/Userland/Applications/Welcome/main.cpp
new file mode 100644
index 0000000000..101b35fd97
--- /dev/null
+++ b/Userland/Applications/Welcome/main.cpp
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "BackgroundWidget.h"
+#include "TextWidget.h"
+#include "UnuncheckableButton.h"
+#include <AK/ByteBuffer.h>
+#include <AK/Optional.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/Vector.h>
+#include <LibCore/File.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/ImageWidget.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/StackWidget.h>
+#include <LibGUI/Window.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <stdio.h>
+#include <unistd.h>
+
+struct ContentPage {
+ String menu_name;
+ String title;
+ String icon = String::empty();
+ Vector<String> content;
+};
+
+static Optional<Vector<ContentPage>> parse_welcome_file(const String& path)
+{
+ auto file = Core::File::construct(path);
+ if (!file->open(Core::IODevice::ReadOnly))
+ return {};
+
+ Vector<ContentPage> pages;
+ StringBuilder current_output_line;
+ bool started = false;
+ ContentPage current;
+ while (file->can_read_line()) {
+ auto line = file->read_line();
+ if (line.is_empty()) {
+ if (!current_output_line.to_string().is_empty())
+ current.content.append(current_output_line.to_string());
+ current_output_line.clear();
+ continue;
+ }
+ switch (line[0]) {
+ case '*':
+ dbgln("menu_item line:\t{}", line);
+ if (started)
+ pages.append(current);
+ else
+ started = true;
+
+ current = {};
+ current.menu_name = line.substring(2, line.length() - 2);
+ break;
+ case '$':
+ dbgln("icon line: \t{}", line);
+ current.icon = line.substring(2, line.length() - 2);
+ break;
+ case '>':
+ dbgln("title line:\t{}", line);
+ current.title = line.substring(2, line.length() - 2);
+ break;
+ case '#':
+ dbgln("comment line:\t{}", line);
+ break;
+ default:
+ dbgln("content line:\t", line);
+ if (current_output_line.length() != 0)
+ current_output_line.append(' ');
+ current_output_line.append(line);
+ break;
+ }
+ }
+
+ if (started) {
+ current.content.append(current_output_line.to_string());
+ pages.append(current);
+ }
+
+ return pages;
+}
+
+int main(int argc, char** argv)
+{
+ if (pledge("stdio shared_buffer rpath unix cpath fattr", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ auto app = GUI::Application::construct(argc, argv);
+
+ if (pledge("stdio shared_buffer rpath", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ if (unveil("/res", "r") < 0) {
+ perror("unveil");
+ return 1;
+ }
+
+ unveil(nullptr, nullptr);
+
+ Optional<Vector<ContentPage>> _pages = parse_welcome_file("/res/welcome.txt");
+ if (!_pages.has_value()) {
+ GUI::MessageBox::show(nullptr, "Could not open Welcome file.", "Welcome", GUI::MessageBox::Type::Error);
+ return 1;
+ }
+ auto pages = _pages.value();
+
+ auto window = GUI::Window::construct();
+ window->set_title("Welcome");
+ window->resize(640, 360);
+ window->center_on_screen();
+
+ auto& background = window->set_main_widget<BackgroundWidget>();
+ background.set_fill_with_background_color(false);
+ background.set_layout<GUI::VerticalBoxLayout>();
+ background.layout()->set_margins({ 16, 8, 16, 8 });
+ background.layout()->set_spacing(8);
+
+ //
+ // header
+ //
+
+ auto& header = background.add<GUI::Label>();
+ header.set_font(Gfx::Font::load_from_file("/res/fonts/PebbletonBold14.font"));
+ header.set_text("Welcome to SerenityOS!");
+ header.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ header.set_fixed_height(30);
+
+ //
+ // main section
+ //
+
+ auto& main_section = background.add<GUI::Widget>();
+ main_section.set_layout<GUI::HorizontalBoxLayout>();
+ main_section.layout()->set_margins({ 0, 0, 0, 0 });
+ main_section.layout()->set_spacing(8);
+
+ auto& menu = main_section.add<GUI::Widget>();
+ menu.set_layout<GUI::VerticalBoxLayout>();
+ menu.layout()->set_margins({ 0, 0, 0, 0 });
+ menu.layout()->set_spacing(4);
+ menu.set_fixed_width(100);
+
+ auto& stack = main_section.add<GUI::StackWidget>();
+
+ bool first = true;
+ for (auto& page : pages) {
+ auto& content = stack.add<GUI::Widget>();
+ content.set_layout<GUI::VerticalBoxLayout>();
+ content.layout()->set_margins({ 0, 0, 0, 0 });
+ content.layout()->set_spacing(8);
+
+ auto& title_box = content.add<GUI::Widget>();
+ title_box.set_layout<GUI::HorizontalBoxLayout>();
+ title_box.layout()->set_spacing(4);
+ title_box.set_fixed_height(16);
+
+ if (!page.icon.is_empty()) {
+ auto& icon = title_box.add<GUI::ImageWidget>();
+ icon.set_fixed_size(16, 16);
+ icon.load_from_file(page.icon);
+ }
+
+ auto& content_title = title_box.add<GUI::Label>();
+ content_title.set_font(Gfx::FontDatabase::default_bold_font());
+ content_title.set_text(page.title);
+ content_title.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ content_title.set_fixed_height(10);
+
+ for (auto& paragraph : page.content) {
+ auto& content_text = content.add<TextWidget>();
+ content_text.set_font(Gfx::FontDatabase::default_font());
+ content_text.set_text(paragraph);
+ content_text.set_text_alignment(Gfx::TextAlignment::TopLeft);
+ content_text.set_line_height(12);
+ content_text.wrap_and_set_height();
+ }
+
+ auto& menu_option = menu.add<UnuncheckableButton>();
+ menu_option.set_font(Gfx::FontDatabase::default_font());
+ menu_option.set_text(page.menu_name);
+ menu_option.set_text_alignment(Gfx::TextAlignment::CenterLeft);
+ menu_option.set_fixed_height(20);
+ menu_option.set_checkable(true);
+ menu_option.set_exclusive(true);
+
+ if (first)
+ menu_option.set_checked(true);
+
+ menu_option.on_click = [content = &content, &stack](auto) {
+ stack.set_active_widget(content);
+ content->invalidate_layout();
+ };
+
+ first = false;
+ }
+
+ window->show();
+ return app->exec();
+}