· 6 years ago · Nov 19, 2019, 10:34 AM
1#!/usr/bin/python
2# encoding: utf-8
3
4# ePad - a simple text editor written in Elementary and Python
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19from __future__ import print_function # May as well bite the bullet
20
21__author__ = "Jeff Hoogland"
22__contributors__ = ["Jeff Hoogland", "Robert Wiley", "Kai Huuhko", "Scimmia22"]
23__copyright__ = "Copyright (C) 2015 Bodhi Linux"
24__appname__ = 'epad'
25__version__ = "0.9.6"
26__description__ = 'A simple text editor for the Enlightenment Desktop.'
27__github__ = 'http://jeffhoogland.github.io/ePad/'
28__source__ = 'Source code and bug reports: {0}'.format(__github__)
29PY_EFL = "https://git.enlightenment.org/bindings/python/python-efl.git/"
30
31AUTHORS = """
32<br>
33<align=center>
34<hilight>Jeff Hoogland (Jef91)</hilight><br>
35<link><a href=http://www.jeffhoogland.com>Contact</a></link><br><br>
36
37<hilight>Robert Wiley (ylee)</hilight><br><br>
38
39<hilight>Kai Huuhko (kuuko)</hilight><br><br>
40</align>
41"""
42
43LICENSE = """<br>
44<align=center>
45<hilight>
46GNU GENERAL PUBLIC LICENSE<br>
47Version 3, 29 June 2007<br><br>
48</hilight>
49
50This program is free software: you can redistribute it and/or modify
51it under the terms of the GNU General Public License as published by
52the Free Software Foundation, either version 3 of the License, or
53(at your option) any later version.<br><br>
54
55This program is distributed in the hope that it will be useful,
56but WITHOUT ANY WARRANTY; without even the implied warranty of
57MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
58GNU General Public License for more details.<br><br>
59
60You should have received a copy of the GNU General Public License
61along with this program. If not, see<br>
62<link><a href=http://www.gnu.org/licenses>http://www.gnu.org/licenses/</a></link>
63</align>
64<br>
65"""
66
67INFO = """
68<align=center>
69<hilight>ePad</hilight> is a simple text editor written in Elementary and Python.<br>
70<br>
71<br>
72</align>
73"""
74
75
76import errno
77import sys
78import os
79import urllib
80import io
81import json
82from collections import Mapping
83import tempfile
84from subprocess import call
85
86try:
87 # Python3
88 import urllib.request
89except ImportError:
90 pass
91import re
92
93from efl import ecore
94from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL, \
95 EVAS_CALLBACK_KEY_UP, EVAS_EVENT_FLAG_ON_HOLD
96from efl import elementary
97from efl.elementary.window import StandardWindow
98from efl.elementary.box import Box
99from efl.elementary.button import Button
100from efl.elementary.label import Label, ELM_WRAP_WORD
101from efl.elementary.icon import Icon
102from efl.elementary.notify import Notify, ELM_NOTIFY_ALIGN_FILL
103from efl.elementary.separator import Separator
104from efl.elementary.scroller import Scroller
105from efl.elementary.frame import Frame
106from efl.elementary.entry import Entry, ELM_TEXT_FORMAT_PLAIN_UTF8, \
107 markup_to_utf8, utf8_to_markup, ELM_WRAP_NONE, ELM_WRAP_MIXED
108from efl.elementary.popup import Popup
109from efl.elementary.toolbar import Toolbar, ELM_OBJECT_SELECT_MODE_DEFAULT
110from efl.elementary.flip import Flip, ELM_FLIP_ROTATE_XZ_CENTER_AXIS, \
111 ELM_FLIP_ROTATE_YZ_CENTER_AXIS, ELM_FLIP_INTERACTION_ROTATE
112from efl.elementary.table import Table
113from efl.elementary.check import Check
114
115# Imported here to stop class resolver complaining when an input event
116# applies to an internal layout object
117from efl.elementary.layout import Layout
118from efl.elementary.theme import Theme
119from elmextensions import AboutWindow, InstanceError
120from elmextensions import FileSelector
121from elmextensions import FontSelector
122from elmextensions import TabbedBox
123
124EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
125EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0
126FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL
127FILL_HORIZ = EVAS_HINT_FILL, 0.5
128EXPAND_NONE = 0.0, 0.0
129ALIGN_CENTER = 0.5, 0.5
130ALIGN_RIGHT = 1.0, 0.5
131ALIGN_LEFT = 0.0, 0.5
132PADDING = 15, 0
133
134def print_err(*args, **kwargs):
135 """
136 error message to stderr
137 """
138 print('[ePad]:', *args, file=sys.stderr, **kwargs)
139
140def errorPopup(window, errorMsg):
141 errorPopup = Popup(window, size_hint_weight=EXPAND_BOTH)
142 errorPopup.callback_block_clicked_add(lambda obj: errorPopup.delete())
143
144 # Add a table to hold dialog image and text to Popup
145 tb = Table(errorPopup, size_hint_weight=EXPAND_BOTH)
146 errorPopup.part_content_set("default", tb)
147 tb.show()
148
149 # Add dialog-error Image to table
150 icon = Icon(errorPopup, resizable=(True, True),
151 size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
152 icon.standard_set('dialog-warning')
153 tb.pack(icon, 0, 0, 1, 1)
154 icon.show()
155
156 # Add dialog text to table
157 dialogLabel = Label(errorPopup, line_wrap=ELM_WRAP_WORD,
158 size_hint_weight=EXPAND_HORIZ,
159 size_hint_align=FILL_BOTH)
160 dialogLabel.text = errorMsg
161 tb.pack(dialogLabel, 1, 0, 1, 1)
162 dialogLabel.show()
163
164 # Ok Button
165 ok_btt = Button(errorPopup)
166 ok_btt.text = "Ok"
167 ok_btt.callback_clicked_add(lambda obj: errorPopup.delete())
168 ok_btt.show()
169
170 # add button to popup
171 errorPopup.part_content_set("button3", ok_btt)
172 errorPopup.show()
173
174
175def closeMenu(obj, label):
176 if not hasattr(closeMenu, 'count'):
177 closeMenu.count = 0
178 if not hasattr(closeMenu, 'name'):
179 closeMenu.lastItem = label
180 if closeMenu.lastItem != label:
181 closeMenu.count = 0
182 if closeMenu.count:
183 obj.selected_set(False)
184 obj.menu_get().close()
185 closeMenu.count = (closeMenu.count + 1) % 2
186
187
188def resetCloseMenuCount(obj):
189 global closeMenu
190 if hasattr(closeMenu, 'count'):
191 closeMenu.count = 0
192
193class Interface(object):
194 def __init__(self):
195 self.confirmPopup = None
196 self.config = ePadConf()
197
198 self.mainWindow = StandardWindow("epad", "Untitled - ePad",
199 size=(600, 400))
200 self.mainWindow.callback_delete_request_add(self.closeChecks)
201 self.mainWindow.elm_event_callback_add(self.eventsCb)
202 #self.mainWindow.repeat_events_set(False)
203
204 icon = Icon(self.mainWindow,
205 size_hint_weight=EXPAND_BOTH,
206 size_hint_align=FILL_BOTH)
207 icon.standard_set('accessories-text-editor')
208 self.mainWindow.icon_object_set(icon)
209
210 self.mainBox = Box(self.mainWindow,
211 size_hint_weight=EXPAND_BOTH,
212 size_hint_align=FILL_BOTH)
213 self.mainBox.show()
214
215 self.mainTb = ePadToolbar(self, self.mainWindow)
216 self.mainTb.focus_allow = False
217 self.mainTb.show()
218
219 self.mainBox.pack_end(self.mainTb)
220
221 # Root User Notification
222 if os.geteuid() == 0:
223 # print_err("Caution: Root User")
224 if self.config["notify_root"]:
225 notifyBox = Box(self.mainWindow, horizontal=True)
226 notifyLabel = Label(self.mainWindow, size_hint_align=FILL_HORIZ)
227 notifyLabel.text = "<b><i>Root User</i></b>"
228 notifyBox.pack_end(notifyLabel)
229 notifyLabel.show()
230 self.mainBox.pack_end(notifyBox)
231 notifyBox.show()
232
233 self.findBox = ePadFindBox(self, self.mainWindow)
234 self.findVisible = False
235
236 self.tabbs = TabbedBox(self.mainWindow, size_hint_weight=EXPAND_BOTH,
237 size_hint_align=FILL_BOTH)
238 self.tabbs.closeCallback = self.closeFile
239 self.tabbs.emptyCallback = self.baseFile
240 self.tabbs.tabChangedCallback = self.tabChanged
241 self.tabbs.show()
242
243 self.mainBox.pack_end(self.tabbs)
244
245 # Build our file selector for saving/loading files
246 self.fileBox = Box(self.mainWindow,
247 size_hint_weight=EXPAND_BOTH,
248 size_hint_align=FILL_BOTH)
249 self.fileBox.show()
250
251 self.fileLabel = Label(self.mainWindow,
252 size_hint_weight=EXPAND_HORIZ,
253 size_hint_align=FILL_BOTH, text="")
254 self.fileLabel.show()
255 self.lastDir = os.getenv("HOME")
256 self.fileSelector = FileSelector(self.mainWindow,
257 defaultPath=self.lastDir,
258 defaultPopulate=False,
259 size_hint_weight=EXPAND_BOTH,
260 size_hint_align=FILL_BOTH)
261 self.fileSelector.callback_activated_add(self.fileSelected)
262 self.fileSelector.callback_directory_open_add(self.updateLastDir)
263 self.fileSelector.callback_cancel_add(self.fileSelCancelPressed)
264 self.fileSelector.setMode("Open")
265 self.fileSelector.show()
266
267 self.fileBox.pack_end(self.fileLabel)
268 self.fileBox.pack_end(self.fileSelector)
269
270 # Flip object has the file selector on one side
271 # and the GUI on the other
272 self.flip = Flip(self.mainWindow, size_hint_weight=EXPAND_BOTH,
273 size_hint_align=FILL_BOTH)
274 self.flip.part_content_set("front", self.mainBox)
275 self.flip.part_content_set("back", self.fileBox)
276 self.mainWindow.resize_object_add(self.flip)
277 self.flip.show()
278
279 self.fontBox = FontSelector(self.mainWindow,
280 size_hint_weight=EXPAND_BOTH,
281 size_hint_align=FILL_BOTH)
282 # FIXME: these two and more should be handled by fs.set_font function
283 if self.config['use_theme']:
284 self.fontBox.use_theme = True
285 else:
286 self.fontBox.set_font(self.config['font'], self.config['font_style'], self.config['font_size'])
287 # FIXME: WTF should all be one function in fontselector obj
288 self.fontBox.default_font = self.config['font']
289 self.fontBox.default_font_style = self.config['font_style']
290 self.fontBox.default_font_size = self.config['font_size']
291 self.fontBox.override_theme_font_size = not self.config['use_theme_font_size']
292 self.fontBox.override_font_size = ePadEntry.default_font_size
293
294 self.fontBox.callback_cancel_add(self.fontSelCancelPressed)
295 self.fontBox.callback_activated_add(self.fontSelected)
296 # We will show when needed
297
298 def tabChanged(self, tabbs, widget):
299 self.mainWindow.title = widget.data["button"].text
300 self.mainTb.savebtn.disabled = not widget.dirty
301
302 def addFile(self, filePath):
303 entryBox = ePadEntry(self, self.tabbs)
304 entryBox.show()
305
306
307 if filePath != "Untitled":
308 entryBox.openFile(filePath)
309 tabName = filePath.split("/")[-1]
310 else:
311 tabName = "Untitled"
312 entryBox.curChanged(entryBox.mainEn, entryBox.line_label)
313 entryBox.checkLineNumbers()
314 self.tabbs.addTab(entryBox, tabName)
315
316 def baseFile(self, tabbs):
317 #This function gets called when all files are closed
318 self.addFile("Untitled")
319
320 def closeFile(self, tabbs, widget):
321 widget.closeChecks()
322
323 def showFile(self, btn):
324 if self.tabbs.currentTab != btn.data["entry"]:
325 self.setFile(btn.data["entry"], btn.text)
326
327 def newFile(self, obj=None, ignoreSave=False):
328 if self.config["new_instance"]:
329 print("Launching new instance")
330 ecore.Exe('epad', ecore.ECORE_EXE_PIPE_READ|ecore.ECORE_EXE_PIPE_ERROR|ecore.ECORE_EXE_PIPE_WRITE)
331 return
332 self.addFile("Untitled")
333
334 def openFile(self, obj=None, ignoreSave=False):
335 self.fileSelector.setMode("Open")
336 self.fileLabel.text = "<b>Select a text file to open:</b>"
337 if self.fileSelector.filepathEntry.text != self.lastDir:
338 self.fileSelector.populateFiles(self.lastDir)
339
340 self.fileBox.show()
341 self.flip.go(ELM_FLIP_ROTATE_YZ_CENTER_AXIS)
342
343
344 def fileSelCancelPressed(self, fs):
345 self.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
346
347 def fontSelCancelPressed(self, fs):
348 self.fontBox.hide()
349 self.flip.part_content_unset("back")
350 self.flip.part_content_set("back", self.fileBox)
351 self.fileBox.show()
352
353 self.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
354
355 def showFind(self, obj=None):
356 if not self.findVisible:
357 self.mainBox.pack_before(self.findBox, self.tabbs)
358 self.findBox.findEntry.text = self.tabbs.currentTab.mainEn.selection_get()
359 self.findBox.findEntry.focus_set(True)
360 self.findBox.findEntry.cursor_end_set()
361 self.findBox.show()
362 self.findVisible = True
363 else:
364 self.hideFind()
365
366 def hideFind(self, obj=None):
367 if self.findVisible:
368 self.mainBox.unpack(self.findBox)
369 self.findBox.hide()
370 self.findVisible = False
371
372 def saveAs(self):
373
374 self.fileSelector.setMode("Save")
375 self.fileLabel.text = "<b>Save new file to where:</b>"
376 if self.fileSelector.filepathEntry.text != self.lastDir:
377 self.fileSelector.populateFiles(self.lastDir)
378 self.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
379
380 def saveFile(self, obj=False):
381 if self.tabbs.currentTab.mainEn.file_get()[0] is None or self.tabbs.currentTab.isNewFile:
382 self.saveAs()
383 else:
384 if not self.tabbs.currentTab.isSaved:
385 file_selected = self.tabbs.currentTab.mainEn.file_get()[0]
386 # Detect save errors as entry.file_save currently returns no errors
387 # even in the case where the file fails to save :(
388 try:
389 newfile = io.open(file_selected, 'w')
390 except IOError as err:
391 if err.errno == errno.EACCES:
392 errorMsg = ("Permision denied: <b>'%s'</b>."
393 "<br><br>Operation failed !!!"
394 % (file_selected))
395 errorPopup(self.mainWindow, errorMsg)
396 else:
397 errorMsg = ("ERROR: %s: '%s'"
398 "<br><br>Operation failed !!!"
399 % (err.strerror, file_selected))
400 errorPopup(self.mainWindow, errorMsg)
401 return
402 newfile.close()
403 # if entry is empty and the file does not exists then
404 # entry.file_save will destroy the file created about by the
405 # open statement above for some odd reason ...
406 if not self.tabbs.currentTab.mainEn.is_empty:
407 self.tabbs.currentTab.mainEn.file_save()
408 self.tabbs.currentTab.setDirty(False)
409 self.tabbs.currentTab.isSaved = True
410
411 def fontSelected(self, fs):
412
413 self.fontBox.hide()
414 self.flip.part_content_unset("back")
415 self.flip.part_content_set("back", self.fileBox)
416 self.fileBox.show()
417 self.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
418 if fs.use_theme:
419 self.config['use_theme'] = True
420 for tab in self.tabbs.tabs:
421 while tab.mainEn.text_style_user_peek():
422 tab.mainEn.text_style_user_pop();
423 while tab.lineList.text_style_user_peek():
424 tab.lineList.text_style_user_pop();
425 if not self.config['use_theme_font_size']:
426 tab.mainEn.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
427 tab.lineList.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
428
429 else:
430 for tab in self.tabbs.tabs:
431 tab.mainEn.text_style_user_push(fs.font_style_str)
432 tab.lineList.text_style_user_push(self.fontBox.get_text_style(self.fontBox.selected_font, None, self.fontBox.selected_font_size))
433 self.config['use_theme'] = False
434 self.config['font'] = fs.selected_font
435 self.config['font_style'] = fs.selected_font_style
436 self.config['font_size'] = fs.selected_font_size
437
438 def fileSelected(self, fs, file_selected, onStartup=False):
439 if not onStartup:
440 self.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
441 # Markup can end up in file names because file_selector name_entry
442 # is an elementary entry. So lets sanitize file_selected.
443 file_selected = markup_to_utf8(file_selected)
444 if file_selected:
445 #print("File Selected: {0}".format(file_selected))
446 self.lastDir = os.path.dirname(file_selected)
447 # This fails if file_selected does not exist yet
448
449 fs.fileEntry.text = file_selected.split("/")[-1]
450
451 IsSave = fs.mode
452
453 if file_selected:
454 if IsSave == "save":
455 if os.path.isdir(file_selected):
456 current_file = os.path.basename(file_selected)
457 errorMsg = ("<b>'%s'</b> is a folder."
458 "<br><br>Operation failed !!!"
459 % (current_file))
460 errorPopup(self.mainWindow, errorMsg)
461 return
462 elif os.path.exists(file_selected):
463 self.tabbs.currentTab.fileExists(file_selected)
464 return
465 else:
466 self.tabbs.currentTab.doSelected(file_selected)
467 return
468 else:
469 self.addFile(file_selected)
470
471 def updateLastDir(self, path):
472 self.lastDir = path
473
474 def showAbout(self):
475 self.about.launch()
476
477 def closeApp(self, obj=False, trash=False):
478 elementary.exit()
479
480 def closeChecks(self, obj=False):
481 allSaved = True
482
483 for en in self.tabbs.tabs:
484 if not en.isSaved:
485 allSaved = False
486
487 if allSaved:
488 self.closeApp()
489 else:
490 self.unsavedWorkPopup()
491
492 def closePopup(self, bt, confirmPopup):
493 self.confirmPopup.delete()
494 self.confirmPopup = None
495
496 def unsavedWorkPopup(self):
497 if self.confirmPopup:
498 return
499 self.confirmPopup = Popup(self.mainWindow,
500 size_hint_weight=EXPAND_BOTH)
501
502 # Add a table to hold dialog image and text to Popup
503 tb = Table(self.confirmPopup, size_hint_weight=EXPAND_BOTH)
504 self.confirmPopup.part_content_set("default", tb)
505 tb.show()
506
507 # Add dialog-error Image to table
508
509 icon = Icon(self.confirmPopup, resizable=(True, True),
510 size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
511 icon.standard_set('dialog-question')
512 tb.pack(icon, 0, 0, 1, 1)
513 icon.show()
514
515 # Add dialog text to table
516 dialogLabel = Label(self.confirmPopup, line_wrap=ELM_WRAP_WORD,
517 size_hint_weight=EXPAND_HORIZ,
518 size_hint_align=FILL_BOTH)
519 dialogLabel.text = "You have unsaved work. Close anyways?<br><br>"
520 tb.pack(dialogLabel, 1, 0, 1, 1)
521 dialogLabel.show()
522
523 # Close without saving button
524 no_btt = Button(self.mainWindow)
525 no_btt.text = "No"
526 no_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
527 no_btt.show()
528 # Save the file and then close button
529 sav_btt = Button(self.mainWindow)
530 sav_btt.text = "Yes"
531 sav_btt.callback_clicked_add(self.closeApp)
532 sav_btt.show()
533
534 # add buttons to popup
535 self.confirmPopup.part_content_set("button1", no_btt)
536 self.confirmPopup.part_content_set("button3", sav_btt)
537 self.confirmPopup.show()
538
539 def eventsCb(self, obj, src, event_type, event):
540 # Ignore if FontSelector is open
541 if not event_type == EVAS_CALLBACK_KEY_UP or \
542 type(self.flip.part_content_get("back")) is FontSelector:
543 return False
544
545 if event.modifier_is_set("Control"):
546 if event.keyname == "n":
547 #newFile(self.newFile)
548 self.newFile()
549 elif event.keyname == "s" and event.modifier_is_set("Shift"):
550 self.saveAs()
551 elif event.keyname == "s":
552 self.saveFile()
553 elif event.keyname == "z" and event.modifier_is_set("Shift"):
554 self.tabbs.currentTab.reDo()
555 elif event.keyname == "z":
556 self.tabbs.currentTab.unDo()
557 elif event.keyname == "o":
558 self.openFile()
559 elif event.keyname == "h":
560 if not self.flip.front_visible_get():
561 self.fileSelector.toggleHidden()
562 elif event.keyname == "q":
563 self.closeChecks()
564 elif event.keyname == "f":
565 self.showFind()
566 elif isinstance(src, Entry) and event.key in ["space", "BackSpace", "Return"]:
567 self.tabbs.currentTab.takeSnapShot()
568
569 event.event_flags = event.event_flags | EVAS_EVENT_FLAG_ON_HOLD
570 return True
571
572 def launch(self, start=[]):
573 if start[0]:
574 for count, ourFile in enumerate(start[0]):
575 if os.path.dirname(ourFile) == '':
576 start[0][count] = os.getcwd() + '/' + ourFile
577
578 if start and start[0]:
579 for ourFile in start[0]:
580 if ourFile[:7] == "file://":
581 try:
582 ourFile = urllib.url2pathname(ourFile[7:])
583 except AttributeError:
584 # Python3
585 ourFile = urllib.request.url2pathname(ourFile[7:])
586 if os.path.isdir(os.path.dirname(ourFile)):
587 if os.path.isfile(ourFile):
588 ##print(ourFile)
589 self.addFile(ourFile)
590 else:
591 errorMsg = ("<b>'%s'</b> is an Invalid path."
592 "<br><br>Open failed !!!" % (ourFile))
593 errorPopup(self.mainWindow, errorMsg)
594 if start and start[1]:
595 if os.path.isdir(start[1]):
596 self.lastDir = start[1]
597 else:
598 pass
599
600 if not len(self.tabbs.tabs):
601 self.addFile("Untitled")
602
603 self.mainWindow.show()
604
605class ePadEntry(Box):
606
607 default_font_size = 14
608
609 def __init__(self, parent, canvas):
610 Box.__init__(self, canvas)
611 self._parent = parent
612 self._canvas = canvas
613 self._config = self._parent.config
614
615 self.size_hint_weight = EXPAND_BOTH
616 self.size_hint_align = FILL_BOTH
617 # py-efl doesn't work correctly with fonts that have spaces in names
618 # Oddly enough remove spaces and works
619 # versions up to 1.18 (inclusive)
620
621 self.font_style = self._parent.fontBox.get_text_style(self._config['font'], self._config['font_style'], str(self._config['font_size']))
622 self.entryBox = Box(self._canvas,
623 size_hint_weight=EXPAND_BOTH,
624 size_hint_align=FILL_BOTH)
625 self.entryBox.horizontal = True
626 self.entryBox.show()
627
628 # Initialize Text entry box and line label
629 self.lineList = Entry(self._canvas,
630 scrollable=False, editable=False,
631 size_hint_weight=(0.0, EVAS_HINT_EXPAND),
632 size_hint_align=(0.0, 0.0),
633 line_wrap=ELM_WRAP_NONE)
634
635 self.currentLinesShown = 1
636 self.lineList.text_set("1<br>")
637 self.lineNums = self._parent.config['line_numbers']
638
639 self.mainEn = Entry(self._canvas, scrollable=False,
640 line_wrap=self._parent.config['word_wrap'],
641 autosave=self._parent.config['autosave'],
642 size_hint_weight=(0.85, EVAS_HINT_EXPAND),
643 size_hint_align=FILL_BOTH)
644 self.mainEn.callback_changed_user_add(self.textEdited)
645 self.mainEn.callback_clicked_add(resetCloseMenuCount)
646 self.mainEn.callback_selection_cut_add(self.takeSnapShot)
647 self.mainEn.callback_selection_paste_add(self.takeSnapShot)
648
649 # Set user Font styles
650 if self._config['use_theme'] and not self._config['use_theme_font_size']:
651 self.lineList.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
652 self.mainEn.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
653 elif not self._config['use_theme']:
654 self.lineList.text_style_user_push(self._parent.fontBox.get_text_style(self._config['font'], None, str(self._config['font_size'])))
655 self.mainEn.text_style_user_push(self.font_style)
656
657 self.totalLines = 0
658 self.mainEn.show()
659 self.mainEn.focus_set(True)
660 self.sep = Separator(self._canvas)
661 self.sep.horizontal_set(False)
662 self.sep.show()
663
664 if self.lineNums:
665 self.lineList.show()
666 self.entryBox.pack_end(self.lineList)
667 self.entryBox.pack_end(self.sep)
668 self.entryBox.pack_end(self.mainEn)
669
670 self.scr = Scroller(self._canvas,
671 size_hint_weight=EXPAND_BOTH,
672 size_hint_align=FILL_BOTH)
673 self.scr.content = self.entryBox
674 self.scr.show()
675
676 self.pack_end(self.scr)
677
678 # Add label to show current cursor position
679 if self._parent.config['show_pos']:
680 self.line_label = Label(self._canvas,
681 size_hint_weight=EXPAND_HORIZ,
682 size_hint_align=ALIGN_RIGHT)
683
684 self.mainEn.callback_cursor_changed_add(self.curChanged,
685 self.line_label)
686 self.curChanged(self.mainEn, self.line_label)
687 self.line_label.show()
688 self.pack_end(self.line_label)
689
690 self.isNewFile = True
691 self.isSaved = True
692 self.doArchive = []
693 self.doSpot = 0
694 self.takeSnapShot()
695 self.setDirty(False)
696
697 def takeSnapShot(self, obj=None):
698 if self.doSpot != len(self.doArchive)-1:
699 for i in range(self.doSpot+1, len(self.doArchive)):
700 self.doArchive.pop(self.doSpot+1)
701
702 curPos = self.mainEn.cursor_pos_get()
703 entryGet = self.mainEn.entry_get()
704
705 if self.doSpot == 0:
706 self.saveSnapShot(curPos, entryGet)
707 elif entryGet != self.doArchive[self.doSpot][1]:
708 self.saveSnapShot(curPos, entryGet)
709
710 def saveSnapShot(self, curPos, entryGet):
711 self.doArchive.append([curPos, entryGet])
712
713 if len(self.doArchive) > 30:
714 self.doArchive.pop(0)
715
716 self.doSpot = len(self.doArchive) - 1
717
718
719 def unDo(self):
720 if self.doSpot > 0:
721 # A check if this is the first time we are undoing that we store the latest data
722 if self.doSpot == len(self.doArchive) - 1:
723 if self.doArchive[self.doSpot][1] != self.mainEn.entry_get():
724 self.takeSnapShot()
725 self.doSpot -= 1
726 self.mainEn.entry_set(self.doArchive[self.doSpot][1])
727 self.mainEn.cursor_pos_set(self.doArchive[self.doSpot][0])
728
729 def reDo(self):
730 if self.doSpot + 1 < len(self.doArchive):
731 self.doSpot += 1
732 self.mainEn.entry_set(self.doArchive[self.doSpot][1])
733 self.mainEn.cursor_pos_set(self.doArchive[self.doSpot][0])
734
735 def checkLineNumbers(self):
736 if self.currentLinesShown < self.totalLines:
737 lines = ""
738 for i in range(self.currentLinesShown+1, self.totalLines+1):
739 lines += "%s<br>"%(i)
740 self.lineList.entry_append(lines)
741 self.currentLinesShown = self.totalLines
742 elif self.currentLinesShown > self.totalLines:
743 lines = "<i>"
744
745 for i in range(1, self.totalLines+1):
746 lines = "%s%s<br>"%(lines, i)
747
748 self.lineList.entry_set(lines)
749 '''for i in range(self.totalLines+1, self.currentLinesShown+1):
750 ll = self.lineList
751 ll.cursor_end_set()
752 ll.cursor_prev()
753 ll.cursor_selection_begin()
754 ll.cursor_line_begin_set()
755 ll.cursor_prev()
756 ll.cursor_selection_end()
757 ll.selection_cut()'''
758
759 self.currentLinesShown = self.totalLines
760
761 def curChanged(self, entry, label):
762 # get linear index into current text
763 index = entry.cursor_pos_get()
764 # Replace <br /> tag with single char
765 # to simplify (line, col) calculation
766 tmp_text = markup_to_utf8(entry.entry_get())
767 self.totalLines = tmp_text.count("\n")+1
768 if self.lineNums:
769 self.checkLineNumbers()
770 text_slice = tmp_text[:index]
771 split_line = text_slice.split("\n")
772 line_n = len(split_line)
773 col = len(split_line[-1]) + 1
774 # Update label text with line, col
775 label.text = "Ln {0} Col {1} ".format(line_n, col)
776
777 def setDirty(self, dirty):
778 if dirty:
779 self.dirty = True
780 self._parent.mainWindow.title = "*" + self._parent.mainWindow.title
781 try:
782 self.data["button"].text = "*" + self.data["button"].text
783 except:
784 pass
785 self._parent.mainTb.savebtn.disabled = False
786 else:
787 self.dirty = False
788 self._parent.mainWindow.title = self._parent.mainWindow.title[1:]
789 try:
790 self.data["button"].text = self.data["button"].text[1:]
791 except:
792 pass
793 self._parent.mainTb.savebtn.disabled = True
794
795 def textEdited(self, obj=None):
796 if not self.dirty:
797 self.setDirty(True)
798 self.isSaved = False
799
800 def openFile(self, filePath):
801 try:
802 self.mainEn.file_set(filePath, ELM_TEXT_FORMAT_PLAIN_UTF8)
803 except (RuntimeWarning, RuntimeError) as msg:
804 print("ePad: Loading Empty File")
805 pass
806 self.mainEn.focus_set(True)
807 self.lineList.cursor_end_set()
808 self.checkLineNumbers()
809 self.isNewFile = False
810 #Reset undo/redo tracks when we open a file
811 self.doArchive = []
812 self.doSpot = 0
813 self.takeSnapShot()
814
815 def closeChecks(self, ourCallback=None):
816 if not self.isSaved:
817 self.confirmSave()
818 else:
819 self._parent.tabbs.deleteTab(self)
820
821 def confirmSave(self):
822 self.confirmPopup = Popup(self._parent.mainWindow,
823 size_hint_weight=EXPAND_BOTH)
824 self.confirmPopup.part_text_set("title,text", "File Unsaved")
825 current_file = self.mainEn.file[0]
826 current_file = \
827 os.path.basename(current_file) if current_file else "Untitled"
828 self.confirmPopup.text = "Save changes to '%s'?" % (current_file)
829 # Close without saving button
830 no_btt = Button(self._parent.mainWindow)
831 no_btt.text = "No"
832 no_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
833 no_btt.callback_clicked_add(lambda o: self._parent.tabbs.deleteTab(self))
834 no_btt.show()
835 # cancel close request
836 cancel_btt = Button(self._parent.mainWindow)
837 cancel_btt.text = "Cancel"
838 cancel_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
839 cancel_btt.show()
840 # Save the file and then close button
841 sav_btt = Button(self._parent.mainWindow)
842 sav_btt.text = "Yes"
843 sav_btt.callback_clicked_add(self._parent.saveFile)
844 sav_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
845 sav_btt.show()
846
847 # add buttons to popup
848 self.confirmPopup.part_content_set("button1", no_btt)
849 self.confirmPopup.part_content_set("button2", cancel_btt)
850 self.confirmPopup.part_content_set("button3", sav_btt)
851 self.confirmPopup.show()
852
853 def closePopup(self, bt, confirmPopup):
854 self.confirmPopup.delete()
855 self.confirmPopup = None
856
857 def fileExists(self, filePath):
858 self.confirmPopup = Popup(self._parent.mainWindow,
859 size_hint_weight=EXPAND_BOTH)
860
861 # Add a table to hold dialog image and text to Popup
862 tb = Table(self.confirmPopup, size_hint_weight=EXPAND_BOTH)
863 self.confirmPopup.part_content_set("default", tb)
864 tb.show()
865
866 # Add dialog-error Image to table
867 icon = Icon(self.confirmPopup, resizable=(True, True),
868 size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
869 icon.standard_set('dialog-question')
870 tb.pack(icon, 0, 0, 1, 1)
871 icon.show()
872
873 # Add dialog text to table
874 dialogLabel = Label(self.confirmPopup, line_wrap=ELM_WRAP_WORD,
875 size_hint_weight=EXPAND_HORIZ,
876 size_hint_align=FILL_BOTH)
877 current_file = os.path.basename(filePath)
878 dialogLabel.text = "'%s' already exists. Overwrite?<br><br>" \
879 % (current_file)
880 tb.pack(dialogLabel, 1, 0, 1, 1)
881 dialogLabel.show()
882
883 # Close without saving button
884 no_btt = Button(self._parent.mainWindow)
885 no_btt.text = "No"
886 no_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
887 no_btt.show()
888 # Save the file and then close button
889 sav_btt = Button(self._parent.mainWindow)
890 sav_btt.text = "Yes"
891 sav_btt.callback_clicked_add(self.doSelected)
892 sav_btt.callback_clicked_add(self.closePopup, self.confirmPopup)
893 sav_btt.show()
894
895 # add buttons to popup
896 self.confirmPopup.part_content_set("button1", no_btt)
897 self.confirmPopup.part_content_set("button3", sav_btt)
898 self.confirmPopup.show()
899
900 def doSelected(self, obj):
901 # Something I should avoid but here I prefer a polymorphic function
902 if isinstance(obj, Button):
903 file_selected = self._parent.fileSelector.selected_get()
904 else:
905 file_selected = obj
906
907 if file_selected:
908 try:
909 newfile = io.open(file_selected, 'w')
910 except IOError as err:
911 pass
912 #print("ERROR: {0}: '{1}'".format(err.strerror,
913 # file_selected))
914 if err.errno == errno.EACCES:
915 errorMsg = ("Permision denied: <b>'%s'</b>."
916 "<br><br>Operation failed !!!</br>"
917 % (file_selected))
918 errorPopup(self._parent.mainWindow, errorMsg)
919 else:
920 errorMsg = ("ERROR: %s: '%s'"
921 "<br><br>Operation failed !!!</br>"
922 % (err.strerror, file_selected))
923 errorPopup(self._parent.mainWindow, errorMsg)
924 return
925 tmp_text = self.mainEn.entry_get()
926 # FIXME: Why save twice?
927 newfile.write(tmp_text)
928 newfile.close()
929 # Suppress error message when empty file is saved
930 try:
931 self.mainEn.file_set(file_selected,
932 ELM_TEXT_FORMAT_PLAIN_UTF8)
933 except RuntimeError:
934 pass
935 #print("Empty file saved:{0}".format(file_selected))
936 self.mainEn.entry_set(tmp_text)
937 # if empty file entry.file_save destroys file :(
938 if len(tmp_text):
939 self.mainEn.file_save()
940 self._parent.mainWindow.title_set(" %s - ePad" % os.path.basename(file_selected))
941 self.data["button"].text = " %s" % os.path.basename(file_selected)
942
943 self.isSaved = True
944 self.isNewFile = False
945 self.setDirty(False)
946
947class ePadFindBox(Box):
948 def __init__(self, parent, canvas):
949 Box.__init__(self, canvas)
950 self._parent = parent
951 self._canvas = canvas
952
953 self.size_hint_weight = EXPAND_HORIZ
954 self.size_hint_align = FILL_HORIZ
955
956 self.currentFind = None
957 self.lastSearch = None
958
959 frameBox = Box(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
960 frameBox.horizontal = True
961 frameBox.show()
962
963 findBox = Frame(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
964 findBox.text = "Find Text:"
965 findBox.show()
966
967 self.findEntry = Entry(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
968 self.findEntry.single_line_set(True)
969 self.findEntry.scrollable_set(True)
970 self.findEntry.callback_activated_add(self.findPressed)
971 self.findEntry.show()
972
973 findBox.content = self.findEntry
974
975 replaceBox = Frame(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
976 replaceBox.text = "Replace Text:"
977 replaceBox.show()
978
979 self.replaceEntry = Entry(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
980 self.replaceEntry.single_line_set(True)
981 self.replaceEntry.scrollable_set(True)
982 self.replaceEntry.show()
983
984 replaceBox.content = self.replaceEntry
985
986 frameBox.pack_end(findBox)
987 frameBox.pack_end(replaceBox)
988
989 buttonBox = Box(self._canvas, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ)
990 buttonBox.horizontal = True
991 buttonBox.show()
992
993 findButton = Button(self._canvas)
994 findButton.text = "Find Next"
995 findButton.callback_pressed_add(self.findPressed)
996 findButton.show()
997
998 replaceButton = Button(self._canvas)
999 replaceButton.text = "Replace All"
1000 replaceButton.callback_pressed_add(self.replacePressed)
1001 replaceButton.show()
1002
1003 closeButton = Button(self._canvas)
1004 closeButton.text = "Done"
1005 closeButton.callback_pressed_add(self._parent.showFind)
1006 closeButton.show()
1007
1008 self.caseCheck = Check(self._canvas, text = "Case Sensitive")
1009 self.caseCheck.state_set(self._parent.config['case_sensitive'])
1010 self.caseCheck.callback_changed_add(self.caseUpdate)
1011 self.caseCheck.show()
1012
1013 buttonBox.pack_end(self.caseCheck)
1014 buttonBox.pack_end(findButton)
1015 buttonBox.pack_end(replaceButton)
1016 buttonBox.pack_end(closeButton)
1017
1018 self.pack_end(frameBox)
1019 self.pack_end(buttonBox)
1020
1021 def replacePressed(self, obj):
1022 tmp_text = markup_to_utf8(self._parent.tabbs.currentTab.mainEn.entry_get())
1023 if not self.caseCheck.state_get():
1024 search_string = self.findEntry.text.lower()
1025 locations = list(self.findAll(tmp_text.lower(), search_string))
1026 else:
1027 search_string = self.findEntry.text
1028 locations = list(self.findAll(tmp_text, search_string))
1029 search_length = len(search_string)
1030 if search_length:
1031 replace_string = self.replaceEntry.text
1032 #if replace_string:
1033 if len(locations):
1034 if not self._parent.config['case_sensitive']:
1035 ourRe = re.compile(search_string, re.IGNORECASE)
1036 else:
1037 ourRe = re.compile(search_string)
1038 tmp_text = ourRe.sub(replace_string, tmp_text).encode('utf-8').strip()
1039 tmp_text = utf8_to_markup(tmp_text)
1040 curPos = self._parent.tabbs.currentTab.mainEn.cursor_pos_get()
1041 self._parent.tabbs.currentTab.mainEn.text_set(tmp_text)
1042 try:
1043 self._parent.tabbs.currentTab.mainEn.cursor_pos_set(curPos)
1044 except:
1045 pass
1046 #print("Error: Can't set cursor position")
1047 self._parent.tabbs.currentTab.textEdited()
1048 self._parent.tabbs.currentTab.takeSnapShot()
1049 else:
1050 errorPopup(self._parent.mainWindow, "Text %s not found. Nothing replaced."%search_string)
1051 #else:
1052 # errorPopup(self._parent.mainWindow, "No replacement string entered.")
1053 else:
1054 errorPopup(self._parent.mainWindow, "No find string entered.")
1055
1056 def findPressed(self, obj):
1057 if not self.caseCheck.state_get():
1058 search_string = self.findEntry.text.lower()
1059 tmp_text = markup_to_utf8(self._parent.tabbs.currentTab.mainEn.entry_get()).lower()
1060 else:
1061 search_string = self.findEntry.text
1062 tmp_text = markup_to_utf8(self._parent.tabbs.currentTab.mainEn.entry_get())
1063 search_length = len(search_string)
1064 if search_length:
1065 locations = list(self.findAll(tmp_text, search_string))
1066 if len(locations):
1067 if self.currentFind == None or search_string != self.lastSearch:
1068 self.lastSearch = search_string
1069 self.currentFind = locations[0]
1070 else:
1071 lastFind = locations.index(self.currentFind)
1072 if lastFind < len(locations)-1:
1073 self.currentFind = locations[lastFind+1]
1074 else:
1075 self.currentFind = locations[0]
1076 self._parent.tabbs.currentTab.mainEn.select_region_set(self.currentFind, self.currentFind+search_length)
1077 else:
1078 errorPopup(self._parent.mainWindow, "Text %s not found."%search_string)
1079 else:
1080 errorPopup(self._parent.mainWindow, "No find string entered.")
1081
1082 def findAll(self, a_str, sub):
1083 start = 0
1084 while True:
1085 start = a_str.find(sub, start)
1086 if start == -1: return
1087 yield start
1088 start += len(sub) + 1
1089
1090 def caseUpdate(self, ck):
1091 self._parent.config['case_sensitive'] = ck.state_get()
1092
1093
1094class ePadToolbar(Toolbar):
1095 def __init__(self, parent, canvas):
1096 Toolbar.__init__(self, canvas)
1097 self._parent = parent
1098 self._canvas = canvas
1099
1100 self.homogeneous = False
1101 self.size_hint_weight = (0.0, 0.0)
1102 self.size_hint_align = (EVAS_HINT_FILL, 0.0)
1103 self.select_mode = ELM_OBJECT_SELECT_MODE_DEFAULT
1104 self.icon_size_set(16)
1105 self.callback_selected_add(self.itemClicked)
1106
1107 self.menu_parent = canvas
1108
1109 self.item_append("document-new", "New",
1110 lambda self, obj: self._parent.newFile())
1111 self.item_append("document-open", "Open",
1112 lambda self, obj: self._parent.openFile())
1113 self.savebtn = self.item_append("document-save", "Save",
1114 lambda self, obj: self._parent.saveFile())
1115 self.savebtn.disabled = True
1116 self.item_append("document-save-as", "Save As",
1117 lambda self, obj: self._parent.saveAs())
1118 self.item_append("printer", "Print", self.printFile)
1119 # -- Edit Dropdown Menu --
1120 tb_it = self.item_append("edit-copy", "Edit")
1121 tb_it.menu = True
1122 menu = tb_it.menu
1123 menu.item_add(None, "Undo", "edit-undo", self.unDoPress)
1124 menu.item_add(None, "Redo", "edit-redo", self.reDoPress)
1125 menu.item_separator_add()
1126 menu.item_add(None, "Copy", "edit-copy", self.copyPress)
1127 menu.item_add(None, "Paste", "edit-paste", self.pastePress)
1128 menu.item_add(None, "Cut", "edit-cut", self.cutPress)
1129 menu.item_separator_add()
1130 menu.item_add(None, "Select All", "edit-select-all",
1131 self.selectAllPress)
1132
1133 self.item_append("edit-find-replace", "Find",
1134 lambda self, obj: self._parent.showFind())
1135 # -----------------------
1136 #
1137 # -- Options Dropdown Menu --
1138 #
1139 # self.item_append("settings", "Options", self.optionsPress)
1140 tb_it = self.item_append("preferences-desktop", "Options")
1141 tb_it.menu = True
1142 menu = tb_it.menu
1143 self._parent.wordwrap = self._parent.config['word_wrap']
1144 it = menu.item_add(None, "Wordwrap", None, self.optionsToggle)
1145 chk = Check(canvas)
1146 it.content = chk
1147 it.content.state = (self._parent.config['word_wrap'] == ELM_WRAP_MIXED)
1148 self.menu_item_ww = it
1149
1150 it = menu.item_add(None, "Line Numbers", None, self.optionsToggle)
1151 chk = Check(canvas)
1152 it.content = chk
1153 it.content.state = self._parent.config['line_numbers']
1154 self.menu_item_ln = it
1155
1156 it = menu.item_separator_add()
1157 it = menu.item_add(None, "Font", "preferences-desktop-font", self.optionsFont)
1158
1159 '''it = menu.item_add(None, "New Instance", None, self.optionsNew)
1160 chk = Check(canvas, disabled=True)
1161 it.content = chk
1162 if self._parent.newInstance:
1163 it.content.state = True
1164 else:
1165 it.content.state = False'''
1166
1167 # ---------------------------
1168
1169 self.item_append("dialog-information", "About",
1170 self.showAbout)
1171
1172 def showAbout(self, obj, it):
1173 try:
1174 AboutWindow(self, title="ePad", standardicon="accessories-text-editor", \
1175 version=__version__, authors=AUTHORS, \
1176 licen=LICENSE, webaddress=__github__, \
1177 info=INFO)
1178 except InstanceError:
1179 pass
1180
1181
1182 #############################################################
1183 # Hack to toogle word wrap state with line numbers state ##
1184 #############################################################
1185 def optionsToggle(self, obj, it):
1186 wordwrap = self._parent.config['word_wrap']
1187 linenum = self._parent.config['line_numbers']
1188
1189 if it.text == "Wordwrap":
1190 if wordwrap == ELM_WRAP_MIXED:
1191 wordwrap = ELM_WRAP_NONE
1192 it.content.state = False
1193 linenum = self.menu_item_ln.content.state = True
1194 else:
1195 wordwrap = ELM_WRAP_MIXED
1196 it.content.state = True
1197 linenum = self.menu_item_ln.content.state = False
1198 else:
1199 if linenum:
1200 linenum = it.content.state = False
1201 wordwrap = ELM_WRAP_MIXED
1202 self.menu_item_ww.content.state = True
1203 else:
1204 linenum = it.content.state = True
1205 wordwrap = ELM_WRAP_NONE
1206 self.menu_item_ww.content.state = False
1207 # Change word wrap state of all open tabs
1208 for tab in self._parent.tabbs.tabs:
1209 tab.mainEn.line_wrap_set(wordwrap)
1210 # for some reason have to reset font style
1211 while tab.mainEn.text_style_user_peek():
1212 tab.mainEn.text_style_user_pop();
1213 #FIXME SHOULD CHECK THEME
1214 # Set user Font styles
1215 if self._parent.config['use_theme'] and not self._parent.config['use_theme_font_size']:
1216 tab.lineList.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
1217 tab.mainEn.text_style_user_push("DEFAULT='font_size={0}'".format(ePadEntry.default_font_size))
1218 elif not self._parent.config['use_theme']:
1219 tab.lineList.text_style_user_push(self._parent.fontBox.get_text_style(self._parent.config['font'], None, str(self._parent.config['font_size'])))
1220 tab.mainEn.text_style_user_push(self._parent.tabbs.currentTab.font_style)
1221 #tab.mainEn.text_style_user_push(self._parent.tabbs.currentTab.font_style)
1222 # Change line number state of all open tabs
1223 if linenum:
1224 for tab in self._parent.tabbs.tabs:
1225 tab.entryBox.pack_before(tab.sep, tab.mainEn)
1226 tab.sep.show()
1227 tab.entryBox.pack_before(tab.lineList, tab.sep)
1228 tab.checkLineNumbers()
1229 tab.lineList.show()
1230 else:
1231 for tab in self._parent.tabbs.tabs:
1232 tab.entryBox.unpack(tab.lineList)
1233 tab.entryBox.unpack(tab.sep)
1234 tab.lineList.hide()
1235 tab.sep.hide()
1236 # Update Config file
1237 self._parent.config['word_wrap'] = wordwrap
1238 self._parent.config['line_numbers'] = linenum
1239 resetCloseMenuCount(None)
1240
1241 def unDoPress(self, obj, it):
1242 self._parent.tabbs.currentTab.unDo()
1243 resetCloseMenuCount(None)
1244
1245 def reDoPress(self, obj, it):
1246 self._parent.tabbs.currentTab.reDo()
1247 resetCloseMenuCount(None)
1248
1249 def copyPress(self, obj, it):
1250 self._parent.tabbs.currentTab.mainEn.selection_copy()
1251 resetCloseMenuCount(None)
1252
1253 def itemClicked(self, obj, item):
1254 if item.menu_get() is None and item.selected_get():
1255 item.selected_set(False)
1256 elif item.menu_get():
1257 closeMenu(item, item.text_get())
1258
1259 def pastePress(self, obj, it):
1260 self._parent.tabbs.currentTab.mainEn.selection_paste()
1261 resetCloseMenuCount(None)
1262
1263 def cutPress(self, obj, it):
1264 self._parent.tabbs.currentTab.mainEn.selection_cut()
1265 resetCloseMenuCount(None)
1266
1267 def selectAllPress(self, obj, it):
1268 self._parent.tabbs.currentTab.mainEn.select_all()
1269 resetCloseMenuCount(None)
1270
1271 def optionsNew(self, obj, it):
1272 self._parent.newInstance = not self._parent.newInstance
1273 if self._parent.newInstance:
1274 it.content.state = True
1275 else:
1276 it.content.state = False
1277 resetCloseMenuCount(None)
1278
1279 def optionsFont(self, obj, it):
1280 self._parent.fileBox.hide()
1281 self._parent.flip.part_content_unset("back")
1282 self._parent.flip.part_content_set("back", self._parent.fontBox)
1283 # FIXME: WTF should all be one function in fontselector obj
1284 self._parent.fontBox.default_font = self._parent.config['font']
1285 self._parent.fontBox.default_font_style = self._parent.config['font_style']
1286 self._parent.fontBox.default_font_size = self._parent.config['font_size']
1287 self._parent.fontBox.override_theme_font_size = not self._parent.config['use_theme_font_size']
1288 ######
1289 self._parent.fontBox.show()
1290 if True:
1291 self._parent.flip.go(ELM_FLIP_ROTATE_XZ_CENTER_AXIS)
1292 resetCloseMenuCount(None)
1293
1294 def printFile(self, obj, it):
1295 file_selected = markup_to_utf8(self._parent.tabbs.currentTab.mainEn.entry_get())
1296 tmp = tempfile.NamedTemporaryFile( prefix='ePad-', delete=False)
1297 with open(tmp.name, "w") as f:
1298 f.write(file_selected)
1299 cmd = 'yad --print --type=TEXT --add-preview --fontname="{}" "{}" {} --filename="{}"'.format(
1300 self._parent.config['font'], self._parent.config['font_style'], self._parent.config['font_size'], tmp.name)
1301 call([cmd], shell=True)
1302
1303class ePadConf(object):
1304 """
1305 config file object
1306 """
1307
1308 # Config version follows major:minor format
1309 # an increase in major version number indicates incompatible
1310 # config file format change
1311 # an increase in minor for compatible config file format change
1312 __config_version_major = 0
1313 __config_version_minor = 1
1314 __config_version = '{0}.{1}'.format(__config_version_major,
1315 __config_version_minor)
1316 default = {"version": __config_version,
1317 "word_wrap": ELM_WRAP_NONE,
1318 "font": "Sans",
1319 "font_size": ePadEntry.default_font_size,
1320 "font_style": "Regular",
1321 "line_numbers": True,
1322 "show_hidden": False,
1323 "show_pos": True,
1324 "max_undo": 100,
1325 "case_sensitive": False,
1326 "notify_root": True,
1327 "new_instance": False,
1328 "show_hidden": False,
1329 "autosave": False,
1330 "use_theme": False,
1331 "use_theme_font_size": False}
1332
1333 def __init__(self):
1334 # sys.platform returns 'linux2' in py2 and linux in py3
1335 # hence hackish way to ensure compatibility
1336 if sys.platform[:5] != 'linux':
1337 # only support for linux is implemented
1338 raise RuntimeError("Unsupported OS: patches accepted.")
1339
1340 self.__file_name = "{0}.json".format(__appname__)
1341 # we follow the XDG spec and support $XDG_CONFIG_HOME.
1342 # Also, traditionally, Linux apps store their data in
1343 # "~/.config/<appname>" instead of "~/.local/share/<appname>".
1344 self.__dir_path = os.getenv('XDG_CONFIG_HOME',
1345 os.path.expanduser('~/.config'))
1346 self.__dir_path = os.path.join(self.__dir_path,
1347 '{0}'.format(__appname__))
1348 # Better way to do the below in py3 but ...
1349 try:
1350 os.makedirs(self.__dir_path)
1351 except OSError:
1352 if not os.path.isdir(self.__dir_path):
1353 raise
1354 self.path = os.path.join(self.__dir_path, self.__file_name)
1355 # Initialize config data
1356 self.get_config()
1357
1358 def get_config(self):
1359 """
1360 Initializes the config data
1361 """
1362 if os.path.exists(self.path):
1363 try:
1364 with open(self.path) as config_file:
1365 self.data = json.load(config_file)
1366 except ValueError:
1367 print_err('Config file corruption!')
1368 self.default_config()
1369 else:
1370 self.default_config()
1371
1372 if not self.__validate():
1373 print_err('Config file corruption!')
1374 self.default_config()
1375
1376 version = self.data["version"]
1377 if version.split('.')[0] != str(self.__config_version_major):
1378 print_err('Config version mismatch')
1379 self.default_config()
1380
1381 def __validate(self):
1382 """
1383 Return true on Valid config keys. No checks on config values
1384 """
1385 return set(ePadConf.default) == set(self.data)
1386
1387 def write(self):
1388 with open(self.path, "w") as config_file:
1389 json.dump(self.data, config_file, sort_keys=True, indent=4, separators=(',', ': '))
1390
1391 def default_config(self):
1392 """
1393 Create a config file
1394 """
1395 self.data = ePadConf.default
1396 self.write()
1397
1398 def __getitem__(self, setting):
1399 """
1400 Returns a setting.
1401 """
1402 try:
1403 value = self.data[setting]
1404 except KeyError:
1405 print_err('Invalid settings: "{0}"'.format(setting))
1406 value = None
1407 return value
1408
1409 def __setitem__(self, setting, value):
1410 """
1411 Update a setting. No validation on Value. Save file after update.
1412 """
1413 self.__safe_set(setting, value)
1414 self.write()
1415
1416 def __safe_set(self, setting, value):
1417 """
1418 Update a setting. No validation on Value.
1419 """
1420 try:
1421 if self.data[setting] == value:
1422 return
1423 except KeyError:
1424 print_err('Invalid settings: "{0}"'.format(setting))
1425 return
1426 self.data[setting] = value
1427
1428 def update(self, settings=None, **kwargs):
1429 """
1430 Update Multiple settings. No validation on Value.
1431 Saves file after update.
1432 Usage:
1433 config = ePadConf()
1434 config.update(font='Mono',font_size=24)
1435 config.update({'font': 'Mono', 'font_size': 24})
1436 """
1437 if settings is not None:
1438 for setting, value in settings.items() if isinstance(settings, Mapping) else settings:
1439 self.__safe_set(setting, value)
1440 for setting, value in kwargs.items():
1441 self.__safe_set(setting, value)
1442 self.write()
1443
1444
1445if __name__ == "__main__":
1446
1447 ourFiles = sys.argv
1448
1449 #Remove ePad.py from the arguments
1450 del ourFiles[0]
1451
1452 # Start App
1453 elementary.init()
1454 GUI = Interface()
1455 if ourFiles:
1456 GUI.launch([ourFiles, None])
1457 else:
1458 GUI.launch([None, os.getcwd()])
1459 elementary.run()
1460 GUI.fileSelector.shutdown()
1461 # Shutdown App
1462 elementary.shutdown()