diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:17:00 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:17:00 +0100 |
commit | 5a325565f917c8b1d233d8e6373756c253400909 (patch) | |
tree | e6e0b475c7ab5c6a7ff4f0ea7ad1b08cecf05e68 /tagit/dialogues | |
parent | e1e77797454ac747b293f589d8f2e0243173a419 (diff) | |
parent | 98e567933723c59d1d97b3a85e649cfdce514676 (diff) | |
download | tagit-0.23.03.tar.gz tagit-0.23.03.tar.bz2 tagit-0.23.03.zip |
Merge branch 'develop'v0.23.03
Diffstat (limited to 'tagit/dialogues')
-rw-r--r-- | tagit/dialogues/__init__.py | 42 | ||||
-rw-r--r-- | tagit/dialogues/autoinput.py | 73 | ||||
-rw-r--r-- | tagit/dialogues/console.kv | 30 | ||||
-rw-r--r-- | tagit/dialogues/console.py | 37 | ||||
-rw-r--r-- | tagit/dialogues/dialogue.kv | 77 | ||||
-rw-r--r-- | tagit/dialogues/dialogue.py | 108 | ||||
-rw-r--r-- | tagit/dialogues/error.py | 45 | ||||
-rw-r--r-- | tagit/dialogues/file_picker.py | 39 | ||||
-rw-r--r-- | tagit/dialogues/license.t | 33 | ||||
-rw-r--r-- | tagit/dialogues/message.py | 47 | ||||
-rw-r--r-- | tagit/dialogues/numeric_input.py | 72 | ||||
-rw-r--r-- | tagit/dialogues/path_picker.kv | 27 | ||||
-rw-r--r-- | tagit/dialogues/path_picker.py | 41 | ||||
-rw-r--r-- | tagit/dialogues/simple_input.kv | 30 | ||||
-rw-r--r-- | tagit/dialogues/simple_input.py | 55 | ||||
-rw-r--r-- | tagit/dialogues/stoken.py | 40 |
16 files changed, 796 insertions, 0 deletions
diff --git a/tagit/dialogues/__init__.py b/tagit/dialogues/__init__.py new file mode 100644 index 0000000..beed253 --- /dev/null +++ b/tagit/dialogues/__init__.py @@ -0,0 +1,42 @@ +"""Popup dialogues. + +A dialogue can be opened from the main application. +It appears on top of the application and prevent its use until the dialogue +is closed. A dialogue contains buttons whose presses can be captured. + +>>> dlg = LabelDialogue(text='Hello world') +>>> dlg.bind(on_ok=...) +>>> dlg.bind(on_cancel=...) +>>> dlg.open() + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# inner-module imports +from .autoinput import AutoTextInput +from .console import Console +from .error import Error +from .file_picker import FilePicker +from .message import Message +from .numeric_input import NumericInput +from .path_picker import PathPicker +from .simple_input import SimpleInput +from .stoken import TokenEdit + +# exports +__all__: typing.Sequence[str] = ( + 'Console', + 'Error', + 'FilePicker', + 'Message', + 'NumericInput', + 'PathPicker', + 'SimpleInput', + 'TokenEdit', + ) + +## EOF ## diff --git a/tagit/dialogues/autoinput.py b/tagit/dialogues/autoinput.py new file mode 100644 index 0000000..a036ed4 --- /dev/null +++ b/tagit/dialogues/autoinput.py @@ -0,0 +1,73 @@ +"""This is a simple example of how to use suggestion text. + +In this example you setup a word_list at the begining. In this case +'the the quick brown fox jumps over the lazy old dog'. This list along +with any new word written word in the textinput is available as a +suggestion when you are typing. You can press tab to auto complete the text. + +Based on & thanks to akshayaurora: + https://gist.github.com/akshayaurora/fa5a68980af585e355668e5adce5f98b + +Part of the tagit module. +A copy of the license is provided with the project. +Modifications authored by: Matthias Baumgartner, 2022 +""" +# standard imports +from bisect import bisect + +# kivy imports +from kivy.uix.textinput import TextInput +import kivy.properties as kp + +# exports +__all__ = ('AutoTextInput', ) + + +## code ## + +class AutoTextInput(TextInput): + + sep = kp.StringProperty(',') + suffix = kp.StringProperty(' ') + vocabulary = kp.ListProperty() + + def on_suggestion_text(self, wx, value): + if not value: + return + + super(AutoTextInput, self).on_suggestion_text(wx, value) + + def keyboard_on_key_down(self, window, keycode, text, modifiers): + if self.suggestion_text and keycode[1] == 'tab': # complete suggestion_text + self.insert_text(self.suggestion_text + self.sep + self.suffix) + self.suggestion_text = '' + return True + return super(AutoTextInput, self).keyboard_on_key_down(window, keycode, text, modifiers) + + def on_text(self, wx, value): + # include all current text from textinput into the word list + # the kind of behavior sublime text has + + # what's on the current line + temp = value[:value.rfind(self.sep)].split(self.sep) + temp = [s.strip() for s in temp] + # combine with static vocabulary + wordlist = sorted(set(self.vocabulary + temp)) + + # get prefix + prefix = value[value.rfind(self.sep)+1:].strip() + if not prefix: + return + + # binary search on (sorted) wordlist + pos = bisect(wordlist, prefix) + + # check if matching string found + if pos == len(wordlist) or not wordlist[pos].startswith(prefix): + self.suggestion_text = '' + return + + # fetch suffix from wordlist + self.suggestion_text = wordlist[pos][len(prefix):] + +## EOF ## diff --git a/tagit/dialogues/console.kv b/tagit/dialogues/console.kv new file mode 100644 index 0000000..b68227d --- /dev/null +++ b/tagit/dialogues/console.kv @@ -0,0 +1,30 @@ + +<Console>: + text: '' + title: 'tagit log console' + ok_on_enter: False + init_at_bottom: True + + DialogueContentTitle: + title: root.title + size_hint_y: 0.6 + + ScrollView: + scroll_y: 0 if root.init_at_bottom else 1 + + Label: + text: root.text + size_hint_y: None + height: self.texture_size[1] + text_size: self.width, None + bold: True + font_name: resource_find('DejaVuSansMono.ttf') # monospace font + markup: True + multiline: True + halign: 'left' + + DialogueButtons_One: + ok_text: "close" + # FIXME: Inform that this action will not terminate the underlying process + +## EOF ## diff --git a/tagit/dialogues/console.py b/tagit/dialogues/console.py new file mode 100644 index 0000000..282b378 --- /dev/null +++ b/tagit/dialogues/console.py @@ -0,0 +1,37 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.clock import mainthread +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Console', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'console.kv')) + +# classes +class Console(Dialogue): + """Dialogue with console output.""" + + text = kp.StringProperty('') + + @mainthread + def update(self, sender, text): + self.text = '\n'.join(text) + +## EOF ## diff --git a/tagit/dialogues/dialogue.kv b/tagit/dialogues/dialogue.kv new file mode 100644 index 0000000..e2cab66 --- /dev/null +++ b/tagit/dialogues/dialogue.kv @@ -0,0 +1,77 @@ +#:import get_root tagit.utils.get_root +# FIXME: remove need for get_root + +<-Dialogue>: + auto_dismiss: True + ok_on_enter: True + +<DialogueContentBase>: + orientation: 'vertical' + padding: '12dp' + size_hint: 0.66, None + height: self.minimum_height + + +<DialogueContentNoTitle@DialogueContentBase>: + # nothing to do + + +<DialogueTitle@Label>: + +<DialogueContentTitle@DialogueContentBase>: + title: '' + title_color: 1,1,1,1 + + DialogueTitle: + text: root.title + size_hint_y: None + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + color: root.title_color + + # small space + Label: + size_hint_y: None + height: 12 + + + +<DialogueButton@Button>: + +<DialogueButtonRow>: + orientation: 'vertical' + size_hint_y: None + height: dp(48+8) + + # small space + Label: + size_hint_y: None + height: dp(8) + + # here come the buttons + + +<DialogueButtons_One@DialogueButtonRow>: + ok_text: 'OK' + + DialogueButton: + text: root.ok_text + on_press: get_root(self).ok() + + +<DialogueButtons_Two@DialogueButtonRow>: + cancel_text: 'Cancel' + ok_text: 'OK' + ok_enabled: True + + BoxLayout: + orientation: 'horizontal' + DialogueButton: + text: root.cancel_text + on_press: get_root(self).cancel() + DialogueButton: + text: root.ok_text + on_press: get_root(self).ok() + disabled: not root.ok_enabled + +## EOF ## diff --git a/tagit/dialogues/dialogue.py b/tagit/dialogues/dialogue.py new file mode 100644 index 0000000..bf72a28 --- /dev/null +++ b/tagit/dialogues/dialogue.py @@ -0,0 +1,108 @@ +"""Popup dialogue. + +Rougly based on code from https://gist.github.com/kived/742397a80d61e6be225a +by Ryan Pessa. The license is provided in the source folder. + +Part of the tagit module. +A copy of the license is provided with the project. +Modifications authored by: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.lang import Builder +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup +import kivy.properties as kp + +# exports +__all__ = ('Dialogue', ) + + +## code ## + +# Load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'dialogue.kv')) + +# classes +class Dialogue(Popup): + """Popup dialogue base class. + + Use like below: + + >>> dlg = Dialogue() + >>> dlg.bind(on_ok=....) + >>> dlg.open() + + """ + + ok_on_enter = kp.BooleanProperty() + + __events__ = ('on_ok', 'on_cancel') + + def __init__(self, *args, **kwargs): + super(Dialogue, self).__init__(*args, **kwargs) + from kivy.core.window import Window + # assumes that the first widget created controls the keyboard + #Window.children[-1].request_exclusive_keyboard() + # Alternatively, you can bind a function (self._on_keyboard) to on_keyboard + # which returns True. This stops the event from being processed by the main + # window. + # However, this still does not keep the 'enter' from 'on_text_validate' from + # being processed by the main window. + Window.bind(on_keyboard=self._on_keyboard) + # By binding to on_key_down, the <enter> key can trigger the ok action. + # This also prevents the enter event to be processed by the main window, + # unlike the 'on_text_validate' of TextInput. + Window.bind(on_key_down=self._key_down) + + def _on_keyboard(self, *args, **kwargs): + # block events from processing in the main window + return True + + def _key_down(self, instance, key, scancode, codepoint, modifiers): + if key == 13 and self.ok_on_enter: + self.ok() + return True + # must not stop other events such that ctrl up/down reach the browser + + def ok(self): + """User pressed the OK button.""" + self.dispatch('on_ok') + self.dismiss() + + def cancel(self): + """User pressed the Cancel button.""" + self.dispatch('on_cancel') + self.dismiss() + + def on_dismiss(self): + from kivy.core.window import Window + # assumes that the first widget created controls the keyboard + #Window.children[-1].release_exclusive_keyboard() + Window.unbind(on_keyboard=self._on_keyboard) + Window.unbind(on_key_down=self._key_down) + super(Dialogue, self).on_dismiss() + + def on_ok(self): + """Event prototype.""" + pass + + def on_cancel(self): + """Event prototype.""" + pass + +# helper classes + +# content bases +class DialogueContentBase(BoxLayout): pass +class DialogueContentTitle(DialogueContentBase): pass +class DialogueContentNoTitle(DialogueContentBase): pass + +# buttons +class DialogueButtonRow(BoxLayout): pass +class DialogueButtons_One(DialogueButtonRow): pass +class DialogueButtons_Two(DialogueButtonRow): pass + +## EOF ## diff --git a/tagit/dialogues/error.py b/tagit/dialogues/error.py new file mode 100644 index 0000000..d93f853 --- /dev/null +++ b/tagit/dialogues/error.py @@ -0,0 +1,45 @@ +"""Dialogue to show an error message. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Error', ) + + +## code ## + +# load kv +Builder.load_string(''' +<Error>: + text: '' + ok_on_enter: False + + DialogueContentNoTitle: + + Label: + markup: True + text: root.text + size_hint_y: None + color: 1, 0, 0, 1 + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + halign: 'center' + + DialogueButtons_One: +''') + +# classes +class Error(Dialogue): + """Error message.""" + text = kp.StringProperty('') + +## EOF ## diff --git a/tagit/dialogues/file_picker.py b/tagit/dialogues/file_picker.py new file mode 100644 index 0000000..283adb6 --- /dev/null +++ b/tagit/dialogues/file_picker.py @@ -0,0 +1,39 @@ +"""Dialogue to pick a file. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.lang import Builder + +# inner-module imports +from .path_picker import PathPicker +from .error import Error + +# exports +__all__ = ('FilePicker', ) + + +## code ## + +# load kv +Builder.load_string(''' +<FilePicker>: + dirselect: False + title: 'Please select a file' +''') + +# classes +class FilePicker(PathPicker): + """Dialogue with a file browser to select a file.""" + def ok(self): + if not os.path.exists(self.path) or not os.path.isfile(self.path): + Error(text='Please select a file').open() + else: + super(FilePicker, self).ok() + +## EOF ## diff --git a/tagit/dialogues/license.t b/tagit/dialogues/license.t new file mode 100644 index 0000000..bbd2830 --- /dev/null +++ b/tagit/dialogues/license.t @@ -0,0 +1,33 @@ + +The dialogues are based on the following code: + +https://gist.github.com/kived/742397a80d61e6be225a + +It ships with license, provided below: + +>>> The following license shall apply to all Public Gists owned by account. It +>>> shall never apply to any Secret Gists, for which no license of any sort is +>>> granted. +>>> +>>> Copyright (c) 2015- Ryan Pessa +>>> +>>> Permission is hereby granted, free of charge, to any person obtaining a copy +>>> of this software and associated documentation files (the "Software"), to deal +>>> in the Software without restriction, including without limitation the rights +>>> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +>>> copies of the Software, and to permit persons to whom the Software is +>>> furnished to do so, subject to the following conditions: +>>> +>>> The above copyright notice and this permission notice shall be included in +>>> all copies or substantial portions of the Software. +>>> +>>> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +>>> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +>>> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +>>> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +>>> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +>>> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +>>> THE SOFTWARE. + +Code modification are subject to the license of the tagit software. + diff --git a/tagit/dialogues/message.py b/tagit/dialogues/message.py new file mode 100644 index 0000000..ab67180 --- /dev/null +++ b/tagit/dialogues/message.py @@ -0,0 +1,47 @@ +"""Dialogue to show some message. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Message', ) + + +## code ## + +# load kv +Builder.load_string(''' +<Message>: + text: '' + align: 'center' + textcolor: 1,1,1,1 + + DialogueContentNoTitle: + Label: + text: root.text + size_hint_y: None + color: root.textcolor + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + halign: root.align + markup: True + + DialogueButtons_One: +''') + +# classes +class Message(Dialogue): + """Dialogue to show a text message.""" + text = kp.StringProperty() + align = kp.StringProperty() + textcolor: kp.ListProperty() + +## EOF ## diff --git a/tagit/dialogues/numeric_input.py b/tagit/dialogues/numeric_input.py new file mode 100644 index 0000000..63491d2 --- /dev/null +++ b/tagit/dialogues/numeric_input.py @@ -0,0 +1,72 @@ +"""Dialogue with a slider. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('NumericInput', ) + + +## code ## + +# load kv +Builder.load_string(''' + +<NumericInput>: + value: int(slider.value) + init_value: 0 + + DialogueContentNoTitle: + + BoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: 50 + + Label: + text: str(root.lo) + size_hint_x: None + width: self.texture_size[0] + + Slider: + id: slider + orientation: 'horizontal' + min: root.lo + max: root.hi + step: 1 + value: root.init_value + + Label: + text: str(root.hi) + size_hint_x: None + width: self.texture_size[0] + + Label: + text: str(root.value) + size_hint: 1, None + height: self.texture_size[1] + halign: 'center' + + DialogueButtons_Two: + +''') + +# classes +class NumericInput(Dialogue): + """Dialogue with a slider.""" + + lo = kp.NumericProperty(0) + hi = kp.NumericProperty(100) + value = kp.NumericProperty(0) + init_value = kp.NumericProperty(0) + + +## EOF ## diff --git a/tagit/dialogues/path_picker.kv b/tagit/dialogues/path_picker.kv new file mode 100644 index 0000000..1837b80 --- /dev/null +++ b/tagit/dialogues/path_picker.kv @@ -0,0 +1,27 @@ +#:import join os.path.join +#:import pwd os.path.curdir + +<PathPicker>: + path: '' + title: 'Please select a file or directory' + filters: [] + dirselect: True + + DialogueContentTitle: + title: root.title + size_hint_y: 0.8 + + FileChooserListView: + text_size: self.width - dp(16), None + halign: 'center' + dirselect: root.dirselect + path: pwd + filters: root.filters + + on_selection: root.path = join(self.path, self.selection[0]); buttons.ok_enabled = True + + DialogueButtons_Two: + id: buttons + ok_enabled: False + +## EOF ## diff --git a/tagit/dialogues/path_picker.py b/tagit/dialogues/path_picker.py new file mode 100644 index 0000000..25bbf32 --- /dev/null +++ b/tagit/dialogues/path_picker.py @@ -0,0 +1,41 @@ +"""Dialogue to pick a file or directory. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue +from .error import Error + +# exports +__all__ = ('PathPicker', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'path_picker.kv')) + +# classes +class PathPicker(Dialogue): + """Dialogue with a file browser to select a file or directory.""" + + title = kp.StringProperty('') + path = kp.StringProperty('') + filters = kp.ListProperty() + + def ok(self): + if not os.path.exists(self.path): + Error(text='Please select a file or directory').open() + else: + super(PathPicker, self).ok() + +## EOF ## diff --git a/tagit/dialogues/simple_input.kv b/tagit/dialogues/simple_input.kv new file mode 100644 index 0000000..b7deb9c --- /dev/null +++ b/tagit/dialogues/simple_input.kv @@ -0,0 +1,30 @@ + +#:import AutoTextInput tagit.dialogues + +<SimpleInput>: + text: '' + ok_on_enter: True + cancel_on_defocus: True + + DialogueContentNoTitle: + + #AutoTextInput: + TextInput: + vocabulary: root.suggestions + sep: root.suggestion_sep + suffix: root.suggestion_suffix + focus: True + text: root.text + size_hint_y: None + multiline: False + height: self.minimum_height + text_size: self.width - dp(16), None + halign: 'center' + + on_text: root.text = self.text + on_focus: root.on_text_focus(*args) + #on_text_validate: root.ok() # handled via the ok_on_enter mechanism + + DialogueButtons_Two: + +## EOF ## diff --git a/tagit/dialogues/simple_input.py b/tagit/dialogues/simple_input.py new file mode 100644 index 0000000..d7cc69f --- /dev/null +++ b/tagit/dialogues/simple_input.py @@ -0,0 +1,55 @@ +"""Dialogue with a single-line text input field. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('SimpleInput', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'simple_input.kv')) + +# classes +class SimpleInput(Dialogue): + """Dialogue with a single-line text input field. + + Pass the default text as **text**. + + >>> SimpleInput(text='Hello world').open() + + In case of touch events, they need to be inhibited to change the focus. + + >>> FocusBehavior.ignored_touch.append(touch) + + """ + + # Defocus problem: + # Buttons defocus when on_press, but on_release is ok. + # Touch events must be blocked via FocusBehavior + + text = kp.StringProperty('') + cancel_on_defocus = kp.BooleanProperty(True) + suggestions = kp.ListProperty() + suggestion_sep = kp.StringProperty(',') + suggestion_suffix = kp.StringProperty(' ') + + + def on_text_focus(self, instance, focus): + if not focus and self.cancel_on_defocus: + self.dismiss() + +## EOF ## diff --git a/tagit/dialogues/stoken.py b/tagit/dialogues/stoken.py new file mode 100644 index 0000000..6e5427a --- /dev/null +++ b/tagit/dialogues/stoken.py @@ -0,0 +1,40 @@ +"""Search token editor + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 + +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .simple_input import SimpleInput + +# exports +__all__ = ('TokenEdit', ) + + +## code ## + +# Load kv +Builder.load_string(''' +#:import AutoTextInput tagit.dialogues + +<TokenEdit>: + text: '' + ok_on_enter: True + cancel_on_defocus: True +''') + +# classes +class TokenEdit(SimpleInput): + """Search token editor + """ + # TODO: Currently this is no different than SimpleInput. + # It should be extend to specify the type and getting help + # with editing ranges and alternative selection. + pass + +## EOF ## |