Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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()
Beispiel #4
0
    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)
Beispiel #5
0
    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()
Beispiel #6
0
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)
Beispiel #7
0
    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)
Beispiel #8
0
    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()
Beispiel #9
0
def setup():
    get_main_window().geometry(config['default_geometry'])
    get_main_window().bind('<<RBTKQuit>>', save_geometry, add=True)
Beispiel #10
0
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