diff --git a/internal/config/model.go b/internal/config/model.go
index e1cf9bc..be4da85 100644
--- a/internal/config/model.go
+++ b/internal/config/model.go
@@ -21,7 +21,7 @@ import (
"sort"
)
-// MPD's track attribute identifiers
+// MPD's track attribute identifiers. These must precisely match the queueListStore's columns declared in player.glade
const (
MTAttrArtist = iota
MTAttrArtistSort
@@ -46,6 +46,10 @@ const (
MTAttrGrouping
MTAttrComment
MTAttrLabel
+ // List store's "artificial" columns used for rendering
+ QueueColumnFontWeight
+ QueueColumnBgColor
+ QueueColumnVisible
)
// MpdTrackAttribute describes an MPD's track attribute
diff --git a/internal/player/builder.go b/internal/player/builder.go
index 3a67dd3..2c4823d 100644
--- a/internal/player/builder.go
+++ b/internal/player/builder.go
@@ -137,6 +137,15 @@ func (b *Builder) getListBox(name string) *gtk.ListBox {
return result
}
+// getListStore finds and returns a list box by its name
+func (b *Builder) getListStore(name string) *gtk.ListStore {
+ result, ok := b.get(name).(*gtk.ListStore)
+ if !ok {
+ log.Fatal(errors.Errorf("%v is not a gtk.ListStore", name))
+ }
+ return result
+}
+
// getMenu finds and returns a menu by its name
func (b *Builder) getMenu(name string) *gtk.Menu {
result, ok := b.get(name).(*gtk.Menu)
@@ -173,6 +182,24 @@ func (b *Builder) getScale(name string) *gtk.Scale {
return result
}
+// getSearchBar finds and returns a search bar by its name
+func (b *Builder) getSearchBar(name string) *gtk.SearchBar {
+ result, ok := b.get(name).(*gtk.SearchBar)
+ if !ok {
+ log.Fatal(errors.Errorf("%v is not a gtk.SearchBar", name))
+ }
+ return result
+}
+
+// getSearchEntry finds and returns a search entry by its name
+func (b *Builder) getSearchEntry(name string) *gtk.SearchEntry {
+ result, ok := b.get(name).(*gtk.SearchEntry)
+ if !ok {
+ log.Fatal(errors.Errorf("%v is not a gtk.SearchEntry", name))
+ }
+ return result
+}
+
// getShortcutsWindow finds and returns a shortcuts window by its name
func (b *Builder) getShortcutsWindow(name string) *gtk.ShortcutsWindow {
result, ok := b.get(name).(*gtk.ShortcutsWindow)
@@ -227,6 +254,15 @@ func (b *Builder) getToolButton(name string) *gtk.ToolButton {
return result
}
+// getTreeModelFilter finds and returns a tree model filter by its name
+func (b *Builder) getTreeModelFilter(name string) *gtk.TreeModelFilter {
+ result, ok := b.get(name).(*gtk.TreeModelFilter)
+ if !ok {
+ log.Fatal(errors.Errorf("%v is not a gtk.TreeModelFilter", name))
+ }
+ return result
+}
+
// getTreeView finds and returns a tree view by its name
func (b *Builder) getTreeView(name string) *gtk.TreeView {
result, ok := b.get(name).(*gtk.TreeView)
diff --git a/internal/player/main-window.go b/internal/player/main-window.go
index 8164a96..9bbe585 100644
--- a/internal/player/main-window.go
+++ b/internal/player/main-window.go
@@ -15,6 +15,7 @@
package player
+import "C"
import (
"bytes"
"fmt"
@@ -54,12 +55,18 @@ type MainWindow struct {
scPlayPosition *gtk.Scale
adjPlayPosition *gtk.Adjustment
// Queue widgets
- bxQueue *gtk.Box
- lblQueueInfo *gtk.Label
- trvQueue *gtk.TreeView
- pmnQueueSort *gtk.PopoverMenu
- pmnQueueSave *gtk.PopoverMenu
- mnQueue *gtk.Menu
+ bxQueue *gtk.Box
+ lblQueueInfo *gtk.Label
+ trvQueue *gtk.TreeView
+ pmnQueueSort *gtk.PopoverMenu
+ pmnQueueSave *gtk.PopoverMenu
+ mnQueue *gtk.Menu
+ btnQueueFilter *gtk.ToggleToolButton
+ queueSearchBar *gtk.SearchBar
+ queueSearchEntry *gtk.SearchEntry
+ lblQueueFilter *gtk.Label
+ queueListStore *gtk.ListStore
+ queueListFilter *gtk.TreeModelFilter
// Queue sort popup
cbxQueueSortBy *gtk.ComboBoxText
// Queue save popup
@@ -104,31 +111,17 @@ type MainWindow struct {
aPlayerRepeat *glib.SimpleAction
aPlayerConsume *glib.SimpleAction
- // Queue list store
- queueListStore *gtk.ListStore
- // Number of items in the play queue
- currentQueueSize int
- // Queue's track index (last) marked as current
- currentQueueIndex int
+ currentQueueSize int // Number of items in the play queue
+ currentQueueIndex int // Queue's track index (last) marked as current
- // Current library path, separated by slashes
- currentLibPath string
+ currentLibPath string // Current library path, separated by slashes
- // Compiled template for player's track title
- playerTitleTemplate *template.Template
+ playerTitleTemplate *template.Template // Compiled template for player's track title
- // Play position manual update flag
- playPosUpdating bool
- // Options update flag
- optionsUpdating bool
+ playPosUpdating bool // Play position manual update flag
+ optionsUpdating bool // Options update flag
}
-var (
- // Indices of "artificial" queue list store columns used for rendering
- queueColNumFontWeight int
- queueColNumBgColor int
-)
-
const (
// Rendering properties for the Queue list
fontWeightNormal = 400
@@ -158,12 +151,18 @@ func NewMainWindow(application *gtk.Application) (*MainWindow, error) {
scPlayPosition: builder.getScale("scPlayPosition"),
adjPlayPosition: builder.getAdjustment("adjPlayPosition"),
// Queue
- bxQueue: builder.getBox("bxQueue"),
- lblQueueInfo: builder.getLabel("lblQueueInfo"),
- trvQueue: builder.getTreeView("trvQueue"),
- pmnQueueSort: builder.getPopoverMenu("pmnQueueSort"),
- pmnQueueSave: builder.getPopoverMenu("pmnQueueSave"),
- mnQueue: builder.getMenu("mnQueue"),
+ bxQueue: builder.getBox("bxQueue"),
+ lblQueueInfo: builder.getLabel("lblQueueInfo"),
+ trvQueue: builder.getTreeView("trvQueue"),
+ pmnQueueSort: builder.getPopoverMenu("pmnQueueSort"),
+ pmnQueueSave: builder.getPopoverMenu("pmnQueueSave"),
+ mnQueue: builder.getMenu("mnQueue"),
+ btnQueueFilter: builder.getToggleToolButton("btnQueueFilter"),
+ queueSearchBar: builder.getSearchBar("queueSearchBar"),
+ queueSearchEntry: builder.getSearchEntry("queueSearchEntry"),
+ lblQueueFilter: builder.getLabel("lblQueueFilter"),
+ queueListStore: builder.getListStore("queueListStore"),
+ queueListFilter: builder.getTreeModelFilter("queueListFilter"),
// Queue sort popup
cbxQueueSortBy: builder.getComboBoxText("cbxQueueSortBy"),
// Queue save popup
@@ -181,33 +180,32 @@ func NewMainWindow(application *gtk.Application) (*MainWindow, error) {
bxPlaylists: builder.getBox("bxPlaylists"),
lbxPlaylists: builder.getListBox("lbxPlaylists"),
lblPlaylistsInfo: builder.getLabel("lblPlaylistsInfo"),
-
- // Other stuff
- queueListStore: createQueueListStore(),
}
- // Bind the list store to the queue tree view
- w.trvQueue.SetModel(w.queueListStore)
+ // Initialise queue filter model
+ w.queueListFilter.SetVisibleColumn(config.QueueColumnVisible)
// Initialise player title template
w.updatePlayerTitleTemplate()
// Map the handlers to callback functions
builder.ConnectSignals(map[string]interface{}{
- "on_mainWindow_delete": w.onDelete,
- "on_mainWindow_map": w.onMap,
- "on_trvQueue_buttonPress": w.onQueueTreeViewButtonPress,
- "on_trvQueue_keyPress": w.onQueueTreeViewKeyPress,
- "on_tselQueue_changed": w.updateQueueActions,
- "on_lbxLibrary_buttonPress": w.onLibraryListBoxButtonPress,
- "on_lbxLibrary_keyPress": w.onLibraryListBoxKeyPress,
- "on_lbxLibrary_selectionChange": w.updateLibraryActions,
- "on_lbxPlaylists_buttonPress": w.onPlaylistListBoxButtonPress,
- "on_lbxPlaylists_keyPress": w.onPlaylistListBoxKeyPress,
- "on_lbxPlaylists_selectionChange": w.updatePlaylistsActions,
- "on_pmnQueueSave_validate": w.onQueueSavePopoverValidate,
- "on_scPlayPosition_buttonEvent": w.onPlayPositionButtonEvent,
- "on_scPlayPosition_valueChanged": w.updatePlayerSeekBar,
+ "on_mainWindow_delete": w.onDelete,
+ "on_mainWindow_map": w.onMap,
+ "on_trvQueue_buttonPress": w.onQueueTreeViewButtonPress,
+ "on_trvQueue_keyPress": w.onQueueTreeViewKeyPress,
+ "on_tselQueue_changed": w.updateQueueActions,
+ "on_queueSearchBar_searchMode": w.queueFilter,
+ "on_queueSearchEntry_searchChanged": w.queueFilter,
+ "on_lbxLibrary_buttonPress": w.onLibraryListBoxButtonPress,
+ "on_lbxLibrary_keyPress": w.onLibraryListBoxKeyPress,
+ "on_lbxLibrary_selectionChange": w.updateLibraryActions,
+ "on_lbxPlaylists_buttonPress": w.onPlaylistListBoxButtonPress,
+ "on_lbxPlaylists_keyPress": w.onPlaylistListBoxKeyPress,
+ "on_lbxPlaylists_selectionChange": w.updatePlaylistsActions,
+ "on_pmnQueueSave_validate": w.onQueueSavePopoverValidate,
+ "on_scPlayPosition_buttonEvent": w.onPlayPositionButtonEvent,
+ "on_scPlayPosition_valueChanged": w.updatePlayerSeekBar,
// For some reason binding actions to menu items keeps them grayed out, so old-school signals are used here
"on_miQueueNowPlaying_activate": w.updateQueueNowPlaying,
@@ -215,6 +213,13 @@ func NewMainWindow(application *gtk.Application) (*MainWindow, error) {
"on_miQueueDelete_activate": w.queueDelete,
})
+ // Configure search bar
+ glib.BindProperty(w.queueSearchBar.Object, "search-mode-enabled", w.btnQueueFilter.Object, "active", glib.BINDING_BIDIRECTIONAL)
+ glib.BindProperty(w.queueSearchBar.Object, "search-mode-enabled", w.lblQueueFilter.Object, "visible", glib.BINDING_DEFAULT)
+
+ // Forcefully disable tree search popup on Ctrl+F
+ w.trvQueue.SetSearchColumn(-1)
+
// Register the main window with the app
application.AddWindow(w.window)
@@ -232,29 +237,6 @@ func NewMainWindow(application *gtk.Application) (*MainWindow, error) {
return w, nil
}
-// createQueueListStore initialises the queue list store object
-func createQueueListStore() *gtk.ListStore {
- // Collect column types from MPD attributes
- countAttrs := len(config.MpdTrackAttributeIds)
- types := make([]glib.Type, countAttrs+2)
- for i := range config.MpdTrackAttributeIds {
- types[i] = glib.TYPE_STRING
- }
-
- // Last 2 columns are font weight and background color
- queueColNumFontWeight = countAttrs
- queueColNumBgColor = countAttrs + 1
- types[queueColNumFontWeight] = glib.TYPE_INT
- types[queueColNumBgColor] = glib.TYPE_STRING
-
- // Create a list store instance
- store, err := gtk.ListStoreNew(types...)
- if err != nil {
- log.Fatal("Failed to create queue tree view list store", err)
- }
- return store
-}
-
func (w *MainWindow) onConnectorStatusChange() {
util.WhenIdle("onConnectorStatusChange()", w.updateAll)
}
@@ -433,11 +415,16 @@ func (w *MainWindow) onQueueTreeViewKeyPress(_ *gtk.TreeView, event *gdk.Event)
mask := gtk.AcceleratorGetDefaultModMask()
state := gdk.ModifierType(evt.State())
switch evt.KeyVal() {
- // Enter
+ // Enter: apply current selection
case gdk.KEY_Return:
if state&mask == 0 {
w.applyQueueSelection()
}
+ // Esc: exit filtering mode if it's active
+ case gdk.KEY_Escape:
+ if state&mask == 0 {
+ w.queueSearchBar.SetSearchMode(false)
+ }
// Delete
case gdk.KEY_Delete:
switch state & mask {
@@ -453,6 +440,11 @@ func (w *MainWindow) onQueueTreeViewKeyPress(_ *gtk.TreeView, event *gdk.Event)
if state&mask == 0 {
w.playerPlayPause()
}
+ // Ctrl+F: activate search bar
+ case gdk.KEY_f:
+ if state&mask == gdk.GDK_CONTROL_MASK {
+ w.queueSearchBar.SetSearchMode(true)
+ }
}
}
@@ -632,6 +624,15 @@ func (w *MainWindow) getQueueSaveNewPlaylistName() string {
return s
}
+// getQueueHasSelection returns whether there's any selected rows in the queue
+func (w *MainWindow) getQueueHasSelection() bool {
+ if sel, err := w.trvQueue.GetSelection(); errCheck(err, "getQueueHasSelection(): trvQueue.GetSelection() failed") {
+ return false
+ } else {
+ return sel.CountSelectedRows() > 0
+ }
+}
+
// getQueueSelectedIndices returns indices of the currently selected rows in the queue
func (w *MainWindow) getQueueSelectedIndices() []int {
// Get the tree's selection
@@ -642,11 +643,17 @@ func (w *MainWindow) getQueueSelectedIndices() []int {
// Get selected nodes' indices
var indices []int
- sel.GetSelectedRows(nil).Foreach(func(item interface{}) {
- if ix := item.(*gtk.TreePath).GetIndices(); len(ix) > 0 {
- indices = append(indices, ix[0])
+ err = sel.SelectedForEach(func(model *gtk.TreeModel, path *gtk.TreePath, iter *gtk.TreeIter, userData interface{}) {
+ // Convert the provided tree (filtered) path into unfiltered one
+ if queuePath := w.queueListFilter.ConvertPathToChildPath(path); queuePath != nil {
+ if ix := queuePath.GetIndices(); len(ix) > 0 {
+ indices = append(indices, ix[0])
+ }
}
})
+ if errCheck(err, "getQueueSelectedIndices(): SelectedForEach() failed") {
+ return nil
+ }
return indices
}
@@ -881,6 +888,65 @@ func (w *MainWindow) queueDelete() {
w.errCheckDialog(err, "Failed to delete tracks from the queue")
}
+// queueFilter applies the currently entered filter substring to the queue
+func (w *MainWindow) queueFilter() {
+ log.Debug("queueFilter()")
+ substr := ""
+
+ // Only use filter pattern if the search bar is visible
+ if w.queueSearchBar.GetSearchMode() {
+ var err error
+ if substr, err = w.queueSearchEntry.GetText(); errCheck(err, "queueSearchEntry.GetText() failed") {
+ return
+ }
+ }
+
+ // Iterate all rows in the list store
+ count := 0
+ err := w.queueListStore.ForEach(func(model *gtk.TreeModel, path *gtk.TreePath, iter *gtk.TreeIter, userData interface{}) bool {
+ // Show all rows if no search pattern given
+ visible := substr == ""
+ if !visible {
+ // We're going to compare case-insensitively
+ substr := strings.ToLower(substr)
+
+ // Scan all known columns in the row
+ for _, id := range config.MpdTrackAttributeIds {
+ // Get column's value
+ v, err := model.GetValue(iter, id)
+ if errCheck(err, "queueFilter(): queueListStore.GetValue() failed") {
+ continue
+ }
+
+ // Convert the value into a string
+ s, _ := v.GetString() // Ignoring the returned error due to https://github.com/gotk3/gotk3/issues/583
+
+ // Check for a match and stop checking if match has already been found
+ visible = s != "" && strings.Contains(strings.ToLower(s), substr)
+ if visible {
+ break
+ }
+ }
+ }
+
+ // Modify the row's visibility
+ if err := w.queueListStore.SetValue(iter, config.QueueColumnVisible, visible); errCheck(err, "queueFilter(): queueListStore.SetValue() failed") {
+ return true
+ }
+ if visible {
+ count++
+ }
+
+ // Proceed to the next row
+ return false
+ })
+ if errCheck(err, "queueListStore.ForEach() failed") {
+ return
+ }
+
+ w.lblQueueFilter.SetText(fmt.Sprintf("%d tracks displayed", count))
+}
+
// queueOne adds or replaces the content of the queue with one specified URI
func (w *MainWindow) queueOne(replace bool, uri string) {
w.queue(replace, []string{uri})
@@ -912,7 +978,7 @@ func (w *MainWindow) queuePlaylist(replace bool, playlistName string) {
// queueSave shows a dialog for saving the play queue into a playlist and performs the operation if confirmed
func (w *MainWindow) queueSave() {
// Tweak widgets
- selection := len(w.getQueueSelectedIndices()) > 0
+ selection := w.getQueueHasSelection()
w.cbQueueSaveSelectedOnly.SetVisible(selection)
w.cbQueueSaveSelectedOnly.SetActive(selection)
w.eQueueSavePlaylistName.SetText("")
@@ -1057,7 +1123,7 @@ func (w *MainWindow) shortcutInfo() {
// Show displays the window and all its child widgets
func (w *MainWindow) Show() {
- w.window.ShowAll()
+ w.window.Show()
}
// setLibraryPath sets the current library path selector and updates its widget and the current library list
@@ -1080,8 +1146,8 @@ func (w *MainWindow) setQueueHighlight(index int, selected bool) {
}
errCheck(
w.queueListStore.SetCols(iter, map[int]interface{}{
- queueColNumFontWeight: weight,
- queueColNumBgColor: bgColor,
+ config.QueueColumnFontWeight: weight,
+ config.QueueColumnBgColor: bgColor,
}),
"lstQueue.SetValue() failed")
}
@@ -1449,8 +1515,9 @@ func (w *MainWindow) updateQueue() {
rowData[id] = value
// Add the "artificial" column values
- rowData[queueColNumFontWeight] = fontWeightNormal
- rowData[queueColNumBgColor] = colorBgNormal
+ rowData[config.QueueColumnFontWeight] = fontWeightNormal
+ rowData[config.QueueColumnBgColor] = colorBgNormal
+ rowData[config.QueueColumnVisible] = true
}
// Add a row to the list store
@@ -1465,7 +1532,7 @@ func (w *MainWindow) updateQueue() {
// Add number of tracks
var status string
- switch len(attrs) {
+ switch w.currentQueueSize {
case 0:
status = "Queue is empty"
case 1:
@@ -1527,8 +1594,8 @@ func (w *MainWindow) updateQueueColumns() {
col.SetFixedWidth(width)
col.SetClickable(true)
col.SetResizable(true)
- col.AddAttribute(renderer, "background", queueColNumBgColor)
- col.AddAttribute(renderer, "weight", queueColNumFontWeight)
+ col.AddAttribute(renderer, "weight", config.QueueColumnFontWeight)
+ col.AddAttribute(renderer, "background", config.QueueColumnBgColor)
// Bind the clicked signal
_, err = col.Connect("clicked", func() { w.onQueueTreeViewColClicked(col, index, &attr) })
@@ -1550,7 +1617,7 @@ func (w *MainWindow) updateQueueColumns() {
func (w *MainWindow) updateQueueActions() {
connected := w.connector.IsConnected()
notEmpty := connected && w.currentQueueSize > 0
- selection := notEmpty && len(w.getQueueSelectedIndices()) > 0
+ selection := notEmpty && w.getQueueHasSelection()
w.aQueueNowPlaying.SetEnabled(notEmpty)
w.aQueueClear.SetEnabled(notEmpty)
w.aQueueSort.SetEnabled(notEmpty)
@@ -1572,7 +1639,14 @@ func (w *MainWindow) updateQueueNowPlaying() {
// Scroll to the currently playing
if w.currentQueueIndex >= 0 {
- if treePath, err := gtk.TreePathNewFromString(strconv.Itoa(w.currentQueueIndex)); err == nil {
+ // Obtain a path in the unfiltered list
+ treePath, err := gtk.TreePathNewFromIndicesv([]int{w.currentQueueIndex})
+ if errCheck(err, "updateQueueNowPlaying(): TreePathNewFromString() failed") {
+ return
+ }
+
+ // Convert the path into one in the filtered list
+ if treePath = w.queueListFilter.ConvertChildPathToPath(treePath); treePath != nil {
w.trvQueue.ScrollToCell(treePath, nil, true, 0.5, 0)
}
}
diff --git a/resources/player.glade b/resources/player.glade
index 66c7f31..db0c0f1 100644
--- a/resources/player.glade
+++ b/resources/player.glade
@@ -45,6 +45,65 @@
+
+
False
@@ -136,6 +196,7 @@
app.queue.clear
Clear
True
+ dialog-close
False
@@ -151,6 +212,7 @@
app.queue.sort
Sort ▾
True
+ view-sort-descending
False
@@ -165,6 +227,7 @@
app.queue.delete
Delete
True
+ edit-delete
False
@@ -180,6 +243,21 @@
app.queue.save
Save ▾
True
+ document-save
+
+
+ False
+ True
+
+
+
+
False
@@ -193,19 +271,46 @@
0
+
+
+
+ False
+ True
+ 1
+
+
True
True
True
True
- in
+ etched-out
True
True
True
True
+ queueListFilter
+ False
True
True
@@ -222,24 +327,52 @@
False
True
- 1
+ 2
-
+
True
False
- True
- False
- ...
- end
- False
+ 6
+
+
+ True
+ False
+ 3
+ 3
+ …
+ end
+ False
+
+
+ True
+ True
+ 0
+
+
+
+
+ False
+ 6
+ 3
+ 3
+ …
+
+
+ False
+ True
+ 1
+
+
+
False
True
- 6
- 2
+ 3
@@ -341,19 +474,33 @@
-
+
True
False
- True
- False
- ...
- end
- False
+ vertical
+
+
+ True
+ False
+ 3
+ 3
+ ...
+ end
+ False
+
+
+ True
+ True
+ 0
+
+
+
False
True
- 6
2
@@ -440,19 +587,33 @@
-
+
True
False
- True
- False
- ...
- end
- False
+ vertical
+
+
+ True
+ False
+ 3
+ 3
+ ...
+ end
+ False
+
+
+ True
+ True
+ 0
+
+
+
False
True
- 6
2
@@ -470,17 +631,6 @@
0
-
-
- True
- False
-
-
- False
- True
- 1
-
-
True
@@ -494,7 +644,7 @@
False
True
6
- 2
+ 1
@@ -676,7 +826,7 @@
False
True
- 3
+ 2