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 }