Beispiel #1
0
def _calculate_popup_geometry(textwidget: tkinter.Text) -> str:
    bbox = textwidget.bbox('insert')
    assert bbox is not None  # cursor must be visible
    (cursor_x, cursor_y, cursor_width, cursor_height) = bbox

    # make coordinates relative to screen
    cursor_x += textwidget.winfo_rootx()
    cursor_y += textwidget.winfo_rooty()

    # leave some space
    cursor_y -= 5
    cursor_height += 10

    popup_width = settings.get('autocomplete_popup_width', int)
    popup_height = settings.get('autocomplete_popup_height', int)
    screen_width = textwidget.winfo_screenwidth()
    screen_height = textwidget.winfo_screenheight()

    # don't go off the screen to the right, leave space between popup
    # and right side of window
    x = min(screen_width - popup_width - 10, cursor_x)

    if cursor_y + cursor_height + popup_height < screen_height:
        # it fits below cursor, put it there
        y = cursor_y + cursor_height
    else:
        # put it above cursor instead. If it doesn't fit there either,
        # then y is also negative and the user has a tiny screen or a
        # huge popup size.
        y = cursor_y - popup_height

    return f'{popup_width}x{popup_height}+{x}+{y}'
Beispiel #2
0
def test_add_option_and_get_and_set(cleared_global_settings):
    settings.add_option('how_many_foos', 123)
    settings.add_option('bar_message', 'hello')

    assert settings.get('how_many_foos', int) == 123
    assert settings.get('bar_message', str) == 'hello'
    settings.set('how_many_foos', 456)
    settings.set('bar_message', 'bla')
    assert settings.get('how_many_foos', int) == 456
    assert settings.get('bar_message', str) == 'bla'
Beispiel #3
0
def test_add_option_and_get_and_set(cleared_global_settings):
    settings.add_option("how_many_foos", 123)
    settings.add_option("bar_message", "hello")

    assert settings.get("how_many_foos", int) == 123
    assert settings.get("bar_message", str) == "hello"
    settings.set_("how_many_foos", 456)
    settings.set_("bar_message", "bla")
    assert settings.get("how_many_foos", int) == 456
    assert settings.get("bar_message", str) == "bla"
Beispiel #4
0
def test_wrong_type(cleared_global_settings):
    settings.add_option('magic_message', 'bla')

    with pytest.raises(
            dacite.exceptions.WrongTypeError,
            match=r'wrong value type .* should be "int" instead of .* "str"'):
        settings.get('magic_message', int)
    with pytest.raises(
            dacite.exceptions.WrongTypeError,
            match=r'wrong value type .* should be "str" instead of .* "int"'):
        settings.set('magic_message', 123)
Beispiel #5
0
    def set_font(self, junk: object = None) -> None:
        font = (settings.get('font_family', str),
                round(settings.get('font_size', int) / 3), ())
        how_to_show_tab = ' ' * self._tab.settings.get('indent_size', int)

        # tkinter doesn't provide a better way to do font stuff than stupid
        # font object
        self.config(
            tabs=self.tk.call('font', 'measure', font, how_to_show_tab))
        self.tag_config('sel', font=font)
        self._update_vast()
Beispiel #6
0
 def set_font(self, junk: object = None) -> None:
     self.tag_config(
         "sel",
         font=(settings.get("font_family", str),
               round(settings.get("font_size", int) / 3), ()),
     )
     textwidget.config_tab_displaying(self,
                                      self._tab.settings.get(
                                          "indent_size", int),
                                      tag="sel")
     self._update_vast()
Beispiel #7
0
def test_unknown_option_in_settings_file(cleared_global_settings):
    load_from_json_string('{"foo": "custom", "unknown": "hello"}')
    with pytest.raises(KeyError):
        settings.get('foo', str)

    settings.add_option('foo', 'default')
    assert settings.get('foo', str) == 'custom'
    settings.set('foo', 'default')
    assert settings.get('foo', str) == 'default'

    assert save_and_read_file() == {'unknown': 'hello'}
Beispiel #8
0
def test_unknown_option_in_settings_file(cleared_global_settings):
    load_from_json_string('{"foo": "custom", "unknown": "hello"}')
    with pytest.raises(KeyError):
        settings.get("foo", str)

    settings.add_option("foo", "default")
    assert settings.get("foo", str) == "custom"
    settings.set_("foo", "default")
    assert settings.get("foo", str) == "default"

    assert save_and_read_file() == {"unknown": "hello"}
Beispiel #9
0
    def _update_row(self, info: pluginloader.PluginInfo) -> None:
        if info.came_with_porcupine:
            how_it_got_installed = "Came with Porcupine"
        else:
            how_it_got_installed = "You installed this"

        disable_list = settings.get('disabled_plugins', List[str])
        if info.status == pluginloader.Status.DISABLED_BY_SETTINGS and info.name not in disable_list:
            message = "Will be enabled upon restart"
        elif info.status != pluginloader.Status.DISABLED_BY_SETTINGS and info.name in disable_list:
            message = "Will be disabled upon restart"
        else:
            message = {
                pluginloader.Status.ACTIVE:
                "Active",
                pluginloader.Status.DISABLED_BY_SETTINGS:
                "Disabled",
                pluginloader.Status.DISABLED_ON_COMMAND_LINE:
                "Disabled on command line",
                pluginloader.Status.IMPORT_FAILED:
                "Importing failed",
                pluginloader.Status.SETUP_FAILED:
                "Setup failed",
                pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR:
                "Circular dependency",
            }[info.status]

        self._treeview.item(info.name,
                            values=(info.name, how_it_got_installed, message))
Beispiel #10
0
    def on_style_changed(junk: object = None) -> None:
        style = styles.get_style_by_name(settings.get("pygments_style", str))
        bg = style.background_color

        # yes, style.default_style can be '#rrggbb', '' or nonexistent
        # this is undocumented
        #
        #   >>> from pygments.styles import *
        #   >>> [getattr(get_style_by_name(name), 'default_style', '???')
        #   ...  for name in get_all_styles()]
        #   ['', '', '', '', '', '', '???', '???', '', '', '', '',
        #    '???', '???', '', '#cccccc', '', '', '???', '', '', '', '',
        #    '#222222', '', '', '', '???', '']
        fg = getattr(style, "default_style", "") or utils.invert_color(bg)
        if callback is None:
            assert isinstance(widget, tkinter.Text)
            widget.config(
                foreground=fg,
                background=bg,
                insertbackground=fg,  # cursor color
                selectforeground=bg,
                selectbackground=fg,
            )
        else:
            callback(fg, bg)
Beispiel #11
0
    def _on_select(self, junk: object = None) -> None:
        [plugin_name] = self._treeview.selection()
        [info] = [
            info for info in pluginloader.plugin_infos
            if info.name == plugin_name
        ]

        if info.status == pluginloader.Status.IMPORT_FAILED:
            text = f"Importing the plugin failed.\n\n{info.error}"
        elif info.status == pluginloader.Status.SETUP_FAILED:
            text = f"The plugin's setup() function failed.\n\n{info.error}"
        elif info.status == pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR:
            assert info.error is not None
            text = info.error
        else:
            text = get_docstring(f'porcupine.plugins.{info.name}')
            # get rid of single newlines
            text = re.sub(r'(.)\n(.)', r'\1 \2', text)

        self._title_label.config(text=plugin_name)
        self._description.config(state='normal')
        self._description.delete('1.0', 'end')
        self._description.insert('1.0', text)
        self._description.config(state='disabled')

        disable_list = settings.get('disabled_plugins', List[str])
        self._enable_disable_button.config(
            state='normal',
            text=("Enable" if plugin_name in disable_list else "Disable"))
Beispiel #12
0
    def _on_select(self, junk: object = None) -> None:
        infos = self._get_selected_infos()
        disable_list = settings.get("disabled_plugins", List[str])

        if len(infos) == 1:
            info = infos[0]
            if info.status == pluginloader.Status.IMPORT_FAILED:
                text = f"Importing the plugin failed.\n\n{info.error}"
            elif info.status == pluginloader.Status.SETUP_FAILED:
                text = f"The plugin's setup() function failed.\n\n{info.error}"
            elif info.status == pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR:
                assert info.error is not None
                text = info.error
            else:
                text = get_docstring(f"porcupine.plugins.{info.name}")
                # get rid of single newlines
                text = re.sub(r"(.)\n(.)", r"\1 \2", text)

            self._title_label.config(text=info.name)
            self._set_description(text)

        else:
            self._title_label.config(text="")
            self._set_description(f"{len(infos)} plugins selected.")

        self.enable_button.config(state=("normal" if any(
            info.name in disable_list for info in infos) else "disabled"))
        self.disable_button.config(state=("normal" if any(
            info.name not in disable_list for info in infos) else "disabled"))
Beispiel #13
0
def get_filetype_for_tab(tab: tabs.FileTab) -> FileType:
    if tab.path is None:
        return filetypes[settings.get('default_filetype', str)]
    # FIXME: this may read the shebang from the file, but the file
    #        might not be saved yet because save_as() sets self.path
    #        before saving, and that's when this runs
    return guess_filetype(tab.path)
Beispiel #14
0
    def check_if_it_finished() -> None:
        if thread.is_alive():
            get_main_window().after(200, check_if_it_finished)
            return

        var = tkinter.StringVar(value=settings.get("pygments_style", str))

        def settings2var(event: tkinter.Event[tkinter.Misc]) -> None:
            var.set(settings.get("pygments_style", str))

        def var2settings(*junk: str) -> None:
            settings.set_("pygments_style", var.get())

        # this doesn't recurse infinitely because <<SettingChanged:bla>>
        # gets generated only when the setting actually changes
        get_tab_manager().bind("<<SettingChanged:pygments_style>>",
                               settings2var,
                               add=True)
        var.trace_add("write", var2settings)

        for style_name in style_names:
            fg, bg = get_colors(style_name)
            menubar.get_menu("Color Styles").add_radiobutton(
                label=style_name,
                value=style_name,
                variable=var,
                foreground=fg,
                background=bg,
                # swapped colors
                activeforeground=bg,
                activebackground=fg,
            )
Beispiel #15
0
def setup() -> None:
    style = ttkthemes.ThemedStyle()
    settings.add_option("ttk_theme", style.theme_use())

    var = tkinter.StringVar()
    for name in sorted(style.get_themes()):
        menubar.get_menu("Ttk Themes").add_radiobutton(label=name, value=name, variable=var)

    # Connect style and var
    var.trace_add("write", lambda *junk: style.set_theme(var.get()))

    # Connect var and settings
    get_main_window().bind(
        "<<SettingChanged:ttk_theme>>",
        lambda event: var.set(settings.get("ttk_theme", str)),
        add=True,
    )
    var.set(settings.get("ttk_theme", str))
    var.trace_add("write", lambda *junk: settings.set_("ttk_theme", var.get()))
Beispiel #16
0
def test_no_json_file(cleared_global_settings):
    assert not settings._get_json_path().exists()
    settings._load_from_file()

    settings.add_option('foo', 'default')
    settings.set('foo', 'custom')

    assert not settings._get_json_path().exists()
    settings.reset_all()
    assert settings.get('foo', str) == 'default'
Beispiel #17
0
def test_no_json_file(cleared_global_settings):
    assert not settings._get_json_path().exists()
    settings._load_from_file()

    settings.add_option("foo", "default")
    settings.set_("foo", "custom")

    assert not settings._get_json_path().exists()
    settings.reset_all()
    assert settings.get("foo", str) == "default"
Beispiel #18
0
    def on_style_changed(self, junk: object = None) -> None:
        style = styles.get_style_by_name(settings.get("pygments_style", str))
        infos = dict(iter(style))  # iterating is documented
        for tokentype in [token.Error, token.Name.Exception]:
            if tokentype in infos:
                for key in ["bgcolor", "color", "border"]:
                    if infos[tokentype][key] is not None:
                        self.frame.config(bg=("#" + infos[tokentype][key]))
                        return

        # stupid fallback
        self.frame.config(bg="red")
Beispiel #19
0
    def __init__(self,
                 manager: TabManager,
                 content: str = '',
                 path: Optional[pathlib.Path] = None,
                 filetype: Optional[Dict[str, Any]] = None) -> None:
        super().__init__(manager)

        self._save_hash: Optional[str] = None
        if path is None:
            self._path = None
        else:
            self._path = path.resolve()

        self.settings = settings.Settings(self, '<<TabSettingChanged:{}>>')
        self.settings.add_option('pygments_lexer',
                                 pygments.lexers.TextLexer,
                                 type=pygments.lexer.LexerMeta,
                                 converter=_import_lexer_class)
        self.settings.add_option('tabs2spaces', True)
        self.settings.add_option('indent_size', 4)
        self.settings.add_option('encoding', 'utf-8')
        self.settings.add_option('line_ending',
                                 settings.get('default_line_ending',
                                              settings.LineEnding),
                                 converter=settings.LineEnding.__getitem__)

        # we need to set width and height to 1 to make sure it's never too
        # large for seeing other widgets
        self.textwidget = textwidget.MainText(self,
                                              width=1,
                                              height=1,
                                              wrap='none',
                                              undo=True)
        self.textwidget.pack(side='left', fill='both', expand=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.textwidget.bind('<<CursorMoved>>', self._update_status, add=True)

        self.scrollbar = ttk.Scrollbar(self.right_frame)
        self.scrollbar.pack(side='right', fill='y')
        self.textwidget.config(yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.textwidget.yview)

        self.mark_saved()
        self._update_title()
        self._update_status()
Beispiel #20
0
    def on_style_changed(self, junk: object = None) -> None:
        style = pygments.styles.get_style_by_name(settings.get('pygments_style', str))
        infos = dict(iter(style))   # iterating is documented
        for tokentype in [pygments.token.Error, pygments.token.Name.Exception]:
            if tokentype in infos:
                for key in ['bgcolor', 'color', 'border']:
                    if infos[tokentype][key] is not None:
                        self.frame.config(bg=('#' + infos[tokentype][key]))
                        return

        # stupid fallback
        self.frame.config(bg='red')
Beispiel #21
0
    def _toggle_enabled(self) -> None:
        [plugin_name] = self._treeview.selection()
        [info] = [
            info for info in pluginloader.plugin_infos
            if info.name == plugin_name
        ]

        disabled = set(settings.get('disabled_plugins', List[str]))
        disabled ^= {plugin_name}
        settings.set('disabled_plugins', list(disabled))
        self._update_row(info)
        self._on_select()
        self._update_plz_restart_label()
Beispiel #22
0
def setup() -> None:
    style = ttkthemes.ThemedStyle()

    # https://github.com/RedFantom/ttkthemes/issues/6
    # this does what style.theme_use() should do
    default_theme = style.tk.eval('return $ttk::currentTheme')
    settings.add_option('ttk_theme', default_theme)

    var = tkinter.StringVar()
    var.trace_add('write', functools.partial(on_theme_changed, style, var))
    var.set(settings.get('ttk_theme', str))
    for name in sorted(style.get_themes()):
        menubar.get_menu("Ttk Themes").add_radiobutton(label=name, value=name, variable=var)
Beispiel #23
0
    def change_font_size(how: Literal['bigger', 'smaller', 'reset']) -> None:
        if how == 'reset':
            settings.reset('font_size')
            return

        size = settings.get('font_size', int)
        if how == 'bigger':
            size += 1
        else:
            size -= 1
            if size < 3:
                return

        settings.set('font_size', size)
Beispiel #24
0
    def change_font_size(how: Literal["bigger", "smaller", "reset"]) -> None:
        if how == "reset":
            settings.reset("font_size")
            return

        size = settings.get("font_size", int)
        if how == "bigger":
            size += 1
        else:
            size -= 1
            if size < 3:
                return

        settings.set_("font_size", size)
Beispiel #25
0
def test_remember_panedwindow_positions(toplevel):
    pw = ttk.PanedWindow(toplevel, orient="horizontal")
    settings.remember_divider_positions(pw, "pw_dividers", [123])
    pw.pack(fill="both", expand=True)

    pw.add(ttk.Label(pw, text="aaaaaaaaaaa"))
    pw.add(ttk.Label(pw, text="bbbbbbbbbbb"))

    pw.update()
    assert pw.sashpos(0) == 123

    pw.sashpos(0, 456)
    pw.event_generate("<ButtonRelease-1>")  # happens after user drags pane
    assert settings.get("pw_dividers", List[int]) == [456]
Beispiel #26
0
def test_reset(cleared_global_settings):
    load_from_json_string(
        '{"foo": "custom", "bar": "custom", "unknown": "hello"}')
    settings.add_option('foo', 'default')
    settings.add_option('bar', 'default')
    settings.add_option('baz', 'default')
    assert settings.get('foo', str) == 'custom'
    assert settings.get('bar', str) == 'custom'
    assert settings.get('baz', str) == 'default'

    settings.reset('bar')
    assert settings.get('foo', str) == 'custom'
    assert settings.get('bar', str) == 'default'
    assert settings.get('baz', str) == 'default'
    assert save_and_read_file() == {'foo': 'custom', 'unknown': 'hello'}

    settings.reset_all()
    assert settings.get('foo', str) == 'default'
    assert settings.get('bar', str) == 'default'
    assert settings.get('baz', str) == 'default'
    assert save_and_read_file() == {}  # even unknown options go away
Beispiel #27
0
def test_reset(cleared_global_settings):
    load_from_json_string(
        '{"foo": "custom", "bar": "custom", "unknown": "hello"}')
    settings.add_option("foo", "default")
    settings.add_option("bar", "default")
    settings.add_option("baz", "default")
    assert settings.get("foo", str) == "custom"
    assert settings.get("bar", str) == "custom"
    assert settings.get("baz", str) == "default"

    settings.reset("bar")
    assert settings.get("foo", str) == "custom"
    assert settings.get("bar", str) == "default"
    assert settings.get("baz", str) == "default"
    assert save_and_read_file() == {"foo": "custom", "unknown": "hello"}

    settings.reset_all()
    assert settings.get("foo", str) == "default"
    assert settings.get("bar", str) == "default"
    assert settings.get("baz", str) == "default"
    assert save_and_read_file() == {}  # even unknown options go away
Beispiel #28
0
    def _set_enabled(self, they_become_enabled: bool) -> None:
        infos = self._get_selected_infos()

        disabled = set(settings.get("disabled_plugins", List[str]))
        if they_become_enabled:
            disabled -= {info.name for info in infos}
        else:
            disabled |= {info.name for info in infos}
        settings.set_("disabled_plugins", list(disabled))

        for info in infos:
            if info.name not in disabled and pluginloader.can_setup_while_running(
                    info):
                pluginloader.setup_while_running(info)
            self._update_row(info)

        self._on_select()
        self._update_plz_restart_label()
Beispiel #29
0
def import_plugins(disabled_on_command_line: List[str]):
    assert not _mutable_plugin_infos
    _mutable_plugin_infos.extend(
        PluginInfo(
            name=name,
            came_with_porcupine=_did_plugin_come_with_porcupine(finder),
            status=Status.LOADING,
            module=None,
            setup_before=set(),
            setup_after=set(),
            error=None,
        ) for finder, name, is_pkg in pkgutil.iter_modules(plugin_paths)
        if not name.startswith('_'))

    for info in _mutable_plugin_infos:
        # If it's disabled in settings and on command line, then status is set
        # to DISABLED_BY_SETTINGS. This makes more sense for the user of the
        # plugin manager dialog.
        if info.name in settings.get('disabled_plugins', List[str]):
            info.status = Status.DISABLED_BY_SETTINGS
            continue
        if info.name in disabled_on_command_line:
            info.status = Status.DISABLED_ON_COMMAND_LINE
            continue

        log.debug(f"trying to import porcupine.plugins.{info.name}")
        start = time.time()

        try:
            info.module = importlib.import_module(
                f'porcupine.plugins.{info.name}')
            info.setup_before = set(getattr(info.module, 'setup_before', []))
            info.setup_after = set(getattr(info.module, 'setup_after', []))
        except Exception:
            log.exception(f"can't import porcupine.plugins.{info.name}")
            info.status = Status.IMPORT_FAILED
            info.error = traceback.format_exc()
            continue

        duration = time.time() - start
        log.debug("imported porcupine.plugins.%s in %.3f milliseconds",
                  info.name, duration * 1000)
Beispiel #30
0
    def _style_changed(self, junk: object = None) -> None:
        # http://pygments.org/docs/formatterdevelopment/#styles
        # all styles seem to yield all token types when iterated over,
        # so we should always end up with the same tags configured
        style = styles.get_style_by_name(settings.get("pygments_style", str))
        for tokentype, infodict in style:
            # this doesn't use underline and border
            # i don't like random underlines in my code and i don't know
            # how to implement the border with tkinter
            self.textwidget.tag_config(
                str(tokentype),
                font=self._fonts[(infodict["bold"], infodict["italic"])],
                # empty string resets foreground
                foreground=("" if infodict["color"] is None else "#" + infodict["color"]),
                background=("" if infodict["bgcolor"] is None else "#" + infodict["bgcolor"]),
            )

            # make sure that the selection tag takes precedence over our
            # token tag
            self.textwidget.tag_lower(str(tokentype), "sel")