1 module editor; 2 3 import pngtext.pngtext; 4 5 import utils.misc; 6 7 import qui.qui; 8 import qui.widgets; 9 10 import std.stdio; 11 import std.conv : to; 12 import std.path; 13 14 /// shortcuts text 15 private enum STATUSBAR_SHORTCUTS = "^C - Exit ^O - Save;"; 16 /// prefix text for file saved label 17 private enum STATUSBAR_FILESAVED_PREFIX = "File saved to "; 18 /// prefix text for quality label 19 private enum STATUSBAR_QUALITY_PREFIX = "Quality: "; 20 /// prefix text for number of bytes label 21 private enum STATUSBAR_BYTECOUNT_PREFIX = "Characters/Max/Limit: "; 22 /// text for when file failed to save 23 private enum STATUSBAR_FILESAVE_ERROR = "Error saving file"; 24 /// time (msecs) until "File saved to: ..." & errors disappears 25 private enum ERROR_DISAPPEAR_TIME = 500; 26 27 /// The top title display 28 private class TitleWidget : QLayout{ 29 private: 30 /// empty space on left 31 SplitterWidget _leftSplitter; 32 /// empty space on right 33 SplitterWidget _rightSplitter; 34 /// text 35 TextLabelWidget _titleLabel; 36 /// background color 37 Color _bgColor; 38 /// text color 39 Color _textColor; 40 public: 41 /// constructor 42 this(dstring text, Color fg = Color.black, Color bg = Color.white){ 43 super(QLayout.Type.Horizontal); 44 _leftSplitter = new SplitterWidget(); 45 _rightSplitter = new SplitterWidget(); 46 _titleLabel = new TextLabelWidget(); 47 this.text = text; 48 _leftSplitter.color = bg; 49 _rightSplitter.color = bg; 50 _titleLabel.backgroundColor = bg; 51 _titleLabel.textColor = fg; 52 this.size.maxHeight = 1; 53 this.size.minHeight = 1; 54 // exaggerate the sizeRatio of _titleLabel, so splitters can become as small as needed 55 _titleLabel.sizeRatio = 1000; 56 this.addWidget([_leftSplitter, _titleLabel, _rightSplitter]); 57 } 58 ~this(){ 59 .destroy(_leftSplitter); 60 .destroy(_rightSplitter); 61 .destroy(_titleLabel); 62 } 63 /// text displayed 64 @property dstring text(){ 65 return _titleLabel.caption; 66 } 67 /// ditto 68 @property dstring text(dstring newVal){ 69 _titleLabel.caption = newVal; 70 _titleLabel.size.maxWidth = newVal.length; 71 this.resizeEvent(); 72 return newVal; 73 } 74 } 75 76 /// To display a log with a title 77 private class LogPlusPlusWidget : QLayout{ 78 private: 79 /// to display a title at top of errors 80 TitleWidget _title; 81 /// log to show the errors 82 LogWidget _log; 83 public: 84 /// constructor 85 this(dstring title, uint lines, Color fg, Color bg){ 86 super(QLayout.Type.Vertical); 87 _title = new TitleWidget(title, bg, fg); 88 _log = new LogWidget(lines); 89 _log.textColor = fg; 90 _log.backgroundColor = bg; 91 _log.size.minHeight = lines; 92 _log.size.maxHeight = lines; 93 this.size.minHeight = lines+1; 94 this.size.maxHeight = lines+1; 95 this.addWidget([_title, _log]); 96 } 97 ~this(){ 98 .destroy(_title); 99 .destroy(_log); 100 } 101 /// clears error messages 102 void clear(){ 103 _log.clear; 104 } 105 /// adds an error message 106 void add(dstring message){ 107 _log.add(message); 108 } 109 /// whether this widget is shown or not 110 override @property bool show(bool newVal){ 111 return super.show = newVal; 112 } 113 /// ditto 114 override @property bool show(){ 115 return super.show; 116 } 117 } 118 119 /// A status bar. Displays TextLabelWidgets with spaces in between. 120 private class StatusBarWidget : QLayout{ 121 private: 122 TextLabelWidget[] _label; 123 SplitterWidget[] _splitter; 124 public: 125 this(ubyte count, Color fg, Color bg){ 126 assert(count, "count cannot be 0"); 127 super(QLayout.Type.Horizontal); 128 _label.length = count; 129 _splitter.length = count -1; 130 _label[0] = new TextLabelWidget(); 131 _label[0].sizeRatio = 1000; 132 _label[0].textColor = fg; 133 _label[0].backgroundColor = bg; 134 this.addWidget(_label[0]); 135 foreach (i; 1 .. count){ 136 _splitter[i-1] = new SplitterWidget(); 137 _splitter[i-1].color = bg; 138 this.addWidget(_splitter[i-1]); 139 _label[i] = new TextLabelWidget(); 140 _label[i].sizeRatio = 1000; 141 _label[i].textColor = fg; 142 _label[i].backgroundColor = bg; 143 this.addWidget(_label[i]); 144 } 145 this.size.maxHeight = 1; 146 } 147 ~this(){ 148 foreach (widget; _label) 149 .destroy(widget); 150 foreach(widget; _splitter) 151 .destroy(widget); 152 } 153 /// Sets text of a label 154 void setText(uint label, dstring newText){ 155 _label[label].caption = newText; 156 _label[label].size.maxWidth = newText.length; 157 requestResize(); 158 } 159 } 160 161 /// Memo with extra functions 162 private class MemoPlusPlusWidget : MemoWidget{ 163 public: 164 this(){ 165 super(true); 166 } 167 /// Calculates number of bytes required to store the characters typed 168 uint bytesCount(){ 169 uint r; 170 dstring[] linesArray = this.lines.toArray; 171 foreach (line; linesArray) 172 r += to!string(line).length; 173 r += lines.length; // for the \n at end of each line 174 return r; 175 } 176 /// Returns: the contents of memo as a single string 177 string getString(){ 178 dstring[] linesArray = this.lines.toArray; 179 uint len; 180 // first calculate total length needed 181 foreach (line; linesArray) 182 len += to!string(line).length; 183 len += linesArray.length; // for the \n at end of each line 184 char[] r; 185 r.length = len; 186 for (uint lineno, writeIndex; lineno < linesArray.length; lineno ++){ 187 string line = linesArray[lineno].to!string; 188 r[writeIndex .. writeIndex + line.length+1] = line~'\n'; 189 writeIndex += line.length+1; 190 } 191 return cast(string)r; 192 } 193 } 194 195 /// The text editor app 196 public class App : QTerminal{ 197 private: 198 /// path of output image 199 string _outputPath; 200 /// the PNGText class instance 201 PNGText _imageMan; 202 /// Title at top 203 TitleWidget _title; 204 /// memo for editing 205 MemoPlusPlusWidget _memo; 206 /// error messages 207 LogPlusPlusWidget _log; 208 /// status bar 209 StatusBarWidget _statusBar; 210 211 /// how many msecs until the shortcut keys are shown again in status bar instead of "File saved..." 212 int _msecsUntilReset; 213 214 /// updates status bar 215 void updateStatusBar(){ 216 immutable int bytesCount = _memo.bytesCount; 217 immutable ubyte density = _imageMan.calculateOptimumDensity(bytesCount); 218 immutable int max = density == 0 ? 0 : _imageMan.capacity(density); 219 _statusBar.setText(0, 220 STATUSBAR_QUALITY_PREFIX~(density == 0 ? "OVER CAPACITY" : _imageMan.qualityName(density).to!dstring)); 221 // now for bytes count 222 _statusBar.setText(1, STATUSBAR_BYTECOUNT_PREFIX~bytesCount.to!dstring~"/"~max.to!dstring~"/"~ 223 _imageMan.capacity(DENSITY_MAX).to!dstring); 224 // now shortcuts or leave it at that "file saved" 225 if (_msecsUntilReset <= 0) 226 _statusBar.setText(2, STATUSBAR_SHORTCUTS); 227 } 228 229 /// loads image, initializes _imageMan. 230 /// 231 /// Displays error in _log if any 232 void initImageMan(){ 233 try{ 234 _imageMan.load(); 235 _imageMan.decode(); 236 _imageMan.filename = _outputPath; 237 }catch (Exception e){ 238 _log.clear; 239 _log.add("Error loading image:"); 240 _log.add(e.msg.to!dstring); 241 _log.show = true; 242 _msecsUntilReset = int.max; 243 } 244 } 245 /// Reads data from _imageMan to memo 246 void initMemo(){ 247 _memo.lines.loadArray((cast(char[])_imageMan.data).to!dstring.separateLines); 248 } 249 /// Writes data from memo to _imageMan and then saves 250 void save(){ 251 string data = _memo.getString(); 252 _imageMan.data = cast(ubyte[])cast(char[])data; 253 _msecsUntilReset = int.max; 254 try{ 255 _imageMan.encode(); 256 _imageMan.save(); 257 _statusBar.setText(2, STATUSBAR_FILESAVED_PREFIX~_outputPath.to!dstring); 258 }catch (Exception e){ 259 _log.clear; 260 _log.add("Error saving image:"); 261 _statusBar.setText(2, STATUSBAR_FILESAVE_ERROR); 262 _log.add(e.msg.to!dstring); 263 _log.show = true; 264 _msecsUntilReset = int.max; 265 } 266 } 267 protected: 268 override void timerEvent(uinteger msecs){ 269 super.timerEvent(msecs); 270 if (_msecsUntilReset > 0) 271 _msecsUntilReset -= msecs; 272 if (_log.show && _msecsUntilReset <= 0) 273 _log.show = false; 274 updateStatusBar(); 275 requestUpdate(); 276 } 277 override void keyboardEvent(KeyboardEvent key){ 278 super.keyboardEvent(key); 279 // nothing happens if there was an error loading 280 if (!_imageMan.imageLoaded) 281 return; 282 if (_msecsUntilReset > ERROR_DISAPPEAR_TIME) 283 _msecsUntilReset = ERROR_DISAPPEAR_TIME; 284 if (key.isCtrlKey && key.key == KeyboardEvent.CtrlKeys.CtrlO){ 285 save(); 286 } 287 } 288 public: 289 /// Constructor 290 this(string imagePath, string outputPath, bool hackerman = false){ 291 // construct the interface 292 _title = new TitleWidget(baseName(imagePath).to!dstring, 293 Color.black, hackerman ? Color.green : Color.white); 294 _memo = new MemoPlusPlusWidget(); 295 if (hackerman) 296 _memo.textColor = Color.green; 297 _log = new LogPlusPlusWidget("Error Messages", 3, hackerman ? Color.green : Color.red, Color.black); 298 _statusBar = new StatusBarWidget(3, Color.black, hackerman ? Color.green : Color.white); 299 // arrange em 300 this.addWidget([_title, _memo, _log, _statusBar]); 301 // make _imageMan 302 _imageMan = new PNGText(); 303 _imageMan.filename = imagePath; 304 _outputPath = outputPath; 305 } 306 ~this(){ 307 .destroy(_imageMan); 308 .destroy(_title); 309 .destroy(_memo); 310 .destroy(_log); 311 .destroy(_statusBar); 312 } 313 /// starts the app 314 override void run(){ 315 initImageMan(); 316 initMemo(); 317 super.run(); 318 } 319 } 320 321 /// reads a single dstring into dstring[], separating the lines 322 private dstring[] separateLines(dstring s){ 323 dstring[] r; 324 for(uinteger i = 0, readFrom = 0; i < s.length; i ++){ 325 if (s[i] == '\n'){ 326 r ~= s[readFrom .. i].dup; 327 readFrom = i+1; 328 }else if (i+1 == s.length){ 329 r ~= s[readFrom .. i + 1].dup; 330 } 331 } 332 return r; 333 }