aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/actions
diff options
context:
space:
mode:
Diffstat (limited to 'tagit/actions')
-rw-r--r--tagit/actions/__init__.py60
-rw-r--r--tagit/actions/action.kv45
-rw-r--r--tagit/actions/action.py257
-rw-r--r--tagit/actions/browser.kv99
-rw-r--r--tagit/actions/browser.py628
-rw-r--r--tagit/actions/misc.py3
6 files changed, 1060 insertions, 32 deletions
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py
index ecff701..876ca1f 100644
--- a/tagit/actions/__init__.py
+++ b/tagit/actions/__init__.py
@@ -11,7 +11,7 @@ import typing
from tagit.utils.builder import BuilderBase
# inner-module imports
-#from . import browser
+from . import browser
from . import filter
from . import grouping
#from . import library
@@ -32,30 +32,30 @@ __all__: typing.Sequence[str] = (
class ActionBuilder(BuilderBase):
_factories = {
## browser
- #'NextPage': browser.NextPage,
- #'PreviousPage': browser.PreviousPage,
- #'ScrollDown': browser.ScrollDown,
- #'ScrollUp': browser.ScrollUp,
- #'JumpToPage': browser.JumpToPage,
- #'SetCursor': browser.SetCursor,
- #'ZoomIn': browser.ZoomIn,
- #'ZoomOut': browser.ZoomOut,
- #'JumpToCursor': browser.JumpToCursor,
- #'MoveCursorFirst': browser.MoveCursorFirst,
- #'MoveCursorLast': browser.MoveCursorLast,
- #'MoveCursorUp': browser.MoveCursorUp,
- #'MoveCursorDown': browser.MoveCursorDown,
- #'MoveCursorLeft': browser.MoveCursorLeft,
- #'MoveCursorRight': browser.MoveCursorRight,
- #'SelectRange': browser.SelectRange,
- #'SelectAdditive': browser.SelectAdditive,
- #'SelectSubtractive': browser.SelectSubtractive,
- #'SelectMulti': browser.SelectMulti,
- #'SelectSingle': browser.SelectSingle,
- #'SelectAll': browser.SelectAll,
- #'SelectNone': browser.SelectNone,
- #'SelectInvert': browser.SelectInvert,
- #'Select': browser.Select,
+ 'NextPage': browser.NextPage,
+ 'PreviousPage': browser.PreviousPage,
+ 'ScrollDown': browser.ScrollDown,
+ 'ScrollUp': browser.ScrollUp,
+ 'JumpToPage': browser.JumpToPage,
+ 'SetCursor': browser.SetCursor,
+ 'ZoomIn': browser.ZoomIn,
+ 'ZoomOut': browser.ZoomOut,
+ 'JumpToCursor': browser.JumpToCursor,
+ 'MoveCursorFirst': browser.MoveCursorFirst,
+ 'MoveCursorLast': browser.MoveCursorLast,
+ 'MoveCursorUp': browser.MoveCursorUp,
+ 'MoveCursorDown': browser.MoveCursorDown,
+ 'MoveCursorLeft': browser.MoveCursorLeft,
+ 'MoveCursorRight': browser.MoveCursorRight,
+ 'SelectRange': browser.SelectRange,
+ 'SelectAdditive': browser.SelectAdditive,
+ 'SelectSubtractive': browser.SelectSubtractive,
+ 'SelectMulti': browser.SelectMulti,
+ 'SelectSingle': browser.SelectSingle,
+ 'SelectAll': browser.SelectAll,
+ 'SelectNone': browser.SelectNone,
+ 'SelectInvert': browser.SelectInvert,
+ 'Select': browser.Select,
## filter
#'AddToken': filter.AddToken,
#'RemoveToken': filter.RemoveToken,
@@ -84,14 +84,14 @@ class ActionBuilder(BuilderBase):
#'SyncObjects': library.SyncObjects,
#'ItemExport': library.ItemExport,
## misc
- #'ShellDrop': misc.ShellDrop,
- #'OpenExternal': misc.OpenExternal,
- #'Menu': misc.Menu,
+ 'ShellDrop': misc.ShellDrop,
+ 'OpenExternal': misc.OpenExternal,
+ 'Menu': misc.Menu,
'ShowConsole': misc.ShowConsole,
'ShowHelp': misc.ShowHelp,
'ShowSettings': misc.ShowSettings,
- #'ClipboardCopy': misc.ClipboardCopy,
- #'ClipboardPaste': misc.ClipboardPaste,
+ 'ClipboardCopy': misc.ClipboardCopy,
+ 'ClipboardPaste': misc.ClipboardPaste,
## objects
#'RotateLeft': objects.RotateLeft,
#'RotateRight': objects.RotateRight,
diff --git a/tagit/actions/action.kv b/tagit/actions/action.kv
new file mode 100644
index 0000000..5352964
--- /dev/null
+++ b/tagit/actions/action.kv
@@ -0,0 +1,45 @@
+
+<Action>:
+ # internas
+ orientation: 'horizontal'
+
+ # responsiveness
+ # *touch_trigger* is enabled automatically if an image or text is shown.
+ # If that is undesired, *touch_trigger* has to be disabled **after** the
+ # declaration of *show*.
+ key_trigger: True
+ touch_trigger: False
+
+ # size
+ # By default the width expands as necessary. To get a fixed width,
+ # set the width manually or via size_hint_x and set *autowidth* to False.
+ # If something is shown, *height* is automatically set to *default_height*
+ # unless specified otherwise in by the caller.
+ default_height: 30
+ size: 0, 0
+ size_hint: None, None
+ autowidth: True
+
+ # behaviour
+ # The default is that no buttons are shown and touch triggers are disabled.
+ # NOTE: Callers need to declare show **last** to ensure that all other
+ # properties are set. The only exception is *touch_trigger* which has
+ # to be disabled **after** show.
+ show: []
+
+ # decoration
+ canvas.before:
+ Color:
+ rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha
+ Rectangle:
+ pos: self.x, self.y + 1
+ size: self.size
+
+ canvas.after:
+ Color:
+ rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha
+ Line:
+ width: 2
+ rectangle: self.x, self.y, self.width, self.height
+
+ ## EOF ##
diff --git a/tagit/actions/action.py b/tagit/actions/action.py
new file mode 100644
index 0000000..e8866ce
--- /dev/null
+++ b/tagit/actions/action.py
@@ -0,0 +1,257 @@
+"""Button for proxy actions.
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import logging
+import os
+
+# kivy imports
+from kivy import metrics
+from kivy.animation import Animation
+from kivy.lang import Builder
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.image import Image
+from kivy.uix.label import Label
+import kivy.properties as kp
+
+# tagit imports
+from tagit.external.tooltip import Tooltip
+
+# exports
+__all__ = ('Action', )
+
+
+## code ##
+
+logger = logging.getLogger(__name__)
+
+# load kv
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'action.kv'))
+
+class Action(ButtonBehavior, BoxLayout, Tooltip):
+ """
+
+ An Action can be triggered in three ways:
+ * Touch event: Clicking or touching on the button
+ * Key event: Entering a keyboard shortcut
+ * Single-shot: Programmatically triggered once, without UI attachment
+
+ For the last, use the *single_shot* classmethod.
+
+ For the first two, declare the Action in a kv file or in code.
+ Note that the remarks below about kv do not apply if the object is
+ created in code.
+
+
+ When an Action is declared in kv, two restrictions apply (also see
+ examples below):
+ * To disable touch_trigger, it must be declared last
+ * show must be declared after all other properties
+
+ Enable key triggers, but hide the Action in the UI:
+ Action:
+ show: []
+
+ Action:
+ # alias for the one above
+
+ Show text, image, or both, with default height and the width
+ stretched as necessary:
+ Action:
+ show: 'text',
+
+ Action:
+ show: 'image',
+
+ Action:
+ show: 'text', 'image'
+
+ Action:
+ width: 200 # has no effect unless autowidth is False
+ show: 'image', 'text'
+
+ Make the Action larger:
+ Action:
+ # increased height. The image width scales accordingly
+ height: 80
+ show: 'image', 'text'
+
+ Action:
+ # scales to parent widget's width
+ autowidth: False
+ size_hint_x: 1
+ show: 'image', 'text'
+
+ Action:
+ # fixed width and height
+ width: 150
+ height: 80
+ autowidth: False
+ show: 'image', 'text' # must be declared **last**
+
+ Show the button but disable touch events:
+ Action:
+ height: 80
+ show: 'image', 'text'
+ touch_trigger: False # must be declared **after** show
+
+ Do the same in code:
+ >>> Action(
+ ... size=(130, 80),
+ ... autowidth=False,
+ ... show=('image', 'text'),
+ ... text='foobar',
+ ... touch_trigger=False)
+
+ """
+ # content
+ tooltip = kp.StringProperty()
+ source = kp.StringProperty()
+ text = kp.StringProperty()
+
+ # visibility flags
+ show = kp.ListProperty([])
+
+ # sizing
+ default_height = kp.NumericProperty(30)
+ autowidth = kp.BooleanProperty(True)
+
+ # responsiveness
+ key_trigger = kp.BooleanProperty(True)
+ touch_trigger = kp.BooleanProperty(True)
+
+ # required such that properties can be set via constructor
+ font_size = kp.StringProperty("15sp")
+ # FIXME: Check why I have to pass size instead of height/width
+ height = kp.NumericProperty(0)
+ width = kp.NumericProperty(0)
+
+ # internal properties
+ root = kp.ObjectProperty(None)
+ selected_alpha = kp.NumericProperty(0)
+ _image = kp.ObjectProperty(None)
+ _label = kp.ObjectProperty(None)
+
+ def __init__(self, **kwargs):
+ show = kwargs.pop('show', [])
+ touch_trigger = kwargs.pop('touch_trigger', None)
+ super(Action, self).__init__(**kwargs)
+ # delay such that on_show is executed once the other
+ # properties are set
+ self.show = show
+ if touch_trigger is not None:
+ self.touch_trigger = touch_trigger
+
+ ## display
+
+ def on_show(self, wx, show):
+ if self._image is not None:
+ self.remove_widget(self._image)
+ if self._label is not None:
+ self.remove_widget(self._label)
+
+ if 'image' in show:
+ self.height = self.default_height if self.height == 0 else self.height
+ self.touch_trigger = True
+ self._image = Image(
+ source=self.source,
+ # size
+ height=self.height,
+ width=self.height, # square image
+ size_hint=(None, None),
+ )
+ self.add_widget(self._image)
+ if self.autowidth:
+ self.width = self.height
+ if 'text' in show:
+ self.height = self.default_height if self.height == 0 else self.height
+ self.touch_trigger = True
+ self._label = Label(
+ # text
+ text=self.text,
+ halign='left',
+ valign='middle',
+ padding=(metrics.dp(10), 0),
+ # size
+ font_size=self.font_size,
+ height=self.height,
+ size_hint=(None, None),
+ )
+ self._label.bind(texture_size=self.on_texture_size)
+ self.add_widget(self._label)
+
+ def on_size(self, wx, size):
+ for child in self.children:
+ child.height = self.height
+ if isinstance(child, Image):
+ child.width = self.height
+ elif not self.autowidth: # must be the label
+ self.on_texture_size(child, None)
+
+ def on_texture_size(self, label, texture_size):
+ if self.autowidth:
+ # adapt the width to the label's width
+ self.width = max(0, sum([child.width for child in self.children]))
+ else:
+ # adapt the label's width
+ others = sum([child.width for child in self.children if child != label])
+ label.width = self.width - others
+ label.height = self.height
+ label.text_size = (self.width - others, self.height)
+
+ def on_tooltip(self, callee, text):
+ self.set_tooltip(text)
+
+
+ ## properties
+
+ @property
+ def cfg(self):
+ return self.root.session.cfg
+
+
+ ## action triggering
+
+ @classmethod
+ def single_shot(cls, root, *args, **kwargs):
+ #logger.info(f'action: {cls.__name__}, {args}, {kwargs}')
+ root.action_log.append(str(cls.__name__))
+ return cls(root=root).apply(*args, **kwargs)
+
+ def on_root(self, wx, root):
+ root.keys.bind(on_press=self.on_keyboard)
+
+ def on_press(self):
+ self.selected_alpha = 1
+
+ def on_release(self):
+ if self.touch_trigger:
+ Animation(selected_alpha=0, d=.25, t='out_quad').start(self)
+ #logger.info(f'action: {type(self).__name__}')
+ self.root.action_log.append(str(type(self).__name__))
+ self.apply()
+
+ def on_keyboard(self, wx, evt):
+ if self.key_trigger and self.ktrigger(evt):
+ #logger.info(f'action: {type(self).__name__}')
+ self.root.action_log.append(str(type(self).__name__))
+ self.apply()
+ # stop the event from further processing
+ return True
+
+
+ ## interfaces for subtypes
+
+ def ktrigger(self, evt):
+ """Return True if the action should be triggered by keyboard event *evt*."""
+ return False
+
+ def apply(self, *args, **kwargs):
+ """Execute the action."""
+ pass
+
+## EOF ##
diff --git a/tagit/actions/browser.kv b/tagit/actions/browser.kv
new file mode 100644
index 0000000..adcfbd6
--- /dev/null
+++ b/tagit/actions/browser.kv
@@ -0,0 +1,99 @@
+#:import resource_find kivy.resources.resource_find
+
+<NextPage>:
+ source: resource_find('atlas://browser/next_page')
+ tooltip: 'One page down'
+
+<PreviousPage>:
+ source: resource_find('atlas://browser/previous_page')
+ tooltip: 'One page up'
+
+<ScrollUp>:
+ source: resource_find('atlas://browser/scroll_up')
+ tooltip: 'One row up'
+
+<ScrollDown>:
+ source: resource_find('atlas://browser/scroll_down')
+ tooltip: 'One row down'
+
+<JumpToPage>:
+ source: resource_find('atlas://browser/jump_to_page')
+ tooltip: 'Jump to a specified page'
+
+<ZoomIn>:
+ source: resource_find('atlas://browser/zoom_in')
+ tooltip: 'Zoom in'
+
+<ZoomOut>:
+ source: resource_find('atlas://browser/zoom_out')
+ tooltip: 'Zoom out'
+
+<JumpToCursor>:
+ source: resource_find('atlas://browser/jump_to_cursor')
+ tooltip: 'Jump to cursor'
+
+<SetCursor>:
+ source: resource_find('atlas://browser/set_cursor')
+ tooltip: 'Set the cursor'
+
+<MoveCursorFirst>:
+ source: resource_find('atlas://browser/cursor_first')
+ tooltip: 'Go to first image'
+
+<MoveCursorLast>:
+ source: resource_find('atlas://browser/cursor_last')
+ tooltip: 'Go to last image'
+
+<MoveCursorUp>:
+ source: resource_find('atlas://browser/cursor_up')
+ tooltip: 'Cursor up'
+
+<MoveCursorDown>:
+ source: resource_find('atlas://browser/cursor_down')
+ tooltip: 'Cursor down'
+
+<MoveCursorLeft>:
+ source: resource_find('atlas://browser/cursor_left')
+ tooltip: 'Cursor left'
+
+<MoveCursorRight>:
+ source: resource_find('atlas://browser/cursor_right')
+ tooltip: 'Cursor right'
+
+<SelectAll>:
+ source: resource_find('atlas://browser/select_all')
+ tooltip: 'Select all'
+
+<SelectNone>:
+ source: resource_find('atlas://browser/select_none')
+ tooltip: 'Clear selection'
+
+<SelectInvert>:
+ source: resource_find('atlas://browser/select_invert')
+ tooltip: 'Invert selection'
+
+<SelectSingle>:
+ source: resource_find('atlas://browser/select_single')
+ tooltip: 'Select one'
+
+<SelectMulti>:
+ source: resource_find('atlas://browser/select_multi')
+ tooltip: 'Select many'
+
+<SelectAdditive>:
+ source: resource_find('atlas://browser/select_add')
+ tooltip: 'Add to selection'
+
+<SelectSubtractive>:
+ source: resource_find('atlas://browser/select_sub')
+ tooltip: 'Remove from selection'
+
+<SelectRange>:
+ source: resource_find('atlas://browser/select_range')
+ tooltip: 'Select range'
+
+<Select>:
+ source: resource_find('atlas://browser/select')
+ tooltip: 'Select/deselect an item'
+
+## EOF ##
diff --git a/tagit/actions/browser.py b/tagit/actions/browser.py
new file mode 100644
index 0000000..2eaead8
--- /dev/null
+++ b/tagit/actions/browser.py
@@ -0,0 +1,628 @@
+"""
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import math
+import os
+
+# kivy imports
+from kivy.lang import Builder
+import kivy.properties as kp
+
+# tagit imports
+from tagit import config, dialogues
+from tagit.utils import clamp
+from tagit.widgets import Binding
+
+# inner-module imports
+from .action import Action
+
+# exports
+__all__ = []
+
+
+## code ##
+
+# load kv
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'browser.kv'))
+
+# classes
+
+class NextPage(Action):
+ """Scroll one page downwards without moving the cursor."""
+ text = kp.StringProperty('Next page')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'page_next'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.offset = clamp(browser.offset + browser.page_size, browser.max_offset)
+
+
+class PreviousPage(Action):
+ """Scroll one page upwards without moving the cursor."""
+ text = kp.StringProperty('Previous page')
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'page_prev'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.offset = max(browser.offset - browser.page_size, 0)
+
+
+class ScrollUp(Action):
+ """Scroll one row up without moving the cursor."""
+ text = kp.StringProperty('Scroll up')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'scroll_up'))
+
+ def on_touch_down(self, touch):
+ scrollcfg = self.cfg('ui', 'standalone', 'browser', 'scroll')
+ scrolldir = 'scrolldown' if scrollcfg == 'mouse' else 'scrollup'
+ if self.root.browser.collide_point(*touch.pos) \
+ and not self.root.keys.ctrl_pressed:
+ if touch.button == scrolldir:
+ self.apply()
+ return super(ScrollUp, self).on_touch_down(touch)
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.offset = clamp(browser.offset - browser.cols, browser.max_offset)
+
+
+class ScrollDown(Action):
+ """Scroll one row down without moving the cursor."""
+ text = kp.StringProperty('Scroll down')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'scroll_down'))
+
+ def on_touch_down(self, touch):
+ scrollcfg = self.cfg('ui', 'standalone', 'browser', 'scroll')
+ scrolldir = 'scrollup' if scrollcfg == 'mouse' else 'scrolldown'
+ if self.root.browser.collide_point(*touch.pos) \
+ and not self.root.keys.ctrl_pressed:
+ if touch.button == scrolldir:
+ self.apply()
+ return super(ScrollDown, self).on_touch_down(touch)
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.offset = clamp(browser.offset + browser.cols, browser.max_offset)
+
+
+class JumpToPage(Action):
+ """Jump to a specified offset."""
+ text = kp.StringProperty('Go to page')
+
+ def apply(self, offset=None):
+ if offset is None:
+ browser = self.root.browser
+ dlg = dialogues.NumericInput(lo=0, hi=browser.max_offset, init_value=browser.offset)
+ dlg.bind(on_ok=lambda wx: self.set_offset(wx.value))
+ dlg.open()
+ else:
+ self.set_offset(offset)
+
+ def set_offset(self, offset):
+ with self.root.browser as browser:
+ browser.offset = clamp(offset, browser.max_offset)
+
+class ZoomIn(Action):
+ """Decrease the grid size."""
+ text = kp.StringProperty('Zoom in')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'zoom_in'))
+
+ # TODO: zoom by gesture
+
+ def on_touch_down(self, touch): # not triggered (but ScrollDown is!)
+ if self.root.browser.collide_point(*touch.pos) \
+ and self.root.keys.ctrl_pressed:
+ if touch.button == 'scrolldown':
+ self.apply()
+ return super(ZoomIn, self).on_touch_down(touch)
+
+ def apply(self):
+ with self.root.browser as browser:
+ step = self.cfg('ui', 'standalone', 'browser', 'zoom_step')
+ if browser.gridmode == browser.GRIDMODE_LIST:
+ cols = browser.cols
+ else:
+ cols = max(1, browser.cols - step)
+ rows = max(1, browser.rows - step)
+ # TODO: Zoom to center? (adjust offset)
+ if cols != browser.cols or rows != browser.rows:
+ # clear widgets first, otherwise GridLayout will
+ # complain about too many childrens.
+ browser.clear_widgets()
+ # adjust the grid size
+ browser.cols = cols
+ browser.rows = rows
+
+
+class ZoomOut(Action):
+ """Increase the grid size."""
+ text = kp.StringProperty('Zoom out')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'zoom_out'))
+
+ # TODO: zoom by gesture
+
+ def on_touch_down(self, touch):
+ if self.root.browser.collide_point(*touch.pos) \
+ and self.root.keys.ctrl_pressed:
+ if touch.button == 'scrollup':
+ self.apply()
+ return super(ZoomOut, self).on_touch_down(touch)
+
+ def apply(self):
+ with self.root.browser as browser:
+ # TODO: Zoom from center? (adjust offset)
+ step = self.cfg('ui', 'standalone', 'browser', 'zoom_step')
+ # get maxcols
+ maxcols = self.cfg('ui', 'standalone', 'browser', 'maxcols')
+ maxcols = float('inf') if maxcols <= 0 else maxcols
+ # get maxrows
+ maxrows = self.cfg('ui', 'standalone', 'browser', 'maxrows')
+ maxrows = float('inf') if maxrows <= 0 else maxrows
+ # set cols/rows
+ if browser.gridmode != browser.GRIDMODE_LIST:
+ browser.cols = min(browser.cols + step, maxcols)
+ browser.rows = min(browser.rows + step, maxrows)
+ # adjust offset to ensure that one full page is visible
+ browser.offset = clamp(browser.offset, browser.max_offset)
+
+
+class JumpToCursor(Action):
+ """Focus the field of view at the cursor."""
+ text = kp.StringProperty('Find cursor')
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.cursor is None:
+ # cursor not set, nothing to do
+ pass
+ else:
+ idx = browser.items.index(browser.cursor)
+ if idx < browser.offset:
+ # cursor is above view, scroll up such that the cursor
+ # is in the first row.
+ offset = math.floor(idx / browser.cols) * browser.cols
+ browser.offset = clamp(offset, browser.max_offset)
+ elif browser.offset + browser.page_size <= idx:
+ # cursor is below view, scroll down such that the cursor
+ # is in the last row.
+ offset = math.floor(idx / browser.cols) * browser.cols
+ offset -= (browser.page_size - browser.cols)
+ browser.offset = clamp(offset, browser.max_offset)
+ else:
+ # cursor is visible, nothing to do
+ pass
+
+
+class SetCursor(Action):
+ """Set the cursor to a specific item."""
+ text = kp.StringProperty('Set cursor')
+
+ def apply(self, obj):
+ with self.root.browser as browser:
+ browser.cursor = obj
+ self.root.trigger('JumpToCursor')
+ # is invoked via mouse click only, hence
+ # the item selection should always toggle
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorFirst(Action):
+ """Set the cursor to the first item."""
+ text = kp.StringProperty('First')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'go_first'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ else:
+ # set cursor to first item
+ old = browser.cursor
+ browser.cursor = browser.items[0]
+ # scroll to first page if need be
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != browser.cursor):
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorLast(Action):
+ """Set the cursor to the last item."""
+ text = kp.StringProperty('Last')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'go_last'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ else:
+ # set cursor to last item
+ old = browser.cursor
+ browser.cursor = browser.items[-1]
+ # scroll to last page if need be
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != browser.cursor):
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorUp(Action):
+ """Move the cursor one item upwards. Scroll if needbe."""
+ text = kp.StringProperty('Cursor up')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_up'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ elif browser.cursor is None:
+ # cursor wasn't set before. Set to last item
+ self.root.trigger('MoveCursorLast')
+ else:
+ # move cursor one row up
+ old = browser.items.index(browser.cursor)
+ # check if the cursor is in the first row already
+ if old < browser.cols: return # first row already
+ # move cursor up
+ new = clamp(old - browser.cols, browser.n_items - 1)
+ browser.cursor = browser.items[new]
+ # fix field of view
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != new):
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorDown(Action):
+ """Move the cursor one item downwards. Scroll if needbe."""
+ text = kp.StringProperty('Cursor down')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_down'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ elif browser.cursor is None:
+ # cursor wasn't set before. Set to first item
+ self.root.trigger('MoveCursorFirst')
+ else:
+ # move cursor one row down
+ old = browser.items.index(browser.cursor)
+ # check if the cursor is in the last row already
+ last_row = browser.n_items % browser.cols
+ last_row = last_row if last_row > 0 else browser.cols
+ if old >= browser.n_items - last_row: return # last row already
+ # move cursor down
+ new = clamp(old + browser.cols, browser.n_items - 1)
+ browser.cursor = browser.items[new]
+ # fix field of view
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != new):
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorLeft(Action):
+ """Move the cursor to the previous item."""
+ text = kp.StringProperty('Cursor left')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_left'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ elif browser.cursor is None:
+ # cursor wasn't set before. Set to the last item
+ self.root.trigger('MoveCursorLast')
+ else:
+ # move cursor one position to the left
+ old = browser.items.index(browser.cursor)
+ new = clamp(old - 1, browser.n_items - 1)
+ browser.cursor = browser.items[new]
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != new):
+ self.root.trigger('Select', browser.cursor)
+
+
+class MoveCursorRight(Action):
+ """Move the cursor to the next item."""
+ text = kp.StringProperty('Cursor right')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_right'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ if browser.n_items == 0:
+ # browser is empty, nothing to do
+ pass
+ elif browser.cursor is None:
+ # cursor wasn't set before. Set to the last item
+ self.root.trigger('MoveCursorFirst')
+ else:
+ # move cursor one position to the right
+ old = browser.items.index(browser.cursor)
+ new = clamp(old + 1, browser.n_items - 1)
+ browser.cursor = browser.items[new]
+ self.root.trigger('JumpToCursor')
+ # fix selection
+ if browser.select_mode != browser.SELECT_MULTI and \
+ (browser.select_mode != browser.SELECT_SINGLE or old != new):
+ self.root.trigger('Select', browser.cursor)
+
+
+class SelectAll(Action):
+ """Select all items."""
+ text = kp.StringProperty('Select all')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'select_all'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.selection = browser.items.as_set().copy()
+
+
+class SelectNone(Action):
+ """Clear the selection."""
+ text = kp.StringProperty('Clear selection')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'select_none'))
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.selection = set()
+
+
+class SelectInvert(Action):
+ """Invert the selection."""
+ text = kp.StringProperty('Invert selection')
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.selection = browser.items.as_set() - browser.selection
+
+
+class SelectSingle(Action):
+ """Select only the cursor."""
+ text = kp.StringProperty('Select one')
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.select_mode = browser.SELECT_SINGLE
+
+
+class SelectAdditive(Action):
+ """Set the selection mode to additive select."""
+ text = kp.StringProperty('Always select')
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.select_mode = browser.SELECT_ADDITIVE
+
+
+class SelectSubtractive(Action):
+ """Set the selection mode to subtractive select."""
+ text = kp.StringProperty('Always deselect')
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.select_mode = browser.SELECT_SUBTRACTIVE
+
+
+class SelectMulti(Action):
+ """Set the selection mode to random access."""
+ text = kp.StringProperty('Select many')
+ browser = kp.ObjectProperty(None, allownone=True)
+
+ def ktrigger(self, evt):
+ key, _, _ = evt
+ if key in self.root.keys.modemap[self.root.keys.MODIFIERS_CTRL]:
+ self.browser = self.root.browser
+ self.apply()
+
+ def on_root(self, wx, root):
+ super(SelectMulti, self).on_root(wx, root)
+ root.keys.bind(on_release=self.on_key_up)
+
+ def on_key_up(self, wx, key):
+ if key in self.root.keys.modemap[self.root.keys.MODIFIERS_CTRL]:
+ if self.browser is not None:
+ with self.browser as browser:
+ if browser.select_mode & browser.SELECT_MULTI:
+ browser.select_mode -= browser.SELECT_MULTI
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.select_mode |= browser.SELECT_MULTI
+
+
+class SelectRange(Action):
+ """Set the selection mode to range select."""
+ text = kp.StringProperty('Select range')
+ browser = kp.ObjectProperty(None, allownone=True)
+
+ def ktrigger(self, evt):
+ key, _, _ = evt
+ if key in self.root.keys.modemap[self.root.keys.MODIFIERS_SHIFT]:
+ self.browser = self.root.browser
+ self.apply()
+
+ def on_root(self, wx, root):
+ super(SelectRange, self).on_root(wx, root)
+ root.keys.bind(on_release=self.on_key_up)
+
+ def on_key_up(self, wx, key):
+ if key in self.root.keys.modemap[self.root.keys.MODIFIERS_SHIFT]:
+ if self.browser is not None:
+ with self.browser as browser:
+ if browser.select_mode & browser.SELECT_RANGE:
+ browser.select_mode -= browser.SELECT_RANGE
+ browser.range_base = set()
+ browser.range_origin = None
+
+ def apply(self):
+ with self.root.browser as browser:
+ browser.select_mode |= browser.SELECT_RANGE
+ browser.range_base = browser.selection.copy()
+ idx = None if browser.cursor is None else browser.items.index(browser.cursor)
+ browser.range_origin = idx
+
+
+class Select(Action):
+ """Select or deselect an item. How the selection changes depends on the selection mode."""
+ text = kp.StringProperty('Select')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'select'))
+
+ def apply(self, obj=None):
+ with self.root.browser as browser:
+ obj = obj if obj is not None else browser.cursor
+
+ if obj is None:
+ # nothing to do
+ pass
+
+ elif browser.select_mode & browser.SELECT_ADDITIVE:
+ browser.selection.add(obj)
+
+ elif browser.select_mode & browser.SELECT_SUBTRACTIVE:
+ if obj in browser.selection:
+ browser.selection.remove(obj)
+
+ elif browser.select_mode & browser.SELECT_RANGE:
+ idx = browser.items.index(obj)
+ lo = min(idx, browser.range_origin)
+ hi = max(idx, browser.range_origin)
+ browser.selection = browser.range_base | set(browser.items[lo:hi+1])
+
+ elif browser.select_mode & browser.SELECT_MULTI:
+ # Toggle
+ if obj in browser.selection:
+ browser.selection.remove(obj)
+ else:
+ browser.selection.add(obj)
+
+ elif browser.select_mode == 0: #elif browser.select_mode & browser.SELECT_SINGLE:
+ # Toggle
+ if obj in browser.selection:
+ browser.selection = set()
+ else:
+ browser.selection = {obj}
+
+
+## config ##
+
+config.declare(('ui', 'standalone', 'browser', 'maxcols'), config.Unsigned(), 0,
+ __name__, 'Column maximum', 'Maximal number of columns. This guards against aggressive zooming, because the application may become unresponsive if too many preview images are shown on the same page. A value of Infinity means that no limit is enforced.')
+
+config.declare(('ui', 'standalone', 'browser', 'maxrows'), config.Unsigned(), 0,
+ __name__, 'Row maximum', 'Maximal number of rows. This guards against aggressive zooming, because the application may become unresponsive if too many preview images are shown on the same page. A value of Infinity means that no limit is enforced.')
+
+config.declare(('ui', 'standalone', 'browser', 'zoom_step'), config.Unsigned(), 1,
+ __name__, 'Zoom step', 'Controls by how much the gridsize is increased or decreased when zoomed in or out. Affects both dimensions (cols/rows) in grid mode.')
+
+config.declare(('ui', 'standalone', 'browser', 'scroll'), config.Enum('mouse', 'touch'), 'mouse',
+ __name__, 'Inverted scroll', 'To scroll downwards, one can either move the fingers in an upward direction (touch) or use the scroll wheel in a downward direction (mouse).')
+
+# keybindings
+
+config.declare(('bindings', 'browser', 'page_next'),
+ config.Keybind(), Binding.simple(Binding.PGDN),
+ __name__, NextPage.text.defaultvalue, NextPage.__doc__)
+
+config.declare(('bindings', 'browser', 'page_prev'),
+ config.Keybind(), Binding.simple(Binding.PGUP),
+ __name__, PreviousPage.text.defaultvalue, PreviousPage.__doc__)
+
+config.declare(('bindings', 'browser', 'scroll_up'),
+ config.Keybind(), Binding.simple('k', None, Binding.mALL),
+ __name__, ScrollUp.text.defaultvalue, ScrollUp.__doc__)
+
+config.declare(('bindings', 'browser', 'scroll_down'),
+ config.Keybind(), Binding.simple('j', None, Binding.mALL),
+ __name__, ScrollDown.text.defaultvalue, ScrollDown.__doc__)
+
+config.declare(('bindings', 'browser', 'zoom_in'),
+ config.Keybind(), Binding.simple('+'),
+ __name__, ZoomIn.text.defaultvalue, ZoomIn.__doc__)
+
+config.declare(('bindings', 'browser', 'zoom_out'),
+ config.Keybind(), Binding.simple('-'),
+ __name__, ZoomOut.text.defaultvalue, ZoomOut.__doc__)
+
+config.declare(('bindings', 'browser', 'go_first'),
+ config.Keybind(), Binding.simple(Binding.HOME),
+ __name__, MoveCursorFirst.text.defaultvalue, MoveCursorFirst.__doc__)
+
+config.declare(('bindings', 'browser', 'go_last'),
+ config.Keybind(), Binding.simple(Binding.END),
+ __name__, MoveCursorLast.text.defaultvalue, MoveCursorLast.__doc__)
+
+config.declare(('bindings', 'browser', 'cursor_up'),
+ config.Keybind(), Binding.simple(Binding.UP),
+ __name__, MoveCursorUp.text.defaultvalue, MoveCursorUp.__doc__)
+
+config.declare(('bindings', 'browser', 'cursor_down'),
+ config.Keybind(), Binding.simple(Binding.DOWN),
+ __name__, MoveCursorDown.text.defaultvalue, MoveCursorDown.__doc__)
+
+config.declare(('bindings', 'browser', 'cursor_left'),
+ config.Keybind(), Binding.simple(Binding.LEFT),
+ __name__, MoveCursorLeft.text.defaultvalue, MoveCursorLeft.__doc__)
+
+config.declare(('bindings', 'browser', 'cursor_right'),
+ config.Keybind(), Binding.simple(Binding.RIGHT),
+ __name__, MoveCursorRight.text.defaultvalue, MoveCursorRight.__doc__)
+
+config.declare(('bindings', 'browser', 'select_all'),
+ config.Keybind(), Binding.simple('a', Binding.mCTRL, Binding.mREST),
+ __name__, SelectAll.text.defaultvalue, SelectAll.__doc__)
+
+config.declare(('bindings', 'browser', 'select_none'),
+ config.Keybind(), Binding.simple('a', (Binding.mCTRL, Binding.mSHIFT), Binding.mREST),
+ __name__, SelectNone.text.defaultvalue, SelectNone.__doc__)
+
+config.declare(('bindings', 'browser', 'select'),
+ config.Keybind(), Binding.simple(Binding.SPACEBAR),
+ __name__, Select.text.defaultvalue, Select.__doc__)
+
+## EOF ##
diff --git a/tagit/actions/misc.py b/tagit/actions/misc.py
index dc939ca..c0f960e 100644
--- a/tagit/actions/misc.py
+++ b/tagit/actions/misc.py
@@ -16,8 +16,7 @@ import webbrowser
# tagit imports
from tagit import config
-#from tagit.io_.sync import export # FIXME: mb/port
-#from tagit.utils import fileopen # FIXME: mb/port
+from tagit.utils import fileopen
from tagit.widgets import Binding
# inner-module imports