def setup(): # this must run even if loading tabs from states below fails get_main_window().bind('<<RBTKQuit>>', save_states, add=True) try: with open(STATE_FILE, 'rb') as file: states = pickle.load(file) except FileNotFoundError: states = [] for tab_class, state in states: tab = tab_class.from_state(get_tab_manager(), state) get_tab_manager().add_tab(tab)
def run_in_thread(blocking_function, done_callback): """Run ``blocking_function()`` in another thread. If the *blocking_function* raises an error, ``done_callback(False, traceback)`` will be called where *traceback* is the error message as a string. If no errors are raised, ``done_callback(True, result)`` will be called where *result* is the return value from *blocking_function*. The *done_callback* is always called from Tk's main loop, so it can do things with Tkinter widgets unlike *blocking_function*. """ root = rbtk.get_main_window() result = [] # [success, result] def thread_target(): # the logging module uses locks so calling it from another # thread should be safe try: value = blocking_function() result[:] = [True, value] except Exception as e: result[:] = [False, traceback.format_exc()] def check(): if thread.is_alive(): # let's come back and check again later root.after(100, check) else: done_callback(*result) thread = threading.Thread(target=thread_target) thread.start() root.after_idle(check)
def errordialog(title, message, monospace_text=None): """This is a lot like ``tkinter.messagebox.showerror``. This function can be called with or without creating a root window first. If *monospace_text* is not None, it will be displayed below the message in a ``tkinter.Text`` widget. Example:: try: do something except SomeError: utils.errordialog("Oh no", "Doing something failed!", traceback.format_exc()) """ root = rbtk.get_main_window() if root is None: window = tkinter.Tk() else: window = tkinter.Toplevel() window.transient(root) # there's nothing but this frame in the window because ttk widgets # may use a different background color than the window big_frame = ttk.Frame(window) big_frame.pack(fill='both', expand=True) label = ttk.Label(big_frame, text=message) if monospace_text is None: label.pack(fill='both', expand=True) geometry = '250x150' else: label.pack(anchor='center') # there's no ttk.Text 0_o this looks very different from # everything else and it sucks :( text = tkinter.Text(big_frame, width=1, height=1) text.pack(fill='both', expand=True) text.insert('1.0', monospace_text) text['state'] = 'disabled' geometry = '400x300' button = ttk.Button(big_frame, text="OK", command=window.destroy) button.pack(pady=10) window.title(title) window.geometry(geometry) window.wait_window()
def start(self): window = rbtk.get_main_window() busy_status = window.tk.call('tk', 'busy', 'status', window) if window.getboolean(busy_status): # we are already pasting something somewhere or something # else is being done log.info("'tk busy status %s' returned 1", window) return log.debug("starting to paste to %s", self.pastebin_name) window.tk.call('tk', 'busy', 'hold', window) self.make_please_wait_window() paste_it = functools.partial( _pastebins[self.pastebin_name], self.content, self.origin) utils.run_in_thread(paste_it, self.done_callback)
def __init__(self, manager, content='', path=None): super().__init__(manager) self._save_hash = None # path and filetype are set correctly below # TODO: try to guess the filetype from the content when path is None self._path = path self._guess_filetype() # this sets self._filetype self.bind('<<PathChanged>>', self._update_title, add=True) self.bind('<<PathChanged>>', self._guess_filetype, add=True) # we need to set width and height to 1 to make sure it's never too # large for seeing other widgets # TODO: document this self.textwidget = textwidget.MainText( self, self._filetype, width=1, height=1, wrap='none', undo=True) self.textwidget.pack(side='left', fill='both', expand=True) self.bind('<<FiletypeChanged>>', lambda event: self.textwidget.set_filetype(self.filetype), add=True) self.textwidget.bind('<<ContentChanged>>', self._update_title, add=True) if content: self.textwidget.insert('1.0', content) self.textwidget.edit_reset() # reset undo/redo self.bind('<<PathChanged>>', self._update_status, add=True) self.bind('<<FiletypeChanged>>', self._update_status, add=True) self.textwidget.bind('<<CursorMoved>>', self._update_status, add=True) # everything seems to work ok without this except that e.g. # pressing Ctrl+O in the text widget opens a file AND inserts a # newline (Tk inserts a newline by default) utils.copy_bindings(rbtk.get_main_window(), self.textwidget) self.scrollbar = ttk.Scrollbar(self) self.scrollbar.pack(side='left', fill='y') self.textwidget['yscrollcommand'] = self.scrollbar.set self.scrollbar['command'] = self.textwidget.yview self.mark_saved() self._update_title() self._update_status()
def invert_color(color): """Return a color with opposite red, green and blue values. Example: ``invert_color('white')`` is ``'#000000'`` (black). This function uses tkinter for converting the color to RGB. That's why a tkinter root window must have been created, but *color* can be any Tk-compatible color string, like a color name or a ``'#rrggbb'`` string. The return value is always a ``'#rrggbb`` string (also compatible with Tk). """ # tkinter uses 16-bit colors for some reason, so gotta convert them # to 8-bit (with >> 8) widget = rbtk.get_main_window() r, g, b = (0xff - (value >> 8) for value in widget.winfo_rgb(color)) return '#%02x%02x%02x' % (r, g, b)
def done_callback(self, success, result): window = rbtk.get_main_window() window.tk.call('tk', 'busy', 'forget', window) self.please_wait_window.destroy() if success: log.info("pasting succeeded") dialog = SuccessDialog(result) dialog.title("Pasting Succeeded") dialog.geometry('450x150') dialog.transient(window) dialog.wait_window() else: # result is the traceback as a string log.error("pasting failed\n%s" % result) utils.errordialog( "Pasting Failed", ("Check your internet connection and try again.\n\n" + "Here's the full error message:"), monospace_text=result)
def make_please_wait_window(self): window = self.please_wait_window = tkinter.Toplevel() window.transient(rbtk.get_main_window()) window.title("Pasting...") window.geometry('350x150') window.resizable(False, False) # disable the close button, there's no good way to cancel this # forcefully :( window.protocol('WM_DELETE_WINDOW', (lambda: None)) content = ttk.Frame(window) content.pack(fill='both', expand=True) label = ttk.Label( content, font=('', 12, ''), text=("Pasting to %s, please wait..." % self.pastebin_name)) label.pack(expand=True) progressbar = ttk.Progressbar(content, mode='indeterminate') progressbar.pack(fill='x', padx=15, pady=15) progressbar.start()
def setup(): get_main_window().geometry(config['default_geometry']) get_main_window().bind('<<RBTKQuit>>', save_geometry, add=True)
def setup_actions(): def new_file(): _tab_manager.add_tab(tabs.FileTab(_tab_manager)) def open_files(): for path in _dialogs.open_files(): try: tab = tabs.FileTab.open_file(_tab_manager, path) except (UnicodeError, OSError) as e: log.exception("opening '%s' failed", path) utils.errordialog( type(e).__name__, "Opening failed!", traceback.format_exc()) continue _tab_manager.add_tab(tab) def close_current_tab(): if _tab_manager.current_tab.can_be_closed(): _tab_manager.close_tab(_tab_manager.current_tab) add_action(new_file, "File/New File", ("Ctrl+N", '<Control-n>')) add_action(open_files, "File/Open", ("Ctrl+O", '<Control-o>')) add_action((lambda: _tab_manager.current_tab.save()), "File/Save", ("Ctrl+S", '<Control-s>'), tabtypes=[tabs.FileTab]) add_action((lambda: _tab_manager.current_tab.save_as()), "File/Save As...", ("Ctrl+Shift+S", '<Control-S>'), tabtypes=[tabs.FileTab]) menubar.get_menu("File").add_separator() # TODO: disable File/Quit when there are tabs, it's too easy to hit # Ctrl+Q accidentally add_action(close_current_tab, "File/Close", ("Ctrl+W", '<Control-w>'), tabtypes=[tabs.Tab]) add_action(quit, "File/Quit", ("Ctrl+Q", '<Control-q>')) def textmethod(attribute): def result(): textwidget = _tab_manager.current_tab.textwidget method = getattr(textwidget, attribute) method() return result # FIXME: bind these in a text widget only, not globally add_action(textmethod('undo'), "Edit/Undo", ("Ctrl+Z", '<Control-z>'), tabtypes=[tabs.FileTab]) add_action(textmethod('redo'), "Edit/Redo", ("Ctrl+Y", '<Control-y>'), tabtypes=[tabs.FileTab]) add_action(textmethod('cut'), "Edit/Cut", ("Ctrl+X", '<Control-x>'), tabtypes=[tabs.FileTab]) add_action(textmethod('copy'), "Edit/Copy", ("Ctrl+C", '<Control-c>'), tabtypes=[tabs.FileTab]) add_action(textmethod('paste'), "Edit/Paste", ("Ctrl+V", '<Control-v>'), tabtypes=[tabs.FileTab]) add_action(textmethod('select_all'), "Edit/Select All", ("Ctrl+A", '<Control-a>'), tabtypes=[tabs.FileTab]) menubar.get_menu("Edit").add_separator() # FIXME: this separator thing is a mess :( menubar.get_menu("Edit").add_separator() add_action(functools.partial(settings.show_dialog, rbtk.get_main_window()), "Edit/RBTK Settings...") # the font size stuff are bound by the textwidget itself, that's why # there are Nones everywhere add_action((lambda: _tab_manager.current_tab.textwidget.on_wheel('up')), "View/Bigger Font", ("Ctrl+Plus", None), tabtypes=[tabs.FileTab]) add_action((lambda: _tab_manager.current_tab.textwidget.on_wheel('down')), "View/Smaller Font", ("Ctrl+Minus", None), tabtypes=[tabs.FileTab]) add_action((lambda: _tab_manager.current_tab.textwidget.on_wheel('reset')), "View/Reset Font Size", ("Ctrl+Zero", None), tabtypes=[tabs.FileTab]) menubar.get_menu("View").add_separator() def add_link(menupath, url): add_action(functools.partial(webbrowser.open, url), menupath) # TODO: an about dialog that shows porcupine version, Python version # and where porcupine is installed # TODO: porcupine starring button ## FIX LINKS add_link("Help/Free help chat", "http://webchat.freenode.net/?channels=%23%23learnpython") add_link("Help/My Python tutorial", "https://github.com/Akuli/python-tutorial/blob/master/README.md") add_link("Help/Official Python documentation", "https://docs.python.org/") menubar.get_menu("Help").add_separator() add_link("Help/Porcupine Wiki", "https://github.com/Akuli/porcupine/wiki") add_link("Help/Report a problem or request a feature", "https://github.com/Akuli/porcupine/issues/new") add_link("Help/Read Porcupine's code on GitHub", "https://github.com/Akuli/porcupine/tree/master/porcupine") # TODO: loading the styles takes a long time on startup... try to # make it asynchronous without writing too complicated code? config = settings.get_section('General') for name in sorted(pygments.styles.get_all_styles()): if name.islower(): label = name.replace('-', ' ').replace('_', ' ').title() else: label = name options = { 'label': label, 'value': name, 'variable': config.get_var('pygments_style'), } style = pygments.styles.get_style_by_name(name) bg = style.background_color # styles have a style_for_token() method, but only iterating # is documented :( http://pygments.org/docs/formatterdevelopment/ # i'm using iter() to make sure that dict() really treats # the style as an iterable of pairs instead of some other # metaprogramming fanciness fg = None style_infos = dict(iter(style)) for token in [pygments.token.String, pygments.token.Text]: if style_infos[token]['color'] is not None: fg = '#' + style_infos[token]['color'] break if fg is None: # do like textwidget.ThemedText._set_style does fg = (getattr(style, 'default_style', '') or utils.invert_color(bg)) options['foreground'] = options['activebackground'] = fg options['background'] = options['activeforeground'] = bg